When your pipeline includes a callback output node, DocPipe sends the processing results to your webhook URL when a run completes.
Setting up callbacks
- Add a Callback output node to your pipeline
- Configure the URL where you want to receive results
- Optionally add custom headers (for example, an authorization token)
- Save and activate the pipeline
DocPipe sends a POST request to your callback URL with a JSON payload containing the run results:
{
"runId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"pipeId": "7fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "Completed",
"documentName": "invoice.pdf",
"data": {
"vendor_name": "Acme Corp",
"invoice_number": "INV-2024-001",
"total_amount": 1250.00,
"currency": "USD",
"line_items": [
{
"description": "Widget A",
"quantity": 10,
"unit_price": 100.00,
"amount": 1000.00
},
{
"description": "Widget B",
"quantity": 5,
"unit_price": 50.00,
"amount": 250.00
}
]
},
"completedAt": "2024-01-15T10:30:00Z"
}
The data field contains the extracted data matching your pipeline’s output schema.
Webhook signing verification
To verify that webhook requests are genuinely from DocPipe, you can use webhook signing keys.
- Generate a webhook signing key in your organization settings
- DocPipe includes a signature in the request headers
- Verify the signature in your application before processing the payload
Verifying the signature
The X-DocPipe-Signature header contains a timestamp and one or more signatures in the format t=timestamp,v1=signature. During key rotation, the header includes multiple v1= signatures, one for each active key. To verify:
- Parse the header to extract the timestamp (
t=) and signature(s) (v1=)
- Reconstruct the signed payload as
{timestamp}.{raw_body}
- Compute an HMAC-SHA256 hash using your signing key
- Compare the result against each signature using a timing-safe comparison
You must use the raw request body bytes for verification, not a parsed or re-serialized object. If your framework parses the body (e.g., as JSON), the re-serialized output may differ from the original payload and verification will fail. Read the raw body before any middleware processes it.
const crypto = require("crypto");
// Express: use express.raw() middleware to get the raw body
// app.post("/webhook", express.raw({ type: "application/json" }), handler)
function verifyWebhookSignature(signatureHeader, rawBody, secrets) {
const parts = signatureHeader.split(",");
const timestamp = parts.find((p) => p.startsWith("t="))?.slice(2);
const signatures = parts
.filter((p) => p.startsWith("v1="))
.map((p) => p.slice(3));
if (!timestamp || signatures.length === 0) return false;
// Protect against replay attacks (5 min tolerance)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (Math.abs(age) > 300) return false;
const signedPayload = `${timestamp}.${rawBody}`;
// Try each secret (supports key rotation)
for (const secret of secrets) {
const expected = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
const expectedBuf = Buffer.from(expected, "hex");
const match = signatures.some((sig) => {
const sigBuf = Buffer.from(sig, "hex");
return (
sigBuf.length === expectedBuf.length &&
crypto.timingSafeEqual(sigBuf, expectedBuf)
);
});
if (match) return true;
}
return false;
}
import hmac
import hashlib
import time
# Flask: use request.get_data() to get the raw body
# Django: use request.body
def verify_webhook_signature(signature_header, raw_body, secrets):
parts = signature_header.split(",")
timestamp = None
signatures = []
for part in parts:
if part.startswith("t="):
timestamp = part[2:]
elif part.startswith("v1="):
signatures.append(part[3:])
if not timestamp or not signatures:
return False
# Protect against replay attacks (5 min tolerance)
age = int(time.time()) - int(timestamp)
if abs(age) > 300:
return False
signed_payload = f"{timestamp}.{raw_body}"
# Try each secret (supports key rotation)
for secret in secrets:
expected = hmac.new(
secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256
).hexdigest()
if any(hmac.compare_digest(expected, sig) for sig in signatures):
return True
return False
Retry behavior
If your webhook endpoint returns a non-2xx status code, DocPipe retries the delivery:
- Up to 3 retries with exponential backoff
- Retry intervals: 1 minute, 5 minutes, 30 minutes
- After all retries fail, the callback is marked as failed (the run itself is not affected)
Testing locally
To test webhooks during development, use a tunnel service to expose your local server:
- Start your local webhook endpoint
- Use a tunneling tool to get a public URL
- Configure the callback output with the public URL
- Upload a test document to trigger the pipeline
Use your production webhook signing key verification in development too, to catch integration issues early.