Invoices
Outbound invoicing — draft, send, mark paid, credit, PDF download. Mixed-rate VAT supported.
Endpoints
GET/api/v1/companies/:companyId/invoices— List invoices for a company.GET/api/v1/companies/:companyId/invoices/:id— Retrieve a single invoice by id.GET/api/v1/companies/:companyId/invoices/:id/pdf— Download the rendered invoice PDF.POST/api/v1/companies/:companyId/invoices— Create a draft invoice, proforma, or delivery note.POST/api/v1/companies/:companyId/invoices/:id/credit— Issue a credit note (kreditfaktura) against an invoice.POST/api/v1/companies/:companyId/invoices/:id/mark-paid— Record a payment against an invoice.POST/api/v1/companies/:companyId/invoices/:id/mark-sent— Transition a draft invoice to sent (without emailing).POST/api/v1/companies/:companyId/invoices/:id/send— Send a draft invoice to the customer by email.POST/api/v1/companies/:companyId/invoices/bulk-create— Create up to 50 draft invoices in one call (partial-success).PATCH/api/v1/companies/:companyId/invoices/:id— Update a draft invoice (metadata fields only).
GET /api/v1/companies/:companyId/invoices {#get-invoices-list}
invoices.list · scope invoices:read
List invoices for a company.
Returns invoices in most-recent-first order. Includes the customer name inline; pass ?expand=customer for the full customer record, ?expand=items for line items.
Use when: You need to enumerate invoices for a company — for AR reporting, payment matching, or building an invoice dashboard.
Don't use for: Fetching a single invoice you already know the id of — use GET /api/v1/companies/{companyId}/invoices/{id}. Supplier invoices are a different resource (supplier-invoices).
Pitfalls
- Draft invoices have invoice_number=null until they are sent.
- remaining_amount is the unpaid portion (total − paid_amount); use status=paid or remaining_amount=0 to filter for closed invoices.
- Credit notes appear with status=credited and a credited_invoice_id field on the detail endpoint.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": [
{
"id": "0e9c…",
"invoice_number": "2026-0042",
"customer_id": "a8f1…",
"customer_name": "Acme AB",
"invoice_date": "2026-05-01",
"due_date": "2026-05-31",
"status": "sent",
"document_type": "invoice",
"currency": "SEK",
"subtotal": 10000,
"vat_amount": 2500,
"total": 12500,
"remaining_amount": 12500,
"paid_at": null,
"created_at": "2026-05-01T09:14:33Z"
}
],
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12",
"next_cursor": null
}
}
GET /api/v1/companies/:companyId/invoices/:id {#get-invoices-get}
invoices.get · scope invoices:read
Retrieve a single invoice by id.
Returns the full invoice record with the customer embedded. Pass ?expand=items for line items, ?expand=payments for payment history, or ?expand=items,payments for both.
Use when: You have an invoice id (from a webhook, the list endpoint, or a customer transaction) and need the full record including amounts, dates, status, and the customer details.
Don't use for: Listing invoices (use GET /api/v1/companies/{companyId}/invoices). Bookkeeping verifikationer tied to the invoice (use the journal-entries endpoints in a later phase).
Pitfalls
- Returns 404 if the invoice does not belong to the company in the URL — does not leak existence across companies.
- paid_at and remaining_amount can lag behind the latest payment by a few seconds during high-volume reconciliation.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": {
"id": "0e9c…",
"invoice_number": "2026-0042",
"customer_id": "a8f1…",
"customer": {
"id": "a8f1…",
"name": "Acme AB"
},
"invoice_date": "2026-05-01",
"due_date": "2026-05-31",
"status": "sent",
"total": 12500,
"remaining_amount": 12500,
"paid_at": null,
"created_at": "2026-05-01T09:14:33Z"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
GET /api/v1/companies/:companyId/invoices/:id/pdf {#get-invoices-pdf}
invoices.pdf · scope invoices:read
Download the rendered invoice PDF.
Returns the invoice as application/pdf. The filename in Content-Disposition reflects the document type: faktura-<number>.pdf for sent invoices, kreditfaktura-<number>.pdf for credit notes, utkast-<id-slice>.pdf for drafts. This endpoint is byte-equivalent to the dashboard download.
Use when: You need to fetch an invoice PDF for archival, forwarding to a customer outside the gnubok send flow, or attaching to an external workflow.
Don't use for: Sending the invoice to the customer — use POST /invoices/{id}/send, which renders the PDF, emails it, and archives it as a verifikationsunderlag in one atomic step.
Pitfalls
- Drafts (no invoice_number yet) render with an "utkast" filename. The PDF carries no F-series number — do not treat it as a finalized invoice.
- PDF rendering can take several hundred milliseconds for invoices with many line items. Cache on the client if requesting repeatedly.
- Credit notes embed the original invoice's löpnummer per ML 17 kap 22–23§ — if the original was hard-deleted (not possible via gnubok but theoretically via a manual DB edit), the reference is omitted.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"_note": "Returns application/pdf binary stream."
}
POST /api/v1/companies/:companyId/invoices {#post-invoices-create}
invoices.create · scope invoices:write
Create a draft invoice, proforma, or delivery note.
Creates an invoice in draft status. The F-series invoice_number is allocated atomically on the first send action (PR-B-2b). Per-item VAT rates are validated against the customer's allowed rates (mixed-rate invoices supported). Non-SEK invoices are converted to SEK at the Riksbanken exchange rate fetched at create time. Idempotent (mandatory Idempotency-Key). Dry-runnable — the preview returns the validated would-be invoice + items with computed totals; no journal entry is involved at draft stage (posting happens on :send).
Use when: You need to issue a new invoice, proforma, or delivery note. Use dry-run first to confirm VAT calculations and currency conversion before committing.
Don't use for: Updating an existing invoice (PATCH instead, drafts only). Issuing a credit note (use POST /:id:credit in PR-B-2b). Posting a previously-created draft to the journal (use POST /:id:send in PR-B-2b).
Pitfalls
- Idempotency-Key is mandatory; calls without it return 400.
- For mixed-rate invoices, set vat_rate per item explicitly. Items where vat_rate is omitted use the customer's default rate from getVatRules().
- Non-SEK currencies require an active Riksbanken exchange-rate fetch. Failure is non-fatal — the invoice is created with null SEK fields and the agent can recompute later.
- invoice_number is null on creation. The number is allocated atomically when the invoice transitions out of draft. Counting on a specific number at create time is a bug.
- document_type='delivery_note' produces no VAT and a different number sequence (D-series). Most use cases want the default document_type='invoice'.
Risk: medium · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"customer_id": "a8f1…",
"invoice_date": "2026-05-12",
"due_date": "2026-06-11",
"currency": "SEK",
"items": [
{
"description": "Konsultation",
"quantity": 8,
"unit": "tim",
"unit_price": 1250
}
]
}
Example response
{
"data": {
"id": "0e9c…",
"invoice_number": null,
"customer_id": "a8f1…",
"invoice_date": "2026-05-12",
"due_date": "2026-06-11",
"status": "draft",
"currency": "SEK",
"subtotal": 10000,
"vat_amount": 2500,
"total": 12500,
"remaining_amount": 12500
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/invoices/:id/credit {#post-invoices-credit}
invoices.credit · scope invoices:write
Issue a credit note (kreditfaktura) against an invoice.
Creates a credit note referencing the original invoice. The credit note carries reversed-sign amounts (matching the original line for line) and gets invoice_number=KR-<original>. The original invoice transitions to status=credited. Under faktureringsmetoden, posts a reversing journal entry (Credit AR 1510 / Debit revenue + Debit output VAT). Under kontantmetoden the credit note still creates the row but defers the reversal entry until refund. Idempotent and dry-runnable. Emits invoice.credited.
Use when: You need to legally cancel an issued invoice (ML 17 kap 22–23§). The original invoice cannot be edited once issued — credit it and reissue corrected.
Don't use for: Cancelling a draft (DELETE the draft instead). Refunding a partial payment without invalidating the whole invoice (book the refund manually via the journal-entries API in a future PR).
Pitfalls
- Idempotency-Key is mandatory. Retried credits with the same key replay the cached response — no duplicate credit note is created.
- The original invoice must be in sent / paid / overdue status. Drafts, cancelled invoices, and already-credited invoices are rejected with specific error codes.
- Credit-note items mirror the original's lines with negated values. To credit only part of an invoice (line-level), credit the full invoice first then reissue with the corrected lines.
- Under kontantmetoden no journal entry is created here — refund booking is deferred. A
JOURNAL_ENTRY_NOT_POSTEDwarning is NOT emitted in this case (the deferral is correct, not a failure).
Risk: high · Idempotent: yes · Reversible: no · Dry-run supported: yes
Example request
{
"reason": "Felaktig kund"
}
Example response
{
"data": {
"id": "ccccccc-c…",
"invoice_number": "KR-2026-0042",
"credited_invoice_id": "0e9c…",
"status": "sent",
"total": -12500,
"journal_entry_id": "8b4b…"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/invoices/:id/mark-paid {#post-invoices-mark-paid}
invoices.mark-paid · scope invoices:write
Record a payment against an invoice.
Marks a sent / overdue invoice as paid (or partially_paid). Books the payment via Debit 1930 / Credit 1510 under faktureringsmetoden, or Debit 1930 / Credit revenue + Credit output VAT under kontantmetoden. Optional body supports partial payments via custom balanced journal lines and exchange-rate adjustments for foreign-currency invoices. Idempotent and dry-runnable. Emits invoice.paid.
Use when: A customer paid an invoice via a channel other than the synced bank account (cash, manual transfer, separate processor). Use dry-run to confirm the booking before committing.
Don't use for: Reverting a payment — the public API does not expose unmark-paid. Issue a credit note via POST /:id/credit to cancel the underlying invoice instead. Bank-matched payments — those flow through the transactions endpoints.
Pitfalls
- Idempotency-Key is mandatory. Retried marks with the same key replay the cached response.
- Custom
linesmust balance (sum of debits = sum of credits, both > 0). Otherwise returns 400 INVOICE_PAID_LINES_UNBALANCED. - For foreign-currency invoices, supply
exchange_rate_difference(SEK delta vs the invoice's booked rate) to book the FX adjustment correctly. Omitting it on a non-SEK invoice will mis-book the FX gain/loss. - Cash basis (kontantmetoden) recognizes revenue HERE, not at :mark-sent. The dashboard tracks this via company_settings.accounting_method.
- Duplicate-payment guard: if an unlinked inbound bank transaction looks like this payment, returns 409 INVOICE_PAID_LIKELY_DUPLICATE with candidate transactions. Retry with
force: trueto bypass — but the retry MUST use a fresh Idempotency-Key (the original is body-hash bound; reusing it returns 400 IDEMPOTENCY_KEY_REUSE). The guard is also evaluated under dry-run, so a successful dry-run does not guarantee a successful commit.
Risk: medium · Idempotent: yes · Reversible: no · Dry-run supported: yes
Example request
{
"payment_date": "2026-05-12"
}
Example response
{
"data": {
"id": "0e9c…",
"invoice_number": "2026-0042",
"status": "paid",
"total": 12500,
"paid_amount": 12500,
"remaining_amount": 0,
"paid_at": "2026-05-12",
"journal_entry_id": "7b3a…"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/invoices/:id/mark-sent {#post-invoices-mark-sent}
invoices.mark-sent · scope invoices:write
Transition a draft invoice to sent (without emailing).
Marks a draft invoice as sent — for invoices delivered outside gnubok (Peppol, postal, manual email). Allocates the F-series invoice_number atomically (ML 17 kap 24§ p.2). On accounting_method=accrual, also posts the invoice journal entry (Debit AR 1510 / Credit revenue + output VAT). Emits invoice.sent. Idempotent and dry-runnable. The companion :send action (PR-B-2b-3) adds PDF rendering and email delivery on top of this same flow.
Use when: You delivered the invoice through a channel other than gnubok's email (Peppol, postal, your own SMTP) and need to record it as sent so the F-series number is allocated and the journal entry is posted.
Don't use for: Sending the invoice via gnubok email — use :send (PR-B-2b-3) for that. Marking an already-sent invoice as paid — use :mark-paid (PR-B-2b-2).
Pitfalls
- Only invoices in
status=draftcan be marked sent. Other states return 409 INVOICE_UPDATE_NOT_DRAFT (re-used; the action is structurally an update). - Allocation is atomic. If a concurrent transition beats the agent's request to the same draft, the runner-up gets 409 INVOICE_UPDATE_NOT_DRAFT and no number is consumed.
- Delivery notes (document_type=delivery_note) don't transition to sent — they were never drafts in the f-series sense. This endpoint will reject them with 400 VALIDATION_ERROR.
- Idempotency-Key is mandatory. A retried mark-sent with the same key replays the cached response.
Risk: medium · Idempotent: yes · Reversible: no · Dry-run supported: yes
Example response
{
"data": {
"id": "0e9c…",
"invoice_number": "2026-0042",
"status": "sent",
"total": 12500,
"journal_entry_id": "7b3a…"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/invoices/:id/send {#post-invoices-send}
invoices.send · scope invoices:write
Send a draft invoice to the customer by email.
The full send pipeline: preflight PDF render → allocate F-series number atomically → final PDF render → email via Resend (PDF attachment, copy to company) → flip status to sent → post journal entry (accrual + real invoice) → archive PDF as underlag → emit invoice.sent. Email failure is a hard 502 before state changes; post-email failures surface as warnings but the invoice IS marked sent.
Use when: You want gnubok to deliver the invoice to the customer via email. For invoices delivered through another channel (Peppol, postal, own SMTP) use :mark-sent instead.
Don't use for: Re-sending an already-sent invoice (returns 409 INVOICE_UPDATE_NOT_DRAFT). Sending a delivery note (no F-series lifecycle). Sending a credit note (use the :credit endpoint to issue the kreditfaktura; subsequent re-send of the credit note via :mark-sent is the supported path).
Pitfalls
- Idempotency-Key is mandatory.
- Email service must be configured — without RESEND_API_KEY + RESEND_FROM_EMAIL the endpoint returns 503 INVOICE_SEND_EMAIL_NOT_CONFIGURED.
- Customer must have an email address. 400 INVOICE_SEND_NO_CUSTOMER_EMAIL otherwise.
- A cancelled invoice is rejected (400 INVOICE_SEND_CANCELLED) — its F-series number is preserved for compliance but the document is not a valid faktura.
- Email failure before the status flip leaves the F-series number consumed but the invoice in
draftstatus. Same orphan window as :mark-sent (architecturally tracked, matches internal route). - After the email succeeds, journal-entry/archive/event failures become warnings on the response; the invoice IS marked sent regardless.
Risk: high · Idempotent: yes · Reversible: no · Dry-run supported: yes
Example response
{
"data": {
"id": "0e9c…",
"invoice_number": "2026-0042",
"status": "sent",
"total": 12500,
"message_id": "re_abc123",
"sent_to": "finance@acme.test",
"cc": "billing@gnubok-user.test",
"journal_entry_id": "7b3a…"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/invoices/bulk-create {#post-invoices-bulk-create}
invoices.bulk-create · scope invoices:write
Create up to 50 draft invoices in one call (partial-success).
Bulk-creation endpoint. Each invoice in the request array is validated and inserted independently. By default, individual failures do not roll back successes — the response carries a per-item results array with ok/error markers and a summary. Idempotent (the whole batch is keyed by the single Idempotency-Key). Dry-runnable.
Use when: You're importing a batch of invoices from another system, or producing many invoices programmatically (e.g. monthly subscription billing). Use dry-run first to validate the whole batch before committing.
Don't use for: Sending the same invoice to multiple customers — POST /invoices once per customer. Long-running imports of > 50 invoices — split into pages. Transactional all-or-nothing imports — not yet supported (passing all_or_nothing: true returns 501 NOT_IMPLEMENTED; the flag is reserved for a future RPC).
Pitfalls
- Idempotency-Key is mandatory and covers the WHOLE batch. A retried bulk-create returns the cached full response — it does not retry only the failed items.
- Passing all_or_nothing: true returns 501 NOT_IMPLEMENTED. Today only partial-success batches exist; omit the flag (or pass false).
- Each per-item invoice still goes through the same VAT-rule validation as POST /invoices. A mismatched per-item vat_rate produces a per-item failure, not a whole-batch failure.
- Currency conversion is best-effort PER ITEM. A failed Riksbanken fetch leaves that item's SEK columns null but does NOT fail the item.
Risk: medium · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"invoices": [
{
"customer_id": "a8f1…",
"invoice_date": "2026-05-12",
"due_date": "2026-06-11",
"currency": "SEK",
"items": [
{
"description": "A",
"quantity": 1,
"unit": "st",
"unit_price": 1000
}
]
}
]
}
Example response
{
"data": {
"results": [
{
"ok": true,
"request_index": 0,
"data": {
"id": "0e9c…",
"invoice_number": null,
"status": "draft",
"total": 1250
}
}
],
"summary": {
"total": 1,
"succeeded": 1,
"failed": 0
}
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
PATCH /api/v1/companies/:companyId/invoices/:id {#patch-invoices-update}
invoices.update · scope invoices:write
Update a draft invoice (metadata fields only).
Partial update for invoices in draft status. Allowed fields: invoice_date, due_date, delivery_date, your_reference, our_reference, notes. customer_id, currency, document_type, items, and computed totals are immutable — replace those by deleting the draft and recreating it. Returns 409 INVOICE_UPDATE_NOT_DRAFT if the invoice is no longer in draft status. Idempotent and dry-runnable.
Use when: You need to correct a typo, push the due date, or update a customer reference on a draft you have not sent yet. The invoice number stays null until the first :send action.
Don't use for: Updating a sent / paid / credited invoice (those are immutable per ML 17 kap; issue a credit note via POST /:id:credit in PR-B-2b). Changing items, currency, or customer — drafts are cheap to delete and recreate.
Pitfalls
- Idempotency-Key is mandatory.
- A 409 INVOICE_UPDATE_NOT_DRAFT means the invoice has been sent / paid / credited / cancelled. The error code name is shared with the DELETE handler.
- Items are immutable here — to change line items, delete the draft and POST a fresh one.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"due_date": "2026-07-15",
"notes": "Förlängd förfallotid"
}
Example response
{
"data": {
"id": "0e9c…",
"status": "draft",
"due_date": "2026-07-15",
"notes": "Förlängd förfallotid"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}