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 pool on Pools → Concurrency.

A pool needs at least one active destination to run async. With none attached, the pool serves requests synchronously instead. Callers can also poll the job URL with pool 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.

Attach to a pool

On the pool 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 pools. Edit the URL once and every attached pool picks it up within a few seconds.

Details and delivery history

Open a webhook from the list to see status, success and failure totals, consecutive failures, attached pools, and recent delivery attempts (time, job id, pool, 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.

Auto-deactivate

After 8 consecutive failures, nthbouncer 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 pools but are omitted from delivery until re-enabled.

If deactivation leaves a pool with no active destination, that pool 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-Nthpool-Signature Hex HMAC-SHA256 of the canonical string
X-Nthpool-Timestamp Unix seconds
X-Nthpool-Job-Id Job id (also the idempotency key)
X-Nthpool-Webhook-Id Destination id
X-Nthpool-Event Event type (same as type in the body)

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-Nthpool-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 pool 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-pool.nthpool.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.nthpool.cloud/pools/your-pool/jobs/job_…"
  }
}
Field Meaning
apiVersion Envelope version (1)
type async.job.completed, async.job.failed, or async.job.expired
id Job id
pool Canonical pool hostname
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 proxy pools and database for database pools. 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 pool 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 pool backend produced the job.

Rotate a secret

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

Next steps