Extract structured data from documents using AI.
Zendix helps developers extract structured JSON data from invoices, receipts, purchase orders, PDFs, images, forms, and other business documents using asynchronous AI extraction jobs.
Create a schema
Create a schema in the Zendix dashboard that defines the exact fields you want to extract from documents.
Send files or text
Upload files, send text content, or combine both in a single extraction request.
Receive structured JSON
Zendix processes the job asynchronously and returns structured extraction results through polling or webhooks.
How Zendix works
Zendix uses asynchronous extraction jobs. This allows your application to continue running while AI processing happens in the background.
When you create a job, Zendix immediately returns a job ID. You can then either poll the job details endpoint or configure webhooks to receive results automatically.
| Job Status | Description |
|---|---|
pending |
The job has been created and is waiting to complete. |
successful |
The extraction completed successfully and results are available. |
failed |
The extraction failed and an error message is available. |
Your first request
The example below uploads an invoice PDF and creates a new extraction job using a schema ID.
import requests
url = "https://zendix.app/api/v1/jobs/create/"
headers = {
"Authorization": "Bearer YOUR_API_KEY"
}
files = [
("files", open("invoice.pdf", "rb"))
]
payload = {
"schema_id": 12,
"document_type": "invoice",
"client_reference_id": "inv_001"
}
response = requests.post(url, headers=headers, files=files, data=payload)
print(response.json())
const formData = new FormData();
formData.append("schema_id", 12);
formData.append("document_type", "invoice");
formData.append("client_reference_id", "inv_001");
formData.append("files", fileInput.files[0]);
const response = await fetch("https://zendix.app/api/v1/jobs/create/", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY"
},
body: formData
});
const data = await response.json();
console.log(data);
curl -X POST https://zendix.app/api/v1/jobs/create/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "schema_id=12" \
-F "document_type=invoice" \
-F "client_reference_id=inv_001" \
-F "files=@invoice.pdf"
{
"success": true,
"data": {
"status": "pending",
"result": null,
"error": null
},
"meta": {
"job_id": 145,
"client_reference_id": "inv_001" // present if job was created with a client_reference_id
}
}
Once the job is created, Zendix processes it in the background. You can either poll the job details endpoint for updates or configure webhooks to receive extraction results automatically.
Authentication
All Zendix API requests must include your API key in the Authorization header.
API keys authenticate requests and identify the business making the request.
Your API keys provide full access to your Zendix account. Never expose them in frontend applications, public repositories, or client-side code.
Create an API key
Generate an API key from the Zendix dashboard.
Include it in requests
Send the API key in the Authorization header using the Bearer format.
response = requests.get(url, headers = {
"Authorization": "Bearer YOUR_API_KEY"
})
const response = await fetch(url, {
headers: {
Authorization: "Bearer YOUR_API_KEY"
}
});
curl https://zendix.app/api/v1/jobs/ \
-H "Authorization: Bearer YOUR_API_KEY"
Authentication errors
Zendix returns standardized error responses when authentication fails.
{
"success": false,
"error": {
"code": "authentication_failed",
"message": "Invalid API key."
},
"meta": null
}
List jobs
Retrieve a paginated list of jobs belonging to your business.
This endpoint is useful for dashboards, activity history, analytics, monitoring failed jobs, and viewing recent extraction activity.
Query parameters
| Field | Type | Description |
|---|---|---|
status |
string | Filter jobs by status. Possible values are pending, successful,
and failed. |
page |
integer | The page number to retrieve. |
page_size |
integer | The number of jobs to return per page. Default is 20. Maximum of 100. |
ordering |
string | Sort jobs by date. Possible values are latest and earliest. |
import requests
response = requests.get(
"https://zendix.app/api/v1/jobs/?status=successful&page=1&page_size=3&ordering=earliest",
headers={"Authorization": "Bearer YOUR_API_KEY"}
)
print(response.json())
const response = await fetch(
"https://zendix.app/api/v1/jobs/?status=successful&page=1&page_size=3&ordering=earliest",
{ headers: { Authorization: "Bearer YOUR_API_KEY" } }
);
const data = await response.json();
console.log(data);
curl "https://zendix.app/api/v1/jobs/?status=successful&page=1&page_size=3&ordering=earliest" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"success": true,
"data": {
"count": 120,
"next": "https://zendix.app/api/v1/jobs/?ordering=earliest&page=2&page_size=3&status=successful",
"previous": null,
"results": [
{
"job_id": 145,
"status": "successful",
"client_reference_id": "inv_001",
"created_at": "2026-05-10T12:45:00Z",
...
},
{
"job_id": 146,
"status": "failed",
"client_reference_id": null,
"created_at": "2026-05-10T12:50:00Z",
...
}
]
},
"meta": null
}
Filtering jobs
Zendix supports filtering jobs by status.
Pagination
Job lists are paginated to improve performance and reduce response sizes.
Zendix returns paginated responses using count, next,
previous, and results fields.
| Field | Type | Description |
|---|---|---|
count |
integer | Number of jobs retrieved for current page. |
next |
URL | The URL to access the next page jobs. |
previous |
URL | The URL to access the previous page jobs. |
results |
list | A list of all jobs for the current page. |
Create job
Create an extraction job by sending files, text content, or both. Zendix processes the job asynchronously and returns a job ID immediately.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
schema_id |
integer | yes | ID of the schema used for extraction. Must belong to your business. |
document_type |
string | yes | The type of document being processed (e.g invoice, receipt, form). |
files |
file[] | optional | Uploaded documents. Max 10 files. Supported formats: pdf, png, jpg, jpeg, webp, txt. |
body |
string | optional | Raw text content to extract data from. |
client_reference_id |
string | optional | External ID used for tracking and idempotency. |
You must provide either files, body, or both.
Supported combinations
| Files | Body | Valid |
|---|---|---|
| ✔ | ✖ | Yes |
| ✖ | ✔ | Yes |
| ✔ | ✔ | Yes |
| ✖ | ✖ | No |
Limits
- Maximum files: 10
- Maximum total upload size: 50MB
- Supported file formats: pdf, png, jpg, jpeg, webp, txt
import requests
url = "https://zendix.app/api/v1/jobs/create/"
headers = {
"Authorization": "Bearer YOUR_API_KEY"
}
files = [
("files", open("invoice.pdf", "rb"))
]
data = {
"schema_id": 12,
"document_type": "invoice",
"client_reference_id": "inv_001"
}
response = requests.post(url, headers=headers, files=files, data=data)
print(response.json())
const formData = new FormData();
formData.append("schema_id", 12);
formData.append("document_type", "invoice");
formData.append("client_reference_id", "inv_001");
formData.append("files", fileInput.files[0]);
const response = await fetch("https://zendix.app/api/v1/jobs/create/", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY"
},
body: formData
});
const data = await response.json();
console.log(data);
curl -X POST https://zendix.app/api/v1/jobs/create/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "schema_id=12" \
-F "document_type=invoice" \
-F "client_reference_id=inv_001" \
-F "files=@invoice.pdf"
{
"success": true,
"data": {
"status": "pending",
"result": null,
"error": null
},
"meta": {
"job_id": 145,
"client_reference_id": "inv_001" // present if job was created with a client_reference_id
}
}
After creating a job, you can poll for results using the job detail endpoint or receive results via webhooks.
Get job details
Retrieve detailed information about a specific job using its job ID. This endpoint is commonly used for polling job status until completion.
Path parameters
| Field | Type | Description |
|---|---|---|
job_id |
integer | The ID of the job to retrieve. |
import requests
job_id = 145
response = requests.get(
f"https://zendix.app/api/v1/jobs/{job_id}/detail/",
headers={"Authorization": "Bearer YOUR_API_KEY"}
)
print(response.json())
const jobId = 145;
const response = await fetch(
`https://zendix.app/api/v1/jobs/${jobId}/detail/`,
{
headers: { Authorization: "Bearer YOUR_API_KEY" }
}
);
const data = await response.json();
console.log(data);
curl https://zendix.app/api/v1/jobs/145/detail/ \
-H "Authorization: Bearer YOUR_API_KEY"
{
"success": true,
"data": {
"status": "successful",
"result": {
"invoice_number": "INV-1001",
"total": 250.00
},
"error": null,
"details": {
"body": "",
"document_type": "invoice",
"schema_id": 12,
"skipped": false,
"file_count": 1,
"created_at": "2026-05-10T12:45:00Z",
...
}
},
"meta": {
"job_id": 145,
"client_reference_id": "inv_001" // present if job was created with a client_reference_id
}
}
You can call this endpoint repeatedly until the job status becomes successful or
failed.
Retry job
Retry a previously failed job. This will reprocess the same job configuration.
Path parameters
| Field | Type | Description |
|---|---|---|
job_id |
integer | ID of the job to retry. |
Retry is only allowed for jobs with status successful or failed.
import requests
job_id = 145
response = requests.post(
f"https://zendix.app/api/v1/jobs/{job_id}/retry/",
headers={"Authorization": "Bearer YOUR_API_KEY"}
)
print(response.json())
const jobId = 145;
const response = await fetch(
`https://zendix.app/api/v1/jobs/${jobId}/retry/`,
{
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY" }
}
);
const data = await response.json();
console.log(data);
curl -X POST https://zendix.app/api/v1/jobs/145/retry/ \
-H "Authorization: Bearer YOUR_API_KEY"
{
"success": true,
"data": {
"status": "pending",
"result": null,
"error": null
},
"meta": {
"job_id": 145,
"client_reference_id": "inv_001" // present if job was created with a client_reference_id
}
}
Delete job
Permanently delete a job and its associated files (if any).
Path parameters
| Field | Type | Description |
|---|---|---|
job_id |
integer | ID of the job to delete. |
This action is irreversible.
import requests
job_id = 145
response = requests.delete(
f"https://zendix.app/api/v1/jobs/{job_id}/delete/",
headers={"Authorization": "Bearer YOUR_API_KEY"}
)
print(response.json())
const jobId = 145;
const response = await fetch(
`https://zendix.app/api/v1/jobs/${jobId}/delete/`,
{
method: "DELETE",
headers: { Authorization: "Bearer YOUR_API_KEY" }
}
);
const data = await response.json();
console.log(data);
curl -X DELETE https://zendix.app/api/v1/jobs/145/delete/ \
-H "Authorization: Bearer YOUR_API_KEY"
{
"success": true,
"data": {
"message": "Job deleted successfully."
},
"meta": {
"job_id": 145
}
}
Get job by client reference ID
Retrieve a job using its client reference ID. Useful when you do not store internal job IDs.
Path parameters
| Field | Type | Description |
|---|---|---|
client_reference_id |
string | The client reference ID used when creating the job. |
import requests
job_id = 145
response = requests.get(
f"https://zendix.app/api/v1/jobs/{job_id}/by-reference/",
headers={"Authorization": "Bearer YOUR_API_KEY"}
)
print(response.json())
const jobId = 145;
const response = await fetch(
`https://zendix.app/api/v1/jobs/${jobId}/by-reference/`,
{
headers: { Authorization: "Bearer YOUR_API_KEY" }
}
);
const data = await response.json();
console.log(data);
curl https://zendix.app/api/v1/jobs/145/by-reference/ \
-H "Authorization: Bearer YOUR_API_KEY"
{
"success": true,
"data": {
"job_id": 145,
"status": "successful"
},
"meta": {
"client_reference_id": "inv_001"
}
}
Rate limiting
Zendix enforces rate limits to ensure fair usage and system stability.
Rate limits are applied per API key and per business.
| Type | Rate Limit | Window |
|---|---|---|
| API key | 100 requests | per minute |
| Business | 300 requests | per minute |
All API keys belonging to a business can each make a maximum of 100 requests per minute, and collectively make a maximum of 300 requests per minute.
File limits
Zendix enforces strict file upload limits to ensure performance and reliability.
| Limit | Value |
|---|---|
| Maximum files | 10 files |
| Total upload size | 50MB |
| Supported formats | pdf, png, jpg, jpeg, webp, txt |
Error codes
All errors follow a standardized structure to make debugging simple and predictable.
{
"success": false,
"error": {
"code": "error_code",
"message": "Human readable message"
},
"meta": null
}
Common error codes
| Code | Meaning |
|---|---|
validation_error |
Invalid request data |
method_not_allowed |
Request method not allowed |
rate_limit_exceeded |
Too many requests |
authentication_failed |
Invalid API key |
parse_error |
Invalid form data |
file_storage_limit_exceeded |
Insufficient file storage |
file_not_stored |
A file in request files failed to upload |
duplicate_client_reference_id |
A job with client reference ID already exists |
job_not_found |
Job does not exist |
job_cannot_be_retried |
Job is pending and can't be retried |
job_cannot_be_deleted |
Job is pending and can't be deleted |
Webhooks
Zendix can send job completion events to your server using webhooks. Webhooks are useful when you want Zendix to notify your backend automatically instead of polling for results.
Webhook delivery is configured on your API key. When a webhook URL exists on the API key, Zendix will attempt to send webhook events for jobs created with that key.
How to enable webhooks
To enable webhooks, set a webhook URL on the API key in your Zendix dashboard. Zendix uses that URL as the destination for webhook events.
| Field | Type | Purpose |
|---|---|---|
webhook_url |
string | The URL Zendix sends webhook requests to. |
webhook_secret |
string | Used to sign webhook payloads so you can verify they came from Zendix. |
How to prepare your server
Your server must accept POST requests with JSON payloads. It should respond with a
successful status code quickly, ideally within a few seconds.
- Use an HTTPS endpoint.
- Accept JSON request bodies.
- Read the raw request body when verifying the signature.
- Return a 2xx response when the webhook is processed successfully.
Webhook signature verification
Zendix signs each webhook using your webhook secret. Your server should verify the signature before trusting the payload.
import hmac
import hashlib
signature = request.headers.get("X-Zendix-Signature")
raw_body = request.body
expected = hmac.new(
webhook_secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return Response(status=401)
import crypto from "crypto";
const signature = req.header("X-Zendix-Signature");
const rawBody = req.rawBody;
const expected = crypto
.createHmac("sha256", webhookSecret)
.update(rawBody)
.digest("hex");
if (signature !== expected) {
return res.sendStatus(401);
}
curl -X POST https://your-server.com/webhooks/zendix \
-H "Content-Type: application/json" \
-H "X-Zendix-Signature: your_signature_here" \
-d '{"event":"job.completed"}'
Webhook payload
{
"event": "job.completed",
"data": {
"status": "successful",
"result": {
"invoice_number": "INV-1001",
"total": 250.00
},
"error": null,
"details": {
"body": "",
"document_type": "invoice",
"schema_id": 12,
"skipped": false,
"file_count": 1,
"created_at": "2026-05-10T12:45:00Z",
...
}
},
"meta": {
"job_id": 145,
"client_reference_id": "inv_001" // present if job was created with a client_reference_id
}
}
{
"event": "job.failed",
"data": {
"status": "failed",
"result": null,
"error": "error_message",
"details": {
"body": "",
"document_type": "invoice",
"schema_id": 12,
"skipped": false,
"file_count": 1,
"created_at": "2026-05-10T12:45:00Z",
...
}
},
"meta": {
"job_id": 145,
"client_reference_id": "inv_001" // present if job was created with a client_reference_id
}
}
Webhook failure tracking
If Zendix fails to send a webhook for a job, the job keeps its extraction result, but the webhook delivery failure is recorded on the job.
job.webhook_failedindicates whether webhook delivery failed.job.webhook_last_errorstores the latest webhook error message.
If a webhook did not arrive, check the job record in Zendix and inspect webhook_failed
and webhook_last_error. This helps you understand whether Zendix attempted delivery and
why it failed. API key webhook settings like webhook URL and webhook secret are copied unto jobs when they are created.
This ensures queued jobs can still deliver webhook events even if the original API key is later changed or deleted. Remember to disable CSRF protection
for your server URL endpoint recieving webhooks, so as to ensure your server doesn't block the job's webhook POST request.
Notes
- Webhooks are sent from the extraction worker.
- Webhook delivery failure does not change the job result.
- Webhook URL is configured at the API key level.