Skip to main content

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

AttemptDelayTotal Wait
1stImmediate0s
2nd5 seconds5s
3rd10 seconds15s

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 200 within 5 seconds. Process results asynchronously on your side.
  • Handle duplicates — In rare cases, the same webhook may be delivered more than once. Use job_id to deduplicate.
  • Use HTTPS — Always use an HTTPS URL for webhook endpoints.
  • Validate the payload — Check that job_id matches a job you initiated.