Skip to main content
When your pipeline includes a callback output node, DocPipe sends the processing results to your webhook URL when a run completes.

Setting up callbacks

  1. Add a Callback output node to your pipeline
  2. Configure the URL where you want to receive results
  3. Optionally add custom headers (for example, an authorization token)
  4. Save and activate the pipeline

Payload format

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.
  1. Generate a webhook signing key in your organization settings
  2. DocPipe includes a signature in the request headers
  3. 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:
  1. Parse the header to extract the timestamp (t=) and signature(s) (v1=)
  2. Reconstruct the signed payload as {timestamp}.{raw_body}
  3. Compute an HMAC-SHA256 hash using your signing key
  4. 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:
  1. Start your local webhook endpoint
  2. Use a tunneling tool to get a public URL
  3. Configure the callback output with the public URL
  4. Upload a test document to trigger the pipeline
Use your production webhook signing key verification in development too, to catch integration issues early.