Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
Documentation menu

HTTP API reference

Every invocation endpoint — flow invoke, SSE streaming, single-node calls, pre-warm — plus the Client Builder management API, Instance creation, adapter-mapping preview, with authentication, request and response fields, rate limits and daily quotas, and the structured error envelope.

View as Markdown

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, pre-warming a flow (POST /v1/flows/warm), the error envelope, and rate limits — plus the Client Builder management API that axiom client drives. For a guided walkthrough, start with Invoke a flow via API. For the exact input and output schema of your flow or package, use the live OpenAPI specs — they are generated from the compiled artifact and are always current.

Base URL and authentication

All invocation endpoints are served by the Axiom gateway at https://api.axiomide.com under the /invocations path prefix. For example:

POST https://api.axiomide.com/invocations/v1/flows/invoke

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

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.

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).

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

{"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.

The invocation burst limit is enforced at ingress admission as well as at the gateway, so it applies even on the direct-to-ingress routing the axiom CLI defaults to in development (which bypasses the gateway entirely).

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 — child executions from parallel branches, runtime graph mutations, debug forks — each draw from the same budget. A flow that spawns 10 parallel 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:

{
  "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 when spawning a large number of parallel branches), 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:

StatusBody errorMeaning
403tenant_suspendedYour tenant has been disabled by the operator. No invocations or resumes are accepted. Contact the operator.
503quota_unavailableThe 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).

curl -X POST 'https://api.axiomide.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

FieldTypeRequiredMeaning
graph_idstringyesThe compiled artifact to execute. Get it from the Use via API dialog or the flow's live OpenAPI spec; editing a flow produces a new artifact with a new ID.
inputobjectone of input/payloadThe entry node's input message as a JSON object. The platform converts it to the typed message, and decodes the result back to JSON.
payloadstring (base64)one of input/payloadThe entry node's input message as protobuf-encoded bytes, base64-encoded. For callers that already speak protobuf. input takes precedence if both are set.
waitbooleannotrue blocks until the execution completes and populates result. Default false: the response returns immediately with just accepted and execution_id.
timeout_secondsintegernoHow long a waiting call blocks before giving up (default 30).
config_idstringnoNamed flow config profile to apply; when omitted the default hierarchy applies (tenant default → flow default). See Flow configs.
debug_session_idstringnoStreams 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.
idempotency_keystringnoDedups retries: a call with the same key from the same tenant within 5 minutes of the first returns that first call's execution_id instead of executing the flow again — at most one execution per key, even if the retry lands while the first call is still in flight. Can also be sent as the Idempotency-Key header, which takes precedence over the body field. When omitted, the platform falls back to a fingerprint of graph_id, config_id, and the payload/input, so byte-identical retries still dedup even if you never set this field.

Response

An accepted invocation returns HTTP 202:

{
  "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.
  • result.error_message — present with wait: true when the execution was accepted and delivered but ran to a failure (result.success is false), for example because the input did not match the flow's start-node schema. The same diagnostic is also copied to the top-level error_message so a caller can read it without descending into result. The response is still HTTP 202 (the request was accepted); the failure is informational, not a transport error.
  • 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.

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).

curl -N -X POST 'https://api.axiomide.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. 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:

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:

FieldMeaning
execution_idSame ID as the unary endpoint — also the trace ID.
frame_indexPosition of this frame in the stream, starting at 0.
payloadThe terminal node's output message for this frame, decoded to JSON. Omitted when the frame carries no decodable payload.
is_finaltrue on the last frame; the stream ends after it.
successtrue 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.
errorError 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.

curl -X POST 'https://api.axiomide.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. A payload that doesn't match the node's current input schema is rejected with HTTP 400: an unknown or renamed field name (rejected by JSON decoding), or an enum field set to an ordinal that names no declared value, yields {"execution_id": "...", "error_message": "invalid value for <Message>: field \"<name>\" carries enum ordinal <n>, which names no declared value ..."}. Re-creating a payload recorded against an older schema version is the usual cause — re-record it against the current schema.
  • 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. The payload is validated against the node's current input schema before it is forwarded: bytes whose wire shape doesn't match (an undeclared field number, or a wire type that conflicts with the declared field kind) are rejected with HTTP 400 and {"success": false, "error_message": "payload does not match the node's current input schema: ..."}, as is any payload whose node schema can't be resolved.

Pre-warm a flow

POST /v1/flows/warm primes a flow's pods ahead of time — a scale-from-zero nudge with no business logic run — so a subsequent real invoke doesn't pay a cold start. Call it, for example, right before you expect a burst of traffic, or after deploying a flow that has been idle.

curl -X POST 'https://api.axiomide.com/invocations/v1/flows/warm' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD"}'

A successful warm returns HTTP 202:

{"warmed": true, "graph_id": "01JX3F8Q4ZJ4M9W4Y0B8T2K7RD"}

warmed: true means the warm was dispatched, not that pods are already serving — it's fire-and-forget, the same way a cold start on invoke is not something you wait on. A flow with nothing deployed (for example, one built entirely from built-in nodes) is a legitimate no-op: you still get warmed: true back. Only the node packages your tenant is entitled to use are warmed; this only matters if a flow's saved graph somehow references a package outside your tenant's access, in which case that package is silently skipped rather than warmed.

Request fields

FieldTypeRequiredMeaning
graph_idstringyesThe compiled artifact to warm — the same id you'd pass to invoke. Must match [A-Za-z0-9_-]+; anything else is a 400.

Two other body fields, tenant_id and flow_owner, are accepted but always ignored — see the next section. Don't rely on either to select which tenant a warm applies to.

Authentication and ownership

Pre-warm authenticates exactly like every other invocation route: your bearer API key resolves to your tenant (see Base URL and authentication). That tenant — never anything in the request body — is what the platform checks the graph_id against: a graph_id your tenant does not own returns HTTP 403, the same ownership check invoke performs. An unknown graph_id returns 404. Either way, no warm is dispatched — a warm request can never be used to spin up capacity for another tenant's flow.

The tenant_id and flow_owner body fields exist in the request schema only so a forged value there has nowhere to go — the platform never reads them. Setting either has no effect; your key's tenant is always the one enforced.

Rate limits and quotas

Pre-warm shares the same per-tenant budgets as invoke — it does not have a separate limit:

  • The rate limits above (100 req/s overall, 5 req/s on invocation routes) apply identically.
  • Each warm debits one unit of the daily invocation quota (the same 2,000/day budget a flow invoke, single-node call, or resume consumes — see Daily quotas). It does not touch the separate executions budget, since warming runs no flow.

A tenant that is suspended, or has exhausted its invocation quota, gets the same 403 tenant_suspended / 429 quota_exceeded responses invoke returns — see Error envelope.

Errors specific to pre-warm

Statuserror classWhen
400invalid_requestgraph_id is missing or doesn't match [A-Za-z0-9_-]+.
403forbiddenThe authenticated tenant does not own graph_id.
404not_foundgraph_id doesn't resolve to any flow.
503warm_unavailableThe warm dispatcher isn't configured on this deployment; retry_after_seconds: 5.

All other statuses (401, 429, 403 tenant_suspended, 503 quota_unavailable) match Error envelope exactly.

Client Builder management API

These routes create and manage clients — tenant-scoped bundles of nodes and flows that compile into a typed SDK — and back the axiom client CLI (see Build a client SDK). They're a separate management surface from the invocation endpoints above: served under the /api path prefix on the same gateway (https://api.axiomide.com), not /invocations.

POST https://api.axiomide.com/api/v1/clients

Authentication is the same bearer API key as every other route. Mutating calls (POST, PUT, DELETE) additionally require a key with the write scope — a read-only-scoped key gets HTTP 403 with {"error": "forbidden: token lacks 'write' scope"}. A request that reaches the registry without a resolved tenant (for example, calling the registry directly instead of through the gateway) gets HTTP 401 with {"error": "missing tenant context"}.

MethodPathDescription
POST/v1/clientsCreate a client.
GET/v1/clientsList the tenant's clients.
GET/v1/clients/{id}Get one client, including its members and versions.
PUT/v1/clients/{id}Replace a client's name/description/default language.
DELETE/v1/clients/{id}Delete a client (cascades to its members and versions).
POST/v1/clients/{id}/membersAdd one member (a node or a flow).
PUT/v1/clients/{id}/membersReplace the full member set in one call.
DELETE/v1/clients/{id}/members/{memberId}Remove one member.
POST/v1/clients/{id}/buildStart a build: a new version, or additional languages on an existing one.
GET/v1/clients/{id}/versionsList every version+language build row.
GET/v1/clients/{id}/versions/{version}?language={language}Get one version+language build row.
GET/v1/clients/{id}/versions/{version}/download?language={language}Download the built SDK zip.

Create / get / list a client

curl -X POST 'https://api.axiomide.com/api/v1/clients' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name": "billing", "description": "Billing SDK", "default_language": "python"}'

Request fields: name (string, required, unique per tenant — 409 {"error": "a client with that name already exists"} on collision), description (string, optional), default_language (string, optional — must be one of go, python, rust, java, typescript, csharp, else 400 {"error": "unsupported language: <value>"}). PUT takes the same three fields to replace them wholesale.

Response (201 for create, 200 for get/list/update):

{
  "id": "01J8Z3K9Q2R4S6T8V0W2X4Y6Z8",
  "tenant_id": "01J8Z3K9Q2R4S6T8V0W2X4Y700",
  "name": "billing",
  "slug": "billing",
  "description": "Billing SDK",
  "default_language": "python",
  "latest_version": 0,
  "created_at": "2026-07-04T12:00:00Z",
  "updated_at": "2026-07-04T12:00:00Z",
  "members": [],
  "versions": [],
  "member_count": 0,
  "version_count": 0
}

GET /v1/clients returns this same shape as a JSON array (never null, even with zero clients); GET /v1/clients/{id} is the only call that populates members/versions — the list form always reports them empty and relies on member_count/version_count instead. GET /v1/clients/{id} returns 404 {"error": "client not found"} for an unknown or foreign-tenant ID.

Add / list / remove a member

curl -X POST 'https://api.axiomide.com/api/v1/clients/01J8Z3K9Q2R4S6T8V0W2X4Y6Z8/members' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"type": "node", "package": "acme/billing-utils", "version": "1.2.0", "node_name": "Summarize", "alias": "summarize"}'

Request fields: type ("node" or "flow", required), and for type: "node"package (required), version (optional; empty or "latest" pins the package's current latest version server-side), node_name or node_ulid (one identifies the node); for type: "flow"artifact_id (required, the saved flow's compiled artifact ID). alias is optional on the wire (derived from the node name if omitted) but must be unique within the client — a collision returns 409 {"error": "a member with that alias already exists — pass a distinct alias"}. PUT /v1/clients/{id}/members takes {"members": [...]} with the same per-member shape and replaces the entire set transactionally.

Response (201 for add, 200 for PUT/list):

{
  "id": "01J8Z3K9Q2R4S6T8V0W2X4Y700",
  "client_id": "01J8Z3K9Q2R4S6T8V0W2X4Y6Z8",
  "member_type": "node",
  "alias": "summarize",
  "package_name": "acme/billing-utils",
  "package_version": "1.2.0",
  "node_ulid": "01J8Z3K9Q2R4S6T8V0W2X4Y701",
  "created_at": "2026-07-04T12:00:00Z"
}

package_name/package_version/node_ulid are omitted for a flow member, which instead carries artifact_id. DELETE .../members/{memberId} returns 204 No Content whether or not the member existed.

Build a version and check status

curl -X POST 'https://api.axiomide.com/api/v1/clients/01J8Z3K9Q2R4S6T8V0W2X4Y6Z8/build' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"languages": ["python", "go"]}'

Request fields: languages (array of strings; falls back to the client's default_language if omitted, deduplicated, each validated against the same six-language set — 400 {"error": "unsupported language: <value>"} for any unknown entry) and version (integer; 0 or omitted cuts a new version from the client's current member set — 400 {"error": "client has no members — add at least one node or flow"} if it has none; a positive value adds the given languages to that existing version's frozen member snapshot instead — 404 {"error": "version not found"} if it doesn't resolve).

The call returns immediately with HTTP 202 and one row per requested language, each starting in pending (build/upload happen asynchronously in the background):

[
  {
    "id": "01J8Z3K9Q2R4S6T8V0W2X4Y702",
    "client_id": "01J8Z3K9Q2R4S6T8V0W2X4Y6Z8",
    "version": 1,
    "language": "python",
    "status": "pending",
    "created_at": "2026-07-04T12:00:00Z"
  }
]

Poll GET /v1/clients/{id}/versions (all rows) or GET /v1/clients/{id}/versions/{version}?language={language} (one row — the language query param is required, 400 {"error": "language query param is required"} without it) until status reaches succeeded or failed; a failed row's error field carries the failure message. No API key is ever embedded in a build (see Call the SDK) — the generated SDK always reads AXIOM_API_KEY from its caller's environment.

Download a built SDK

curl 'https://api.axiomide.com/api/v1/clients/01J8Z3K9Q2R4S6T8V0W2X4Y6Z8/versions/1/download?language=python' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -o billing-sdk-py.zip

Both version (path) and language (query, required) select the exact build row. 200 OK streams Content-Type: application/zip with Content-Disposition: attachment; filename="<client-slug>-sdk-<ext>.zip". A row that hasn't reached succeeded returns 409 {"error": "build is not ready (status: <status>)"} instead of a body; an unknown version/language pair returns 404 {"error": "version not found"}.

Instance creation API

This route materializes an Instance — a Generic node's port(s) bound to a real type — entirely server-side (no git commit, no build, no Knative deploy) and backs axiom instance create (see the axiom-instance-authoring skill). Like the Client Builder routes above, it's served under the /api path prefix, and mutating calls require a bearer API key with the write scope.

POST https://api.axiomide.com/api/v1/instances

Create an Instance

curl -X POST 'https://api.axiomide.com/api/v1/instances' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "generic_package": "acme/postgres-connector",
    "generic_node": "Query",
    "generic_version": "1.2.0",
    "name": "your-handle/orders-query",
    "output": {
      "message_name": "OrderSummary",
      "fields": {"id": "int64", "total": "double", "status": "string"}
    }
  }'

Request fields: generic_package / generic_node (required) locate the Generic node to bind. generic_version is optional — empty or "latest" resolves and pins whatever is latest at creation time (it does not track "latest" going forward). name (required) is the Instance's scoped name, your-handle/instance-name; the scope must equal the caller's own handle — a mismatched scope gets 403. At least one of output / input is required, matching whichever port(s) the Generic node declared generic — each is {"message_name": string, "fields": {name: fieldDef}} with at least one field.

A fieldDef value is either a bare scalar-kind string (as in the example above — string, int32, int64, uint32, uint64, double, float, bool, bytes) or an object for anything richer: {"kind": "<scalar>", "repeated": true}, {"kind": "<scalar>", "optional": true} (mutually exclusive — proto3 forbids repeated optional), or {"kind": "message", "message": {"message_name": "...", "fields": {...}}} for a field whose type is itself another message, defined inline — recursively, so that nested message's own fields may in turn include a message-kind field. Every message in the tree — the top-level facade and every nested one, however deep — is synthesized in the same transaction that creates the Instance: nothing is written if the request fails validation at any level, and nothing exists as an intermediate row. See Configure an Instance from a Generic node for a full worked example.

Response (201) is the created Instance row:

{
  "package_id": "01J8Z3K9Q2R4S6T8V0W2X4Y6Z8",
  "node_ulid": "01J8Z3K9Q2R4S6T8V0W2X4Y700",
  "name": "your-handle/orders-query",
  "version": "1",
  "scope": "your-handle",
  "owner_tenant_id": "01J8Z3K9Q2R4S6T8V0W2X4Y701",
  "visibility": 0,
  "generic_package": "acme/postgres-connector",
  "generic_version": "1.2.0",
  "generic_node_ulid": "01J8Z3K9Q2R4S6T8V0W2X4Y702",
  "output_message": "OrderSummary"
}

input_message is present instead of (or alongside) output_message when the corresponding port was bound. Errors: 400 for a missing/malformed field ({"error": "generic_package and generic_node are required"}, "name is required", "at least one of output or input must be bound", an invalid message/field identifier, a name collision in the draft tree, or binding a node that isn't kind: generic), 403 if the scope doesn't match the caller's handle, 409 {"error": "an instance with that name already exists"} on a duplicate name, and 413 {"error": "request body too large"} over the body-size cap below. Both an unknown Generic package and one the caller can't read return the identical 404 {"error": "generic package not found"} — a denial can't be used as an existence oracle for another tenant's private package.

Draft-tree limits

A request's output/input field tree — the top-level facade plus every nested message — is bounded so a request fails fast with a clear error instead of an unbounded compile:

LimitBound
Nesting depth (top-level message counts as depth 1)10
Distinct messages in one tree (both ports combined)100
Fields on any one message200
Request body size1 MiB

A name reused for two different shapes within one request 400s as a collision; the identical name and shape drafted twice (for example, the same nested message referenced from both output and input) is a harmless dedup, not an error. These same limits apply to a structured draft sent to POST /adapters/preview below.

Preview an adapter mapping

POST /adapters/preview runs a field mapping (a single-edge adapter, or a multi-source compose mapping) against sample JSON and returns the exact value it produces — the same engine (PrepareAdapter/ExecuteAdapter, CEL, transforms) the registry compiler and worker use at real compile/dispatch time, not an approximation. It backs the canvas's live mapping preview and axiom instance preview. Same auth posture as the Instance route above: served under /api, bearer API key required.

POST https://api.axiomide.com/api/adapters/preview
curl -X POST 'https://api.axiomide.com/api/adapters/preview' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "dst_msg_name": "OrderSummary",
    "sources": [
      {"edge_id": "e1", "msg_name": "Order", "sample": {"amount": 42.5}}
    ],
    "adapter": {"total": "$.amount"}
  }'

Request fields: dst_msg_name (required) names the mapping's target message. sources (required, at least one entry) is each incident edge's edge_id + msg_name + sample sample JSON payload. Exactly one of adapter (a single-edge DSL map, target field → expression) or compose_mapping (edge_adapters + compose_plan, for a multi-source compose consumer) is required; adapter requires exactly one sources entry.

Any msg_name slot — dst_msg_name or a sources[] entry — accepts either a bare registry message name (a JSON string, as above) or an inline structured draft in place of it: the identical {"message_name": ..., "fields": {...}} tree shape the Instance-creation fields block uses (including nested message-kind fields and repeated/optional modifiers). A draft is rendered into .proto text and compiled transiently for that one request only — never persisted — so a mapping can be previewed against a message that doesn't exist in the registry yet:

{
  "dst_msg_name": {"message_name": "OrderSummary", "fields": {"total": "double"}},
  "sources": [{"edge_id": "e1", "msg_name": "Order", "sample": {"amount": 42.5}}],
  "adapter": {"total": "$.amount"}
}

Response (200) is always {"output": <json>} or {"error": "<message>"}, never a 4xx for a mapping-level failure (bad expression, type mismatch, unresolvable field) — that's a property of the in-progress mapping, not a malformed request:

{"output": {"total": 42.5}}

A malformed request body, an unresolvable msg_name not found in the registry, or exceeding the draft-tree limits above is a 4xx with a plain-text body, not the {"error": ...} JSON envelope other routes on this page use.

Error envelope

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

{
  "error": "payload_too_large",
  "message": "payload exceeds hard cap: actual=18874368 max=16777216",
  "max_bytes": 16777216,
  "actual_bytes": 18874368
}
Statuserror classExtra fieldsWhen
400{"accepted": false, "error_message": "invalid request body: ..."}The request body is not valid JSON.
401unauthorizedMissing, invalid, or revoked API key.
413payload_too_largemax_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.
429rate limit exceededRetry-After: 1 headerPer-tenant rate limit exceeded.
429quota_exceededretry_after_seconds to next UTC midnight, mirrored in the Retry-After headerPer-tenant daily quota (invocations or executions) exhausted.
403tenant_suspendedYour tenant has been disabled by the operator.
503quota_unavailableretry_after_seconds: 5The quota service is unreachable; the request is rejected as a safety measure. Retry with backoff.
503upstream_unavailableretry_after_seconds: 5The flow could not be queued; a platform dependency was temporarily unavailable. Retry with backoff.
503blob_storage_unavailableretry_after_seconds: 5A 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 waited 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 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.
  • 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.