Loading...
Loading...
Get notified when async OCR jobs complete by providing a callback URL.
When you submit an OCR request in async mode with a webhook_url, OpenOCR will POST the results to your URL when processing completes (or fails).
webhook_url202 Accepted response immediatelycurl -X POST https://api.open-ocr.com/v1/ocr \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"input": {
"type": "url",
"url": "https://example.com/invoice.pdf"
},
"engine": "mistral-mistral-ocr-4-1",
"mode": "async",
"page_count": 10,
"webhook_url": "https://your-server.com/ocr-callback"
}'Your endpoint receives a JSON POST with the full OCR result:
{
"event": "ocr.completed",
"request_id": "req_abc123",
"status": "succeeded",
"engine": "mistral-mistral-ocr-4-1",
"provider_latency_ms": 1250,
"provider_error": null,
"extracted_text": "INVOICE #1234\nDate: 2026-02-23\nTotal: $1,250.00",
"extracted_text_preview": "INVOICE #1234\nDate: 2026-02-23\n...",
"cost_usd": 0.015,
"pages": 3,
"pages_total": 3,
"warning": null,
"timestamp": "2026-02-23T14:05:11.123+00:00"
}{
"event": "ocr.completed",
"request_id": "req_def456",
"status": "refunded",
"engine": "mistral-mistral-ocr-4-1",
"provider_latency_ms": 60000,
"provider_error": "Engine timeout after 60s",
"extracted_text": null,
"extracted_text_preview": null,
"cost_usd": 0,
"warning": null,
"timestamp": "2026-02-23T14:06:11.123+00:00"
}A failure whose engine never ran carries cost_usd: 0, as above. When the engine did run before failing, the provider bills us for the work its model did and we pass that through at cost — so cost_usd is non-zero. The payload is otherwise identical — a charged failure reports the same status it would have reported for free, and explains itself through provider_error:
{
"event": "ocr.completed",
"request_id": "req_def789",
"status": "refunded",
"engine": "google-gemini-3.1-flash-lite",
"provider_latency_ms": 18400,
"provider_error": "Engine timed out after generating a partial response",
"extracted_text": null,
"extracted_text_preview": null,
"cost_usd": 0.0004,
"warning": "the engine ran and produced billable usage; you were charged the provider's cost for the attempt ($0.0004), not the full price",
"timestamp": "2026-02-23T14:07:11.123+00:00"
}Full policy on the Error Codes page.
| Field | Type | Description |
|---|---|---|
| event | string | Always "ocr.completed" |
| request_id | string | The original request ID from your submission |
| status | string | 'succeeded' on success. On failure, 'partial' when the preview holds some recovered text, otherwise 'refunded'. 'failed' when the router could not read the request record back from its database |
| engine | string | Engine ID that processed the request |
| extracted_text | string | null | Full extracted text (null on failure) |
| extracted_text_preview | string | null | Truncated preview of extracted text |
| cost_usd | number | Credits charged in USD. 0 when the engine never ran; the provider's cost for the attempt, passed through at cost, when it ran and then failed |
| provider_latency_ms | integer | null | Engine processing time in ms |
| provider_error | string | null | Error message if processing failed |
| pages | integer | Success only, when known: pages processed and billed |
| pages_total | integer | Success only, when known: pages in the source document. Larger than pages means only part of it was processed; set page_count for more |
| warnings | string[] | Success only, when there is something to report, e.g. "document has 3 pages; processed 1. Set page_count to process more." |
| warning | string | null | Non-fatal note about a successful result, when the engine reported one |
| timestamp | string | ISO 8601 time the webhook was sent. If pages is absent (failures, or a count the router could not determine), fetch GET /v1/ocr/{request_id} |
OpenOCR retries webhook delivery up to 3 times with exponential backoff (500ms, 1s, 1.5s) if your endpoint returns a non-2xx status or times out (10s limit).
Verify webhook authenticity using these techniques:
X-OpenOCR-Request-Id header matches a request you submittedrequest_id in the body against your records// Express.js webhook handler
import express from 'express';
const app = express();
app.use(express.json());
// Replace with a lookup in your own database of the request_id values you saved when you submitted.
async function isMyRequest(requestId) { /* look up requestId in your DB */ return true; }
async function processOcrResult(payload) { /* store or forward the result */ }
app.post('/ocr-callback', async (req, res) => {
const requestId = req.headers['x-openocr-request-id'];
// Verify this is a request you actually made
if (!(await isMyRequest(requestId))) {
return res.status(404).send();
}
// Acknowledge immediately
res.status(200).send();
// Process asynchronously
processOcrResult(req.body).catch(console.error);
});The webhook payload includes a preview of the extracted text. To get the full text, poll the status endpoint:
curl https://api.open-ocr.com/v1/ocr/req_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"Webhook URLs, labels, and events can be managed through dashboard API routes. These endpoints live on open-ocr.com/api/webhooks (the webapp), not api.open-ocr.com/v1/webhooks (which 404s — that surface isn't routed by the OCR router).
Auth: these routes require a logged-in dashboard session cookie (Supabase auth), not an API key. They're intended to back the webhooks UI in the dashboard, not for headless CLI automation. If you need programmatic management, open an issue.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/webhooks | List your webhooks (all fields except secret). |
| POST | /api/webhooks | Create a webhook. Body: { url, label?, events? }. Returns secret exactly once. Max 10 per user. |
| PATCH | /api/webhooks/{id} | Update url, label, events, or is_active. All fields optional. |
| DELETE | /api/webhooks/{id} | Permanently remove a webhook. |
| POST | /api/webhooks/{id}/test | Send a synthetic webhook.test ping; recorded in delivery log. |
| GET | /api/webhooks/{id}/deliveries | Recent delivery attempts (status, response code, latency, payload). |
# From a browser session (or with a Supabase access-token cookie set)
curl -X POST https://open-ocr.com/api/webhooks \
-H "Content-Type: application/json" \
-H "Cookie: $YOUR_SESSION_COOKIE" \
-d '{
"url": "https://yourapp.example.com/ocr-callback",
"label": "production",
"events": ["ocr.completed", "ocr.failed"]
}'
# Response includes the signing secret — store it; we don't show it again.
# {"id":"…","url":"…","label":"production","secret":"whsec_…","events":[…],"is_active":true,"created_at":"…"}