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
FromValid targets via PATCH
draftsent, paid, partial, overdue, cancelled, written_off
sentpaid, partial, overdue, cancelled, written_off
partialpaid, overdue, cancelled, written_off
overduepaid, 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:

TypeDirectionJournal
salesIssued to a customerDR Trade Debtors, CR Revenue
purchaseReceived 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:

FieldDefaultPurpose
toContact's emailOverride the recipient address
subject"Invoice INV-XXXX from Org Name"Override the subject line
messageNoneCustom 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 totalResulting status
Equal to totalpaid
Less than totalpartial

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
FromValid targets via PATCH
draftsent, accepted, declined
sentaccepted, 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:

  1. Creates a new draft invoice copying the contact, all line items, notes, and terms
  2. Assigns a new invoice number from the standard numbering sequence
  3. Sets a 30-day payment term from today
  4. Marks the quote as converted with 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:

  1. Create a quote - POST /quotes with the contact, scope, and pricing
  2. Send the quote - PATCH /quotes/quo_42/status with {"status": "sent"}
  3. Client accepts - PATCH /quotes/quo_42/status with {"status": "accepted"}
  4. Convert to invoice - POST /quotes/quo_42/convert creates a draft invoice
  5. Review the invoice - GET /invoices/inv_88 to check the copied details
  6. Send the invoice - POST /invoices/inv_88/email delivers the branded email and transitions to sent
  7. Download a copy - GET /invoices/inv_88/pdf for your records
  8. Record payment - POST /invoices/inv_88/payment when the client pays

Related endpoints