Webhooks let Platendoc push events to your server the moment a generation completes or fails — no polling required.
Setup
- Open app.platendoc.com and navigate to Webhooks in the sidebar.
- Click Add webhook.
- Enter your HTTPS endpoint URL and select the events you want to receive.
- Copy the signing secret — it’s shown only once. Store it in an environment variable (e.g.
PLATENDOC_WEBHOOK_SECRET).
Events
| Event | When it fires |
|---|
generation.completed | Document rendered successfully — outputUrl is ready |
generation.failed | Rendering failed after all retries |
Platendoc sends a POST request to your endpoint with the following headers:
Content-Type: application/json
X-Platendoc-Event: generation.completed
X-Platendoc-Signature: sha256=<hmac-sha256>
Payload
{
"event": "generation.completed",
"payload": {
"generationId": "gen_01j8z..."
},
"timestamp": 1737036600000
}
For generation.failed events, payload also includes an error field:
{
"event": "generation.failed",
"payload": {
"generationId": "gen_01j8z...",
"error": "Template rendering failed: unknown variable 'foo'."
},
"timestamp": 1737036600000
}
After receiving the event, call GET /generations/{generationId} to fetch the full generation object including outputUrl.
Verifying signatures
Every delivery includes an X-Platendoc-Signature header. Always verify it before processing.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verifySignature(rawBody: string, signature: string, secret: string): boolean {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex')
try {
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
} catch {
return false
}
}
// Express example
app.post('/webhooks/platendoc', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-platendoc-signature'] as string
if (!verifySignature(req.body.toString(), signature, process.env.PLATENDOC_WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature')
}
const { event, payload } = JSON.parse(req.body.toString())
// handle event...
res.sendStatus(200)
})
import hashlib
import hmac
def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask example
@app.route('/webhooks/platendoc', methods=['POST'])
def webhook():
signature = request.headers.get('X-Platendoc-Signature', '')
if not verify_signature(request.get_data(), signature, os.environ['PLATENDOC_WEBHOOK_SECRET']):
return 'Invalid signature', 401
data = request.get_json()
# handle data['event']...
return '', 200
Always use a timing-safe comparison (e.g. timingSafeEqual, hmac.compare_digest).
Standard string equality (===, ==) is vulnerable to timing attacks.
Retries
| Attempt | Delay |
|---|
| 1st retry | 1s |
| 2nd retry | 2s |
| 3rd retry | 4s |
| 4th retry | 8s |
| 5th retry | 16s |
After 5 failed attempts the delivery is dropped. Return a 2xx status code quickly — perform any heavy processing asynchronously.
Respond with 200 OK immediately, then enqueue the event for processing. This prevents timeouts from causing unnecessary retries.