Webhooks
Instead of polling for job results, provide a webhook_url and the API will POST results to your server when processing completes.
How to Use
Add webhook_url to any endpoint that supports async processing:
curl -X POST https://developer-api.qomplement.com/v1/extract \
-H "Authorization: Bearer sd_YOUR_API_KEY" \
-F "file=@large_document.pdf" \
-F "webhook_url=https://your-server.com/api/webhooks/qomplement"
Webhook Payload
When the job completes, the API sends a POST request to your URL:
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "extract",
"status": "completed",
"result": {
"model": "qomplement-OCR-v1",
"document_type": "invoice",
"language": "en",
"confidence": 95,
"fields": {
"invoice_number": "INV-2025-001234",
"total_amount": "1500.50",
"vendor_name": "ABC Corp"
},
"tables": [],
"pages_processed": 12,
"processing_time_ms": 45000
}
}
}
For fill jobs:
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "fill_pdf",
"status": "completed",
"result": {
"download_url": "/v1/jobs/550e8400-e29b-41d4-a716-446655440000/download",
"fields_filled": 8,
"fields_total": 12
}
}
}
For failed jobs:
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "extract",
"status": "failed",
"error": {
"code": "processing_error",
"message": "Could not extract text from document"
}
}
}
Retry Policy
| Attempt | Delay | Total Wait |
|---|---|---|
| 1st | Immediate | 0s |
| 2nd | 5 seconds | 5s |
| 3rd | 10 seconds | 15s |
A delivery is considered successful if your server returns an HTTP 2xx status code. After 3 failed attempts, the webhook is marked as undeliverable. You can still poll GET /v1/jobs/{id} to retrieve results.
Webhook Receiver Examples
Python (Flask)
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/api/webhooks/qomplement", methods=["POST"])
def handle_webhook():
payload = request.json
job_id = payload["job_id"]
data = payload["data"]
if data["status"] == "completed":
result = data["result"]
if data["type"] == "extract":
print(f"Extraction {job_id}: {result['document_type']}")
print(f" Fields: {result['fields']}")
elif data["type"] in ("fill_pdf", "fill_excel"):
print(f"Fill {job_id}: download at {result['download_url']}")
else:
print(f"Job {job_id} failed: {data['error']['message']}")
return jsonify({"received": True}), 200
Python (FastAPI)
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/api/webhooks/qomplement")
async def handle_webhook(request: Request):
payload = await request.json()
job_id = payload["job_id"]
data = payload["data"]
if data["status"] == "completed":
if data["type"] == "extract":
fields = data["result"]["fields"]
# Process extracted fields...
elif data["type"] in ("fill_pdf", "fill_excel"):
download_url = data["result"]["download_url"]
# Download and save the filled file...
else:
error = data["error"]
# Handle error...
return {"received": True}
Node.js (Express)
const express = require("express");
const app = express();
app.use(express.json());
app.post("/api/webhooks/qomplement", (req, res) => {
const { job_id, data } = req.body;
if (data.status === "completed") {
if (data.type === "extract") {
console.log(`Extraction ${job_id}:`, data.result.fields);
} else if (data.type === "fill_pdf" || data.type === "fill_excel") {
console.log(`Fill ${job_id}: download at ${data.result.download_url}`);
}
} else {
console.error(`Job ${job_id} failed:`, data.error.message);
}
res.status(200).json({ received: true });
});
app.listen(3000);
Go
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type WebhookPayload struct {
JobID string `json:"job_id"`
Data map[string]interface{} `json:"data"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
var payload WebhookPayload
json.NewDecoder(r.Body).Decode(&payload)
status := payload.Data["status"].(string)
jobType := payload.Data["type"].(string)
if status == "completed" {
result := payload.Data["result"].(map[string]interface{})
if jobType == "extract" {
fmt.Printf("Extraction %s: %v\n", payload.JobID, result["fields"])
} else {
fmt.Printf("Fill %s: download at %s\n", payload.JobID, result["download_url"])
}
} else {
errData := payload.Data["error"].(map[string]interface{})
fmt.Printf("Job %s failed: %s\n", payload.JobID, errData["message"])
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"received": true}`))
}
func main() {
http.HandleFunc("/api/webhooks/qomplement", webhookHandler)
http.ListenAndServe(":3000", nil)
}
Best Practices
- Respond quickly — Return
200within 5 seconds. Process results asynchronously on your side. - Handle duplicates — In rare cases, the same webhook may be delivered more than once. Use
job_idto deduplicate. - Use HTTPS — Always use an HTTPS URL for webhook endpoints.
- Validate the payload — Check that
job_idmatches a job you initiated.