🔔 Webhooks

Get real-time notifications when documents finish processing. Set up webhooks to automate your workflow without polling.

Overview

Webhooks allow you to receive HTTP POST notifications when events occur in your ExtractBill account. Instead of repeatedly polling our API to check if a document has finished processing, we'll push updates to your server automatically.

Why Use Webhooks?

  • Real-time updates: Get notified instantly when processing completes
  • No polling required: Save API calls and reduce latency
  • Automatic retries: We retry failed deliveries for ~6 minutes
  • Flexible setup: Set global webhook or per-document override

Setup

There are two ways to configure webhooks: globally for all documents, or per-document when uploading.

Option 1: Global Webhook (Recommended)

Set a default webhook URL in your account settings. This URL will be used for all documents unless overridden.

  1. 1. Go to SettingsWebhooks
  2. 2. Enter your webhook URL (must be HTTPS)
  3. 3. Optionally set a secret token for verification
  4. 4. Click "Save"

💡 Tip: Global webhooks are perfect for production environments where all documents should trigger the same notification endpoint.

Option 2: Per-Document Webhook

Override the global webhook by specifying webhook_url when uploading a document via API.

curl -X POST https://www.extractbill.com/api/v1/documents \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@invoice.pdf" \
  -F "webhook_url=https://your-app.com/webhooks/extractbill"

⚠️ Note: Per-document webhooks override the global webhook. If both are set, only the per-document webhook will be called.

Event Types

ExtractBill sends webhooks for the following events:

document.completed

Sent when a document has been successfully parsed. Contains full extracted data in the payload.

document.failed

Sent when document parsing fails due to an error (invalid file, OpenAI error, etc.). Contains error details in the payload.

Payload Structure

All webhook requests are sent as POST with Content-Type: application/json.

For a detailed breakdown of the parsed document structure, see the JSON Schema Documentation.

Successful Document (document.completed)

{
  "event": "document.completed",
  "document": {
    "id": "d-abc123xyz789",
    "status": "completed",
    "file_name": "invoice_2024_001.pdf",
    "file_size": 245760,
    "file_type": "application/pdf",
    "parsed_data": {
      "identifier": "INV-2024-001",
      "date": "2024-01-15",
      "total_amount": 1250.00,
      "currency": "USD",
      "supplier": {
        "name": "Acme Corp",
        "address": "123 Main St, City, State 12345",
        "tax_id": "12-3456789"
      },
      "items": [
        {
          "description": "Web Development Services",
          "quantity": 1,
          "unit_price": 1000.00,
          "total_price": 1000.00
        },
        {
          "description": "Hosting (Annual)",
          "quantity": 1,
          "unit_price": 250.00,
          "total_price": 250.00
        }
      ]
    },
    "created_at": "2024-01-20T10:30:00Z",
    "completed_at": "2024-01-20T10:30:05Z"
  }
}

Failed Document (document.failed)

{
  "event": "document.failed",
  "document": {
    "id": "d-abc123xyz789",
    "status": "failed",
    "file_name": "corrupted.pdf",
    "file_size": 12345,
    "file_type": "application/pdf",
    "error_message": "Failed to extract text from PDF: File is corrupted",
    "created_at": "2024-01-20T10:30:00Z",
    "failed_at": "2024-01-20T10:30:03Z"
  }
}

Security

Webhook security is critical. Always verify incoming requests to prevent unauthorized access.

Secret Token Verification

ExtractBill sends a secret token in the X-ExtractBill-Token header with every webhook request. Compare this token with the one you set in Settings.

// Verify webhook token
$receivedToken = $request->header('X-ExtractBill-Token');
$expectedToken = config('services.extractbill.webhook_secret');

if ($receivedToken !== $expectedToken) {
    abort(401, 'Invalid webhook token');
}

// Process webhook...
$payload = $request->json()->all();
// Verify webhook token
const receivedToken = req.headers['x-extractbill-token'];
const expectedToken = process.env.EXTRACTBILL_WEBHOOK_SECRET;

if (receivedToken !== expectedToken) {
  return res.status(401).json({ error: 'Invalid token' });
}

// Process webhook...
const payload = req.body;
# Verify webhook token
received_token = request.headers.get('X-ExtractBill-Token')
expected_token = os.getenv('EXTRACTBILL_WEBHOOK_SECRET')

if received_token != expected_token:
    return jsonify({'error': 'Invalid token'}), 401

# Process webhook...
payload = request.get_json()

🔒 Security Best Practice: Always verify the token before processing the webhook payload. Never trust incoming requests without validation.

HTTPS Required

All webhook URLs must use HTTPS. HTTP URLs are rejected for security reasons.

SSRF Protection

ExtractBill blocks webhook URLs pointing to private IP ranges (localhost, 192.168.x.x, 10.x.x.x, etc.) to prevent Server-Side Request Forgery attacks.

Retries & Failures

If your webhook endpoint is temporarily unavailable, ExtractBill will automatically retry delivery.

Retry Schedule

  • Attempt 1: Immediate delivery (when event occurs)
  • Attempt 2: +10 seconds after first failure
  • Attempt 3: +1 minute after second failure
  • Attempt 4: +5 minutes after third failure (final attempt)

Total retry window: ~6 minutes across 4 attempts

Success Criteria

Your webhook endpoint must respond with HTTP status 200 within 5 seconds for the delivery to be considered successful.

  • HTTP 200-299: Success (no retries)
  • HTTP 4xx, 5xx: Failure (retries triggered)
  • Timeout: No response within 5s (retries triggered)

⚠️ Important: After 4 failed attempts, the webhook is marked as permanently failed. You can view failed deliveries in SettingsWebhooks and manually retry them.

Testing Webhooks

Test your webhook endpoint before going live.

Local Development with ngrok

Use ngrok to expose your local development server to the internet for webhook testing.

  1. 1. Install ngrok: brew install ngrok
  2. 2. Start your local server on port 8000
  3. 3. Run: ngrok http 8000
  4. 4. Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
  5. 5. Set this as your webhook URL in Settings

Webhook Testing Tools

Use these services to inspect webhook payloads:

  • webhook.site - Instantly get a unique webhook URL and inspect requests
  • requestbin.com - Collect and debug webhook requests

Implementation Examples

Complete webhook handler examples in different languages.

// routes/web.php
Route::post('/webhooks/extractbill', [WebhookController::class, 'handle']);

// app/Http/Controllers/WebhookController.php
class WebhookController extends Controller
{
    public function handle(Request $request)
    {
        // 1. Verify token
        $token = $request->header('X-ExtractBill-Token');
        if ($token !== config('services.extractbill.webhook_secret')) {
            abort(401, 'Invalid token');
        }

        // 2. Parse payload
        $event = $request->input('event');
        $document = $request->input('document');

        // 3. Handle event
        match ($event) {
            'document.completed' => $this->handleCompleted($document),
            'document.failed' => $this->handleFailed($document),
            default => Log::warning("Unknown webhook event: {$event}"),
        };

        return response()->json(['success' => true]);
    }

    private function handleCompleted(array $document)
    {
        // Save to database, send email, etc.
        Log::info("Document {$document['id']} completed", $document);
    }

    private function handleFailed(array $document)
    {
        // Alert admin, retry, etc.
        Log::error("Document {$document['id']} failed: {$document['error_message']}");
    }
}
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/extractbill', (req, res) => {
  // 1. Verify token
  const receivedToken = req.headers['x-extractbill-token'];
  const expectedToken = process.env.EXTRACTBILL_WEBHOOK_SECRET;

  if (receivedToken !== expectedToken) {
    return res.status(401).json({ error: 'Invalid token' });
  }

  // 2. Parse payload
  const { event, document } = req.body;

  // 3. Handle event
  switch (event) {
    case 'document.completed':
      handleCompleted(document);
      break;
    case 'document.failed':
      handleFailed(document);
      break;
    default:
      console.warn(`Unknown event: ${event}`);
  }

  res.json({ success: true });
});

function handleCompleted(document) {
  console.log(`Document ${document.id} completed`, document);
  // Save to database, send notification, etc.
}

function handleFailed(document) {
  console.error(`Document ${document.id} failed: ${document.error_message}`);
  // Alert admin, retry, etc.
}

app.listen(3000, () => console.log('Webhook server running on port 3000'));
from flask import Flask, request, jsonify
import os

app = Flask(__name__)

@app.route('/webhooks/extractbill', methods=['POST'])
def handle_webhook():
    # 1. Verify token
    received_token = request.headers.get('X-ExtractBill-Token')
    expected_token = os.getenv('EXTRACTBILL_WEBHOOK_SECRET')

    if received_token != expected_token:
        return jsonify({'error': 'Invalid token'}), 401

    # 2. Parse payload
    data = request.get_json()
    event = data.get('event')
    document = data.get('document')

    # 3. Handle event
    if event == 'document.completed':
        handle_completed(document)
    elif event == 'document.failed':
        handle_failed(document)
    else:
        print(f"Unknown event: {event}")

    return jsonify({'success': True})

def handle_completed(document):
    print(f"Document {document['id']} completed")
    # Save to database, send notification, etc.

def handle_failed(document):
    print(f"Document {document['id']} failed: {document['error_message']}")
    # Alert admin, retry, etc.

if __name__ == '__main__':
    app.run(port=5000)

Need Help with Webhooks?

Having trouble setting up webhooks? Check our Getting Started guide or contact support.

We use cookies to provide and improve our service. Essential cookies are required for the site to function. Analytics cookies help us understand how you use the site. Learn more