CentraBills API
The CentraBills API lets your applications drop invoices and receipts straight into an organisation's CentraBills account — no email required. Upload a document, and CentraBills queues it for extraction exactly as if it had arrived in the organisation's inbox. You can then poll for the extracted totals, tax, currency, and due date.
https://api.centrabills.com/v1
All requests must be made over HTTPS. All responses are JSON.
Quick start
- Get an API key. In CentraBills, go to Settings → API and create a key. Only a Super Admin of the organisation can issue keys. The key is shown once — store it somewhere safe.
-
Upload a document.
Send a
POST /v1/documentsrequest with your file. You'll get a202 Acceptedresponse containing the document'sid. -
Poll for results.
Call
GET /v1/documents/{id}untilstatusis no longerpending. Once it isextracted, the response includes the full extraction object.
Authentication
Every request must carry your API key as a bearer token:
Authorization: Bearer YOUR_API_KEY
API keys have the following properties:
- Organisation-scoped. A key belongs to exactly one organisation; documents you upload land in that organisation's account, and you can only read that organisation's documents.
- Write-only scope. Keys carry the
documents:writeability, covering document upload and status polling. - 18-month expiry. Keys expire 18 months after they are issued. Expired keys receive
401 Unauthorized. - Revocable. A Super Admin can revoke a key at any time from Settings → API. Revocation takes effect immediately.
403 Forbidden response.
Re-issue keys from a current Super Admin account if this happens.
Upload a document
POST https://api.centrabills.com/v1/documents
Uploads a single invoice or receipt as multipart/form-data
and queues it for extraction. Returns 202 Accepted
— extraction happens asynchronously, so poll the returned document to get results.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| file | file | Yes | One file, max 10 MB. Accepted types: pdf, png, jpg, jpeg, webp, gif, tif, tiff. |
| sender_name | string | No | Who sent the document (max 255 characters). Shown as the sender in CentraBills. |
| company | string | No | The sending company (max 255 characters). Used as the sender when sender_name is omitted. |
| reference | string | No | Your reference for the document (max 255 characters). Defaults to the file name. |
unsupported.
Example request
curl
curl -X POST https://api.centrabills.com/v1/documents \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-F "[email protected]" \
-F "company=Acme Ltd" \
-F "reference=INV-2041"
PHP (Guzzle)
$client = new \GuzzleHttp\Client();
$response = $client->post('https://api.centrabills.com/v1/documents', [
'headers' => [
'Authorization' => 'Bearer YOUR_API_KEY',
'Accept' => 'application/json',
],
'multipart' => [
['name' => 'file', 'contents' => fopen('invoice.pdf', 'r'), 'filename' => 'invoice.pdf'],
['name' => 'company', 'contents' => 'Acme Ltd'],
['name' => 'reference', 'contents' => 'INV-2041'],
],
]);
$document = json_decode($response->getBody(), true)['data'];
JavaScript (fetch)
const form = new FormData();
form.append('file', fileInput.files[0]);
form.append('company', 'Acme Ltd');
form.append('reference', 'INV-2041');
const response = await fetch('https://api.centrabills.com/v1/documents', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
},
body: form,
});
const { data } = await response.json();
Example response 202 Accepted
{
"data": {
"id": 123,
"status": "pending",
"filename": "invoice.pdf",
"received_at": "2026-07-03T09:30:00+00:00",
"links": {
"self": "https://api.centrabills.com/v1/documents/123"
}
}
}
Retrieve a document
GET https://api.centrabills.com/v1/documents/{id}
Returns a document's current status and, once extraction has completed successfully, its
extracted data. Documents are only visible to the organisation the API key belongs to —
unknown ids and documents belonging to other organisations both return
404 Not Found.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| id | integer (path) | Yes | The document id returned when it was uploaded. |
Example request
curl
curl https://api.centrabills.com/v1/documents/123 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
PHP (Guzzle)
$client = new \GuzzleHttp\Client();
$response = $client->get('https://api.centrabills.com/v1/documents/123', [
'headers' => [
'Authorization' => 'Bearer YOUR_API_KEY',
'Accept' => 'application/json',
],
]);
$document = json_decode($response->getBody(), true)['data'];
JavaScript (fetch)
const response = await fetch('https://api.centrabills.com/v1/documents/123', {
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
},
});
const { data } = await response.json();
Example response 200 OK, extracted
{
"data": {
"id": 123,
"status": "extracted",
"filename": "invoice.pdf",
"received_at": "2026-07-03T09:30:00+00:00",
"extraction": {
"document_type": "invoice",
"company_name": "Acme Ltd",
"currency": "GBP",
"subtotal": "100.00",
"tax_amount": "20.00",
"shipping": "5.00",
"total": "125.00",
"invoice_due_date": "2026-08-02",
"extracted_at": "2026-07-03T09:31:12+00:00"
},
"links": {
"self": "https://api.centrabills.com/v1/documents/123"
}
}
}
The extraction key is entirely
absent until the document's status is extracted
— while pending (and for unsupported or failed documents) the response contains only the top-level fields.
Status lifecycle
Every document starts as pending
and settles into exactly one terminal state:
pending → extracted | unsupported | failed
| Status | Meaning |
|---|---|
| pending | The document is queued or being processed. Keep polling. |
| extracted | Extraction succeeded. The response now includes the extraction object. |
| unsupported | The file could not be processed for extraction (for example, an image under 4 KB). Terminal. |
| failed | Extraction was attempted but did not succeed. Terminal. |
Most documents are extracted within a couple of minutes. A polling interval of 15–30 seconds with a sensible timeout works well.
The extraction object
Present only when status is
extracted. Monetary values are
decimal strings (for example "100.00"),
timestamps are ISO 8601, and dates are YYYY-MM-DD.
Any field the extractor could not determine is null.
| Field | Type | Description |
|---|---|---|
| document_type | string | One of invoice, receipt, credit_note, statement, other. |
| company_name | string | The issuing company as read from the document. |
| currency | string | Three-letter ISO 4217 currency code, e.g. GBP. |
| subtotal | decimal string | Amount before tax and shipping. |
| tax_amount | decimal string | Total tax on the document. |
| shipping | decimal string | Shipping or delivery charge. |
| total | decimal string | The grand total. |
| invoice_due_date | date | Payment due date, YYYY-MM-DD. |
| extracted_at | datetime | When extraction completed, ISO 8601. |
Errors
Errors are returned as JSON with a human-readable message.
Validation errors additionally include an errors
object keyed by field name.
| Status | Meaning |
|---|---|
| 401 | Missing, invalid, or expired API key. |
| 403 | The key is no longer valid for the organisation — for example, its issuer is no longer a Super Admin. |
| 404 | The document does not exist or belongs to another organisation. |
| 422 | Validation failed — a missing file, an unsupported type, an oversized file, or a field over 255 characters. |
| 429 | Rate limit exceeded. Wait for the number of seconds in the Retry-After header and retry. |
Example error 422 Unprocessable Content
{
"message": "The file field is required.",
"errors": {
"file": [
"The file field is required."
]
}
}
Rate limits
Each API key may make up to 60 requests per minute. Every response includes the standard rate-limit headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
When the limit is exceeded, the API responds with 429 Too Many Requests,
a JSON message, and a
Retry-After header telling you
how many seconds to wait before retrying.