Agents API
Create, drive, and observe cloud agents over REST — the same API that powers Outgate chat.
Overview
Everything chat does, your code can do: one REST surface for the whole agent platform.
The Agents API exposes the agent platform at https://api.outgate.ai under /api/v1/agents. It is not a side door with a subset of features — Outgate chat is built on these same endpoints, so anything you can do in the UI you can automate.
POST /api/v1/agents/threads create an agent
POST .../threads/{id}/events send a message
GET .../threads/{id}/events read what happened
GET <region events url>?wait=25 follow live output (long-poll)
POST /api/v1/agents/webhooks get notified without pollingThe typical integration is small: create a thread, send input, follow events until the turn completes, read the result. Webhooks replace polling for fire-and-forget automation.
Authentication and scopes
API keys with explicit read/write scopes; one consistent error envelope.
Create an API key in the console. Keys are prefixed gw_ and are shown once at creation. Send the key as a bearer token on every request.
curl https://api.outgate.ai/api/v1/agents/regions \
-H "Authorization: Bearer gw_your_key"| Scope | Grants |
|---|---|
| outgate.agents.read | List and inspect threads, read event history, list models, configs, and regions. |
| outgate.agents.write | Create, drive, and delete threads; manage workspaces, webhooks, and provider sign-in. |
Every error, on every agents endpoint, uses the same envelope: an error object with a stable machine-readable code and a human-readable message. Match on the code, not the message.
{
"error": {
"code": "thread_not_found",
"message": "No thread with this id exists in your organization."
}
}Requests are rate limited per organization according to your plan; responses include X-RateLimit headers so clients can pace themselves.
Creating a thread
Creation is asynchronous: you get a thread immediately, the sandbox becomes live seconds later.
First discover what your organization can run: GET /api/v1/agents/configs lists the available agent configurations (each pins an agent type, region, and sandbox resources), and GET /api/v1/agents/regions lists regions. Then create the thread.
curl -X POST https://api.outgate.ai/api/v1/agents/threads \
-H "Authorization: Bearer gw_your_key" \
-H "Content-Type: application/json" \
-d '{
"agentConfigId": "acfg-example",
"metadata": { "ticket": "OPS-1423" }
}'{
"threadId": "6f9d2c1e-...",
"status": "starting",
"tool": "claude",
"regionId": "de-west-1",
"ephemeral": true,
"expiresAt": null,
"events": {
"url": "https://<region>/v1/agents/threads/6f9d2c1e-.../events",
"token": "<consumer token>",
"expiresAt": "2026-07-11T10:00:00Z"
}
}The response is immediate with status starting; sandbox provisioning continues in the background. Poll GET /api/v1/agents/threads/{threadId} until status is live, or watch the events URL — a lifecycle event reports spawned or spawn_failed, so failures are explicit rather than silent.
| Field | Meaning |
|---|---|
| agentConfigId | Required. Which agent configuration to run (from GET /configs). |
| regionId | Optional. Defaults to the configuration’s region. |
| workspaceId | Optional. Attach a persistent workspace volume. Omit it for an ephemeral thread with scratch-only storage. |
| ttlSeconds | Optional bounded thread lifetime in seconds: 0 (never expires), 60, 300, 3600, 28800, 86400, or 2592000. Omit it to use the region default — threads on Outgate-managed regions are long-lived by default, so expiresAt comes back null. |
| idleTimeoutSeconds | Optional. How long the sandbox stays up without activity before sleeping (60–86400). |
| permissionMode | ask (agent requests approval for tools) or skip (autonomous). |
| effort | Reasoning effort: low, medium, high, xhigh, or max. |
| metadata | Up to 4 KB of your own JSON, stored on the thread and returned on reads. |
Threads can be updated after creation: PATCH /api/v1/agents/threads/{threadId} changes model, effort, or permission mode (the sandbox restarts in place with the conversation intact), POST .../resume wakes an idle thread explicitly, and DELETE tears it down.
Sending input
Post events to the thread: text, interrupts, permission answers, titles.
curl -X POST https://api.outgate.ai/api/v1/agents/threads/{threadId}/events \
-H "Authorization: Bearer gw_your_key" \
-H "Content-Type: application/json" \
-d '{
"kind": "input.text",
"payload": { "text": "Run the test suite and summarize the failures." },
"correlation_id": "my-req-42"
}'| Kind | Payload | Effect |
|---|---|---|
| input.text | { "text": "..." } | A user message. Wakes the sandbox if it is idle. |
| interrupt | {} | Stops the agent’s current turn. |
| permission_response | { "granted": true } | Answers a pending permission_request. |
| thread.title | { "title": "..." } | Renames the thread. |
The response is the appended event’s sequence number and timestamp. If you pass a correlation_id (up to 128 characters), it is echoed on the canonical event so optimistic UIs can reconcile their local copy with the stream.
Only these consumer kinds are accepted; server-generated kinds are rejected with a forbidden_kind error, so a buggy client cannot forge agent output.
Reading events
The full thread history is an ordered, filterable event log.
Everything that happens on a thread — user input, agent text, tool calls, lifecycle transitions — is an event with a monotonically increasing sequence number. GET /api/v1/agents/threads/{threadId}/events reads the log.
| Query parameter | Meaning |
|---|---|
| since / until | Sequence-number bounds for the page. |
| limit | Maximum events to return; nextCursor continues the page. |
| kinds | Comma-separated filter, e.g. kinds=text_delta,turn_complete. |
| reverse=true | Newest first — combine with limit to fetch the latest activity. |
| from / to | Timestamp bounds as ISO 8601. |
Fetching the latest events
The log is returned oldest-first by default. To show "what just happened", use reverse=true with a limit and re-order client-side — do not page from sequence zero on every refresh.
{
"events": [
{ "seq": 41, "ts": "2026-07-11T09:14:03Z", "kind": "input.text",
"payload": { "text": "Run the test suite..." } },
{ "seq": 42, "ts": "2026-07-11T09:14:09Z", "kind": "text_delta",
"payload": { "text": "Running pytest..." } },
{ "seq": 57, "ts": "2026-07-11T09:15:40Z", "kind": "turn_complete",
"payload": {} }
],
"nextCursor": null,
"thread": { "id": "6f9d2c1e-...", "maxSeq": 57 }
}Following live output
Long-poll the region-direct events endpoint for low-latency streaming.
For live output, use the region-direct events URL returned when you created the thread. It serves the same event log but supports a wait parameter: the request blocks until new events arrive or the window elapses, giving you sub-second delivery without a websocket.
# events.url and events.token come from thread creation
curl "$EVENTS_URL?since=$LAST_SEQ&wait=25" \
-H "Authorization: Bearer $EVENTS_TOKEN"- wait accepts up to 25 seconds. An empty events array after the window is normal — just re-issue the request with the same cursor.
- The events token is scoped to this one thread and expires; POST /api/v1/agents/threads/{threadId}/events-token rotates it (the old token is revoked).
- On a 401 from the region endpoint, rotate the token and retry — this is the standard recovery path, not an error state.
The loop is: request with your last seen sequence number and wait=25, apply whatever comes back, update the cursor, repeat. Combined with async creation this gives you a fully event-driven client with two moving parts.
Webhooks
Subscribe once, get signed notifications for the moments that matter.
If you do not want to hold a long-poll open, subscribe your endpoint to coarse thread events. Outgate posts a signed delivery for each occurrence across all threads in your organization.
curl -X POST https://api.outgate.ai/api/v1/agents/webhooks \
-H "Authorization: Bearer gw_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://ops.example.com/hooks/outgate",
"events": ["lifecycle", "error", "turn_complete", "permission_request"]
}'The response includes a whsec_ signing secret — shown once, never retrievable later. Subscribable kinds are lifecycle, error, turn_complete, permission_request, and the app run kinds app.run and app.run_result. An organization can hold up to 10 subscriptions.
Each delivery carries the event kind in X-Outgate-Event and an HMAC-SHA256 signature of the raw body in X-Outgate-Signature. Verify before trusting:
import crypto from 'node:crypto';
function verify(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac('sha256', secret)
.update(rawBody).digest('hex');
const got = signatureHeader.replace(/^sha256=/, '');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}- Deliveries time out after 5 seconds and are retried once on network errors or 5xx — make your handler fast and idempotent.
- A failing endpoint never blocks the agent or other subscriptions.
- Answer permission_request deliveries by posting a permission_response event to the thread — that closes the loop for fully autonomous approval policies.
Models and provider sign-in
Inspect the live model catalog and complete provider sign-in programmatically.
GET /api/v1/agents/threads/{threadId}/models returns the model catalog as the sandbox actually sees it — the models your provider account offers, refreshed from the provider. Pair it with PATCH on the thread to switch models.
Provider sign-in — the Connect Claude / Connect ChatGPT flow from chat — is also exposed: POST .../login starts the flow and returns the URL the user must approve; POST .../login/code submits the resulting code; GET .../auth reports whether the thread is signed in; POST .../logout clears the session. This lets you embed agent onboarding in your own product without sending users to Outgate chat.
Files and git
Inspect the workspace and run git operations against real repositories.
GET /api/v1/agents/threads/{threadId}/files lists workspace contents (pass path to descend into directories). It is the same view as the Files tab in chat.
POST /api/v1/agents/threads/{threadId}/git runs a git command inside the sandbox:
{
"cmd": "clone",
"args": ["https://github.com/acme/service.git", "."],
"auth": { "token": "<token for private repos>" }
}The response carries stdout, stderr, and the exit code. Tokens passed this way are used for the single operation and are not persisted in the sandbox. For GitHub repositories covered by the Outgate GitHub App installation, agents get short-lived scoped tokens automatically and no token needs to be passed at all.
Workspaces
Create and manage the persistent volumes threads mount at /workspace.
Workspaces are named persistent volumes that live in a region. Create one, then pass its ID as workspaceId when creating threads — every thread that mounts it sees the same /workspace contents. Omit workspaceId to get an ephemeral thread instead.
# workspace routes are addressed per region
curl -X POST https://api.outgate.ai/api/v1/agents/workspaces \
-H "Authorization: Bearer gw_your_key" \
-H "X-Region-Id: de-west-1" \
-H "Content-Type: application/json" \
-d '{ "workspaceId": "ci-nightly" }'- Workspace IDs are lowercase slugs (letters, digits, dashes, underscores; up to 64 characters).
- GET /api/v1/agents/workspaces/{workspaceId} reports current size in bytes.
- DELETE removes the volume and its contents — threads currently mounting it should be ended first.
- All workspace routes require the X-Region-Id header, because volumes are physical resources in one region.
Route reference
The complete v1 surface at a glance.
| Method | Path | Scope |
|---|---|---|
| POST | /api/v1/agents/threads | write |
| GET | /api/v1/agents/threads | read |
| GET | /api/v1/agents/threads/{id} | read |
| PATCH | /api/v1/agents/threads/{id} | write |
| DELETE | /api/v1/agents/threads/{id} | write |
| POST | /api/v1/agents/threads/{id}/resume | write |
| POST | /api/v1/agents/threads/{id}/events | write |
| GET | /api/v1/agents/threads/{id}/events | read |
| POST | /api/v1/agents/threads/{id}/events-token | write |
| GET | /api/v1/agents/threads/{id}/models | read |
| POST | /api/v1/agents/threads/{id}/login | write |
| POST | /api/v1/agents/threads/{id}/login/code | write |
| POST | /api/v1/agents/threads/{id}/logout | write |
| GET | /api/v1/agents/threads/{id}/auth | read |
| GET | /api/v1/agents/threads/{id}/files | read |
| POST | /api/v1/agents/threads/{id}/git | write |
| GET | /api/v1/agents/threads/{id}/apps | read |
| GET | /api/v1/agents/threads/{id}/apps/{appId} | read |
| POST | /api/v1/agents/threads/{id}/apps/{appId}/run | write |
| POST | /api/v1/agents/threads/{id}/apps/{appId}/unpublish | write |
| PATCH | /api/v1/agents/threads/{id}/apps/{appId} | write |
| DELETE | /api/v1/agents/threads/{id}/apps/{appId} | write |
| GET | /api/v1/agents/threads/{id}/apps/{appId}/secrets | read |
| PUT | /api/v1/agents/threads/{id}/apps/{appId}/secrets/{name} | write |
| DELETE | /api/v1/agents/threads/{id}/apps/{appId}/secrets/{name} | write |
| GET | /api/v1/agents/threads/{id}/apps/{appId}/webhooks | read |
| POST | /api/v1/agents/workspaces | write |
| GET | /api/v1/agents/workspaces/{id} | read |
| DELETE | /api/v1/agents/workspaces/{id} | write |
| GET | /api/v1/agents/configs | read |
| GET | /api/v1/agents/regions | read |
| POST | /api/v1/agents/webhooks | write |
| GET | /api/v1/agents/webhooks | read |
| DELETE | /api/v1/agents/webhooks/{id} | write |
| POST | /api/v1/hooks/apps/{appId} | hook token |
Errors and statuses
Stable machine-readable codes for every failure mode.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed body, missing field, or invalid enum value; details lists the fields. |
| 400 | forbidden_kind | The event kind is server-generated and cannot be posted by consumers. |
| 401 | unauthorized | Missing, invalid, or revoked credentials. |
| 403 | forbidden | The key is valid but lacks the required scope. |
| 404 | thread_not_found | No such thread in your organization (cross-org access also reads as 404). |
| 409 | thread_lost | The sandbox no longer exists; create a new thread. |
| 409 | run_in_flight | An app run is already executing on this thread. |
| 502 | region_unavailable | The region did not accept the command; retry with backoff. |
| 504 | region_timeout | The region did not answer within the command window; retry with backoff. |
| Thread status | Meaning |
|---|---|
| starting | Sandbox provisioning in progress; input is not yet accepted. |
| live | Agent is running and accepting input. |
| idle | Sandbox is asleep; state is intact and the next message wakes it. |
| ended | The thread expired or was deleted; its sandbox is gone. |