Webhooks allow your application to receive real-time notifications when events occur. Our API sends an HTTP POST request to your configured endpoint whenever a subscribed event is triggered. Currently, we support only envelope events. See the Webhook Events reference for the full list.
First, configure a webhook endpoint. You can create one from the Dashboard or by using the Create Webhook API.
To create a webhook from the Dashboard:
For API-managed webhooks, see the Create Webhook, Get Webhook, and Delete Webhook endpoint references.
See below how a webhook configuration looks. In this example, we demonstrate a webhook endpoint that listens to the envelope.expired event. However, you can specify more events and listen to multiple events in the same webhook endpoint.

See below a minimal JavaScript code example to verify your webhook works.
1import express from 'express';2const app = express();3const port = 3000;45app.use(express.json());67app.post('/pdfgate-callback', (req, res) => {8 console.log('Webhook received:', req.body);9 res.sendStatus(200);10});1112app.listen(port, () => {13 console.log(`Server running on port ${port}`);14});This example does not verify webhook signatures. For production use, see the signature verification section below.
We recommend testing your webhooks locally using the third-party tool ngrok. First, copy the code snippet from the example above and run it. This will start your local server listening on port 3000. Then, open a new terminal and start an ngrok tunnel with the following command:
1ngrok http 3000Next, update your webhook URL host with the URL provided by ngrok. You can do this from the Dashboard or by creating a webhook endpoint with the API. It will look like: https://e0c2-xxxx.ngrok-free.app
Your webhook URL should be updated like: https://e0c2-xxxx.ngrok-free.app/{your_slug}
You should now start receiving webhook events on your local server.
Each webhook request includes an x-pdfgate-signature header. Use this header to verify that the request was sent by PDFGate and that the payload was not modified in transit.
The signature header contains:
t: the Unix timestamp when the webhook was signedv1: one or more values that are HMAC-SHA256 signaturesExample:
x-pdfgate-signature: "t=1712345678,v1=abc123,v1=def456"
To verify the webhook:
v1 signatures from the x-pdfgate-signature header{timestamp}.{raw_body}We may include multiple signatures during secret rotation. Your code should consider the webhook valid if any of the v1 signatures matches the expected value.
See below a JavaScript code example of signature verification. We are currently implementing this in our SDKs so you won't need to implement it manually.
1import express from 'express';2import { createHmac, timingSafeEqual } from 'crypto';34const app = express();5const port = 3000;67// IMPORTANT: use raw body for signature verification8app.use(express.raw({ type: 'application/json' }));910function verifySignature(secret, signatureHeader, payload) {11 const parts = signatureHeader.split(',').map(p => p.trim());1213 let timestamp = null;14 const signatures = [];1516 for (const part of parts) {17 const [key, value] = part.split('=');18 if (key === 't') timestamp = Number(value);19 if (key === 'v1') signatures.push(value);20 }2122 if (!timestamp || signatures.length === 0) {23 throw new Error('Missing signature');24 }2526 // 5 minute tolerance27 const now = Math.floor(Date.now() / 1000);28 if (Math.abs(now - timestamp) > 300) {29 throw new Error('Signature expired');30 }3132 const signedPayload = `${timestamp}.${payload.toString('utf8')}`;33 const expected = createHmac('sha256', secret)34 .update(signedPayload)35 .digest('hex');3637 const isValid = signatures.some(sig => {38 const a = Buffer.from(sig, 'hex');39 const b = Buffer.from(expected, 'hex');40 return a.length === b.length && timingSafeEqual(a, b);41 });4243 if (!isValid) {44 throw new Error('Invalid signature');45 }4647 return JSON.parse(payload.toString('utf8'));48}4950app.post('/pdfgate-callback', (req, res) => {51 const secret = 'whsecret_...'; // replace with your webhook secret52 const signature = req.get('x-pdfgate-signature');5354 try {55 const event = verifySignature(secret, signature, req.body);56 console.log('Verified event:', event);5758 res.sendStatus(200);59 } catch (err) {60 console.error('Webhook error:', err.message);61 res.sendStatus(400);62 }63});6465app.listen(port, () => {66 console.log(`Server running on port ${port}`);67});Each webhook delivers an Event type JSON payload:
1{2 "eventId": "69b8f935cde793a4db494e46",3 "event": "envelope.completed",4 "timestamp": "2024-01-01T12:00:00Z",5 "resource": {6 "kind": "envelope",7 "id": "69b78j35cde793a4db494ea7"8 },9 "data": {10 "envelope": {11 "id": "69b78j35cde793a4db494ea7",12 "status": "completed"13 }14 }15}Webhook deliveries may be retried and delivered out of order. Before applying updates, we recommend retrieving the current state of the referenced resource from the API to ensure consistency.
If your endpoint does not return a 2xx status code, delivery is retried. Retries are scheduled relative to the original event time and processed by a background job every 3 minutes. After the final attempt, the webhook is marked as failed.
| Attempt | Delay |
|---|---|
| Attempt #1 | 5 minutes |
| Attempt #2 | 15 minutes |
| Attempt #3 | 1 hour |
| Attempt #4 | 6 hours |
| Attempt #5 | 24 hours |
| Attempt #6 | 48 hours |
If your signing secret is compromised, you can rotate it from the dashboard.
When rotation begins:
x-pdfgate-signature header will include multiple v1 values.If rotation is canceled during the grace period: