Vai al contenuto

Webhooks

v1 · Last updated 2026-07-17

Questa documentazione è disponibile solo in inglese.

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 (matches type in the body)
  • Pluma-Signature: sha256=<hex> — HMAC-SHA256 of the raw body
  • Pluma-Delivery-Id — unique per attempt; safe to dedupe on
  • Pluma-Timestamp — delivery time, Unix seconds
  • Pluma-Signature-V2: t=<unix seconds>,v1=<hex> — HMAC-SHA256 of <timestamp>.<raw body>; verify this one and reject deliveries older than five minutes, which rules out replays

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.

EventWhenAvailability
job.completedAsync bulk job finished and the ZIP is ready to download.Hosted + self-hosted
job.failedAsync bulk job aborted. data.error carries a sanitised, customer-safe message — never URLs or stack traces.Hosted + self-hosted
render.url_expiringFires ~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.requestedA sign request was created and the invite email sent.Hosted paid tiers, self-hosted enterprise
signature.signedA signer submitted their signature.Hosted paid tiers, self-hosted enterprise
signature.declinedA signer declined. data.declineReason carries their reason, or null.Hosted paid tiers, self-hosted enterprise
signature.expiredA pending request passed expiresAt unsigned.Hosted paid tiers, self-hosted enterprise
publish.completedA 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');
// Pluma-Signature-V2: t=<unix seconds>,v1=<hex> — signed over `<t>.<raw body>`
function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
  if (!m) return false;
  const [, t, v1] = m;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false; // stale or replayed
  const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
  return expected.length === v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Same HMAC-SHA256 pattern in any language. The older Pluma-Signature: sha256=<hex> header (HMAC of the body alone) is still sent for existing integrations; compare it with a constant-time function only after checking the lengths match. A non-2xx delivery is retried up to four times after the first attempt, with exponential backoff (30 s, 2 min, 8 min, 32 min); after the fifth failed attempt it is marked failed — the dashboard shows every attempt.

Need help?

Open a ticket from the in-app Support page, or email [email protected].