Skip to main content
Developers

Webhook Events and Payload Reference

Subscribe to events, validate HMAC signatures, handle retries and dead-letters.

Webhook Events and Payload Reference

Webhooks push events to your endpoint when documents are uploaded, workflows complete, or other things happen. Better than polling.

Subscribe

Create a webhook subscription under Admin → Webhooks → New. Specify:

  • A URL to receive events
  • A set of event types to subscribe to
  • A signing secret (auto-generated, shown once)
  • Optional filters (e.g., only events on documents tagged invoice)

Event types

Event When fired
document.uploaded A document is uploaded
document.classified AI classification completes
document.extracted Field extraction completes
document.updated Document metadata changed
document.deleted Document soft-deleted
document.shared.external An external share link is created
workflow.started A workflow instance starts
workflow.task.assigned A task assigned to a user
workflow.task.completed A task is approved / rejected
workflow.completed A workflow instance completes
signature.requested A signature request is sent
signature.signed A signature is captured
signature.completed All signatures collected
user.created A new user is added
user.removed A user is removed

Payload structure

{
  "id": "evt_2c8f...",
  "type": "document.uploaded",
  "createdAt": "2026-05-13T14:32:11Z",
  "tenantId": "11111111-1111-1111-1111-111111111111",
  "tenantSlug": "yourtenant",
  "data": {
    "documentId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "title": "invoice.pdf",
    "uploadedBy": "user_abc123",
    "folderId": "...",
    "size": 245680,
    "classification": null,
    "status": "Processing"
  }
}

Signature verification

Every webhook delivery includes an X-Papyrus-Signature header:

X-Papyrus-Signature: t=1715608331,v1=a3f8b2c4d5e6...

Verify with HMAC-SHA256 over t=<timestamp>.<body>:

import hmac
import hashlib

def verify(signature_header, body_bytes, secret):
    parts = dict(p.split('=', 1) for p in signature_header.split(','))
    t = parts['t']
    expected = parts['v1']
    signed = f"{t}.{body_bytes.decode()}"
    computed = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, expected)

Reject anything older than 5 minutes (replay protection).

Retry behaviour

Failed deliveries (non-2xx response) retry with exponential backoff:

  • Attempt 1: immediate
  • Attempt 2: 30 seconds
  • Attempt 3: 2 minutes
  • Attempt 4: 10 minutes
  • Attempt 5: 1 hour
  • Attempt 6: 4 hours
  • Attempt 7: 12 hours
  • Attempt 8: 24 hours (final)

After 8 failed attempts, the event goes to the dead-letter queue, visible at Admin → Webhooks → Failed Deliveries. You can manually retry from there.

Best practices

  • Return 200 quickly — process asynchronously
  • Validate the signature before parsing the body
  • Be idempotent — the same event might arrive twice if your handler crashes mid-processing
  • Subscribe only to events you actually handle
  • Use filters to reduce traffic

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.