Webhooks
Webhooks let Papyrus push document lifecycle events to your backend in real time. This guide covers setup, signature verification, retries, and handler design. For the full event-by-event payload reference, use the Webhook Reference. For related document and template operations, use the API Reference.
When to use webhooks
Webhooks are a good fit when you want to:
- Mark an internal record complete as soon as a document finishes
- React immediately when a signer signs or declines
- Track reminders, voids, and send activity without polling
- Keep downstream systems in sync with Papyrus
If webhooks are enabled for your team or plan, configure them from Settings > Developer.
Create a webhook
- Go to Settings > Developer
- Scroll to the Webhooks section
- Enter your destination URL
- Choose the events you want
- Click Create Webhook
Papyrus generates a signing secret for each webhook. Copy it when it is shown.
Important: The current secret is only shown once. Store it in a secrets manager or environment variable before you leave the page.
Event types
| Event | When it fires |
|---|---|
document.sent |
A document is sent to signer recipients |
recipient.signed |
An individual signer completes signing |
recipient.declined |
An individual signer declines the signing request |
document.completed |
All required signers have finished and Papyrus finalizes the file |
document.voided |
The sender voids a document |
reminder.sent |
Papyrus sends a reminder email, whether it was manual or automatic |
Payload contract
Webhook payloads now use the same camelCase conventions as the REST API.
Every webhook uses this envelope:
{
"id": "01JQZXA7Y6J5P2N5Q2M6A2R8F9",
"event": "document.completed",
"occurredAt": "2026-03-25T18:15:00Z",
"data": {}
}
Shared rules:
- Every payload includes
data.document document.*events populatedata.recipientsand setdata.recipienttonullrecipient.*andreminder.sentpopulatedata.recipientand setdata.recipientstonull- The
documentobject may includeexternalId; treat it as optional metadata and rely on Papyrus IDs plus the webhookidfor core processing
Use the Webhook Reference for exact payloads and example bodies for every event. The API Reference covers incoming REST endpoints; webhook deliveries are documented on these webhook pages because Papyrus sends them outbound to your application.
When you send a test ping from the dashboard, Papyrus sends:
{
"id": "01JQZXC8N7H4R2V9B5P1A6T3K8",
"event": "ping",
"occurredAt": "2026-03-25T18:15:00Z",
"data": {
"message": "Ping from Papyrus"
}
}
Verify the signature
Every webhook request includes these headers:
X-Webhook-SignatureX-Webhook-IdX-Webhook-EventX-Webhook-Attempt
Compute an HMAC-SHA256 over the raw request body using your webhook secret,
then compare it to X-Webhook-Signature.
PHP example
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $payload, $webhookSecret);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);
Node.js example
const crypto = require('crypto');
const express = require('express');
function verifySignature(payload, signature, secret) {
const expected =
'sha256=' +
crypto.createHmac('sha256', secret).update(payload).digest('hex');
if (!signature || signature.length !== expected.length) {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
app.post(
'/webhooks/papyrus',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-webhook-signature'];
const rawBody = req.body.toString('utf-8');
if (!verifySignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
res.status(200).send('OK');
},
);
Delivery and retries
Papyrus sends the first delivery attempt immediately and records every attempt in delivery history.
Papyrus retries on:
- Network failures and timeouts
408 Request Timeout429 Too Many Requests- Any
5xxresponse
Papyrus does not retry most 4xx responses because they usually indicate a
permanent handler issue.
Retry schedule:
- First attempt immediately
- Retry after 1 minute
- Retry after 5 minutes
- Retry after 15 minutes
- Retry after 1 hour
- Retry after 6 hours
Delivery statuses
| Status | Meaning |
|---|---|
pending |
The delivery has been queued but not attempted yet |
retrying |
The last attempt failed with a retryable error |
delivered |
Your endpoint returned a 2xx response |
failed |
Papyrus exhausted retries or encountered a non-retryable fail |
You can inspect delivery history from Settings > Developer > Webhooks.
Build a reliable webhook handler
This page is the implementation guide. Use it for setup, signature validation, retry expectations, and operational behavior. Use the Webhook Reference when you need the exact JSON shape for a specific event.
Return success quickly
Papyrus waits up to 15 seconds for a response. Keep the request itself light:
- Verify the signature
- Parse the payload
- Enqueue internal work
- Return
200 OK
Handle duplicates safely
Webhook delivery is at least once, so your handler must be idempotent.
Deduplicate on id first. The X-Webhook-Id header contains the same value as
the payload envelope.
Use externalId when reconciling
If you set externalId when creating a document, Papyrus includes it in webhook
payloads so your backend can correlate directly to your own records.
If you do not use externalId, the webhook still contains everything required
to operate safely:
idfor webhook-level idempotencydata.document.idfor the Papyrus document identitydata.recipient.idfor recipient-specific events
Expect related events, not perfectly simultaneous ones
recipient.signed tells you a signer finished their part. document.completed
follows when Papyrus finishes finalizing the document, so your backend should
not assume those two events arrive at exactly the same time.
Managing existing webhooks
Disable without deleting
To pause future deliveries temporarily:
- Go to Settings > Developer
- Find the webhook
- Click Toggle
Rotate the secret
If a secret may have leaked:
- Go to Settings > Developer
- Find the webhook
- Click Rotate Secret
- Update your application immediately
Send a test ping
Use Ping from the webhook row to verify reachability and signature handling without waiting for a real document event.
Review delivery history
The deliveries view shows the latest event, attempt count, last HTTP status, and timing information for each delivery.
Best practices
- Verify
X-Webhook-Signatureon the raw request body - Use HTTPS for every webhook destination
- Treat
idas the idempotency key - Return a 2xx response only after your app has accepted the event
- Queue heavier downstream work
- Log failures with the Papyrus event type and webhook ID
Where to go next
- Webhook Reference - exact payloads for every event
- API Authentication - auth and team-scoped API requests
- Sending for Signature - the document lifecycle behind these events
- API Reference - exact document and template operations
