Skip to main content
Webhooks notify your application when flow executions complete, eliminating the need for polling.

How Webhooks Work

  1. You provide a webhookUrl when starting an execution
  2. Doclo sends an HTTP POST to your URL when the execution completes
  3. Your server verifies the signature and processes the result
// Start execution with webhook
const execution = await client.flows.run('flow_abc123', {
  input: {
    document: { base64, filename, mimeType: 'application/pdf' }
  },
  webhookUrl: 'https://your-app.com/api/doclo-webhook'
});

// Execution runs asynchronously
// Your webhook receives the result when complete

Webhook Payload

Doclo sends a JSON payload with the execution result:
interface WebhookEvent<T = unknown> {
  event: 'execution.completed' | 'execution.failed';
  timestamp: string;  // ISO 8601 format
  data: Execution<T>;
}
Example payload:
{
  "event": "execution.completed",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "id": "exec_abc123",
    "flowId": "flow_xyz789",
    "status": "success",
    "createdAt": "2024-01-15T10:29:55.000Z",
    "completedAt": "2024-01-15T10:30:00.000Z",
    "duration": 5000,
    "output": {
      "invoiceNumber": "INV-2024-001",
      "totalAmount": 1250.00
    },
    "metrics": {
      "tokensUsed": 1500,
      "cost": 0.0023,
      "stepsRun": 2,
      "stepsTotal": 2
    }
  }
}

Signature Verification

Webhooks include a signature header to verify authenticity. Always verify signatures in production.

Header Format

X-Doclo-Signature: sha256=abc123...

Verification with SDK

import { verifyWebhookSignature, parseWebhookEvent } from '@doclo/client';

app.post('/api/doclo-webhook', async (req, res) => {
  const signature = req.headers['x-doclo-signature'] as string;

  // Verify signature (requires raw body)
  const isValid = await verifyWebhookSignature(
    req.rawBody,  // Raw request body as string or Buffer
    signature,
    process.env.DOCLO_WEBHOOK_SECRET!
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Parse and validate the event
  const event = parseWebhookEvent(req.body);

  if (event.event === 'execution.completed') {
    console.log('Extraction complete:', event.data.output);
  } else if (event.event === 'execution.failed') {
    console.error('Extraction failed:', event.data.error);
  }

  res.status(200).json({ received: true });
});

Manual Verification

If you need to verify without the SDK:
import { createHmac } from 'crypto';

function verifySignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const [algorithm, providedHash] = signature.split('=');

  if (algorithm !== 'sha256') {
    return false;
  }

  const expectedHash = createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(providedHash),
    Buffer.from(expectedHash)
  );
}

Framework Examples

Next.js API Route

// app/api/doclo-webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyWebhookSignature, parseWebhookEvent } from '@doclo/client';

export async function POST(request: NextRequest) {
  const rawBody = await request.text();
  const signature = request.headers.get('x-doclo-signature');

  if (!signature) {
    return NextResponse.json(
      { error: 'Missing signature' },
      { status: 401 }
    );
  }

  const isValid = await verifyWebhookSignature(
    rawBody,
    signature,
    process.env.DOCLO_WEBHOOK_SECRET!
  );

  if (!isValid) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 401 }
    );
  }

  const body = JSON.parse(rawBody);
  const event = parseWebhookEvent(body);

  // Process the event
  if (event.event === 'execution.completed') {
    await processResult(event.data);
  }

  return NextResponse.json({ received: true });
}

Express

import express from 'express';
import { verifyWebhookSignature, parseWebhookEvent } from '@doclo/client';

const app = express();

// Use raw body for signature verification
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook', async (req, res) => {
  const signature = req.headers['x-doclo-signature'] as string;

  const isValid = await verifyWebhookSignature(
    req.body,
    signature,
    process.env.DOCLO_WEBHOOK_SECRET!
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = parseWebhookEvent(JSON.parse(req.body.toString()));

  // Process event...

  res.status(200).send('OK');
});

Event Types

EventDescription
execution.completedExecution finished successfully
execution.failedExecution failed with error

Retry Behavior

Doclo retries failed webhook deliveries:
AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
A delivery is considered failed if:
  • Your server returns a non-2xx status code
  • Connection times out (30 seconds)
  • Connection cannot be established
Always return a 200 response quickly. Process webhooks asynchronously (e.g., queue the work) if processing takes time.

Timestamp Validation

The SDK validates webhook timestamps by default to prevent replay attacks:
const event = parseWebhookEvent(body, {
  maxAgeSeconds: 300  // Reject webhooks older than 5 minutes (default)
});
If a webhook is too old, parseWebhookEvent throws an error.

Webhook Security

Never commit your webhook secret to version control.
Best practices:
  1. Always verify signatures in production
  2. Use HTTPS for your webhook endpoint
  3. Validate timestamps to prevent replay attacks
  4. Return 200 quickly and process asynchronously
  5. Store the secret securely in environment variables

Get Your Webhook Secret

  1. Log in to app.doclo.ai
  2. Navigate to Settings → Webhooks
  3. Copy your webhook signing secret
  4. Store it as DOCLO_WEBHOOK_SECRET in your environment

Next Steps