Skip to main content

Webhooks

Blank delivers events to your HTTPS endpoint as signed CloudEvents 1.0 JSON. Webhooks replace polling for anything you need to react to promptly. Managing endpoints requires webhooks:write; reading endpoints and deliveries requires webhooks:read.

Limits

Deleted endpoints do not count toward the endpoint limit. The per-batch figure is a fair-share bound rather than a quota: dispatch takes at most that many of your deliveries per pass so one account’s backlog cannot delay everyone else’s, and your remaining deliveries are picked up on subsequent passes in the usual order.

Operations

All mutations require an Idempotency-Key. Update, delete, and rotate additionally require If-Match with the endpoint’s current version.

Event types

Types are versioned with a .v1 suffix. A breaking payload change ships as a new type, never as a mutation of an existing one.

Create an endpoint

The signing secret is returned exactly once, at creation and at rotation. Store it in your secret manager immediately. If you lose it, rotate — there is no way to read it back.

URL policy

Endpoint URLs are validated at creation and again on every delivery attempt. A URL is rejected with 422 webhook_url_disallowed unless it:
  • uses https: on the default port (no port, or 443)
  • carries no userinfo and no fragment
  • resolves to a public, fully qualified hostname — not localhost, and not a hostname ending in .localhost, .local, .internal, .home.arpa, or .onion
  • is not a private, loopback, link-local, carrier-grade NAT, documentation, or multicast IP address, in IPv4 or IPv6
Redirects are followed at most three times, and every hop is re-validated against the same policy.

The delivery request

Blank sends POST with these headers: The body is a CloudEvent:
data carries the allowlisted payload for that event type. Treat unknown fields as additive and ignore them.

Verifying the signature

Blank-Signature is an HMAC-SHA-256 over "{timestamp}.{raw request body}", hex-encoded. Verify the exact raw bytes before parsing JSON — re-serializing the body changes the signature.
verifyWebhookSignature enforces a timestamp tolerance of 300 seconds by default, rejecting replays outside that window. Override it with toleranceSeconds (1 to 3,600) only if you have a reason to. It checks every v1 signature against secret, and every v0 signature against previousSecret when you supply one.
Do not implement string comparison of signatures by hand. If you must verify outside the SDK, use a constant-time comparison and reject any request whose timestamp is outside your tolerance window.

Responding

  • Return any 2xx only after the event is durably accepted. Write it to a queue or a table first, then acknowledge.
  • Anything other than 2xx, a timeout, or a transport failure counts as a failed attempt and is retried.
  • The delivery timeout is 10 seconds. Do the work asynchronously.
  • Deduplicate on Blank-Event-Id. Retries and replays reuse the same event ID with a new delivery ID, and at-least-once delivery is the contract.

Retries and dead letters

Each delivery gets up to 7 attempts: the first, then six retries at roughly 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours, and 24 hours. Each delay is jittered, and a Retry-After header on your response is honoured up to 24 hours. Delivery status moves through:

Inspecting deliveries

Delivery fields: id, endpointId, eventId, eventType, status, attemptCount, nextAttemptAt, responseStatus, lastErrorCode, deliveredAt, createdAt, updatedAt. Filter by status and paginate with cursor and limit. lastErrorCode distinguishes transport failures (request_timeout, network_error, url_disallowed, redirect_limit_exceeded, redirect_location_missing) from HTTP rejections, which are recorded as http_<status>. The complete stable list is in Errors.

Replaying

A replay creates a fresh delivery for the same immutable event, with a new Blank-Delivery-Id and the original Blank-Event-Id. Use it to drain dead letters after fixing your endpoint. A delivery that is not in a replayable state returns 409 webhook_delivery_not_replayable; an unknown or foreign delivery returns 404 webhook_delivery_not_found.

Rotating the signing secret

Rotation issues a new secret and keeps the previous one valid for a bounded overlap window, so you can deploy without dropping in-flight deliveries.
overlapHours is an integer from 1 to 24, defaulting to 24. During the overlap, Blank signs each delivery with both secrets: v1 with the new secret and v0 with the previous one. Safe rotation:
  1. Rotate and store the new secret as BLANK_WEBHOOK_SECRET.
  2. Move the old value to BLANK_PREVIOUS_WEBHOOK_SECRET and deploy, so your handler accepts both.
  3. Once the overlap window closes, remove BLANK_PREVIOUS_WEBHOOK_SECRET.

Updating and deleting

Both use optimistic concurrency:
update accepts any non-empty subset of name, url, eventTypes, and status (active or disabled). A stale version returns 412 precondition_failed — re-read the endpoint and retry. delete soft-deletes the endpoint, stops all future fanout, and returns { id, deletedAt }.

Errors

Shared errors apply too. See Errors and Conventions.