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 MarkdownThis 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/invokeEvery 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:
| 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).
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
| 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; 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 waiting 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. |
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. |
idempotency_key | string | no | Dedups 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 withwait: true.result.outputis the terminal node's output message decoded to JSON (when you invoked withinput);result.payloadcarries base64-encoded protobuf bytes instead when you invoked withpayloador when JSON decoding was not possible.result.error_message— present withwait: truewhen the execution was accepted and delivered but ran to a failure (result.successisfalse), for example because the input did not match the flow's start-node schema. The same diagnostic is also copied to the top-levelerror_messageso a caller can read it without descending intoresult. 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:
| 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.
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) orPOST /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
| Field | Type | Required | Meaning |
|---|---|---|---|
graph_id | string | yes | The 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
| Status | error class | When |
|---|---|---|
| 400 | invalid_request | graph_id is missing or doesn't match [A-Za-z0-9_-]+. |
| 403 | forbidden | The authenticated tenant does not own graph_id. |
| 404 | not_found | graph_id doesn't resolve to any flow. |
| 503 | warm_unavailable | The 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/clientsAuthentication 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"}.
| Method | Path | Description |
|---|---|---|
POST | /v1/clients | Create a client. |
GET | /v1/clients | List 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}/members | Add one member (a node or a flow). |
PUT | /v1/clients/{id}/members | Replace the full member set in one call. |
DELETE | /v1/clients/{id}/members/{memberId} | Remove one member. |
POST | /v1/clients/{id}/build | Start a build: a new version, or additional languages on an existing one. |
GET | /v1/clients/{id}/versions | List 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.zipBoth 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/instancesCreate 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:
| Limit | Bound |
|---|---|
| Nesting depth (top-level message counts as depth 1) | 10 |
| Distinct messages in one tree (both ports combined) | 100 |
| Fields on any one message | 200 |
| Request body size | 1 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/previewcurl -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
}| 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 (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 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, andgraph_idis 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). OnePOST /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 atGET /api/packages/{name}@{version}/docs.