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
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
| Field | Type | Description |
|---|---|---|
input required | string | The instruction for the agent, in natural language. |
mode optional | string | One of execute, plan, or auto. Defaults to auto (the agent decides whether to plan first or act directly). |
model optional | string | Override the model for this mission. Defaults to the agent's configured model. |
effort optional | string | Reasoning effort for the mission. Defaults to the agent's setting. |
title optional | string | A label for the mission, shown in the Houston app. Auto-generated if omitted. |
webhook optional | object | Delivers a callback when the mission finishes, signed if you set a secret. See Webhooks. |
webhook.url | string | Public https:// URL to POST to. Private and internal addresses are rejected with 400. Required when webhook is present. |
webhook.secret | string | Signing 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
Returns the agent's missions, newest first.
| Query | Type | Description |
|---|---|---|
limit optional | integer | How 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
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)
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"
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
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
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.
| Status | Code | When |
|---|---|---|
400 | The request is invalid. The error message says why, for example missing 'input', invalid 'mode' (want execute|plan|auto), or a rejected webhook URL. | |
401 | The key is missing, revoked, or wrong: missing bearer token or invalid or expired token. | |
403 | api_key_scope | The key was used on a route outside the public API. |
403 | not_member | x-houston-org (or ?org=) names an organization you are not a member of. |
403 | needs_upgrade | The team organization's plan has lapsed, so it refuses new work until an owner renews it. |
404 | agent not found or mission not found. A mission that belongs to another agent or another organization also reads as not found. | |
409 | agent_busy | The agent is already running a turn. Retry once it finishes. |
409 | mission_finished | Cancel was called on a mission that already ended. |
409 | no_provider | No AI provider is connected, so the agent cannot run. Connect one in the Houston app, then retry. |
413 | The request body is over 1 MiB. | |
429 | message_limit | Your plan's message limit is reached. The body includes limit and resetsAt, and the Retry-After header gives the seconds until the reset. |
502 | engine unavailable: the agent's engine refused or failed the request. A detail field says more. | |
503 | pool_unavailable | The 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
| Header | Value |
|---|---|
X-Houston-Event | The event name, e.g. mission.completed. |
X-Houston-Delivery | An 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-Signature | sha256=<hex>, the HMAC-SHA256 of the raw request body keyed with your webhook.secret. Sent only when the mission has a secret. |
Delivery & reliability
- Houston waits up to 10 seconds for your endpoint to respond.
- Delivery is attempted up to 6 times: the initial POST plus 5 retries, backing off 30s, 1m, 2m, 4m, then 8m.
- Delivery is at-least-once. Retries can duplicate an event, so treat handlers as idempotent. Deduplicate on
mission.idplusevent, and useX-Houston-Deliveryto spot plain retries. - The URL must be a public
https://address. Private, loopback, and internal addresses are rejected.
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);
});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.