Events · Coming soon

Webhooks

BulkRender POSTs a JSON payload to a URL you own when something happens in your account. Subscribe once, then your endpoint receives events as they occur. No polling.

Coming soon

Webhooks are built but not yet released. The behaviour below is accurate to the current code and has not been through public testing. Endpoints, event names, and headers may change before launch. Do not build against this page yet. Want early access? Email us.

Overview

What webhooks do

A subscription holds a destination URL and a list of event patterns. When a matching event happens, we POST it to your URL and sign the request. Your endpoint verifies the signature, acknowledges, and does the work.

Deliveries that fail are retried. Every attempt is recorded, so you can check what we sent and what your server answered.

Events

Event catalogue

Event Fires when
document.generated A document finishes rendering
template.uploaded A template is added
template.deleted A template is deleted
credits.adjusted Your credit balance changes
payment.succeeded A payment clears
payment.failed A payment is declined
subscription.created A plan subscription starts
subscription.canceled A plan subscription ends
plan.changed Your plan tier changes
user.created A user joins the organization
user.role_changed A user's role changes
user.deactivated A user is deactivated
user.login A user signs in
login.failed A sign-in attempt fails

Pattern forms

Subscriptions accept three pattern forms:

  • *: every event
  • document.generated: one exact event
  • payment.*: every event under a prefix
Payload

Payload shape

Every delivery is a POST with Content-Type: application/json and this body:

Example payload
{
  "type": "document.generated",
  "data": { },
  "timestamp": "2026-08-26T11:04:22.108Z"
}

type is the event name. data carries the event-specific fields. timestamp is ISO 8601 UTC, set when we build the payload.

Security

Verify the signature

Each request carries an X-Webhook-Signature header: an HMAC-SHA256 of the raw request body, keyed with your subscription secret, hex encoded. Compute the same value and compare. Reject anything that does not match.

Hash the raw bytes, not a re-serialized object. Re-encoding JSON changes key order and whitespace, which changes the hash.

Node.js
import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(header)
  );
}
Python
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

Use a constant-time comparison. A plain equality check leaks timing information.

Store the secret: your secret is shown once, in the create response. It is not retrievable later.
Handler

Respond fast

Return any 2xx status to acknowledge. Anything else counts as a failure.

Acknowledge first, then do the work in a background job. Requests time out, and a slow handler turns a successful event into a retry.

Reliability

Retries

Failed deliveries retry with exponential backoff:

Attempt Delay after previous try
1 1 minute
2 2 minutes
3 4 minutes
4 8 minutes
5 16 minutes

Backoff is capped at 24 hours. After 5 attempts the delivery is marked permanently failed and is not retried again.

Expect duplicates: retries mean your endpoint can see the same event twice. Make your handler idempotent: key on the event and skip work you have already done. See Idempotency.
Endpoint

Delivery history

GET /api/webhook-subscriptions/{id}/deliveries

Returns recent attempts for one subscription.

Query parameter Default Notes
limit 50 Clamped to 1-500
offset 0 For paging
status none Filter by delivery status

Each record reports the event type, status, attempts, the response_code we received, any error_message, last_attempt_at, and created_at. Use it to confirm a delivery landed, or to see why it did not.

Management

Managing subscriptions

Coming soon. Subscription management is dashboard-only today. The /api/webhook-subscriptions routes accept a browser login session, not an API key, so you cannot create or edit subscriptions from code yet. API-key support is on the roadmap.

A subscription holds a destination url, an events array of patterns, an is_active flag, and the signing secret. Creating one returns the secret exactly once.

Destination URLs must be public. Private and internal addresses are rejected at create time to prevent server-side request forgery.

Notes

Good to know

  • Delivery is over HTTPS to the URL you supply.
  • We do not guarantee ordering. Use the timestamp field if order matters.
  • The rest of the platform is documented in the API Reference, with the full schema at openapi.json.

Ready to start?

Create an account, upload your first template, and generate your first document in under five minutes.