Signal HTTP Reference
A signal is how anything outside Waxell starts an agent run: your backend,
a webhook from another service, a cron job, a CI step. You POST a JSON payload
to a named signal; every agent whose manifest subscribes to that signal name
runs with the payload as its input. You then follow the run through its event
log, either by polling or over a Server-Sent Events (SSE) stream.
This page is the wire-level reference. The wax signals fire command wraps the
same endpoints. See Your first agent with wax init.
| Endpoint | Purpose |
|---|---|
POST /api/v1/signals/<name>/ | Fire a signal |
GET /api/v1/runs/<run_id>/events/ | Poll a run's event log |
GET /api/v1/runs/<run_id>/events/stream/ | Stream a run's event log (SSE) |
All three are served from https://api.waxell.dev. The URL is the same for
every tenant, and your API key identifies the tenant.
Authentication
Send a Waxell API key in either header:
X-Wax-Key: wax_sk_...
Authorization: Bearer wax_sk_...
The signal endpoint accepts both publishable (wax_pk_…) and secret
(wax_sk_…) keys, so you can give a third-party webhook a publishable key
without handing out a secret one. The run-events endpoints take the same
headers.
Fire a signal
POST /api/v1/signals/<name>/
Path
<name> is the signal name your agent subscribes to in waxell.yaml
(signals: - name: …). Names are made of letters, digits, underscores (_) and
hyphens (-). Keep the trailing slash.
Body
The request body is the payload itself: a JSON object that becomes the
run's input. Send it with Content-Type: application/json. Your agent sees its
fields directly. For a Claude Agent SDK agent, a prompt field (or the
payload's only string field) becomes the user prompt.
{ "prompt": "Summarise yesterday's failed orders", "region": "eu-west" }
Reserved payload keys
Keys that start with an underscore are reserved for Waxell. Keep your own field names free of a leading underscore.
| Key | Shape | Effect |
|---|---|---|
_sub_user_identity | object, see below | Runs the agent as one of your end users. Their identity flows into policy evaluation, the per-user audit trail, per-user secrets, and the signed headers sent to your domains. |
_callback_url | string (URL) | When the run finishes (success or failure), Waxell POSTs the result to this URL. |
_callback_secret | string | HMAC key for signing the callback body. Use it together with _callback_url. |
_agent_version | string (exact semver, e.g. "1.4.0") | Runs that version of the subscribing agent instead of the routed one. Use it to exercise a candidate version before you promote it. |
_sub_user_identity
{
"_sub_user_identity": {
"sub_user_id": "alice",
"email": "alice@acme.com",
"display_name": "Alice Ng",
"attributes": { "plan": "enterprise", "region": "eu" },
"roles": ["approver"],
"permissions": ["orders:read", "orders:refund"]
}
}
| Field | Alias | Notes |
|---|---|---|
sub_user_id | id | Required. Your stable identifier for the end user. Becomes the sub claim of the end-user token. |
email | Optional. | |
display_name | name | Optional. |
attributes | attrs | Optional free-form object. |
roles | Optional list of strings. | |
permissions | Optional list of strings (perms claim on the token). |
The first time Waxell sees a sub_user_id, it registers that end user in your
workspace. Every signal carrying the identity is then recorded on that user's
activity stream.
Run-completion callback
With _callback_url set, Waxell sends one POST when the run reaches a
terminal state:
POST <your _callback_url>
Content-Type: application/json
User-Agent: Waxell-RunCallback/1.0
X-Wax-Run-Id: 4821
X-Wax-Signal-Id: 6f1d2c1e-...
X-Wax-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with _callback_secret>
{
"run_id": "4821",
"workflow_id": "…",
"agent_name": "order_triage",
"status": "completed",
"result": { "…": "…" },
"duration": 14.2,
"started_at": "2026-10-13T09:00:01.120000+00:00",
"completed_at": "2026-10-13T09:00:15.322000+00:00",
"trace_id": "…",
"signal_id": "6f1d2c1e-..."
}
Verify the signature over the exact bytes you received, with a constant-time
comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node).
Waxell waits 5 seconds for a response and retries once on a network error or a
5xx. A 4xx is treated as final. De-duplicate on run_id.
Optional headers
| Header | Purpose |
|---|---|
X-Idempotency-Key | Exactly-once delivery. Your own key for this event (a webhook delivery id, an order number). A second POST to the same signal name with the same key within 24 hours starts nothing and returns 200 with "status": "duplicate" and the run_id of the first delivery. Keys are scoped per signal name and per workspace; 200 characters maximum. Idempotency-Key is accepted as an alias. With wax: wax signals fire <name> --payload … --idempotency-key <key>. |
X-Correlation-ID | Your own identifier for this request. Stored on the signal envelope for correlation in traces and logs. Not used for de-duplication. |
X-Waxell-Workflow-Id | Lineage. When your endpoint fires a signal while handling a domain call from a Waxell run, echo the X-Waxell-Workflow-Id header Waxell sent you. The new run is then linked to the run that caused it. |
X-Waxell-Agent-Name | Lineage. Echo alongside X-Waxell-Workflow-Id, as above. |
Response — 202 Accepted
{
"signal_id": "6f1d2c1e-8a0b-4f7e-9d55-0c3a1b2e7f10",
"status": "matched",
"signal_name": "run",
"tenant": "acme",
"run_id": "4821",
"poll_url": "/api/v1/observe/query/traces/detail/?trace_id=4821",
"events_url": "/api/v1/runs/4821/events/",
"matched_agent": "…",
"matched_framework": "claude_agent_sdk",
"matched_workflow": ""
}
| Field | Always present | Meaning |
|---|---|---|
signal_id | yes | Unique id of this signal delivery. |
status | yes | matched: an agent subscribed and a run was started (the fields below are present). accepted or queued: the signal was received, and no run id is returned, for example because no agent subscribes to this name. Check the name against your manifest's signals:. duplicate (HTTP 200): the X-Idempotency-Key was already used for this signal within 24 hours; signal_id, run_id and events_url describe the first delivery. |
idempotency_key | when sent | Echo of your X-Idempotency-Key. |
signal_name | yes | Echo of <name>. |
tenant | yes | Slug of the workspace the key belongs to. |
run_id | when matched | Id of the run. Use it with the run-events endpoints below. |
events_url | when matched | Path of the polling event log for this run. |
poll_url | when matched | Path of the full run detail (run row, spans and result). |
matched_agent | when matched | Canonical id of the agent version that is running. |
matched_framework | when matched | The agent's framework: value. See Framework support. |
matched_workflow | when matched | Workflow the subscription triggers, if any. |
When several agents subscribe to the same signal name, all of them run. The response describes the run you can follow.
Errors
| Status | Body | Cause |
|---|---|---|
401 | {"error": "API key required. …"} | No X-Wax-Key or Authorization: Bearer header. |
401 | {"error": "Invalid or expired API key"} | Key not recognised, revoked or expired. |
400 | {"error": "Invalid signal name"} | <name> contains characters other than letters, digits, _ and -. |
403 | {"error": "tenant_mismatch", "detail": "…"} | You called a tenant-scoped host (for example acme.api.waxell.dev) with a key from a different workspace. Use https://api.waxell.dev, or the host that matches your key. |
500 | {"error": "Failed to dispatch signal", "signal_id": "…"} | The signal could not be dispatched. Safe to retry. |
Example
curl -sS -X POST "https://api.waxell.dev/api/v1/signals/run/" \
-H "X-Wax-Key: $WAXELL_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: zendesk-delivery-9f3c2a" \
-H "X-Correlation-ID: support-ticket-88123" \
-d '{
"prompt": "Draft a reply to ticket 88123 and flag anything that needs a refund.",
"ticket_id": "88123",
"_sub_user_identity": {
"sub_user_id": "alice",
"email": "alice@acme.com",
"display_name": "Alice Ng",
"roles": ["support_agent"]
},
"_callback_url": "https://app.acme.com/hooks/waxell-run-complete",
"_callback_secret": "'"$WAXELL_CALLBACK_SECRET"'"
}'
Poll a run's events
GET /api/v1/runs/<run_id>/events/?since=<cursor>&limit=<n>
Each event is one span of the run: the agent itself, an LLM call, a tool call, or a progress message the agent emitted. Poll with the cursor from the previous response to receive only what's new.
| Query param | Default | Notes |
|---|---|---|
since | 0 | Return completed events with seq greater than this. Pass the previous response's next_cursor. |
limit | 200 | Maximum completed events per response. Values above 1000 are capped at 1000. |
Response — 200 OK
{
"run_id": "4821",
"status": "running",
"events": [
{
"id": "b7f0…",
"seq": 3,
"ts": "2026-10-13T09:00:02.481000+00:00",
"kind": "llm",
"name": "anthropic.messages",
"status": "ok",
"duration_ms": 1840,
"completed": true,
"message": null,
"user_facing": false,
"attributes": { "…": "…" },
"parent_id": "a12c…"
}
],
"in_flight": [
{
"id": "c9e4…",
"seq": 4,
"ts": "2026-10-13T09:00:04.310000+00:00",
"kind": "tool",
"name": "orders__lookup",
"status": "running",
"duration_ms": null,
"completed": false,
"message": null,
"user_facing": false,
"attributes": { "…": "…" },
"parent_id": "a12c…"
}
],
"next_cursor": 3,
"terminal": false
}
| Field | Meaning |
|---|---|
run_id | The run. |
status | Current run status. |
events | Newly completed events after since, in seq order. |
in_flight | Events that have started and not finished: a snapshot of what's running right now. An in-flight event appears on every poll until it completes, then moves to events. |
next_cursor | Highest seq in events. Pass it as since on the next poll. |
terminal | true once the run has finished (completed, success, failed, error or cancelled). Stop polling. |
Event fields:
| Field | Meaning |
|---|---|
id | Span id. |
seq | Position in the run, used for the cursor. |
ts | Start time (ISO 8601). |
kind | Span kind, for example agent, llm, tool, governance, or custom for progress messages. |
name | Span name: the model call, or the tool name (domain tools are <domain>__<action>). |
status | Span status. |
duration_ms | Duration once completed. |
completed | true when the span has ended. |
message | Human-readable progress text, when the span carries one. |
user_facing | true for progress messages meant to be shown to an end user. |
attributes | Full span attributes. |
parent_id | Parent span id, for building the tree. |
A run id that doesn't exist in your workspace returns 404 {"detail": "Not found"}.
Example
RUN_ID=4821
CURSOR=0
while :; do
RESP=$(curl -sS "https://api.waxell.dev/api/v1/runs/$RUN_ID/events/?since=$CURSOR&limit=200" \
-H "X-Wax-Key: $WAXELL_API_KEY")
echo "$RESP" | jq -r '.events[] | "\(.seq)\t\(.kind)\t\(.name)\t\(.message // "")"'
CURSOR=$(echo "$RESP" | jq '.next_cursor')
[ "$(echo "$RESP" | jq '.terminal')" = "true" ] && break
sleep 2
done
Stream a run's events (SSE)
GET /api/v1/runs/<run_id>/events/stream/
The same events, pushed to you as Server-Sent Events
so you don't have to poll. The response is Content-Type: text/event-stream.
| Frame | When | data |
|---|---|---|
event: snapshot | Whenever there are new completed events or open spans | {"events": [...], "in_flight": [...], "next_cursor": 12, "terminal": false, "status": "running"}. events holds only what's new since the previous snapshot. |
event: terminal | The run finished | {"status": "completed"}. The stream then closes. |
: keepalive (comment) | Every 15 seconds | No data. Keeps proxies from closing the connection. |
event: timeout | The stream has been open for 10 minutes | {"reason": "max_stream_duration"}. The stream then closes. |
A stream is capped at 10 minutes. For a longer run, reconnect, or switch to the
polling endpoint and pass the last next_cursor you received as since.
Example
curl -sS -N "https://api.waxell.dev/api/v1/runs/4821/events/stream/" \
-H "X-Wax-Key: $WAXELL_API_KEY" \
-H "Accept: text/event-stream"
event: snapshot
data: {"events": [], "in_flight": [{"id": "a12c…", "seq": 1, "kind": "agent", "name": "order_triage", "completed": false, …}], "next_cursor": 0, "terminal": false, "status": "running"}
event: snapshot
data: {"events": [{"id": "b7f0…", "seq": 3, "kind": "llm", …}], "in_flight": [{"id": "c9e4…", "seq": 4, "kind": "tool", "name": "orders__lookup", …}], "next_cursor": 3, "terminal": false, "status": "running"}
: keepalive
event: terminal
data: {"status": "completed"}
Related
- Your first agent with
wax init: fire and follow signals from the CLI - Verifying end-user tokens: what your service receives when a run carries
_sub_user_identity - Register a Domain: let agents call back into your app
- Operating Agents: schedules, replay and alerts