CentraBills API Docs
centrabills.com

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.

Base URL https://api.centrabills.com/v1

All requests must be made over HTTPS. All responses are JSON.

Quick start

  1. 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.
  2. Upload a document. Send a POST /v1/documents request with your file. You'll get a 202 Accepted response containing the document's id.
  3. Poll for results. Call GET /v1/documents/{id} until status is no longer pending. Once it is extracted, 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:write ability, 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.
Important: a key is tied to the Super Admin who issued it. If that person loses Super Admin status of the organisation — for example after a role transfer or being removed — every key they issued stops working immediately with a 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.
Note: very small images (under 4 KB) pass validation but are treated as decorative and skipped by extraction — they come back with status 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.