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:

Start a job and get a webhook when it ends
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, 202 or 204 for the delivery to count. The job's webhook_status then becomes delivered; any other answer, or no answer in time, sets it to failed. This is recorded for the completion events (document.completed, batch.completed). For document.failed and batch.failed the call is made but its outcome is not recorded, so webhook_status stays empty: for failures, rely on reading the job.
  • The request carries Content-Type: application/json and User-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 204 as soon as you have stored the event.
  • Be idempotent. Use job_id (or batch_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.
Receive a webhook (your server)
# 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.