Expenses
Recorded costs and purchase invoices. Each expense is returned with its embedded vendor summary and chart-of-accounts reference.
Operations
Amount convention:
amountis the cost excluding BTW (net), andvat_amountis the BTW itself —amount + vat_amountis what the receipt's total says. This matches what the ledger and the BTW-aangifte read. The web form asks users for the receipt total and derives the net; API clients are expected to send the net directly.
Credit notes carry a negative sign in responses. Send positive magnitudes on create and update, whatever the
type— the server stores and returns acredit_noteexpense with negativeamountandvat_amount, so summing a list of expenses nets credit notes off without special-casing. Changingtypere-signs the stored values accordingly.
VAT treatment: the optional
purchase_tax_treatment(defaultdomestic) decides whatvat_amountmay hold, becausevat_amountis always BTW you can deduct. Forreverse_charged_domestic,reverse_charged_euandreverse_charged_non_eu, the invoice carries no BTW, so avat_amountyou send is dropped; billey calculates the BTW you self-account fromamountandvat_rate. Forforeign_vat_charged(a supplier outside the Netherlands charged BTW that you cannot deduct), send the net and the BTW on the invoice as usual. billey adds that BTW to the cost, so the response returns the total asamount,nullasvat_amount, and the BTW on the invoice asnon_deductible_vat_amount. A laterPATCHofamountis still the net: sendamountminusnon_deductible_vat_amount, not the total you read back. Every response also carriespurchase_tax_treatment.
| Method & path | Ability |
|---|---|
GET /api/v1/expenses |
expenses:read |
GET /api/v1/expenses/{expense} |
expenses:read |
POST /api/v1/expenses |
expenses:write |
PATCH /api/v1/expenses/{expense} |
expenses:write |
DELETE /api/v1/expenses/{expense} |
expenses:write |
Create an expense
A create always makes a Draft — any status you send is ignored (change it later with a PATCH). Required: type (invoice, receipt, credit_note, or subscription), vat_rate (0, 9, or 21), and date. As a create it takes an Idempotency-Key.
curl -X POST https://billey.nl/api/v1/expenses \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"vendor_id": 3,
"type": "receipt",
"amount": "49.99",
"vat_rate": 21,
"date": "2026-07-21",
"description": "Printer paper"
}'
{
"data": {
"id": 88,
"description": "Printer paper",
"type": "receipt",
"status": "draft",
"amount": "49.99",
"vat_rate": "21.00",
"date": "2026-07-21",
"vendor": { "id": 3, "name": "Office Supplies B.V." },
"account": { "id": 12, "code": "4400", "name": "Office costs" }
}
}
Attach a source document
On create you may attach a source document in one of two mutually exclusive ways (sending both is a 422):
- Link an existing one — pass
document_id(JSON or multipart). Useful for a document that already arrived by email ingestion. It must exist in this administration and not already be linked to another expense. - Upload a new one — send the request as
multipart/form-datawith afilepart alongside the expense fields. The file is stored as a first-class document (sourceupload) and linked to the new expense, so a scanned receipt (tankbon) or a vendor-invoice PDF goes in with a single call:
curl https://app.billey.nl/api/v1/expenses \
-H "Authorization: Bearer <token>" \
-H "Idempotency-Key: $(uuidgen)" \
-F "type=receipt" \
-F "vat_rate=21" \
-F "date=2026-07-21" \
-F "amount=49.99" \
-F "file=@/path/to/tankbon.jpg"
The file must be ≤ 10 MB and one of pdf, jpg, jpeg, png, webp, heic, tiff, tif, bmp, xml (validated by content, not the filename). Its searchable text is extracted asynchronously, so the returned document starts at extraction_status: processing; the stored bytes are available immediately via GET /api/v1/documents/{document}/download. Attaching a document (either way) is accepted only on create.
Update an expense
PATCH is a genuine partial merge — only the keys you send change (contrast the invoice PATCH; see PATCH behaviour). Status transitions (Draft → Pending → Paid) ride the status field here, which is accepted only on update, never on create.
Delete an expense
DELETE soft-deletes the expense (and cleans up an orphaned source document), returning 204. Refused with 409 for a period-locked or asset-linked expense.
See the OpenAPI spec for the full field list.