# Iosis API documentation

> Iosis is the best way to run reproducible, auditable strategy research. Define data pipelines, backtests, and execution logic as a single graph that validates schemas, dependencies, null policies, and time axes before compute begins, then runs deterministically every time.

- Base URL: https://iosis.dev
- Human docs: https://iosis.dev/docs
- Quickstart: https://iosis.dev/quickstart (markdown: https://iosis.dev/quickstart.md)
- Agent index: https://iosis.dev/llms.txt
- Auth: `Authorization: Bearer $IOSIS_API_KEY` on every request. Keys are created at https://iosis.dev/app/account, shown once, limited to 1 per workspace.

## Authentication

Workspace-scoped API keys. Key format: `iosis_<8 hex prefix>_<secret>`. Missing, invalid, or revoked keys return `401 {"error": "invalid_api_key"}`. Treat keys like passwords: env var or secret manager, never client-side code or source control. Revocation is immediate.

```python
from iosisclient import IosisClient
client = IosisClient()  # reads IOSIS_API_KEY
```

## Rate limits and credits

Per-key fixed one-minute windows:

| Bucket | Endpoints | Limit |
| --- | --- | --- |
| Get | GET runs, charts, datasets, tsfns | 20/min |
| Render | POST /api/graphs/render | 10/min |
| Runs | POST /api/runs | 10/min |

Over-limit: `429 {"error": "rate_limit_exceeded"}` with `Retry-After`.

Compute: 1 credit = 6 seconds of worker compute. Result-cache hits cost zero credits. Free workspaces include a monthly allowance (default 100 credits); paid tiers add larger allowances managed at https://iosis.dev/app/account. Exhaustion: `POST /api/runs` returns `insufficient_credits`.

Check balance:

```console
curl https://iosis.dev/api/credits -H "Authorization: Bearer $IOSIS_API_KEY"
# {"credits": {"granted": 100, "total": 100, "consumedMs": 78000, "consumedCredits": 13, "remaining": 87}}
```

```python
credits = client.get_credits()
```

## Installation

```console
pip install iosisclient
```

Requires Python 3.11+. The HTTP client is stdlib-only; the package depends on `iosislib` for local graph execution and validation.

## Endpoints

| Method | Endpoint | Description |
| --- | --- | --- |
| POST | https://iosis.dev/api/runs | Submit a strategy run (Content-Type: application/yaml) |
| GET | https://iosis.dev/api/runs/:runId | Get run status + signed artifact URLs |
| GET | https://iosis.dev/api/runs/:runId/charts | Get chart URLs for a run |
| GET | https://iosis.dev/api/runs/charts | List succeeded runs with charts |
| POST | https://iosis.dev/api/graphs/render | Render a strategy graph as SVG |
| GET | https://iosis.dev/api/credits | Get remaining credit balance |
| GET | https://iosis.dev/api/versions | Oldest supported client/library versions (no auth) |
| GET | https://iosis.dev/api/datasets | List published datasets |
| GET | https://iosis.dev/api/datasets/lookup?name=<name> | Look up a dataset by name |
| GET | https://iosis.dev/api/datasets/manifest | List datasets with full manifests |
| GET | https://iosis.dev/api/tsfns | List allowed time-series functions |
| GET | https://iosis.dev/api/schema/strategy | Strategy JSON Schema (v0.1.0) |

## Runs

Submit (always send `Idempotency-Key: <uuid>`; reuse the same key only when retrying the identical payload — the same key with different YAML returns 409 `idempotency_conflict`):

```console
curl -X POST https://iosis.dev/api/runs \
  -H "Authorization: Bearer $IOSIS_API_KEY" \
  -H "Content-Type: application/yaml" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @strategy.yaml
# 202 {"id": "8f3c...", "status": "queued"}
```

```python
run = client.submit_run("strategy.yaml")
result = client.wait_for_run(run["id"])
```

Get a run:

```console
curl https://iosis.dev/api/runs/8f3c... -H "Authorization: Bearer $IOSIS_API_KEY"
```

Statuses: `queued`, `running`, `succeeded`, `failed` (`run.error` explains failures). Result URLs expire after 60s, charts after 5min. Download immediately.

Charts:

```console
curl https://iosis.dev/api/runs/8f3c.../charts -H "Authorization: Bearer $IOSIS_API_KEY"
```

```python
charts = client.get_charts("8f3c...")
paths = client.download_artifacts("8f3c...", "./output")
```

## Graphs

```console
curl -X POST https://iosis.dev/api/graphs/render \
  -H "Authorization: Bearer $IOSIS_API_KEY" \
  -H "Content-Type: application/yaml" \
  --data-binary @strategy.yaml --output graph.svg
```

Returns `200 image/svg+xml` laid out server-side in the app's ReactFlow visual language. Unknown ops render as generic nodes. Errors: 400 malformed/empty graph, 401 bad key, 413 body too large, 415 non-YAML content type.

## Datasets

```console
curl https://iosis.dev/api/datasets -H "Authorization: Bearer $IOSIS_API_KEY"
curl "https://iosis.dev/api/datasets/lookup?name=polymarket_weather" -H "Authorization: Bearer $IOSIS_API_KEY"
curl https://iosis.dev/api/datasets/manifest -H "Authorization: Bearer $IOSIS_API_KEY"
```

```python
client.list_datasets()
client.lookup_dataset("polymarket_weather")
client.list_dataset_manifests()
```

Use any returned `name` directly as `name` in `source.dataset` nodes. The
node's schema (time column + value columns) comes from the dataset manifest —
do not declare `schema`. Optionally bound the scan with `time_range`:

```yaml
nodes:
  market:
    op: source.dataset
    version: 0.1.0
    params:
      name: polymarket_weather
      time_range:
        start: 2026-09-01
        end: 2026-09-30
```

## Capabilities (TSFNs)

```console
curl https://iosis.dev/api/tsfns -H "Authorization: Bearer $IOSIS_API_KEY"
```

Returns `iosis.tsfn-catalog` v0.1.0: op name + version, category (`transform`, `source`, `model`, `backtest`), parameters, null/lookahead policy, input/output frame signature. Reference ops as `op@version`. Data-dependent signatures resolve to `"signature": null`.

## Errors

All errors are `{"error": "<code>", "message": "<human-readable>"}`:

| Status | Meaning |
| --- | --- |
| 400 | Malformed request (invalid YAML, missing parameter, bad ID) |
| 401 | Missing, invalid, or revoked API key |
| 403 | Account not approved, workspace not active, or monthly credits exhausted |
| 404 | Run or dataset not found |
| 409 | Idempotency key reused with different YAML (`idempotency_conflict`); strategy integrity failure (`strategy_integrity_failed`) |
| 413 | Request body too large |
| 415 | Unsupported media type |
| 429 | Rate limit exceeded (see Retry-After) |
| 500 | Internal error |

```python
from iosisclient import IosisError
try:
    client.get_run("bad-id")
except IosisError as e:
    print(e.status, e.code, e.message)
```

## Strategy format

Strategies are `iosis.strategy` v0.1.0 YAML: nodes + outputs. Fetch the machine-readable schema at `GET https://iosis.dev/api/schema/strategy` (`client.get_strategy_schema()`).

```yaml
format: iosis.strategy
version: 0.1.0
name: market-zscore
description: Rolling z-scores of changes in market probability.
nodes:
  market:
    op: source.dataset
    version: 0.1.0
    params:
      name: polymarket_weather
  changes:
    op: transform.pct_change
    version: 0.1.0
    inputs:
      price: market.yes_price
    params:
      input_column: price
      output_column: changes
  zscore:
    op: transform.rolling_z_score
    version: 0.1.0
    inputs:
      changes: changes.changes
    params:
      input_column: changes
      output_column: zscore
      periods: 20
      min_samples: 10
outputs:
  price: market.yes_price
  zscore: zscore.zscore
```

- `format` must be exactly `iosis.strategy`. `version` is the SemVer document contract (current `0.1.0`).
- `nodes` maps stable identifiers to declarations; order has no meaning. `outputs` maps public names to `node.output` references. `result.parquet` holds the alphabetically-first declared output; every declared output is also returned as its own signed artifact (`summary.outputs`).
- Node: `op`, `version`, `params` (JSON-compatible), `inputs` (input name -> `node.output`), optional `materialize`.
- `source.dataset` params: `name` (required) + optional `time_range` with `start`/`end` (ISO-8601 timestamp or bare date, `.Z` suffix allowed; inclusive on both ends — a bare-date `end` covers that whole day). The schema is derived from the dataset manifest; declaring `schema` or any other key is rejected with 400 `invalid_node_params`.
- Inputs short form: `price: market.yes_price`. Expanded form adds `from`, `tolerance` (number or Polars-style duration like `5m`), `nulls` (`error`, `propagate`, `drop`, `fill`, `pass`), `fill` (required with `fill`).
- Identifiers start with a letter; letters, digits, `_`, `-` only. Dots separate references.
- Walk-forward models (e.g. `model.light_gbm@0.3.0`) emit `NaN` until the first retrain boundary (with the default `{ every: 100 }` scheduler, the first 100 rows); exclude warm-up rows before full-series metrics.
- Metric nodes (`metrics.mse`, `metrics.mae`, `metrics.max_drawdown`, `metrics.sharpe`, `metrics.total_return`) emit one row (`timestamp` + the metric column) and are declared as outputs like any other node. They reject non-finite values and nulls by default; set `drop_nonfinite: true` to drop non-finite rows and bind inputs with `nulls: drop` to skip warm-up nulls.
- Charts render only from `metadata.charts`: a list of declarations with `kind` (`line`, `bars`, `equity`, `scatter`), `name`, `output` (a declared output name), `columns` (line/bars/equity) or `x`/`y` (scatter), and optional `title`. A chart that cannot render returns a placeholder SVG. `metadata.metrics` is rejected.
- Model nodes fail loudly on nulls: any null in `features`/`target` aborts the run with `NullPolicy.ERROR`. Warm-up nulls from upstream transforms (e.g. the first `pct_change` row, which then propagates through `rolling_*`) are enough to trip it, so always declare per-input handling on model inputs: `nulls: drop`, or `nulls: fill` with `fill:`.

## Strategy construction guide

1. Pick a dataset from `GET /api/datasets`. 2. Declare a `source.dataset` node with `name` (+ optional `time_range` to bound the scan). 3. Chain transforms via `inputs` maps. 4. Declare `outputs`. 5. Optionally add a model node. Tips: browse ops via `GET /api/tsfns`, validate with `render_graph()` before submitting, always send `Idempotency-Key`.

## Backtest reference

`backtest.backtest` runs signal-driven backtests with `feed` (currently `l1` only), `policy` (`signal` or `threshold`), and optional `risk_policy` (`fractional_limit` or `fractional_kelly`):

```yaml
nodes:
  book:
    op: source.dataset
    version: 0.1.0
    params:
      name: polymarket_weather_lob
  odds:
    op: transform.logit
    version: 0.1.0
    inputs:
      value: book.mid
    params:
      output_column: log_odds
  backtest:
    op: backtest.backtest
    version: 3.0.0
    inputs:
      bid: book.bid
      ask: book.ask
      signal: odds.log_odds
    params:
      feed:
        kind: l1
        venue:
          name: polymarket
          universe: [nyc-high-2026-09-24, london-low-2026-09-24]
      policy:
        kind: signal
        signal_column: signal
      initial_cash: 10000.0
outputs:
  equity: backtest.equity
```

## Pricing

Compute is metered in credits (1 credit = 6s worker compute; cache hits are free). Free workspaces include a monthly allowance (default 100 credits). Paid tiers add larger monthly allowances and are purchased at https://iosis.dev/app/account via Stripe. See live prices and usage in Account.
