AI-powered document extraction API

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.

1

Create a schema

Create a schema in the Zendix dashboard that defines the exact fields you want to extract from documents.

2

Send files or text

Upload files, send text content, or combine both in a single extraction request.

3

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.

Create Request
Upload Files
Queue Job
AI Processing
Structured Result
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.

POST/api/v1/jobs/create/
Python
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())
JavaScript
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
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"
JSON Response
{
  "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
  }
}
What happens next?

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.

Keep your API keys secure

Your API keys provide full access to your Zendix account. Never expose them in frontend applications, public repositories, or client-side code.

1

Create an API key

Generate an API key from the Zendix dashboard.

2

Include it in requests

Send the API key in the Authorization header using the Bearer format.

Python
response = requests.get(url, headers = {
    "Authorization": "Bearer YOUR_API_KEY"
})
JavaScript
const response = await fetch(url, {
  headers: {
    Authorization: "Bearer YOUR_API_KEY"
  }
});
cURL
curl https://zendix.app/api/v1/jobs/ \
  -H "Authorization: Bearer YOUR_API_KEY"

Authentication errors

Zendix returns standardized error responses when authentication fails.

Invalid API Key
{
  "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.

GET/api/v1/jobs/

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.
Python
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())
JavaScript
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
curl "https://zendix.app/api/v1/jobs/?status=successful&page=1&page_size=3&ordering=earliest" \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON Response
{
  "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.

pending
successful
failed

Pagination

Job lists are paginated to improve performance and reduce response sizes.

Default pagination

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.

POST/api/v1/jobs/create/

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.
Important rule

You must provide either files, body, or both.

Supported combinations

Files Body Valid
Yes
Yes
Yes
No

Limits

Python
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())
JavaScript
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
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"
JSON Response
{
  "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
  }
}
Next step

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.

GET/api/v1/jobs/{job_id}/detail/

Path parameters

Field Type Description
job_id integer The ID of the job to retrieve.
Python
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())
JavaScript
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
curl https://zendix.app/api/v1/jobs/145/detail/ \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON Response
{
  "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
  }
}
Polling behavior

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.

POST/api/v1/jobs/{job_id}/retry/

Path parameters

Field Type Description
job_id integer ID of the job to retry.
Important

Retry is only allowed for jobs with status successful or failed.

Python
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())
JavaScript
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
curl -X POST https://zendix.app/api/v1/jobs/145/retry/ \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON Response
{
  "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).

DELETE/api/v1/jobs/{job_id}/delete/

Path parameters

Field Type Description
job_id integer ID of the job to delete.
Warning

This action is irreversible.

Python
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())
JavaScript
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
curl -X DELETE https://zendix.app/api/v1/jobs/145/delete/ \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON Response
{
  "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.

GET/api/v1/jobs/{client_reference_id}/by-reference/

Path parameters

Field Type Description
client_reference_id string The client reference ID used when creating the job.
Python
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())
JavaScript
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
curl https://zendix.app/api/v1/jobs/145/by-reference/ \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON Response
{
  "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.

Note

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.

Error response format
{
  "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.

Important

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.

Webhook signature verification

Zendix signs each webhook using your webhook secret. Your server should verify the signature before trusting the payload.

Python verification
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)
JavaScript verification
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 test request
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

Example payload (job successful)
{
    "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
    }
}
Example payload (job failed)
{
    "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.

How to debug delivery issues

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