Skip to main content
A webhook is an HTTP POST that Wibble sends to a URL you control when a humanization job reaches a terminal state. Instead of polling GET /humanize/{id}, you give Wibble a URL up front and it notifies you once the job succeeds or fails. The request body contains the terminal job result plus a type field that names the event. Webhooks are a convenience, not a guarantee. Network failures and downtime happen, so keep polling available as a fallback. See Jobs for the lifecycle and the polling flow.

Enable webhooks

Pass a webhook_url when you submit a job. Wibble sends a signed POST to that URL when the job finishes.
curl https://wibble.ai/api/v1/humanize \
  -H "Authorization: Bearer wib_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "The mitochondria is the powerhouse of the cell.",
    "webhook_url": "https://example.com/webhooks/wibble"
  }'
The webhook_url must:
  • Use https.
  • Be publicly reachable from the internet.
  • Contain no embedded credentials (no user:pass@ in the URL).
  • Not resolve to a private or loopback address.
  • Be 2048 characters or fewer.
A URL that fails these checks is rejected at submit time with a 400 validation_error. See Errors for the response shape.

Events

There are two events. Both deliver the same payload shape: job identifiers, billing fields, status_url, timestamps, and either output or error, with a type field added. Webhooks do not include polling-only progress fields such as current_stage or detected_language.
EventSent whenNotable fields
humanize.job.succeededThe job completes and status is succeeded.output holds the humanized text.
humanize.job.failedThe job ends and status is failed. Reserved words are refunded before the event is sent.error holds the code and message. words_charged is 0.
This is an example humanize.job.succeeded payload:
humanize.job.succeeded
{
  "id": "7b42f1d6-0a8c-4e8f-9a21-c6d9f47c2b10",
  "type": "humanize.job.succeeded",
  "status": "succeeded",
  "mode": "humanize",
  "input_words": 9,
  "words_reserved": 9,
  "words_charged": 9,
  "status_url": "https://wibble.ai/api/v1/humanize/7b42f1d6-0a8c-4e8f-9a21-c6d9f47c2b10",
  "created_at": "2026-06-14T12:00:00.000Z",
  "completed_at": "2026-06-14T12:00:42.000Z",
  "output": "Mitochondria are the cell's powerhouses."
}
For the exact schema of each event, see the reference pages:

Job succeeded

The payload Wibble sends when a job succeeds.

Job failed

The payload Wibble sends when a job fails.

Delivery headers

Every delivery carries four headers.
HeaderMeaning
X-Wibble-DeliveryUnique delivery ID (UUID). Stable across retries of the same event, so you can deduplicate.
X-Wibble-EventThe event type, matching the payload type field (humanize.job.succeeded or humanize.job.failed).
X-Wibble-SignatureHMAC-SHA256 signature of the request, of the form sha256=<hex>.
X-Wibble-TimestampUnix timestamp in seconds at which the request was signed. Recomputed on each retry.

Verify the signature

Anyone who learns your webhook_url could POST to it. Verify the signature before you trust a delivery. Wibble signs each request with the API key’s webhook signing secret. The secret is shown once when the key is created and has the format whsec_.... Store it where your webhook handler can read it.
The signing secret is shown only when the key is created. Save it then. If you lose it, create a new key.
To verify a delivery, recompute the signature and compare it to the header:
  1. Read the raw request body as it arrived, before any JSON parsing. Parsing and re-serializing changes the bytes and breaks the signature.
  2. Build the signed string "{X-Wibble-Timestamp}.{raw body}" using the timestamp from the X-Wibble-Timestamp header on this request.
  3. Compute the HMAC-SHA256 of that string with your whsec_... secret and hex-encode it.
  4. Compare your value to the hex after sha256= in X-Wibble-Signature using a constant-time comparison.
Recompute against the header timestamp on every delivery. Each retry is signed with a fresh timestamp, so a value cached from an earlier attempt will not match.
import crypto from "node:crypto";

// `rawBody` is the exact bytes received, before JSON.parse.
function verifyWibbleSignature(rawBody, headers, signingSecret) {
  const timestamp = headers["x-wibble-timestamp"];
  const signature = headers["x-wibble-signature"];
  if (!timestamp || !signature) return false;

  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", signingSecret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Many web frameworks expose only a parsed body. Configure your handler to capture the raw bytes for the webhook route. If you sign a re-serialized object, verification fails even when the request is genuine.

Protect against replays

A valid request that is captured and resent later still carries a valid signature. Use X-Wibble-Timestamp to bound how old a delivery you accept. After the signature checks out, compare the timestamp to the current time and reject deliveries outside a tolerance window, such as five minutes. Account for retries: a genuine delivery may arrive a few seconds after the first attempt, each with its own fresh timestamp.
Node.js
function isFresh(headers, toleranceSeconds = 300) {
  const timestamp = Number(headers["x-wibble-timestamp"]);
  const now = Math.floor(Date.now() / 1000);
  return Number.isFinite(timestamp) && Math.abs(now - timestamp) <= toleranceSeconds;
}

Delivery semantics

BehaviorDetail
AttemptsUp to 3 per event.
BackoffExponential between attempts.
Timeout5,000 ms per attempt.
AcknowledgmentAny 2xx response. Non-2xx (or a timeout) is retried until the attempt limit is reached.
Delivery IDX-Wibble-Delivery is stable across retries of the same event. The timestamp and signature change per attempt.
If all attempts fail, Wibble does not keep retrying past the limit. Fetch the job with GET /humanize/{id} to recover the result.

Best practices

Confirm the signature on every delivery before reading the payload or doing any work. Reject requests that fail verification or fall outside your timestamp tolerance.
Return a 2xx quickly, within the 5,000 ms timeout. Acknowledge first, then do slower work (database writes, downstream calls) asynchronously. Holding the connection open risks a timeout and an unnecessary retry.
The same event can arrive more than once. Treat X-Wibble-Delivery (or the job id plus type) as a key and ignore deliveries you have already processed.
If a webhook never arrives, fall back to polling GET /humanize/{id}. Webhooks reduce polling; they do not replace your ability to read a job’s final state.

Next steps

Jobs

The job object, statuses, and polling versus webhooks.

Quickstart

Go from an API key to your first humanized result.