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 follow defined lifecycles with enforced state transitions, automatic journal creation, 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 follow a state machine. The API enforces valid transitions - you cannot skip states or move backwards.
draft → sent → paid
→ partial → paid
→ overdue → paid
→ cancelled
→ written_off
| From | Valid targets via PATCH |
|---|---|
draft | sent, paid, partial, overdue, cancelled, written_off |
sent | paid, partial, overdue, cancelled, written_off |
partial | paid, overdue, cancelled, written_off |
overdue | paid, partial, cancelled, written_off |
Terminal states (paid, cancelled, written_off) cannot be transitioned further. To correct a finalised invoice, update its status to cancelled and create a new one.
What happens at each transition
draft to sent - SpeyBooks creates the double-entry journal transaction: debit Trade Debtors, credit the revenue accounts specified in the line items. This journal is immutable once created.
sent to partial - triggered automatically when a payment is recorded that is less than the invoice total.
sent/partial to paid - triggered automatically when cumulative payments equal the invoice total.
any to cancelled - no journal reversal. The invoice is preserved for audit but excluded from reports.
Invoice types
Invoices have an invoiceType set at creation, which is immutable:
| Type | Direction | Journal |
|---|---|---|
sales | Issued to a customer | DR Trade Debtors, CR Revenue |
purchase | Received from a supplier (bill) | DR Expense, CR Trade Creditors |
The type determines which reports the invoice appears in (Aged Debtors for sales, Aged Creditors for purchase) and which direction the journal entries flow.
Creating an invoice
An invoice requires an invoiceType, contactId, issueDate, dueDate, and at least one line item. Each line needs a description, quantity, unitPrice (in minor units), and vatRate. 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 (immutable, journal created immediately).
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 in minor units. The date defaults to today if omitted. 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
| From | Valid targets via PATCH |
|---|---|
draft | sent, accepted, declined |
sent | accepted, declined |
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