Developer documentation
The Doxbox API
Read your account's invoices and receipts, fetch the original files, list suppliers, generate exports and upload new documents. JSON over HTTPS, one bearer token, no SDK required.
curl https://app.doxbox.io/api/v1/documents \
-H "Authorization: Bearer $DOXBOX_API_KEY"
- Base URL
https://app.doxbox.io- Base path
/api/v1- Format
- JSON, UTF-8
- Auth
- Bearer token
What you can do
Seven endpoints. Everything is read-only unless your key was created with write access.
Access: read by default. Uploading documents requires a write-enabled key.
Quickstart
- Create a key, as an account admin, go to Account settings → Connections → API keys → Create key. Copy it (shown once).
- Set it in your shell:
export DOXBOX_API_KEY="dbx_..."Then make your first call:
curl https://app.doxbox.io/api/v1/documents \
-H "Authorization: Bearer $DOXBOX_API_KEY"
const res = await fetch("https://app.doxbox.io/api/v1/documents", { headers: { Authorization: `Bearer ${process.env.DOXBOX_API_KEY}` } }); if (!res.ok) throw new Error(`${res.status} ${(await res.json()).code}`); const { data, nextCursor } = await res.json();
import os, requests r = requests.get( "https://app.doxbox.io/api/v1/documents", headers={"Authorization": f"Bearer {os.environ['DOXBOX_API_KEY']}"}, timeout=30, ) r.raise_for_status() payload = r.json()
Authentication
Every request except /health requires an API key sent as a Bearer token:
Authorization: Bearer dbx_<prefix>_<secret>Each key belongs to a single account and can only ever touch that account's data. Revoking a key takes effect immediately.
Scopes
A key carries one of two permission levels, chosen at creation. Keys are
read-only unless you opt in to write; calling a write endpoint with a
read-only key returns 403 API_KEY_FORBIDDEN.
| Scope | Grants |
|---|---|
read | Read documents & suppliers, generate exports (default) |
write | Everything in read, plus uploading documents |
Conventions
Pagination
List endpoints use cursor pagination: ?limit= (1–100, default 25) and ?cursor= (the last item id from the previous page). Responses
carry nextCursor, an id, or null on the last page.
Rate limiting
Requests are limited per key (default 120 / minute).
Every response carries rate-limit headers; exceeding the limit returns 429 with a Retry-After header.
X-RateLimit-Limit: 120 X-RateLimit-Remaining: 118 X-RateLimit-Reset: 1785690000
Quotas
Uploads count against your plan's monthly document limit. When the limit
is reached, POST /documents returns 403 PLAN_LIMIT_EXCEEDED and
no documents in the batch are ingested, all-or-nothing, so you're never
left with a partial upload.
Duplicate handling
Uploads are de-duplicated by file content. Re-uploading a file Doxbox already has does not
create a second document, the response marks it "duplicate" and returns the
existing id. Uploads are safe to retry.
Recipes
Three things people actually do with this API, end to end.
Page through every document
Keep passing nextCursor back as cursor until it comes back
null. Nothing else terminates the loop.
const base = "https://app.doxbox.io/api/v1"; const auth = { Authorization: `Bearer ${process.env.DOXBOX_API_KEY}` }; let cursor = null, all = []; do { const url = new URL(base + "/documents"); url.searchParams.set("limit", "100"); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { headers: auth }); if (res.status === 429) { // respect the limiter await new Promise(r => setTimeout(r, (+res.headers.get("Retry-After") || 5) * 1000)); continue; } const page = await res.json(); all.push(...page.data); cursor = page.nextCursor; } while (cursor);
Download the original file of every unpaid invoice
The file endpoint answers with a 302 to a short-lived signed URL, so follow redirects and stream the body straight to disk.
import os, requests base = "https://app.doxbox.io/api/v1" auth = {"Authorization": f"Bearer {os.environ['DOXBOX_API_KEY']}"} docs, cursor = [], None while True: params = {"limit": 100, **({"cursor": cursor} if cursor else {})} page = requests.get(f"{base}/documents", headers=auth, params=params, timeout=30).json() docs += page["data"] cursor = page["nextCursor"] if not cursor: break for d in docs: if d["documentType"] != "Invoice" or d["paymentStatus"] != "Unpaid": continue r = requests.get(f"{base}/documents/{d['id']}/file", headers=auth, allow_redirects=True, timeout=60) r.raise_for_status() with open(f"{d['documentNumber'] or d['id']}.pdf", "wb") as f: f.write(r.content)
limit and cursor. Anything else, by type, status or supplier,
you filter yourself on the returned objects.Build last month's file for the accountant
One call. The response carries a signed download URL that is good for about 12 hours.
curl -X POST https://app.doxbox.io/api/v1/exports \ -H "Authorization: Bearer $DOXBOX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"exportType":"excel_monthly","filters":{"year":2026,"month":7}}'
Swap exportType for csv, pdf_merged or
zip. To export a hand-picked set instead of a whole month, send
"scope":"selected" with selectedDocumentIds.
Errors
Errors return a JSON body with a message and, where useful, a machine-readable code:
{ "message": "API key has expired", "code": "API_KEY_EXPIRED" }| HTTP | code | Meaning |
|---|---|---|
| 400 | , | Invalid request (bad parameters or body) |
| 401 | API_KEY_MISSING | No Authorization: Bearer header |
| 401 | API_KEY_INVALID | Unknown key |
| 401 | API_KEY_REVOKED | Key was revoked |
| 401 | API_KEY_EXPIRED | Key is past its expiry |
| 403 | API_KEY_FORBIDDEN | Key lacks the required scope (e.g. write) |
| 403 | PLAN_LIMIT_EXCEEDED | Monthly document quota reached |
| 404 | , | Not found, or not in your account |
| 429 | RATE_LIMITED | Rate limit exceeded (see Retry-After) |
Field reference
Enumerated values you'll see in responses:
documentType | Invoice · Receipt · CreditInvoice · Other |
paymentStatus | Paid · Unpaid |
currency | ILS · USD · EUR · GBP · JPY · AUD |
Amount fields are numbers in the document's currency. Dates are ISO 8601
strings and may be null when Doxbox could not read them from the document.
Service health and version. No authentication.
{ "status": "ok", "api": "v1" }List the account's documents, newest first. Cursor-paginated (limit, cursor).
curl "https://app.doxbox.io/api/v1/documents?limit=2" \ -H "Authorization: Bearer $DOXBOX_API_KEY"
{
"data": [
{
"id": 1042,
"documentNumber": "INV-2026-118",
"totalAmount": 1170.0,
"currency": "ILS",
"documentType": "Invoice",
"paymentStatus": "Unpaid",
"supplierName": "Acme Ltd"
}
],
"nextCursor": 1041
}A single document with full metadata and the ids of its image pages. A document that
isn't in your account returns 404, no cross-account existence leak.
{
"id": 1042,
"documentNumber": "INV-2026-118",
"documentType": "Invoice",
"netAmount": 1000.0,
"vat": 170.0,
"totalAmount": 1170.0,
"currency": "ILS",
"paymentStatus": "Unpaid",
"supplier": { "id": 9, "name": "Acme Ltd", "crn": "514123456" },
"images": [{ "id": 5001 }, { "id": 5002 }]
}Returns a 302 redirect to a fresh, short-lived signed URL for the
document's file. The storage bucket stays private. Optional ?image=<id> selects a specific page (defaults to the first).
curl -L https://app.doxbox.io/api/v1/documents/1042/file \
-H "Authorization: Bearer $DOXBOX_API_KEY" \
-o invoice.pdfUpload one or more documents. They run through the same pipeline as documents added in the
app, OCR, supplier matching, and de-duplication, and appear in your account tagged as
uploaded via the API. Send a multipart/form-data body with one or more files
in the files field. Counts against your monthly quota; the batch is
all-or-nothing.
curl -X POST https://app.doxbox.io/api/v1/documents \ -H "Authorization: Bearer $DOXBOX_API_KEY" \ -F "files=@invoice-july.pdf" \ -F "files=@receipt-123.pdf"
{
"results": [
{ "filename": "invoice-july.pdf", "documentId": 1055, "status": "created" },
{ "filename": "receipt-123.pdf", "documentId": 980, "status": "duplicate" }
],
"summary": { "total": 2, "created": 1, "duplicate": 1, "failed": 0 }
}Per-file status is created, duplicate, or failed (a failed file includes a reason and doesn't stop the rest
of the batch).
The account's suppliers (those with documents in the account), with per-supplier aggregates, sorted by total amount.
{
"data": [
{ "id": 8, "name": "Globex", "crn": "514000222", "documentCount": 12, "totalAmount": 18400.0 },
{ "id": 9, "name": "Acme Ltd", "crn": "514123456", "documentCount": 5, "totalAmount": 5850.0 }
]
}Generate an export file for a month's documents. Returns a signed download URL valid for about 12 hours. Exporting via the API never changes your documents.
| Field | Type | Notes |
|---|---|---|
exportType | string | excel_monthly · csv · pdf_merged · zip |
scope | string | all_results (default) or selected |
selectedDocumentIds | number[] | required when scope is selected |
filters.year | number | e.g. 2026 |
filters.month | number | 1–12 |
curl -X POST https://app.doxbox.io/api/v1/exports \ -H "Authorization: Bearer $DOXBOX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "exportType": "excel_monthly", "filters": { "year": 2026, "month": 7 } }'
{
"fileName": "Acme_2026-07.xlsx",
"fileUrl": "https://storage.googleapis.com/.../signed...",
"totalDocuments": 17,
"skippedFiles": 0,
"provider": "server"
}fileUrl is a temporary signed URL (~12h).
Versioning and support
Versioning
Every path is prefixed with the version, /api/v1. Additive changes, a new
field on a response or a new optional parameter, can appear in v1 at any
time, so parse responses leniently and ignore fields you do not recognise. Breaking
changes ship under a new version and are announced in advance; v1 is not
removed from under you.
Getting help
Questions about the API, a response you cannot explain, or an endpoint you need that is not here: inbox@doxbox.io.
It saves a round trip if you include the request you made with the key redacted, the
full response body including the code, and the time it happened.
- inbox@doxbox.io
- Try it live
- Swagger UI
- OpenAPI spec
- openapi.json
- Status
GET /api/v1/health
