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.
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.
| 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 |
Subscriptions accept three pattern forms:
*: every eventdocument.generated: one exact eventpayment.*: every event under a prefix
Every delivery is a POST with
Content-Type: application/json
and this body:
{
"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.
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.
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)
);
} 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.
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.
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.
/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.
/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.
timestamp field if order matters.Create an account, upload your first template, and generate your first document in under five minutes.