Webhooks: Receive Real-Time Document Generation Notifications
Build reliable document-generation webhooks: verify events, acknowledge fast, process duplicates safely, and retrieve temporary files before expiry.
Short answer: configure the document provider to emit generation-success and generation-failure events, expose a public HTTPS endpoint, verify each notification using that provider’s documented scheme, durably enqueue it, and return the required 2xx response quickly. Process the queued event idempotently, retrieve the output before any temporary URL expires, and keep a reconciliation path for missed deliveries.
A webhook is an event-triggered HTTP request. It removes repeated status polling for the event it covers, but it does not remove the need for authentication, retries, duplicate handling, or a fallback status check.
1. The delivery flow
- Subscribe. Select success and failure generation events. Event names, opt-in defaults, and per-document versus batch events are provider-specific.
- Register a receiver. Use a publicly reachable HTTPS URL and implement any registration handshake, such as a verification GET.
- Authenticate. Verify signatures, client identifiers, timestamps, or tokens exactly as documented by the provider before accepting the event.
- Persist and acknowledge. Validate the envelope, write it to durable storage or a queue, then return the provider’s accepted 2xx code. Do not download a PDF or send email in the acknowledgement path.
- Process once. Deduplicate with the provider event ID, or a stable event-plus-document key, and make every downstream action safe to retry.
- Retain the result. Download or copy the generated file while its link is valid, then update application state.
2. Provider contracts differ
| Contract to check | Why it matters |
|---|---|
| Event names and granularity | Some APIs expose separate success and failure events; others add batch-complete or opt-in item events. |
| Verification | PDFMonkey uses Svix signatures; Acrobat Sign requires its client ID echoed in a successful response. These mechanisms are not interchangeable. |
| Acknowledgement | Accepted status codes and deadlines vary. Microsoft Graph counts delivery when it receives 2xx within three seconds and recommends queueing then returning 202 for slow work. |
| Retries and exhaustion | DocSpring documents exponential retries for up to three days; Microsoft Graph documents retries for up to four hours. Check the selected API’s policy. |
| Payload and output link | The file may be inline or represented by a URL. PDF-API.io documents a temporary URL that expires after 15 minutes. |
| Replay and lifecycle | Find out whether delivery logs, replay, subscription renewal, or auto-disable controls exist. |
Read the current contract before shipping: DocSpring, PDFMonkey, Microsoft Graph, Adobe Acrobat Sign, and PDF-API.io.
3. Build a receiver
Minimal data model
Store the provider name, event ID, event type, document ID, received time, raw body (or a tamper-evident copy), verification result, processing status, attempt count, and last error. Put a unique constraint on the event ID. If the provider has no event ID, derive a deterministic key from the event type, document ID, and provider delivery timestamp.
Node.js (Express) receiver
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const secret = process.env.WEBHOOK_SECRET;
const seen = new Set(); // Replace with a durable database/queue.
function validSignature(raw, supplied) {
if (!secret || !supplied) return false;
const expected = crypto.createHmac('sha256', secret).update(raw).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(supplied));
}
app.post('/webhooks/documents', express.raw({type: '*/*'}), (req, res) => {
const raw = req.body;
const signature = req.get('X-Webhook-Signature'); // Replace with your provider’s header/algorithm.
if (!validSignature(raw, signature)) return res.status(401).send('invalid signature');
let event;
try { event = JSON.parse(raw.toString('utf8')); } catch { return res.status(400).send('invalid JSON'); }
const id = event.id || `${event.type}:${event.document_id}`;
if (!seen.has(id)) {
seen.add(id);
// Persist/enqueue event here before acknowledging.
console.log({id, type: event.type, document: event.document_id});
}
return res.sendStatus(202);
});
app.listen(process.env.PORT || 3000);
The HMAC header in this sample is a placeholder. Replace it with the selected provider’s exact verification library and canonicalization rules. Keep the raw request bytes available because parsing and re-serializing JSON can change the signed value.
Python (Flask) receiver
import hashlib, hmac, json, os
from flask import Flask, request, abort
app = Flask(__name__)
secret = os.environ['WEBHOOK_SECRET'].encode()
seen = set() # Replace with durable storage.
@app.post('/webhooks/documents')
def documents_webhook():
raw = request.get_data()
supplied = request.headers.get('X-Webhook-Signature', '')
expected = hmac.new(secret, raw, hashlib.sha256).hexdigest()
if not supplied or not hmac.compare_digest(expected, supplied): abort(401)
try: event = json.loads(raw)
except ValueError: abort(400)
event_id = event.get('id') or f"{event.get('type')}:{event.get('document_id')}"
if event_id not in seen:
seen.add(event_id)
# Persist/enqueue before returning.
app.logger.info('queued %s', event_id)
return ('', 202)
if __name__ == '__main__': app.run(port=int(os.getenv('PORT', '3000')))
cURL: send a local test event
body='{"id":"evt_123","type":"documents.generation.success","document_id":"doc_456","download_url":"https://provider.example/file.pdf"}'
signature=$(printf %s "$body" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl -i https://your-domain.example/webhooks/documents \
-H 'Content-Type: application/json' \
-H "X-Webhook-Signature: $signature" \
--data "$body"
Use a provider CLI or signed fixture where one exists. Never disable verification in a shared or production environment just to make a test pass.
4. Handle success and failure events
On success, validate that the event identifies the expected document and that a download URL or retrievable document reference is present. Download it to durable storage, verify the content type and size, and mark the document complete. On failure, retain the provider’s failure code or failure_cause, mark the job failed, and expose a retry or support path. PDFMonkey documents documents.generation.success and documents.generation.failure; its success example includes download_url.
Do not treat a missing URL as a successful generation. Some providers send the file inline; branch on the documented payload shape.
5. Verification, acknowledgement, and queues
- Verify before parsing fields that trigger work.
- Reject stale timestamps if the provider signs timestamps and documents a replay window.
- Write the event and its dedupe key transactionally, then enqueue processing.
- Return the documented success code as soon as durable receipt succeeds. Microsoft Graph’s three-second figure applies to Graph, not every API.
- Make queue workers retry downloads and downstream updates independently of webhook delivery.
Acrobat Sign’s verification flow includes an HTTPS GET during registration and requires the client ID in a successful response. Follow that provider-specific handshake instead of assuming every service uses a POST-only flow.
6. Idempotency and ordering
Retries and concurrent deliveries can produce duplicates or out-of-order events. A unique event-ID constraint prevents duplicate inserts. For providers without IDs, use a stable document state transition such as “complete only if current state is not complete,” and store the highest provider version or timestamp when one is supplied. Make file writes content-addressed or overwrite-safe, and make notifications use an idempotency key.
7. Temporary URLs and retention
Download linked output in the worker, not the HTTP handler. Encrypt it at rest, apply your retention policy, and record the provider object ID for later retrieval. PDF-API.io documents a 15-minute expiry for its temporary URL; other services have different lifetimes. Adobe recommends considering an API retrieval after a signed-document event in some flows.
8. Monitoring and reconciliation
- Measure received, rejected, acknowledged, queued, completed, and failed counts.
- Alert on signature failures, queue age, repeated document failures, and subscription disablement.
- Keep raw event IDs and provider response codes for support.
- Run a scheduled reconciliation that lists in-progress provider jobs and compares them with your database.
- Provide a replay path for stored events and a manual “retrieve document” action.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No deliveries | Endpoint is private, DNS/TLS fails, or subscription is disabled. | Check public HTTPS reachability, provider delivery logs, and subscription status. |
| 401 or signature errors | Wrong secret, header, timestamp tolerance, or signed bytes. | Use the provider’s verification method and raw body; rotate secrets deliberately. |
| Repeated retries | Slow response, non-2xx status, or crash before persistence. | Persist/enqueue first, return the documented 2xx quickly, and inspect logs. |
| Duplicate files or emails | Retry or concurrent delivery was processed twice. | Add a unique event key and idempotent downstream operations. |
| Success but no file | Temporary URL expired or payload used an inline/object reference. | Download immediately and implement the provider’s retrieval API. |
| Events stop after an outage | Retry window exhausted or subscription auto-disabled. | Reconcile provider state, re-enable the subscription, and replay where supported. |
10. Performance, reliability, and cost
Keep the receiver constant-time relative to document size: authentication, validation, durable enqueue, and acknowledgement only. Scale workers separately for downloads and PDF post-processing. Set bounded timeouts, exponential backoff with jitter for worker calls, and a dead-letter queue. Cost depends on the document provider’s generation, storage, download, and webhook terms; webhook HTTP requests are not automatically free or billable across providers, so check the selected contract. Avoid polling every job when an event covers the same state transition, but retain low-rate reconciliation for correctness.
11. Or skip the browser setup
If the notification is for a screenshot or PDF capture rather than a document template service, ScreenshotNeo provides an API and MCP server. Its async jobs support signed webhooks, so an application can receive completion notifications while keeping the same verify, enqueue, deduplicate, and retrieve pattern.
For a synchronous capture, the one-call request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo documentation for async jobs and signed webhook configuration. Cookie banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account to receive 1,000 screenshots a month without a card.
FAQ
Can a webhook replace polling completely?
It can replace polling for the subscribed event, but keep reconciliation because deliveries can fail, expire, or be disabled.
Should the webhook download the PDF itself?
No. Acknowledge after durable enqueue, then let a worker download and store the output.
What response should I return?
Return the success status documented by that provider. Microsoft Graph’s 202 guidance and three-second window are specific to Graph.
How long should I keep raw events?
Keep enough history to investigate duplicates, replay failures, and satisfy your retention requirements; the provider’s own retention does not define yours.


