Skip to main content
Browse the documentation

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

AreaEvents
Runsrun.completed, run.failed, run.cancelled
Agentsagent.created, agent.updated, agent.deleted
Usagequota.threshold, quota.exceeded
Safetyguardrail.violated, eval.auto_rollback
Billingbilling.invoice.created, billing.payment.failed
Initiativesinitiative.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:

HeaderContents
X-UARP-Event-TypeThe event name.
X-UARP-Event-IdThe event's id. It stays the same on every retry — deduplicate on it.
X-UARP-Delivery-IdA new id for each attempt, useful for tracing a single request.
X-UARP-TimestampWhen the attempt was signed, in milliseconds since the epoch.
X-UARP-Signature-V2t=<timestamp>,v1=sha256=<hex> — the signature to verify.
X-UARP-Signaturesha256=<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

CallWhat it does
GET /api/v1/webhooksLists 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.