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 polling

The 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"
ScopeGrants
outgate.agents.readList and inspect threads, read event history, list models, configs, and regions.
outgate.agents.writeCreate, 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.

FieldMeaning
agentConfigIdRequired. Which agent configuration to run (from GET /configs).
regionIdOptional. Defaults to the configuration’s region.
workspaceIdOptional. Attach a persistent workspace volume. Omit it for an ephemeral thread with scratch-only storage.
ttlSecondsOptional 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.
idleTimeoutSecondsOptional. How long the sandbox stays up without activity before sleeping (60–86400).
permissionModeask (agent requests approval for tools) or skip (autonomous).
effortReasoning effort: low, medium, high, xhigh, or max.
metadataUp 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"
  }'
KindPayloadEffect
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 parameterMeaning
since / untilSequence-number bounds for the page.
limitMaximum events to return; nextCursor continues the page.
kindsComma-separated filter, e.g. kinds=text_delta,turn_complete.
reverse=trueNewest first — combine with limit to fetch the latest activity.
from / toTimestamp 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.

MethodPathScope
POST/api/v1/agents/threadswrite
GET/api/v1/agents/threadsread
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}/resumewrite
POST/api/v1/agents/threads/{id}/eventswrite
GET/api/v1/agents/threads/{id}/eventsread
POST/api/v1/agents/threads/{id}/events-tokenwrite
GET/api/v1/agents/threads/{id}/modelsread
POST/api/v1/agents/threads/{id}/loginwrite
POST/api/v1/agents/threads/{id}/login/codewrite
POST/api/v1/agents/threads/{id}/logoutwrite
GET/api/v1/agents/threads/{id}/authread
GET/api/v1/agents/threads/{id}/filesread
POST/api/v1/agents/threads/{id}/gitwrite
GET/api/v1/agents/threads/{id}/appsread
GET/api/v1/agents/threads/{id}/apps/{appId}read
POST/api/v1/agents/threads/{id}/apps/{appId}/runwrite
POST/api/v1/agents/threads/{id}/apps/{appId}/unpublishwrite
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}/secretsread
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}/webhooksread
POST/api/v1/agents/workspaceswrite
GET/api/v1/agents/workspaces/{id}read
DELETE/api/v1/agents/workspaces/{id}write
GET/api/v1/agents/configsread
GET/api/v1/agents/regionsread
POST/api/v1/agents/webhookswrite
GET/api/v1/agents/webhooksread
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.

HTTPCodeMeaning
400invalid_requestMalformed body, missing field, or invalid enum value; details lists the fields.
400forbidden_kindThe event kind is server-generated and cannot be posted by consumers.
401unauthorizedMissing, invalid, or revoked credentials.
403forbiddenThe key is valid but lacks the required scope.
404thread_not_foundNo such thread in your organization (cross-org access also reads as 404).
409thread_lostThe sandbox no longer exists; create a new thread.
409run_in_flightAn app run is already executing on this thread.
502region_unavailableThe region did not accept the command; retry with backoff.
504region_timeoutThe region did not answer within the command window; retry with backoff.
Thread statusMeaning
startingSandbox provisioning in progress; input is not yet accepted.
liveAgent is running and accepting input.
idleSandbox is asleep; state is intact and the next message wakes it.
endedThe thread expired or was deleted; its sandbox is gone.