Quotes
Quotations, with a full read/write surface mirroring invoices — create, replace, and delete a draft quote, alongside line items and an embedded client summary. Reads carry the quotes:read ability, writes the quotes:write ability.
Changed: quotes used to be read-only, riding the
invoices:readability. As of this release they have their ownquotes:read/quotes:writeabilities instead — a token issued with onlyinvoices:readno longer reads quotes, andinvoices:writenever wrote them and still doesn't. If you have an existing integration that reads quotes, reissue its token withquotes:read. This is a deliberate pre-launch breaking change (see Tokens).
Operations
| Method & path | Ability |
|---|---|
GET /api/v1/quotes |
quotes:read |
GET /api/v1/quotes/{quote} |
quotes:read |
GET /api/v1/quotes/{quote}/pdf |
quotes:read |
POST /api/v1/quotes |
quotes:write |
PATCH /api/v1/quotes/{quote} |
quotes:write |
DELETE /api/v1/quotes/{quote} |
quotes:write |
POST /api/v1/quotes/{quote}/send |
quotes:write |
POST /api/v1/quotes/{quote}/mark-sent |
quotes:write |
POST /api/v1/quotes/{quote}/mark-accepted |
quotes:write |
POST /api/v1/quotes/{quote}/mark-declined |
quotes:write |
POST /api/v1/quotes/{quote}/convert-to-invoice |
quotes:write |
Get a quote
curl https://billey.nl/api/v1/quotes/15 \
-H "Authorization: Bearer $TOKEN"
{
"data": {
"id": 15,
"quote_number": "Q-2026-0042",
"status": "sent",
"currency": "EUR",
"quote_date": "2026-07-10",
"valid_until": "2026-08-10",
"subtotal": "4750.00",
"btw_total": "997.50",
"total": "5747.50",
"converted_invoice_id": null,
"client": { "id": 7, "name": "Example Client B.V." },
"line_items": [
{
"id": 1,
"description": "Project phase 1",
"quantity": "1.00",
"unit_price": "4750.00",
"btw_percentage": "21.00",
"btw_amount": "997.50",
"line_total": "4750.00"
}
]
}
}
status is one of draft, sent, accepted, declined. When a quote has been turned into an invoice, converted_invoice_id points at it. The OpenAPI spec lists every field.
Download the PDF
GET /api/v1/quotes/{quote}/pdf renders and returns the same PDF the web app's "Download PDF" button produces (not JSON). This route carries its own, tighter rate limit — see Rate limits.
curl -L https://billey.nl/api/v1/quotes/15/pdf \
-H "Authorization: Bearer $TOKEN" \
-o quote.pdf
Create a quote
POST /api/v1/quotes requires an Idempotency-Key header (see Idempotency) and creates a draft. quote_number is optional — when omitted the next number is allocated automatically, exactly like an invoice. items must contain at least one line.
Money and quantities on the API are strict, locale-free: plain dot-decimal strings or native numbers only ("103.31", not "103,31") — see Money & decimals for why.
curl -X POST https://billey.nl/api/v1/quotes \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: quote-2026-08-03-001" \
-H "Content-Type: application/json" \
-d '{
"client_id": 7,
"currency": "EUR",
"quote_date": "2026-08-03",
"valid_until": "2026-09-02",
"items": [
{ "description": "Project phase 1", "quantity": "1", "unit_price": "4750.00", "btw_percentage": 21 }
]
}'
A fresh create returns 201; replaying the same Idempotency-Key returns 200 with the original quote unchanged, no second row created.
Line item shape
Each entry in items requires:
| Field | Type | Notes |
|---|---|---|
description |
string | Max 255 characters. |
quantity |
string/number | Strict decimal, minimum 0.01. |
unit_price |
string/number | Strict money, minimum 0. |
btw_percentage |
number | Must be one of the book's configured BTW rates. |
Unlike an invoice line item, a quote line item has no icp_category — the column doesn't exist on quotes, since ICP/rubriek-3b reporting only applies to issued invoices.
Tax treatment and KOR
tax_treatment (or the legacy reverse_charge_applied boolean) works exactly as it does on invoices — see Invoices's "Cross-border VAT" section. One deliberate difference from invoices: a quote's line btw_percentage is never adjusted for KOR. An invoice under a KOR-registered book has its rates zeroed automatically; a quote does not, because a quote is a proposal, not a KOR-eligible turnover document — the rate you send is the rate stored.
Replace a quote (PATCH)
PATCH /api/v1/quotes/{quote} is a full replace, not a partial merge — see PATCH behaviour. Resend every header field and the complete items array; an omitted notes becomes null.
quote_number cannot be changed via PATCH at all — unlike an invoice, there is no renumbering capability. A quote's number is fixed the moment it's created; any quote_number in the PATCH payload is silently ignored.
Only a draft quote can be edited — a PATCH on a sent, accepted, or declined quote returns 409 Conflict.
Delete a quote
DELETE /api/v1/quotes/{quote} hard-deletes the quote (Quote does not soft-delete, unlike Invoice/Client/Vendor). There is no status restriction — a quote in any status, including one already converted to an invoice, can be deleted; the converted invoice itself is untouched (its converted_invoice_id link on the deleted quote simply goes with it).
Lifecycle transitions
Move a quote through draft → sent → accepted or declined with these bodyless POSTs (no Idempotency-Key); an invalid transition returns 409:
send— email the quote to the client and mark it Sent (the client must have an email address, else422).mark-sent— mark Sent without emailing, for when you deliver the quote through your own channel.mark-accepted— transition Sent → Accepted.mark-declined— transition Sent → Declined.
curl -X POST https://billey.nl/api/v1/quotes/15/send \
-H "Authorization: Bearer $TOKEN"
Convert a quote to an invoice
POST /api/v1/quotes/{quote}/convert-to-invoice creates a new draft invoice from the quote's client, currency, tax treatment, and line items — the same writer POST /api/v1/invoices uses — links it back onto the quote as converted_invoice_id, and sets the quote's status to accepted. As a create, it takes an Idempotency-Key.
curl -X POST https://billey.nl/api/v1/quotes/15/convert-to-invoice \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)"
A quote converts once. Calling this again on an already-converted quote never creates a second invoice — it returns the existing one, with 200 instead of 201, even if you send a different Idempotency-Key than the original call. This is enforced with a row lock inside the write transaction, not just a stale read-then-write check, so it holds under genuinely concurrent requests as well as a simple double-click or retry.
That "existing one" is returned regardless of whether it still exists: an invoice created from a quote is an ordinary draft, and a draft invoice is deletable (DELETE /api/v1/invoices/{invoice}). If the resulting invoice is later deleted, a repeat convert-to-invoice call on that quote still returns 200 with the (now soft-deleted) invoice's data — converted_invoice_id isn't cleared by the invoice's deletion, and this endpoint never re-checks that the row it points at is still live. A GET /api/v1/invoices/{id} for that same id would 404. Don't treat a 200 here as proof the invoice is currently retrievable.
A quote with no line items returns 422 rather than converting an empty invoice.