LeapOCRLeapOCR DocsAPI, SDKs, and integration guides

Webhooks

Configure LeapOCR webhooks to receive signed job lifecycle events in real time

Webhooks

LeapOCR webhooks let your application react to OCR jobs as they move through the pipeline. When something important happens, such as a job completing or failing, LeapOCR sends a signed POST request to the endpoint you configured in the dashboard.

Webhooks are available on Starter, Growth, Scale, and Enterprise plans.

Events

You can subscribe to these event types:

EventWhen it fires
job.createdA new OCR job is created
job.startedProcessing begins
job.completedProcessing finishes successfully
job.failedProcessing ends with an error

Each subscription can include up to 10 event selections. Your endpoint must use HTTPS. In production, LeapOCR rejects localhost, loopback, private-network IPs, and direct IP addresses.

Set Up in the Dashboard

Creating a webhook in LeapOCR is straightforward:

Open Webhooks

Open the Webhooks section in your dashboard.

Create a subscription

Add a public HTTPS endpoint, choose the events you want to receive, and optionally add a description.

Save the secret immediately

When the webhook is created, copy the signing secret and store it securely. You will use it to verify incoming requests.

Test before going live

Before you rely on it in production, send a test event from the dashboard and confirm that your endpoint accepts and verifies the request.

What LeapOCR Sends

Webhook deliveries are sent as JSON POST requests with these headers:

  • Content-Type: application/json
  • X-R2-Signature: <hex hmac digest>
  • User-Agent: LeapOCR-Webhook/1.0

The body contains event metadata plus the job fields that are relevant for that stage of processing.

Example job.completed payload

{
  "event_type": "job.completed",
  "event_id": "b2837c9d-3356-4d6e-b31c-1d931fb8e6ec",
  "timestamp": "2026-03-19T10:21:44Z",
  "job_id": "6d3e2a79-8f80-4d71-9ab9-b8410d14ed1d",
  "user_id": "user_123",
  "team_id": "team_456",
  "organization_id": "org_123",
  "status": "completed",
  "file_name": "invoice.pdf",
  "file_size": 483920,
  "total_pages": 3,
  "processed_pages": 3,
  "created_at": "2026-03-19T10:21:10Z",
  "completed_at": "2026-03-19T10:21:44Z",
  "success_pages": 3,
  "empty_pages": 0,
  "total_tokens": 1842,
  "credits_used": 3
}

Example job.failed payload

{
  "event_type": "job.failed",
  "event_id": "6dc45bf5-2c8f-4cbc-b7b6-1015d3e64c2e",
  "timestamp": "2026-03-19T10:28:12Z",
  "job_id": "6d3e2a79-8f80-4d71-9ab9-b8410d14ed1d",
  "user_id": "user_123",
  "team_id": "team_456",
  "organization_id": "org_123",
  "status": "failed",
  "file_name": "invoice.pdf",
  "file_size": 483920,
  "total_pages": 3,
  "processed_pages": 1,
  "created_at": "2026-03-19T10:21:10Z",
  "error_message": "Document processing failed"
}

Example: Use job.completed to fetch the result

One useful pattern is to let the webhook tell you when the job is ready, then use the SDK to load the full result.

  1. Receive the signed webhook.
  2. Verify the signature.
  3. Read job_id from the payload.
  4. Use the SDK to fetch the full OCR result for that job.

Example in Node.js with the JavaScript SDK:

import express from "express";
import { LeapOCR } from "leapocr";

const app = express();

const leapocr = new LeapOCR({
  apiKey: process.env.LEAPOCR_API_KEY,
});

app.post(
  "/leapocr/webhooks",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.header("X-R2-Signature");
    const secret = process.env.LEAPOCR_WEBHOOK_SECRET;

    if (!signature || !secret) {
      res.status(400).send("Missing signature headers");
      return;
    }

    const rawBody = req.body.toString("utf8");

    if (!(await LeapOCR.verifyWebhookSignature(rawBody, signature, secret))) {
      res.status(401).send("Invalid signature");
      return;
    }

    const event = JSON.parse(rawBody);

    if (event.event_type === "job.completed" && event.job_id) {
      const result = await leapocr.ocr.getJobResult(event.job_id);

      console.log("Completed job:", event.job_id);
      console.log("Credits used:", result.credits_used);
      console.log("Pages:", result.total_pages);
      console.log("First page result:", result.pages?.[0]?.result);

      // Persist the result, trigger a workflow, or notify your users here.
    }

    if (event.event_type === "job.failed" && event.job_id) {
      console.error("Job failed:", event.job_id, event.error_message);
    }

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

Verify Signatures

LeapOCR signs every outbound webhook with HMAC-SHA256. The X-R2-Signature header contains the lowercase hex digest of the raw request body, signed with your webhook secret.

Verify the signature against the raw request body exactly as it was received. If you parse and re-serialize JSON before verifying, the signature check can fail.

Verify with an SDK Helper

import express from "express";
import { LeapOCR } from "leapocr";

const app = express();

app.post(
  "/leapocr/webhooks",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const rawBody = req.body.toString("utf8");
    const signature = req.header("X-R2-Signature") ?? "";
    const secret = process.env.LEAPOCR_WEBHOOK_SECRET ?? "";

    if (!(await LeapOCR.verifyWebhookSignature(rawBody, signature, secret))) {
      res.status(401).send("Invalid signature");
      return;
    }

    const event = JSON.parse(rawBody);
    res.status(200).json({ received: true, eventType: event.event_type });
  },
);
import json
import os

from fastapi import FastAPI, Header, HTTPException, Request
from leapocr import LeapOCR

app = FastAPI()

@app.post("/leapocr/webhooks")
async def leapocr_webhook(
    request: Request,
    x_r2_signature: str | None = Header(default=None),
):
    raw_body = await request.body()
    secret = os.environ.get("LEAPOCR_WEBHOOK_SECRET", "")

    if not LeapOCR.verify_webhook_signature(raw_body, x_r2_signature or "", secret):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = json.loads(raw_body)
    return {"received": True, "eventType": event["event_type"]}
<?php

use LeapOCR\LeapOCR;

$rawBody = file_get_contents('php://input') ?: '';
$signature = $_SERVER['HTTP_X_R2_SIGNATURE'] ?? '';
$secret = (string) getenv('LEAPOCR_WEBHOOK_SECRET');

if (!LeapOCR::verifyWebhookSignature($rawBody, $signature, $secret)) {
    http_response_code(401);
    echo 'Invalid signature';
    exit;
}

$event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
http_response_code(200);
echo json_encode(['received' => true, 'eventType' => $event['event_type']]);
package main

import (
    "encoding/json"
    "io"
    "net/http"
    "os"
    ocr "github.com/leapocr/leapocr-go"
)

func leapocrWebhook(w http.ResponseWriter, r *http.Request) {
    rawBody, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Invalid body", http.StatusBadRequest)
        return
    }

    signature := r.Header.Get("X-R2-Signature")
    secret := os.Getenv("LEAPOCR_WEBHOOK_SECRET")

    if !ocr.VerifyWebhookSignature(rawBody, signature, secret) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    var event map[string]any
    if err := json.Unmarshal(rawBody, &event); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    _ = json.NewEncoder(w).Encode(map[string]any{
        "received":  true,
        "eventType": event["event_type"],
    })
}

Delivery Behavior

LeapOCR retries temporary delivery failures automatically:

  • Transport errors are retried automatically.
  • 429, 408, and 5xx responses are retried with backoff.
  • Other 4xx responses are treated as final delivery failures.
  • Successful deliveries reset the subscription failure counter.
  • After 5 consecutive failed deliveries, the subscription is automatically disabled.

Your endpoint should return a 2xx response as soon as the event is accepted. If you need to do heavier work, queue it in your own background job system after verification.

Best Practices

  • Respond with 2xx as soon as the event is accepted.
  • Verify X-R2-Signature before doing any work.
  • Store and de-duplicate event_id values in your own system.
  • Regenerate the secret immediately if you suspect it was exposed.
  • Use the dashboard test action before switching real automations on.

For exact request and response schemas, see the SDK API Reference or download the OpenAPI spec.

On this page