Missions.

The REST Missions API is the most direct way to drive a Houston agent. Create a mission, then poll it, stream it, or wait for a webhook. All paths are on https://gateway.gethouston.ai and take Authorization: Bearer hst_....

:slug is the agent's id from GET /agents, 16 hex characters such as 3f9a1c07b2e84d65. Mission ids are 32 hex characters. Every request acts in one organization: your personal organization by default, or the organization named by the x-houston-org header (see Organizations).

Create a mission

POST /v1/agents/:slug/missions

Starts a mission and returns 202 Accepted right away. The agent runs in the background; use the id in the response to track it.

Request body

FieldTypeDescription
input requiredstringThe instruction for the agent, in natural language.
mode optionalstringOne of execute, plan, or auto. Defaults to auto (the agent decides whether to plan first or act directly).
model optionalstringOverride the model for this mission. Defaults to the agent's configured model.
effort optionalstringReasoning effort for the mission. Defaults to the agent's setting.
title optionalstringA label for the mission, shown in the Houston app. Auto-generated if omitted.
webhook optionalobjectDelivers a callback when the mission finishes, signed if you set a secret. See Webhooks.
webhook.urlstringPublic https:// URL to POST to. Private and internal addresses are rejected with 400. Required when webhook is present.
webhook.secretstringSigning secret used to compute the X-Houston-Signature HMAC. Without a secret, deliveries are unsigned.
curl -X POST https://gateway.gethouston.ai/v1/agents/3f9a1c07b2e84d65/missions \
  -H "Authorization: Bearer hst_9f2c1a7b4e8d0364f1a2b3c4d5e6f7089f2c1a7b4e8d0364f1a2b3c4d5e6f708" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Draft the weekly competitor summary for the Lisbon property.",
    "mode": "auto",
    "model": "claude-opus-4-5",
    "title": "Weekly competitor summary",
    "webhook": {
      "url": "https://api.acme.com/hooks/houston",
      "secret": "whsec_a1b2c3d4e5f6"
    }
  }'

202 Accepted

{
  "id": "a7f3c91e0b2d4856e19c3f70b4a2d8e5",
  "agentSlug": "3f9a1c07b2e84d65",
  "conversationId": "5d0e8b3a6c1f4927b8e2a4c6d9f01b37",
  "status": "running",
  "createdAt": "2026-07-12T09:31:07.482Z"
}

An agent runs one turn at a time. If it is already working, for example on another mission or a chat, you get 409 Conflict; retry once the current work finishes. Other failures are listed under Errors.

{ "error": "the agent is busy with another turn", "code": "agent_busy" }

List missions

GET /v1/agents/:slug/missions

Returns the agent's missions, newest first.

QueryTypeDescription
limit optionalintegerHow many to return. Defaults to 50. Values above 200 are capped at 200.
curl "https://gateway.gethouston.ai/v1/agents/3f9a1c07b2e84d65/missions?limit=20" \
  -H "Authorization: Bearer hst_9f2c1a7b4e8d0364f1a2b3c4d5e6f7089f2c1a7b4e8d0364f1a2b3c4d5e6f708"
{
  "missions": [
    {
      "id": "a7f3c91e0b2d4856e19c3f70b4a2d8e5",
      "agentSlug": "3f9a1c07b2e84d65",
      "conversationId": "5d0e8b3a6c1f4927b8e2a4c6d9f01b37",
      "status": "completed",
      "result": "Weekly competitor summary is ready...",
      "createdAt": "2026-07-12T09:31:07.482Z",
      "completedAt": "2026-07-12T09:34:51.903Z"
    }
  ]
}

Get a mission

GET /v1/agents/:slug/missions/:id

Returns the mission's current state. status is one of running, completed, failed, or canceled. Once the status is terminal, result carries the agent's final text, error describes a failure, and completedAt records when it ended. These three fields are left out while they are empty.

If the mission was created with a webhook, this endpoint (and only this one) adds a webhook block. It always has url, deliveredAt (null until a delivery succeeds), and lastError (the error from the last failed attempt, set once Houston gives up, and an empty string until then). The secret is never returned.

curl https://gateway.gethouston.ai/v1/agents/3f9a1c07b2e84d65/missions/a7f3c91e0b2d4856e19c3f70b4a2d8e5 \
  -H "Authorization: Bearer hst_9f2c1a7b4e8d0364f1a2b3c4d5e6f7089f2c1a7b4e8d0364f1a2b3c4d5e6f708"
{
  "id": "a7f3c91e0b2d4856e19c3f70b4a2d8e5",
  "agentSlug": "3f9a1c07b2e84d65",
  "conversationId": "5d0e8b3a6c1f4927b8e2a4c6d9f01b37",
  "status": "completed",
  "result": "Weekly competitor summary is ready. Three properties moved rates...",
  "createdAt": "2026-07-12T09:31:07.482Z",
  "completedAt": "2026-07-12T09:34:51.903Z",
  "webhook": {
    "url": "https://api.acme.com/hooks/houston",
    "deliveredAt": "2026-07-12T09:34:52.140Z",
    "lastError": ""
  }
}

Stream events (SSE)

GET /v1/agents/:slug/missions/:id/events

Streams the mission's progress as Server-Sent Events and closes when the mission reaches a terminal state. Each event has an id. If the connection drops, reconnect and send the Last-Event-ID header to resume without gaps (native SSE clients do this for you), or pass the last id you saw as ?after=<id>. The header wins when both are set.

curl -N https://gateway.gethouston.ai/v1/agents/3f9a1c07b2e84d65/missions/a7f3c91e0b2d4856e19c3f70b4a2d8e5/events \
  -H "Authorization: Bearer hst_9f2c1a7b4e8d0364f1a2b3c4d5e6f7089f2c1a7b4e8d0364f1a2b3c4d5e6f708"
Browser (EventSource)

EventSource cannot send an Authorization header, so this endpoint also accepts the key as a query parameter: ?token=hst_.... It cannot send x-houston-org either, so pass the organization as ?org=<organization id>. Keep that URL out of logs.

Cancel a mission

POST /v1/agents/:slug/missions/:id/cancel

Cancels a running mission and answers 200 OK with the mission JSON, now with status canceled. A webhook, if set, fires with mission.canceled. If the mission has already reached a terminal state, you get 409 Conflict:

{ "error": "mission already finished", "code": "mission_finished" }

List agents

GET /agents

Lists the agents in the organization the request acts in. Each agent's id is the slug you pass to the mission endpoints. In a team organization, members other than the owner see only the agents assigned to them.

curl https://gateway.gethouston.ai/agents \
  -H "Authorization: Bearer hst_9f2c1a7b4e8d0364f1a2b3c4d5e6f7089f2c1a7b4e8d0364f1a2b3c4d5e6f708"

The response is a bare JSON array:

[
  {
    "id": "3f9a1c07b2e84d65",
    "name": "Revenue Manager",
    "createdAt": 1780401067482,
    ...
  }
]

createdAt is milliseconds since the Unix epoch. Items also carry fields the Houston app uses, such as workspaceId, role, assigned, and access. You only need id.

API-key scope & limits

API keys are deliberately narrow. A key works only on the programmatic surfaces: the Missions endpoints, A2A, MCP, and GET /agents. Point it at any other part of the gateway and you get 403 Forbidden:

{ "error": "api keys are limited to the public API", "code": "api_key_scope" }

That includes /v1/keys: a key cannot list, create, or revoke keys. You manage keys in the Houston app under Settings → API keys, where you can hold up to 20 active keys. Revoke an old one to make room for a new one.

A key belongs to you, not to an organization. It reaches every organization you are a member of, one organization per request, chosen with the x-houston-org header. See Organizations.

Errors

Errors are JSON with an error message. Most also carry a stable code; match on code when it is present, and on the status otherwise.

StatusCodeWhen
400The request is invalid. The error message says why, for example missing 'input', invalid 'mode' (want execute|plan|auto), or a rejected webhook URL.
401The key is missing, revoked, or wrong: missing bearer token or invalid or expired token.
403api_key_scopeThe key was used on a route outside the public API.
403not_memberx-houston-org (or ?org=) names an organization you are not a member of.
403needs_upgradeThe team organization's plan has lapsed, so it refuses new work until an owner renews it.
404agent not found or mission not found. A mission that belongs to another agent or another organization also reads as not found.
409agent_busyThe agent is already running a turn. Retry once it finishes.
409mission_finishedCancel was called on a mission that already ended.
409no_providerNo AI provider is connected, so the agent cannot run. Connect one in the Houston app, then retry.
413The request body is over 1 MiB.
429message_limitYour plan's message limit is reached. The body includes limit and resetsAt, and the Retry-After header gives the seconds until the reset.
502engine unavailable: the agent's engine refused or failed the request. A detail field says more.
503pool_unavailableThe mission could not start right now and nothing ran. Retry after the Retry-After header.

Webhooks

Pass a webhook object when you create a mission and Houston will POST a payload to your URL as soon as the mission finishes, signed when you set a secret. This is the recommended pattern for servers: no polling, no open connections.

Payload

{
  "event": "mission.completed",
  "mission": {
    "id": "a7f3c91e0b2d4856e19c3f70b4a2d8e5",
    "agentSlug": "3f9a1c07b2e84d65",
    "orgSlug": "8c41e7a0d39b2f16",
    "conversationId": "5d0e8b3a6c1f4927b8e2a4c6d9f01b37",
    "status": "completed",
    "result": "Weekly competitor summary is ready...",
    "createdAt": "2026-07-12T09:31:07.482Z",
    "completedAt": "2026-07-12T09:34:51.903Z"
  }
}

event is one of mission.completed, mission.failed, or mission.canceled. mission has the same fields as Get a mission, without the webhook block, plus orgSlug: the organization ID the mission ran in. error and completedAt appear when they are set.

Headers

HeaderValue
X-Houston-EventThe event name, e.g. mission.completed.
X-Houston-DeliveryAn id for this delivery, the same on every retry of it. A redelivery after a Houston restart, or the webhook for a later turn of the same mission, gets a new id.
X-Houston-Signaturesha256=<hex>, the HMAC-SHA256 of the raw request body keyed with your webhook.secret. Sent only when the mission has a secret.

Delivery & reliability

Verify the signature

Compute the HMAC over the raw request body (not a re-serialized object) and compare in constant time:

import crypto from "node:crypto";

// `rawBody` is the exact bytes Houston sent (a Buffer or string).
// In Express, capture it with express.raw({ type: "application/json" }).
function verifyHoustonWebhook(rawBody, signatureHeader, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(signatureHeader ?? "");
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/hooks/houston", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyHoustonWebhook(
    req.body,
    req.get("X-Houston-Signature"),
    process.env.HOUSTON_WEBHOOK_SECRET,
  );
  if (!ok) return res.sendStatus(401);

  // Deduplicate on mission id + event, then process.
  const payload = JSON.parse(req.body.toString("utf8"));
  const dedupeKey = `${payload.mission.id}:${payload.event}`;
  // ... handle payload.event / payload.mission ...
  res.sendStatus(200);
});
Sign over raw bytes

Frameworks that auto-parse JSON change the byte stream. Verify against the untouched body Houston sent, or the HMAC will not match even with the right secret.