API Reference
The Neural Factory REST API lets you programmatically interact with the platform — send messages to agents, manage knowledge bases, and retrieve analytics data. Agent work runs as a durable run:POST /chat enqueues a run that a background worker executes, so a request can outlive any single HTTP connection. This page documents that contract end-to-end — how to start a run, the three ways to get its result, human-in-the-loop pauses, cancellation, idempotency, and webhooks.
Base URL
All API requests are made to your Neural Factory instance:Authentication
All API requests require authentication using an API key. Include it as a Bearer token in theAuthorization header:
Request format
POST /chatismultipart/form-data— it accepts file uploads, so send fields as form parts (-Fin curl), not a JSON body.- Run-control endpoints that take a body (e.g.
POST /runs/{id}/resume) acceptapplication/json. - Dates are ISO 8601 strings; IDs are strings.
The durable-run model
POST /chat enqueues a run and returns a run_id. How the initial response behaves depends on two form params, streaming and wait:
Whichever mode you choose, there are three ways to obtain the result of a run that is still executing:
- Poll
GET /runs/{run_id}until the status is terminal. - Stream
GET /runs/{run_id}/stream(Server-Sent Events). - Webhook — pass
callback_urlonPOST /chatto receive a terminal push.
A dropped connection never cancels a run. Whether you stream, poll, or disconnect, the worker finishes, saves the result, and bills exactly once. Reattach any time with the
run_id.POST /chat
Start (or continue) an agent run. Encode the body asmultipart/form-data.
Form fields
202 — run handle
Returned bywait=false, or by the default observe mode when the run outlives the observe window (still queued, running, or awaiting_input):
interrupts lists any pending human-in-the-loop requests (empty unless the run paused as awaiting_input).
A run whose task queue is briefly unreachable is returned the same way, as a queued run handle, and starts when the queue recovers. A run that cannot be started within minutes ends failed, with the reason in its error.
200 — completed result
Whenwait=true and the run finishes within the observe window, the response is the agent’s answer object (the same payload as the result field of GET /runs/{id}), plus agent_id and usage_status. A run cancelled within the window returns { "run_id", "status": "cancelled", "agent_id" }; a failed run returns 500 with the error in detail.
One active run per conversation
Achat_id may have only one active run (queued, running, or awaiting_input) at a time — the conversation’s checkpoint thread has a single writer. Posting to a chat that is already busy returns 409 with the run that holds it:
- It answers a pending clarification. If the active run is
awaiting_inputon a clarification, your message is the answer: posting it resumes that run rather than conflicting. on_conflict=enqueue. The message is parked on the active run and started as its own turn the moment that run reaches a terminal status. The response is the active run’s handle plus the parked record — so a client can send without waiting, and without polling for a gap to slot into. Up to 5 messages may be parked per run; past that you get the409.- Otherwise —
409as above. Handle it by attaching toactive_run_id(poll or stream it) and sending once it finishes.
idempotency_key re-attaches to your own existing run rather than conflicting with it.
POST /runs/{run_id}/followups with {"message": "…"} parks a message against a known run directly — the same behavior addressed by run id. It returns 409 if the run is no longer active or its queue is full, in which case send normally.
To run work in parallel, use separate chat_ids — subject to the concurrency limits.
Streaming (streaming=true)
Returns application/x-ndjson — one JSON object per line — with response headers including x-agent-run-id: <run_id>. Capture that header to reattach (poll/stream/cancel) if the connection drops.
Fire-and-forget with a webhook
Getting the result
Poll — GET /runs/
Returns the run’s public view. Poll while the status isqueued or running; stop when it is terminal, and act (don’t poll) when it is awaiting_input.
Returns
404 if the run does not exist, 403 if it is outside your org scope.
A terminal run is not removed after a fixed period. It stays readable until its chat or the owning account is deleted, or until your organization’s retention override removes it; after that the request returns 404.
Stream — GET /runs//stream
Server-Sent Events over the run’s lifecycle. Heartbeat comments arrive roughly every 15 s to keep proxies open.Last-Event-ID is tolerated but is not a replay cursor: every reconnect gets a fresh init snapshot that supersedes prior state, so clients merge idempotently. There is no missed-event gap to backfill.Webhook — callback_url
Passcallback_url (HTTPS) on POST /chat to receive a push at each terminal transition (completed, failed, cancelled, timed_out) and at each pause (awaiting_input). Delivery is exactly-once per state, best-effort with bounded retry on 5xx, and can never affect the run.
event is agent_run.awaiting_input when the run pauses (with interrupts populated), otherwise agent_run.terminal.
Verify the signature. When a webhook secret is configured, each delivery carries an HMAC over the raw request body:
HMAC-SHA256(secret, raw_body) and compare (constant-time) against the sha256= value before trusting the payload.
Idempotency
Network retries ofPOST /chat can otherwise create duplicate runs — and duplicate billing. Send a stable idempotency_key and a retried enqueue re-attaches to the original run instead of starting a new one. Use a key derived from the unit of work (e.g. a request id), not a random value per attempt.
Human-in-the-loop (HITL)
An agent can pause mid-run for a human decision. The run’s status becomesawaiting_input and its interrupts array lists what is pending. There are two ways to continue, depending on what the agent asked for:
Approvals — resume with the decision(s):
409, as does a double-submit or a resume that races a cancel. On success the run flips back to queued and re-dispatches. An optional decided_by records who decided.
Clarifications — when the agent asked a follow-up question, simply POST /chat the next message with the same chat_id; the conversation continues.
Cancelling a run
queued or awaiting_input run stops immediately; a running run receives a cooperative stop and settles to cancelled shortly after; a terminal run is unchanged.
Run status lifecycle
awaiting_input is an active status but does not count against concurrency caps — it is a human queue, not worker load. A pause left undecided for 72 hours expires and the run is cancelled.
Time limits apply to active execution only: a run accrues no time while paused for input, and a crashed-then-resumed run is not charged for the gap.
List a chat’s runs — GET /chats//runs
Returns the chat’s most recent runs (public view). Passactive=true to return only runs that are queued, running, or awaiting_input — useful to reattach to an in-flight turn.
Error handling
Error responses carry an HTTP status and adetail message:
Common status codes
Concurrency limits
Each API key is capped on concurrently active runs per user and per organization (queued + running; awaiting_input is excluded). For direct-API keys the per-user cap is attributed to the key owner, so varying name/email cannot escape it.
Exceeding a cap returns 429 Too Many Requests with a Retry-After header (seconds):