Skip to main content

HTTP API reference

Extraction

Run OCR + structured extraction on expense or revenue documents. POST accepts one or many files/URLs and always returns { results, summary }.

Endpoints in this topic

POST/api/expenses

Extract one or many expense documents (JSON file_url/items or multipart files). client_id required.

Request
Single or bulk (autodetected):

JSON — one file:
{
  "client_id": "<uuid>",
  "file_url": "https://…"
}

JSON — bulk (max 20):
{
  "client_id": "<uuid>",
  "items": [{ "file_url": "https://…" }, …]
}

multipart/form-data:
- client_id (required)
- file (one or many; max 20 per request)
Response
200 application/json — always:

{
  results: [
    { index, status: "extracted"|"cache"|"duplicate"|"quota_exceeded"|"plan_required"|"error", invoiceId?, invoice?, duplicate?, error? },
    …
  ],
  summary: { ok, failed, quotaExceeded, planRequired }
}

Each successful invoice spreads InvoiceExtract fields (counterparty, invoice_number, date, total, currency, items[], …).

POST/api/revenue

Extract one or many revenue documents (same shape as POST /api/expenses). client_id required.

Request
Single or bulk (autodetected):

JSON — one file:
{
  "client_id": "<uuid>",
  "file_url": "https://…"
}

JSON — bulk (max 20):
{
  "client_id": "<uuid>",
  "items": [{ "file_url": "https://…" }, …]
}

multipart/form-data:
- client_id (required)
- file (one or many; max 20 per request)
Response
200 application/json — always:

{
  results: [
    { index, status: "extracted"|"cache"|"duplicate"|"quota_exceeded"|"plan_required"|"error", invoiceId?, invoice?, duplicate?, error? },
    …
  ],
  summary: { ok, failed, quotaExceeded, planRequired }
}

Each successful invoice spreads InvoiceExtract fields (counterparty, invoice_number, date, total, currency, items[], …).

POST /api/expenses

API documentation

This page documents POST /api/extract-invoice: JSON with a public file_url; the server downloads the file and returns structured invoice fields. To upload a file directly instead of hosting a URL, use POST /api/extract-invoice-file (multipart form). Authenticate with an dashboard API key or a session access token (same JWT the browser sends after sign-in). Saved invoice routes are listed in the main API reference.

Endpoint

Method
POST
Path
/api/expenses
Full URL (production)
https://YOUR_APP_HOST/api/expenses

Authentication

Every request must send a bearer token. The server accepts either:

  • API key — create one in the dashboard; keys start with bf_live_. Use the raw key string as the bearer token.
  • Session JWT — same access_token the web app uses after sign-in.
Authorization: Bearer <YOUR_API_KEY_OR_ACCESS_TOKEN>

Send Content-Type: application/json with a JSON body (see below). Do not send form data for this route.

Request body

The JSON field file_url must point to a PDF or other supported format (including images when OCR applies). The server fetches the file; the URL must be publicly reachable and pass inbill.dev's URL safety checks.

{
  "file_url": "https://example.com/invoices/invoice-2026-01.pdf",
  "invoice_kind": "expense"
}

Field file_url is required and must be a valid URL string. Optional invoice_kind: expense (default) or revenue.

Examples

cURL (API key)

Replace YOUR_HOST and YOUR_API_KEY.

curl -X POST "https://YOUR_HOST/api/expenses" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_url":"https://cdn.example.com/docs/invoice.pdf","invoice_kind":"expense"}'

cURL (session JWT)

curl -X POST "https://YOUR_HOST/api/expenses" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"file_url":"https://example.com/invoice.pdf","invoice_kind":"expense"}'

Node.js (fetch)

const res = await fetch("https://YOUR_HOST/api/expenses", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.INBILL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    file_url: "https://example.com/invoice.pdf",
    invoice_kind: "expense",
  }),
});

const data = await res.json();
if (!res.ok) {
  throw new Error(JSON.stringify(data.error ?? data));
}
return data;

Python (requests)

import os
import requests

r = requests.post(
    "https://YOUR_HOST/api/expenses",
    headers={
        "Authorization": f"Bearer {os.environ['INBILL_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "file_url": "https://example.com/invoice.pdf",
        "invoice_kind": "expense",
    },
    timeout=120,
)
r.raise_for_status()
print(r.json())

Success response

200 OK with JSON. Fields may be null or omitted when the model cannot infer them; items defaults to an empty array. counterparty names the other party on the invoice. An optional meta object may be present (e.g. source: extracted | cache | duplicate, invoiceId) when the row was cached, deduplicated by invoice number, or newly stored.

{
  "invoice_number": "INV-12345",
  "date": "2026-03-20",
  "counterparty": "Acme Corp",
  "total": 1200,
  "subtotal": 1300,
  "discount": 100,
  "tax": 180,
  "currency": "USD",
  "items": [
    { "description": "Service", "quantity": 1, "price": 1020, "line_total": null }
  ],
  "meta": { "source": "extracted", "invoiceId": "uuid" }
}

Error responses

Errors use JSON with an error object containing code and message (and sometimes validation details).

  • 400 400 — VALIDATION (body not valid JSON or fails schema), or INVALID_URL (URL not allowed).
  • 401 401 — UNAUTHORIZED — missing or invalid bearer token.
  • 402 402 — QUOTA_EXCEEDED — plan quota used up.
  • 422 422 — FETCH_FAILED (could not download file) or EXTRACTION_FAILED (parse, OCR, or model error).
  • 500 500 — PERSIST_FAILED or other server errors when saving usage.

Example error body:

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid credentials"
  }
}

Quotas

Free: successful extractions are limited per rolling window (default 30 days) and a small cap per account. Pro: monthly quota from your plan. Only successful pipeline runs that pass usage accounting count toward the quota.