Invoices & Quotes
Invoices and quotes are the core commercial documents in SpeyBooks. Invoices represent amounts owed to or by your organisation; quotes are proposals sent before work begins. Both carry tamper-evident audit trails, server-side calculation, and built-in PDF and email delivery.
This guide covers the full workflow from creating a draft through to payment, plus the quote-to-invoice conversion path. For the full endpoint reference, see Invoices API and Quotes API.
Invoice lifecycle
Invoices carry a status: draft, sent, partial, paid, overdue, cancelled, or written_off.
One transition rule is enforced at every layer, including the database: an invoice that has left draft can never return to it. Issuing is irreversible. Other transitions between issued statuses are accepted by the API; treat paid, cancelled, and written_off as practically terminal. (Stricter lifecycle enforcement is under development and will be documented when it ships.)
Three things drive status in practice:
The status endpoint - PATCH /invoices/{id}/status for explicit transitions such as issuing a draft (sent) or closing one out (cancelled, written_off).
Payments - recording a payment sets partial or paid automatically from the cumulative amount paid. Use the payment endpoint for these rather than setting them by hand, so the status and the amounts always agree.
Email - emailing a draft invoice issues it (transitions it to sent) atomically with the send.
Invoice types
Invoices have an invoiceType set at creation, which is immutable:
| Type | Direction |
|---|---|
sales |
Issued to a customer |
purchase |
Received from a supplier (bill) |
The type determines which reports the invoice appears in: Aged Debtors for sales, Aged Creditors for purchase.
Creating an invoice
An invoice requires an invoiceType, contactId, issueDate, dueDate, and at least one line item. Each line needs a description, quantity, unitPrice, and vatRate. quantity and unitPrice are exact decimal strings - unitPrice in POUNDS ("850.00"), quantity in units, fractional values accepted ("2.5"). JSON numbers are rejected with a 400; response amounts are integers in pence. The optional accountId on each line maps the revenue or expense to your chart of accounts.
The invoice number is assigned automatically from the organisation's numbering sequence. Totals (subtotal, vatAmount, total) are computed server-side from line items using Decimal.js - you do not set these yourself.
Invoices can be created in draft status (default, editable) or sent status (issued immediately, no longer editable).
Editing or deleting a draft
Use PUT /invoices/{id} to update a draft invoice. Lines are replaced entirely - existing lines are deleted and the new set is inserted. This is a full replacement, not a merge. Once an invoice leaves draft status, it cannot be edited.
You can permanently delete a draft invoice using DELETE /invoices/{id}. Only invoices in draft status can be deleted - the endpoint returns 400 for any other status. To remove a non-draft invoice from active use, transition it to cancelled instead.
Custom metadata
Invoices support Stripe-style key-value metadata (up to 50 keys, string values). Keys prefixed with _sb_ are reserved for internal use. Metadata is replaced entirely on update, not merged.
Sending an invoice
The email endpoint sends a branded HTML invoice to the contact's email address. The email includes the organisation header, contact details, line item table with VAT breakdown, totals, and optional notes/terms sections. A plain text fallback is generated for email clients that do not render HTML.
If the invoice is in draft status when emailed, it is automatically transitioned to sent. Re-sending an already-sent invoice sends a new email but does not change the status.
All body fields are optional:
| Field | Default | Purpose |
|---|---|---|
to |
Contact's email | Override the recipient address |
subject |
"Invoice INV-XXXX from Org Name" | Override the subject line |
message |
None | Custom message displayed in a highlighted block above the invoice details |
If the contact has no email address and no to override is provided, the endpoint returns 400.
Recording payments
The payment endpoint records a payment against an invoice and automatically transitions the status:
| Cumulative paid vs total | Resulting status |
|---|---|
| Equal to total | paid |
| Less than total | partial |
The amount is an integer in PENCE (minor units) - the one place the request wire uses pence rather than pound strings. Overpayment is rejected with a 400 error.
Payments cannot be reversed via the API. To correct a wrong payment, cancel the invoice and create a replacement.
PDF generation
Download an invoice as a professionally formatted PDF. The endpoint returns a binary PDF stream (not a JSON envelope), with Content-Type: application/pdf and a Content-Disposition attachment header.
The PDF includes the organisation header (name, address, VAT number, company number), contact details, invoice metadata (number, dates, status), a line item table, financial summary (subtotal, VAT, total, amount paid), and any notes or terms.
PDFs are generated fresh on every request from current database state. There is no server-side caching, so the PDF always reflects the latest data.
The filename is derived from the invoice number with non-alphanumeric characters (except hyphens) replaced by underscores (e.g. INV_0043.pdf). Use curl's -OJ flag to save with the server-provided filename, or -o invoice.pdf to specify your own.
Preview calculations
The preview endpoint calculates line totals, VAT, and the invoice total without creating anything. Use it for real-time UI previews while editing line items.
Pass an array of lines with quantity, unitPrice, and vatRate. The response returns per-line calculations and the invoice-level totals. The calculation is stateless and deterministic.
Quote lifecycle
Quotes follow a separate but parallel lifecycle:
draft → sent → accepted → converted
→ declined → declined → declined
| From | Valid targets via PATCH |
|---|---|
draft |
sent, declined |
sent |
accepted, declined |
accepted |
declined |
Quote transitions are enforced at both the service and the database; an illegal transition returns 400. A quote cannot move straight from draft to accepted - it must be sent first.
Only draft quotes can be edited or deleted. Sent, accepted, declined, and converted quotes are preserved for the audit trail. A quote can only reach the converted state via the dedicated /convert endpoint, not via a status PATCH.
Quote numbering and expiry
Quotes use the pattern QT-{year}-{sequence} (e.g. QT-2026-0001). The prefix and next number are configurable in organisation settings.
Quotes have a validUntil date. Expired quotes (where validUntil is past and status is sent) are tracked in the list stats (expiredCount) but are not automatically transitioned - the status remains sent.
Creating a quote
A quote requires a contactId, issueDate, validUntil, and at least one line item. The structure is the same as invoice line items: description, quantity, unitPrice, vatRate, and optional accountId.
Totals are computed server-side. The quote number is auto-generated.
Converting a quote to an invoice
Only accepted quotes can be converted. The conversion:
- Creates a new draft invoice copying the contact, all line items, notes, and terms
- Assigns a new invoice number from the standard numbering sequence
- Sets a 30-day payment term from today
- Marks the quote as
convertedwith a link to the new invoice
The entire operation runs within a savepoint. The response returns the new invoice ID and number. From there, you can edit the draft invoice if needed, then send it through the normal invoice workflow.
If the quote is not in accepted status or has already been converted, the endpoint returns 400.
Worked example: quote to payment
A typical workflow from proposal to cash:
- Create a quote -
POST /quoteswith the contact, scope, and pricing - Send the quote -
PATCH /quotes/quo_42/statuswith{"status": "sent"} - Client accepts -
PATCH /quotes/quo_42/statuswith{"status": "accepted"} - Convert to invoice -
POST /quotes/quo_42/convertcreates a draft invoice - Review the invoice -
GET /invoices/inv_88to check the copied details - Send the invoice -
POST /invoices/inv_88/emaildelivers the branded email and transitions tosent - Download a copy -
GET /invoices/inv_88/pdffor your records - Record payment -
POST /invoices/inv_88/paymentwhen the client pays
Related endpoints
- Invoices API - full invoice endpoint reference
- Quotes API - full quote endpoint reference
- Contacts API - manage the contacts referenced by invoices
- Reports Guide - Aged Debtors and Aged Creditors reports for outstanding invoices