Webhooks
A webhook is an HTTP request DocSlice sends to your server when an asynchronous job ends, so you do not have to poll.
Turn it on
Send mode=async together with a webhook_url:
curl -s https://api.docslice.app/v1/convert \
-H "Authorization: Bearer $DOCSLICE_API_KEY" \
-F "file=@invoice.pdf" \
-F "format=json" \
-F "mode=async" \
-F "webhook_url=https://example.com/webhooks/docslice"
import json
import os
import requests
api_key = os.environ["DOCSLICE_API_KEY"]
with open("invoice.pdf", "rb") as handle:
response = requests.post(
"https://api.docslice.app/v1/convert",
headers={"Authorization": f"Bearer {api_key}"},
files={"file": ("invoice.pdf", handle, "application/pdf")},
data={"format": "json", "mode": "async", "webhook_url": "https://example.com/webhooks/docslice"},
timeout=120,
)
print(json.dumps(response.json(), indent=2))
const fs = require("node:fs");
const form = new FormData();
form.append("file", new Blob([fs.readFileSync("invoice.pdf")], { type: "application/pdf" }), "invoice.pdf");
form.append("format", "json");
form.append("mode", "async");
form.append("webhook_url", "https://example.com/webhooks/docslice");
fetch("https://api.docslice.app/v1/convert", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.DOCSLICE_API_KEY}` },
body: form,
})
.then((response) => response.json())
.then((data) => console.log(JSON.stringify(data, null, 2)));
<?php
$curl = curl_init('https://api.docslice.app/v1/convert');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('DOCSLICE_API_KEY')],
CURLOPT_POSTFIELDS => [
'file' => new CURLFile('invoice.pdf', 'application/pdf', 'invoice.pdf'),
'format' => 'json',
'mode' => 'async',
'webhook_url' => 'https://example.com/webhooks/docslice',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
echo curl_exec($curl);
curl_close($curl);
The URL must be an absolute http or https address. In production it must point to a public host: addresses on private or internal networks are refused with 400.
Events
DocSlice sends a POST with a JSON body. The event field tells you which one.
document.completed: a single document was converted.
{
"event": "document.completed",
"job_id": "3f2b8c1e",
"file_name": "invoice.pdf",
"pages_count": 1,
"output_format": "json",
"schema_applied": true,
"ocr_applied": false,
"input_truncated": false,
"data": {"invoice_number": "INV-2847", "total": 1240.0},
"completed_at": "2026-10-12T09:30:00"
}
document.failed: the conversion did not finish.
{"event": "document.failed", "job_id": "3f2b8c1e", "file_name": "invoice.pdf", "error": "..."}
batch.completed: a batch finished. It carries batch_id, total_files, successful_files, failed_files, total_pages_billed, output_format, results (one entry per file, as in the batch answer) and completed_at.
batch.failed: the batch failed as a whole, with batch_id and error.
Delivery rules
- DocSlice makes one attempt, with a timeout of 10 seconds, and does not follow redirects. There are no retries.
- Your endpoint must answer
200,201,202or204for the delivery to count. The job'swebhook_statusthen becomesdelivered; any other answer, or no answer in time, sets it tofailed. This is recorded for the completion events (document.completed,batch.completed). Fordocument.failedandbatch.failedthe call is made but its outcome is not recorded, sowebhook_statusstays empty: for failures, rely on reading the job. - The request carries
Content-Type: application/jsonandUser-Agent: DocSlice-Webhook/1.0. It is not signed.
Build a reliable receiver
- Answer quickly and do the heavy work afterwards: the timeout is short. Return
204as soon as you have stored the event. - Be idempotent. Use
job_id(orbatch_id) to recognise an event you already processed. - Do not trust the body alone. Because requests are not signed, confirm what matters by reading the job with
GET /v1/jobs/{job_id}using your API key. - Keep a fallback. With no retries, a missed delivery is lost: poll the jobs you are waiting for after a while, and check
webhook_status(set for completion events). The result stays available for 24 hours. - Serve the endpoint over HTTPS.
# simulate a delivery to a receiver listening on localhost:8000
curl -s -X POST http://localhost:8000/webhooks/docslice \
-H "Content-Type: application/json" \
-d '{"event": "document.completed", "job_id": "3f2b8c1e", "file_name": "invoice.pdf", "pages_count": 1, "output_format": "json", "schema_applied": true, "ocr_applied": false, "input_truncated": false, "data": {"invoice_number": "INV-2847", "total": 1240.0}, "completed_at": "2026-10-12T09:30:00"}'
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
if payload["event"] == "document.completed":
print("done:", payload["job_id"], payload["pages_count"], "pages")
elif payload["event"] == "document.failed":
print("failed:", payload["job_id"], payload["error"])
self.send_response(204) # 200, 201, 202 or 204 all count as delivered
self.end_headers()
HTTPServer(("0.0.0.0", 8000), Receiver).serve_forever()
const http = require("node:http");
http
.createServer((request, response) => {
let body = "";
request.on("data", (chunk) => (body += chunk));
request.on("end", () => {
const payload = JSON.parse(body);
if (payload.event === "document.completed") {
console.log("done:", payload.job_id, payload.pages_count, "pages");
} else if (payload.event === "document.failed") {
console.log("failed:", payload.job_id, payload.error);
}
response.writeHead(204); // 200, 201, 202 or 204 all count as delivered
response.end();
});
})
.listen(8000);
<?php
// webhook.php: point webhook_url at this script
$payload = json_decode(file_get_contents('php://input'), true);
if ($payload['event'] === 'document.completed') {
error_log("done: {$payload['job_id']} {$payload['pages_count']} pages");
} elseif ($payload['event'] === 'document.failed') {
error_log("failed: {$payload['job_id']} {$payload['error']}");
}
http_response_code(204); // 200, 201, 202 or 204 all count as delivered
The cURL tab simulates a delivery to a receiver running on your machine, which is a quick way to test your handler before going live.