Webhooks
Overview
Webhooks let you receive real-time notifications when extractions complete or fail. Instead of polling the API, DocMap sends a POST request to your endpoint with the extraction result as soon as the status changes.
Key facts:
- Maximum 10 active webhooks per account
- Payloads are signed with HMAC-SHA256 so you can verify authenticity
- Delivery timeout is 10 seconds per webhook
- Delivery is fire-and-forget -- one webhook failing does not block others
Creating a Webhook
From the Dashboard
- Navigate to Webhooks in your DocMap dashboard
- Click Create Webhook
- Enter your endpoint URL (must be HTTPS in production)
- Select which events to subscribe to
- Copy the signing secret immediately -- it will not be shown again
From the API
curl -X POST https://api.docmap.io/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/docmap",
"events": ["extraction.completed", "extraction.failed"]
}'const response = await fetch("https://api.docmap.io/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://your-app.com/webhooks/docmap",
events: ["extraction.completed", "extraction.failed"],
}),
});
const { data } = await response.json();
// Store this immediately -- it will never be shown again
console.log("Signing Secret:", data.secret);
console.log("Webhook ID:", data.webhook.id);import requests
response = requests.post(
"https://api.docmap.io/v1/webhooks",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"url": "https://your-app.com/webhooks/docmap",
"events": ["extraction.completed", "extraction.failed"],
},
)
data = response.json()["data"]
# Store this immediately -- it will never be shown again
print("Signing Secret:", data["secret"])
print("Webhook ID:", data["webhook"]["id"])WARNING
The signing secret is only returned once when the webhook is created. Store it securely in an environment variable or secrets manager.
Payload Format
When an event occurs, DocMap sends a POST request to your webhook URL with the following structure:
{
"event": "extraction.completed",
"data": {
"id": "extract-abc123",
"userId": "uid_xyz789",
"templateId": "tpl_inv001",
"templateName": "Standard Invoice",
"fileName": "invoice-march.pdf",
"status": "completed",
"extractedData": {
"vendor_name": "Acme Corp",
"invoice_number": "INV-2024-003",
"total_amount": 4750.00
},
"error": null,
"variables": [...],
"source": "api",
"runId": null,
"processingTimeMs": 3842,
"createdAt": "2024-11-20T14:30:00.000Z"
},
"timestamp": "2024-11-20T14:30:04.000Z"
}Headers
Each webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-DocMap-Signature | HMAC-SHA256 signature: sha256={hex_digest} |
X-DocMap-Event | Event name (e.g., extraction.completed) |
Verifying Signatures
Always verify the X-DocMap-Signature header to confirm the request came from DocMap. The signature is an HMAC-SHA256 hash of the raw request body, using your signing secret as the key.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verifyWebhookSignature(
body: string,
signature: string,
secret: string,
): boolean {
const expected = createHmac('sha256', secret)
.update(body)
.digest('hex')
const received = signature.replace('sha256=', '')
return timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex'),
)
}
// Express example
app.post('/webhooks/docmap', (req, res) => {
const signature = req.headers['x-docmap-signature'] as string
const body = JSON.stringify(req.body)
if (!verifyWebhookSignature(body, signature, process.env.DOCMAP_WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature')
}
const { event, data } = req.body
console.log(`Received ${event} for extraction ${data.id}`)
// Process the webhook...
res.status(200).send('OK')
})import hmac
import hashlib
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
body,
hashlib.sha256,
).hexdigest()
received = signature.replace('sha256=', '')
return hmac.compare_digest(expected, received)
# Flask example
from flask import Flask, request
app = Flask(__name__)
@app.post('/webhooks/docmap')
def handle_webhook():
signature = request.headers.get('X-DocMap-Signature', '')
body = request.get_data()
if not verify_webhook_signature(body, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
payload = request.get_json()
event = payload['event']
data = payload['data']
print(f"Received {event} for extraction {data['id']}")
# Process the webhook...
return 'OK', 200TIP
Always use a constant-time comparison (like timingSafeEqual or hmac.compare_digest) to prevent timing attacks.
Events Reference
| Event | Trigger |
|---|---|
extraction.completed | An extraction finished successfully. data.status is "completed" and data.extractedData contains the results. |
extraction.failed | An extraction failed. data.status is "failed" and data.error contains the error message. |
Best Practices
Use HTTPS. Always use an HTTPS endpoint for your webhook URL. HTTP endpoints may be rejected in production.
Verify signatures. Always verify the
X-DocMap-Signatureheader before processing a webhook. This confirms the request is from DocMap and hasn't been tampered with.Respond quickly. Return a 2xx status code within a few seconds. DocMap waits up to 10 seconds for a response. If your processing takes longer, acknowledge the webhook immediately and process it asynchronously.
Handle duplicates. In rare cases, a webhook may be delivered more than once. Use the extraction
idto deduplicate if needed.Store the secret securely. Keep your signing secret in an environment variable or secrets manager. Never hardcode it in source code or commit it to version control.
Monitor failures. If your endpoint is consistently failing, consider checking your server logs. DocMap logs delivery failures but does not retry.
