Skip to main content

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:
For enterprise deployments, replace with your custom domain.

Authentication

All API requests require authentication using an API key. Include it as a Bearer token in the Authorization header:
API keys are created and managed in the API Keys section of the admin panel. Keys are organization-scoped. A key can only reach agents and runs belonging to its organization; a key with no organization can reach only resources that also have none. Every request is also subject to per-user and per-organization concurrency caps (see Concurrency limits).

Request format

  • POST /chat is multipart/form-data — it accepts file uploads, so send fields as form parts (-F in curl), not a JSON body.
  • Run-control endpoints that take a body (e.g. POST /runs/{id}/resume) accept application/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:
  1. Poll GET /runs/{run_id} until the status is terminal.
  2. Stream GET /runs/{run_id}/stream (Server-Sent Events).
  3. Webhook — pass callback_url on POST /chat to 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 as multipart/form-data.

Form fields

202 — run handle

Returned by wait=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

When wait=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

A chat_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:
A message arriving into a busy chat is resolved in a fixed order:
  1. It answers a pending clarification. If the active run is awaiting_input on a clarification, your message is the answer: posting it resumes that run rather than conflicting.
  2. 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 the 409.
  3. Otherwise — 409 as above. Handle it by attaching to active_run_id (poll or stream it) and sending once it finishes.
A matching 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 is queued 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

Pass callback_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:
Recompute HMAC-SHA256(secret, raw_body) and compare (constant-time) against the sha256= value before trusting the payload.

Idempotency

Network retries of POST /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 becomes awaiting_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):
The body must decide exactly the currently pending interrupt ids — a partial or mismatched set returns 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

Cancellation is idempotent and always returns the run’s public view. A 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). Pass active=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 a detail 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):
Back off for the indicated interval, or wait for an in-flight run to finish, before retrying.