---
title: "HTTP API reference"
description: "Every invocation endpoint — flow invoke, SSE streaming, single-node calls — with authentication, request and response fields, rate limits and daily quotas, and the structured error envelope."
category: reference
surfaces: [http-api]
related: [getting-started/invoke-via-api, guides/api-keys, guides/use-interactive-api-docs, guides/flow-configs, guides/debug-a-flow, reference/error-catalog, concepts/execution-model]
last_reviewed: 2026-06-11
---

# HTTP API reference

This page is the reference for Axiom's invocation HTTP surface: invoking a
flow (`POST /v1/flows/invoke`), streaming a pipeline-mode flow
(`POST /v1/flows/invoke/stream`), invoking a single node, the error
envelope, and rate limits. For a guided walkthrough, start with
[Invoke a flow via API](../getting-started/invoke-via-api.md). For the exact
input and output schema of *your* flow or package, use the
[live OpenAPI specs](#live-openapi-specs) — they are generated from the
compiled artifact and are always current.

## Base URL and authentication

All invocation endpoints are served on your deployment's origin under the
`/invocations` path prefix. For example:

```text
POST https://<your-axiom-origin>/invocations/v1/flows/invoke
```

Every request must carry an API key as a bearer token:

```text
Authorization: Bearer <your 64-character API key>
```

API keys are 64-character hex strings created under **Console → API Keys**
in the app, or minted automatically by `axiom login` (named `cli`). The raw
key is shown exactly once at creation; the platform stores only its SHA-256
hash. See [Create and manage API keys](../guides/api-keys.md).

The key resolves to your tenant. Every execution it starts is scoped to that
tenant — the platform enforces isolation, so a key can never invoke another
tenant's flows or read another tenant's data
([Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md)).

A missing, invalid, or revoked key gets HTTP 401 with the body:

```json
{"error": "unauthorized"}
```

Revocation takes effect on the key's very next use — there is no key cache.

## Rate limits

Two per-tenant token buckets apply to authenticated requests:

- **All routes:** 100 requests/second, burst 200.
- **Invocation routes** (everything under `/invocations/`): an additional
  5 requests/second, burst 10 — a beta guardrail.

Exceeding either returns HTTP 429 with a `Retry-After: 1` header and the
body `{"error":"rate limit exceeded"}`.

## Daily quotas

On top of the per-second rate limits, each tenant has two daily budgets.
Both reset at midnight UTC. The beta defaults are:

- **2,000 invocations per day.** An invocation is one authenticated request
  that triggers compute: a flow invoke (plain or streaming), a single-node
  call, a paused-flow resume (including resume webhooks), or a debug-session
  fork.
- **500 executions per day.** An execution is one flow run. Most invocations
  start exactly one, but executions a flow spawns internally — parallel
  fan-out branches that run as child executions, runtime graph mutations,
  debug forks — each draw from the same budget. A fan-out into 10 branches
  costs 10 executions.

A request that would exceed either budget is rejected before any compute
runs, with HTTP 429, a `Retry-After` header, and a structured body whose
`retry_after_seconds` counts down to the next UTC midnight:

```json
{
  "error": "quota_exceeded",
  "message": "daily invocations quota exceeded (cap 2000)",
  "retry_after_seconds": 74380
}
```

If a running flow exhausts the execution budget mid-run (for example at a
large fan-out), the flow fails with the same `quota exceeded` message as its
error.

The defaults are per-tenant and the operator can raise or lower them for
your tenant — if you hit them with a legitimate workload, get in touch.
Two related responses you may also see on invocation routes:

| Status | Body `error` | Meaning |
|---|---|---|
| 403 | `tenant_suspended` | Your tenant has been disabled by the operator. No invocations or resumes are accepted. Contact the operator. |
| 503 | `quota_unavailable` | The quota service could not be reached, so the request was rejected as a safety measure. Transient — retry after `retry_after_seconds` (5 s). |

## Invoke a flow

`POST /v1/flows/invoke` executes a compiled artifact once and returns the
result inline (with `wait`) or an execution ID immediately (without).

```bash
curl -X POST 'https://flows.example-axiom-host.com/invocations/v1/flows/invoke' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD",
  "input": { "text": "hello" },
  "wait": true
}'
```

### Request fields

| Field | Type | Required | Meaning |
|---|---|---|---|
| `graph_id` | string | yes | The compiled artifact to execute. Get it from the **Use via API** dialog or the flow's [live OpenAPI spec](#live-openapi-specs); editing a flow produces a new artifact with a new ID. |
| `input` | object | one of `input`/`payload` | The entry node's input message as a JSON object. The platform converts it to the typed message, and decodes the result back to JSON. |
| `payload` | string (base64) | one of `input`/`payload` | The entry node's input message as protobuf-encoded bytes, base64-encoded. For callers that already speak protobuf. `input` takes precedence if both are set. |
| `wait` | boolean | no | `true` blocks until the execution completes and populates `result`. Default `false`: the response returns immediately with just `accepted` and `execution_id`. |
| `timeout_seconds` | integer | no | How long a `wait`ing call blocks before giving up (default 30). |
| `config_id` | string | no | Named flow config profile to apply; when omitted the default hierarchy applies (tenant default → flow default). See [Flow configs](../guides/flow-configs.md). |
| `debug_session_id` | string | no | Streams per-node debug events for this execution to the named debug session. Can also be sent as the `X-Debug-Session-Id` header, which takes precedence over the body field. See [Debug a flow](../guides/debug-a-flow.md). |

### Response

An accepted invocation returns HTTP 202:

```json
{
  "accepted": true,
  "execution_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "result": {
    "success": true,
    "output": { "text": "hello" },
    "completed_at": 1765432100000
  }
}
```

- `execution_id` — a 32-character hex ID assigned when the request is
  accepted. It is also the execution's trace ID, so one ID references the
  execution everywhere: results, debug events, and traces.
- `result` — present only with `wait: true`. `result.output` is the
  terminal node's output message decoded to JSON (when you invoked with
  `input`); `result.payload` carries base64-encoded protobuf bytes instead
  when you invoked with `payload` or when JSON decoding was not possible.
- Without `wait`, the body is just
  `{"accepted": true, "execution_id": "..."}` and the flow runs
  asynchronously.

Failures return a non-202 status with an error body — see
[Error envelope](#error-envelope).

## Stream a flow over SSE

`POST /v1/flows/invoke/stream` invokes a flow in pipeline mode and streams
result frames back as Server-Sent Events. Use it for flows that emit a
sequence of output frames rather than a single result
([Execution model](../concepts/execution-model.md)).

```bash
curl -N -X POST 'https://flows.example-axiom-host.com/invocations/v1/flows/invoke/stream' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD",
  "input": { "text": "hello" }
}'
```

The request body accepts `graph_id`, `input` (or `payload`),
`timeout_seconds`, `config_id`, and `debug_session_id` exactly as in
[Invoke a flow](#invoke-a-flow). `wait` does not apply — frames are always
delivered as they are produced.

The response is `Content-Type: text/event-stream`. Each event is one
`data:` line of JSON:

```text
data: {"execution_id":"4bf92f3577b34da6a3ce929d0e0e4736","frame_index":0,"payload":{"text":"chunk 1"},"is_final":false}

data: {"execution_id":"4bf92f3577b34da6a3ce929d0e0e4736","frame_index":1,"payload":{"text":"chunk 2"},"is_final":true,"success":true}
```

Frame fields:

| Field | Meaning |
|---|---|
| `execution_id` | Same ID as the unary endpoint — also the trace ID. |
| `frame_index` | Position of this frame in the stream, starting at 0. |
| `payload` | The terminal node's output message for this frame, decoded to JSON. Omitted when the frame carries no decodable payload. |
| `is_final` | `true` on the last frame; the stream ends after it. |
| `success` | `true` on the final frame of a successful execution. On failure the field is omitted entirely (it is never serialized as `false`) — treat a final frame without `success: true` as failed and read `error`. |
| `error` | Error message when a frame reports failure. |

The stream times out after 30 seconds by default; set `timeout_seconds` to
extend it. A timeout ends the stream with a final frame whose `error` is
`"timeout waiting for pipeline result"`.

## Invoke a single node

A published node can be called directly, without composing a flow. This is
the endpoint that Client Builder SDKs use —
one method per node.

```bash
curl -X POST 'https://flows.example-axiom-host.com/invocations/v1/nodes/axiom-official/pdf-chunker/1.0.0/Chunk' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{ "url": "https://example.com/doc.pdf" }'
```

- **Path forms:** `POST /v1/nodes/{owner}/{package}/{version}/{node}`
  (name-based, as in the example) or `POST /v1/nodes/{node_ulid}` (the
  node's 26-character registry ID).
- **Request body:** the node's input message as a plain JSON object — no
  envelope.
- **Response:** HTTP 200 with the node's output message as JSON. A node
  that returns an error gets HTTP 422 with
  `{"error_message": "..."}`; an unknown or undeployed node gets HTTP 404.
- **Streaming nodes:** a pipeline-mode node (or any request with
  `Accept: text/event-stream`) streams its output frames as Server-Sent
  Events instead of a single JSON response.

A binary-protobuf variant exists at `POST /v1/nodes/invoke` with the JSON
body `{"node_id": "...", "payload": "<base64 protobuf bytes>"}`, returning
`{"success": ..., "payload": ..., "error_message": ...}` — for callers that
serialize the messages themselves.

## Error envelope

Invocation failures with a known cause return a structured body with a
stable, machine-readable `error` class:

```json
{
  "error": "payload_too_large",
  "message": "payload exceeds hard cap: actual=18874368 max=16777216",
  "max_bytes": 16777216,
  "actual_bytes": 18874368
}
```

| Status | `error` class | Extra fields | When |
|---|---|---|---|
| 400 | — | `{"accepted": false, "error_message": "invalid request body: ..."}` | The request body is not valid JSON. |
| 401 | `unauthorized` | — | Missing, invalid, or revoked API key. |
| 413 | `payload_too_large` | `max_bytes`, `actual_bytes` (when the rejecting code path knows the sizes) | The input payload exceeds the platform's 16 MiB hard cap. |
| 422 | — | `{"error_message": "..."}` | Single-node invocation only: the node itself returned an error. |
| 429 | `rate limit exceeded` | `Retry-After: 1` header | Per-tenant rate limit exceeded. |
| 429 | `quota_exceeded` | `retry_after_seconds` to next UTC midnight, mirrored in the `Retry-After` header | Per-tenant [daily quota](#daily-quotas) (invocations or executions) exhausted. |
| 403 | `tenant_suspended` | — | Your tenant has been disabled by the operator. |
| 503 | `quota_unavailable` | `retry_after_seconds: 5` | The quota service is unreachable; the request is rejected as a safety measure. Retry with backoff. |
| 503 | `upstream_unavailable` | `retry_after_seconds: 5` | The flow could not be queued; a platform dependency was temporarily unavailable. Retry with backoff. |
| 503 | `blob_storage_unavailable` | `retry_after_seconds: 5` | A large input payload could not be stored; a platform dependency was temporarily unavailable. Retry with backoff. |
| 500 | — | `{"accepted": false, "error_message": "..."}` | Any failure that does not classify into the rows above (for example, a missing `graph_id`). |

Errors inside a successful HTTP exchange surface differently: a `wait`ed
invoke returns 202 with `result.success: false`, and a stream delivers a
final frame with `error` set and no `success: true` (the frame's `success`
field is omitted on failure, never serialized as `false`). The
[error catalog](../reference/error-catalog.md) lists the error messages
themselves.

## Live OpenAPI specs

Hand-maintained docs cannot know your flow's input schema — the live,
generated OpenAPI 3.0 specs can. Use them as the authoritative request and
response schemas:

- **Per flow:** `GET /api/graphs/{artifact_id}/openapi.json`
  (authenticated, same bearer key). Generated on demand from the compiled
  artifact: the entry node's input message is the documented input schema,
  the terminal node's output message is the result schema, and `graph_id`
  is pinned to that exact artifact. Pipeline-mode flows document
  `/v1/flows/invoke/stream`; unary flows document `/v1/flows/invoke`. The
  same spec backs the **Open interactive docs** button in the flow
  inspector's **API** section — see
  [Use the interactive API docs](../guides/use-interactive-api-docs.md).
- **Per package:** `GET /api/packages/{name}@{version}/openapi.json`
  (public, no auth). One `POST /v1/nodes/{owner}/{package}/{version}/{node}`
  operation per node, with input and output schemas and examples. An
  interactive try-it page for the same spec is served at
  `GET /api/packages/{name}@{version}/docs`.
