Register URLs in the Webhooks page to receive events. Each delivery is a JSON POST with this envelope:
{
"type": "<event-type>",
"timestamp": "<ISO-8601 timestamp, e.g. 2026-06-06T12:34:56.789Z>",
"data": { /* event-specific fields */ }
}and the following headers:
Pluma-Event— event type (matchestypein the body)Pluma-Signature: sha256=<hex>— HMAC-SHA256 of the raw bodyPluma-Delivery-Id— unique per attempt; safe to dedupe on
Event catalog
The complete list of event types we currently emit. Subscribe each webhook to a subset (the dashboard exposes a checkbox per event), or leave it empty to receive every event.
| Event | When | Availability |
|---|---|---|
job.completed | Async bulk job finished and the ZIP is ready to download. | Hosted + self-hosted |
job.failed | Async bulk job aborted. data.error carries a sanitised, customer-safe message — never URLs or stack traces. | Hosted + self-hosted |
render.url_expiring | Fires ~24 h before a download URL closes while the artifact still has retention left. Call data.refreshUrl for a fresh URL. | Hosted + self-hosted |
signature.requested | A sign request was created and the invite email sent. | Hosted paid tiers, self-hosted enterprise |
signature.signed | A signer submitted their signature. | Hosted paid tiers, self-hosted enterprise |
signature.declined | A signer declined. data.declineReason carries their reason, or null. | Hosted paid tiers, self-hosted enterprise |
signature.expired | A pending request passed expiresAt unsigned. | Hosted paid tiers, self-hosted enterprise |
publish.completed | A workspace publish job finished. data.imageDigest is the canonical reference for the published artifact. | Self-hosted only |
Example payload
Every event follows the same envelope — here's job.completed; the data fields per event are listed in the catalog above:
{
"type": "job.completed",
"timestamp": "2026-06-06T12:34:56.789Z",
"data": {
"jobId": "5e4a0a32-8b91-4d8f-9b56-0c2b1ef8d3a1",
"templateId": "7b1c3f10-6a08-4b97-9f1a-12e6e0c7f0a8",
"itemsTotal": 100
}
}Verify the signature
const crypto = require('crypto');
function verify(rawBody, headerSig, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(headerSig));
}Same HMAC-SHA256 pattern in any language. Non-2xx deliveries are retried up to 5 times with exponential backoff (30 s, 2 min, 8 min, 32 min), then marked failed — the dashboard shows every attempt.
Need help?
Open a ticket from the in-app Support page, or email [email protected].