Webhooks

Introduction

Webhooks are HTTPS destinations that receive a signed POST when an async job reaches a terminal status (completed, failed, or expired). Create them under Webhooks, then attach one or more to a source on Sources → Concurrency.

A source needs at least one active destination to run async. With none attached, the source serves requests synchronously instead. Callers can also poll the job URL with source auth; see Async for statuses and TTL.

Async admission is plan-gated (Team and Business). See Billing.

Create a webhook

  1. Open Webhooks → Add webhook.
  2. Name it and set an https:// URL.
  3. Copy the signing secret. It is shown once on create (and again on rotate).

Private, loopback, and link-local destinations are rejected.

An assistant on the MCP server can do the same with create_webhook (optional source_id to attach and enable async). Scripts use POST /api/v1/webhooks on the Control API. Both return the signing secret once. They cannot point a destination at loopback or private URLs.

Attach to a source

On the source Concurrency tab, enable async and select webhook destinations. Save webhook attachments separately from the rest of the concurrency form.

The same webhook can attach to many sources. Edit the URL once and every attached source picks it up within a few seconds.

From MCP, set_source_webhooks replaces the destinations on a granted source and turns async on if it is still off. From the Control API, PUT /api/v1/sources/{subdomain}/webhooks does the same. The dashboard does not turn async on by itself when you only change attachments.

Details and delivery history

Open a webhook from the list to see status, success and failure totals, consecutive failures, attached sources, and recent delivery attempts (time, job id, source, result, HTTP status or error, attempt, latency).

Success and timeouts

A delivery succeeds when your receiver returns HTTP 2xx within 5 seconds. Timeouts, TLS errors, and non-2xx responses count as failures.

A failed delivery is retried with backoff (5 s, 10 s, 20 s, … up to 5 minutes) for up to 8 attempts per job (fewer when the job itself waited through retries before it ran). Retrying a webhook never re-runs the job. After the last attempt the destination is marked as given up for that job.

Auto-deactivate

After 8 consecutive failures, swarmvia deactivates the webhook. Deliveries stop until you fix the receiver and click Re-enable on the details page. Success resets the failure streak. Deactivated endpoints stay attached to sources but are omitted from delivery until re-enabled.

If deactivation leaves a source with no active destination, that source goes back to serving requests synchronously until you re-enable one.

Verify signatures

Each delivery is a POST with JSON body and these headers:

Header Meaning
X-Swarmvia-Signature Hex HMAC-SHA256 of the canonical string
X-Swarmvia-Timestamp Unix seconds
X-Swarmvia-Job-Id Job id (also the idempotency key)
X-Swarmvia-Webhook-Id Destination id
X-Swarmvia-Event Event type (same as type in the body)

The same values are also sent under the previous X-Nthpool-* names.

Canonical string (newline-separated):

TIMESTAMP
JOB_ID
SHA256_HEX(RAW_BODY)

Recompute the HMAC with your signing secret and compare with a constant-time equals. Reject if the timestamp is too far from your clock.

Deliveries are at-least-once. Key idempotency on id / X-Swarmvia-Job-Id.

Payload

The body is a versioned event envelope. Results are inlined when they fit the 256 KiB UTF-8 body cap. When the result is too large (or not UTF-8 text), detail omits the payload and sets resultUrl instead so you can fetch it with source auth. truncated: true means the stored result was already incomplete; size overflow alone does not set it.

{
  "apiVersion": "1",
  "type": "async.job.completed",
  "id": "job_…",
  "pool": "your-source.swarmvia.cloud",
  "reason": "requested",
  "createdAt": 1710000000000,
  "completedAt": 1710000123000,
  "detail": {
    "kind": "http",
    "status": 200,
    "headers": { "content-type": "application/json" },
    "body": "{\"ok\":true}"
  }
}

Large result (reference only):

{
  "detail": {
    "kind": "http",
    "status": 200,
    "headers": { "content-type": "application/json" },
    "resultUrl": "https://api.swarmvia.cloud/pools/your-source/jobs/job_…"
  }
}
Field Meaning
apiVersion Envelope version (1)
type async.job.completed, async.job.failed, or async.job.expired
id Job id
pool Canonical hostname of the source (the key keeps its original name)
reason Why the job was accepted: requested (header), busy (open wait), or wait_timeout
createdAt / completedAt Unix milliseconds
detail Result payload; omitted when there is nothing to report (for example some expired jobs)

detail.kind is http for API sources and database for database sources. Prefer the inlined payload (body, columns/rows, or results for a queries job) when present; when resultUrl is set, fetch that URL with the same source auth you use for poll. A database batch inlines every statement the same way a small single query inlines rows — the 256 KiB cap is the only reason to fall back. Switch on type and detail.kind; do not sniff fields to learn which source backend produced the job.

Rotate a secret

Use Rotate on the Webhooks page or details page. Update your receiver before the next delivery. Attached sources are re-pushed with the new secret.

Next steps