Automation
Webhooks
Let Snaga tell your systems when something happens — a run finished, a budget was crossed — with signed HTTP deliveries you can verify.
Polling for news wastes effort on both sides. A webhook subscription lets Snaga call your server the moment something happens: a run completes, an agent changes, a quota is crossed. Every delivery is signed, so you can be sure it came from Snaga and was not altered on the way.
Subscribe
curl -X POST https://api.snaga.ai/api/v1/webhooks \
-H "Authorization: Bearer $SNAGA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/snaga/events",
"events": ["run.completed", "run.failed"]
}'
The answer is 201 with the subscription — and its secret. Store the
secret now. It is shown in full only in this response; every later read
returns [redacted].
The URL must point at a public address: it is checked before the
subscription is saved and again before each delivery, and addresses inside
private networks are refused. https is not required, but use it anyway — a
plain http address sends every payload in the clear. Each company can hold up
to 50 subscriptions.
Events you can subscribe to
| Area | Events |
|---|---|
| Runs | run.completed, run.failed, run.cancelled |
| Agents | agent.created, agent.updated, agent.deleted |
| Usage | quota.threshold, quota.exceeded |
| Safety | guardrail.violated, eval.auto_rollback |
| Billing | billing.invoice.created, billing.payment.failed |
| Initiatives | initiative.budget_alert, initiative.budget_exceeded, initiative.objective_failed, initiative.goal_completed, initiative.paused |
Any other name fails validation. The older company.* spellings of the
initiative events are still accepted and stored under the initiative.*
name.
What a delivery looks like
Snaga sends a POST with a JSON body, at most 1 MiB:
{
"event_id": "<event_id>",
"event_type": "run.completed",
"tenant_id": "<company_id>",
"timestamp": "2026-10-19T09:00:00.000Z",
"data": { }
}
and these headers:
| Header | Contents |
|---|---|
X-UARP-Event-Type | The event name. |
X-UARP-Event-Id | The event's id. It stays the same on every retry — deduplicate on it. |
X-UARP-Delivery-Id | A new id for each attempt, useful for tracing a single request. |
X-UARP-Timestamp | When the attempt was signed, in milliseconds since the epoch. |
X-UARP-Signature-V2 | t=<timestamp>,v1=sha256=<hex> — the signature to verify. |
X-UARP-Signature | sha256=<hex> — an older form, kept for existing receivers. |
Verify the signature
The V2 signature is an HMAC-SHA256, keyed with your subscription's secret, over the timestamp, a dot, and the raw request body. Because the timestamp is inside the signature, a captured delivery cannot be replayed later: reject anything whose timestamp is more than a few minutes from your clock.
import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str, tolerance_s: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts = parts["t"]
expected = parts["v1"].removeprefix("sha256=")
signed = ts.encode() + b"." + raw_body
digest = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
fresh = abs(time.time() * 1000 - int(ts)) <= tolerance_s * 1000
return fresh and hmac.compare_digest(digest, expected)
Verify against the body exactly as received, before parsing it. The older
X-UARP-Signature is the same HMAC over the body alone, without the
timestamp; prefer V2.
Answering, retries and failures
Answer with any 2xx status, quickly: Snaga waits 10 seconds for an answer.
What happens otherwise:
5xx,408,425,429, or no answer — the delivery is retried. There are 5 attempts in total: the first try and 4 retries, with waits of 60 seconds, 5 minutes, 30 minutes and 2 hours between them. The last retry fires about 2 h 36 min after the first try.- Any other
4xx— treated as a permanent refusal and not retried. - Redirects are not followed and count as a failure.
A subscription that keeps failing moves from active to failing, and after
repeated failures to disabled, when deliveries stop. failure_count and
last_failure_at show the trend; one successful delivery resets the count.
Because a delivery can arrive more than once, make your handler idempotent:
remember the X-UARP-Event-Id values you have processed.
Test and inspect
curl -X POST https://api.snaga.ai/api/v1/webhooks/<webhook_id>/test \
-H "Authorization: Bearer $SNAGA_API_KEY"
This sends one synthetic event to your receiver. A subscription that is not
active answers 422, so a disabled endpoint never looks healthy.
GET /api/v1/webhooks/{webhookId}/deliveries is the delivery log, newest
first: each attempt with the event, its number, the status your server
returned, latency and any error. Entries are kept for seven days, and one
response shows at most 100; truncated: true means older ones exist.
Manage subscriptions
| Call | What it does |
|---|---|
GET /api/v1/webhooks | Lists subscriptions with URL, events, status and failure count. |
GET /api/v1/webhooks/{webhookId} | One subscription. |
DELETE /api/v1/webhooks/{webhookId} | Stops deliveries. Safe to repeat. |
There is no call to edit a subscription. To change its URL or events, create a new one and delete the old.
Webhooks that start an agent
The other direction exists too. An agent can open its own listening address
with its listen_webhook tool; each call to
POST /api/v1/webhooks/sensor/{webhookId} then starts a run of that agent
with the request body as payload, and answers {received: true, run_id}
without waiting. When the listener has a secret, the caller must send
X-Sensor-Signature: sha256=<hex HMAC-SHA256 of the raw body>, or the call
is refused with 401.
Every field is in the API reference.