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
- Open Webhooks → Add webhook.
- Name it and set an
https://URL. - 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
- Async (headers, poll, modes)
- Concurrency
- Long NetSuite writes without client timeouts