Issuance and auto-send
Issuing an invoice yourself or leaving it to the daily run, a booked invoice that was not delivered, and credit notes.
A draft invoice is Tabba's own record. Issuing it makes the accounting system book the invoice and send it to the customer. From then on the invoice is frozen. There are two ways to issue, and both behave the same on either accounting system: see Accounting platforms.
Issue it yourself
Call the issue endpoint when your own workflow says the invoice is ready:
# A one-time invoice.
curl -X POST https://www.tabba.io/api/v1/invoices/ext:crm:acme-migration/issue \
-H "Authorization: Bearer $TABBA_API_KEY"
# An occurrence of a recurring series, named by the first day of its period.
curl -X POST https://www.tabba.io/api/v1/recurring-series/ext:crm:acme-retainer/occurrences/2026-07-01/issue \
-H "Authorization: Bearer $TABBA_API_KEY"The two endpoints are POST /invoices/{ref}/issue and
POST /recurring-series/{ref}/occurrences/{period}/issue. Neither takes a
body.
The accounting system books and sends the invoice inside the request, and the
answer is the issued invoice: status is issued, and issued_at,
platform_ref and platform_number are set. It is the explicit "bill this
one now": an invoice with auto_send off is issued all the same, and one
with no issue_date, or a date ahead, is dated today.
Sending it again is safe. An invoice that is issued already is answered
as it stands, 200, and nothing is booked twice. While another attempt holds
the invoice, the answer is 409 with ISSUE_IN_PROGRESS and a Retry-After
header: send the same request again after the interval.
An occurrence that is not an invoice yet is made one, and that invoice is
issued. {period} is the first day of the occurrence's billing period, as
GET /recurring-series/{ref}/occurrences states it.
Both endpoints are counted against the platform_write rate limit as well as
the default one: see Errors.
Leave it to the daily run
Every invoice carries an auto_send boolean. While it is on, the daily run
issues the invoice without your involvement:
- An invoice with an
issue_dateis issued on that date. - An invoice with no
issue_dateis issued once all the work on it is complete.
auto_send is on for a new invoice unless the create says otherwise.
Subscribe to the invoice.issued event to learn of the booking:
// Your webhook receiver, after the signature is verified.
app.post('/webhooks/tabba', async (req, res) => {
const event = req.body
if (event.type === 'invoice.issued') {
await syncInvoiceToYourSystem(event.data)
}
res.status(200).end()
})To turn it off on a draft, replace the draft with PUT /invoices/{ref} and
auto_send: false. The body of a PUT is the whole draft: a line that
states its id stays the line it was, and a line the body leaves out is
removed. Left out, auto_send stays as it is.
Booked, and not delivered
Issuing is two things: the accounting system books the invoice, and it
sends it to the customer. Booking is what puts the invoice in the books.
When the booking succeeds and the delivery is not known to have succeeded,
the answer is still 200, with a send_error:
{
"object": "invoice",
"id": "…",
"status": "issued",
"issued_at": "2026-07-01T08:00:00.000Z",
"platform_ref": "…",
"platform_number": 1042,
"send_error": {
"reason": "…",
"platform_reason": "…"
}
}reason is a code of Tabba's own, and platform_reason is the accounting
system's own words, where it gave any. On an invoice whose delivery
succeeded, send_error is null.
Do not issue it again. The invoice is in the books, and a second call
answers the same invoice. The invoice.send_failed event is sent beside
invoice.issued for such an invoice. Following up on the delivery is done in
the workspace: the API has no endpoint that sends a booked invoice again.
When an invoice is not issued
Anything short of issued is an error with its own code, never a 200:
| Code | Status | What happened |
|---|---|---|
HOURLY_INCOMPLETE | 409 | A person on an hourly line is below full completion |
NOTHING_TO_BILL | 409 | The invoice has no hours to bill |
CUSTOMER_GONE, PRODUCT_GONE | 409 | The accounting system no longer holds what the invoice names |
ISSUE_IN_PROGRESS | 409 | Another attempt holds the invoice. Retry after Retry-After |
MISSING_LINES | 422 | The invoice has no lines |
INVALID_SEND_METHOD, MISSING_EINVOICE_DETAILS | 422 | The customer cannot be sent to. Checked before anything is booked |
ISSUE_REFUSED | 422 | The accounting system refused to book. refusal says why |
PLATFORM_API_ERROR | 502 | The accounting system could not be reached, or did not answer |
Issuing an occurrence adds three: OCCURRENCE_SKIPPED (409) for an
occurrence an admin skipped, OCCURRENCE_HAS_NO_LINES (422) where the
series states no line for the occurrence, and OCCURRENCE_OUT_OF_RANGE
(404) for a date that starts no period of the series.
Changing an issued invoice
An issued invoice is frozen. PUT /invoices/{ref} and
DELETE /invoices/{ref} on one are refused with INVOICE_ALREADY_ISSUED
(409). To take it back, in full or in part, raise a credit note:
curl -X POST https://www.tabba.io/api/v1/credit-notes \
-H "Authorization: Bearer $TABBA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "crm:acme-migration:credit-1",
"invoice": "ext:crm:acme-migration",
"kind": "partial",
"credit_date": "2026-07-15",
"lines": [
{ "invoice_line_id": "5f8e2c1a-3b4d-4e6f-8a9b-0c1d2e3f4a5b", "amount": 2500 }
]
}'kind: "full"takes the whole invoice back and reads nolines. It is refused withINVOICE_ALREADY_CREDITEDonce anything was credited.kind: "partial"names lines of the invoice, each by no more than is left on it. Amounts are before VAT. Too much is aVALIDATION_FAILEDwithAMOUNT_EXCEEDS_BILLEDunder the line.
The accounting system books and sends the credit note inside the request, and
the answer is the booked credit note, 201. As with an invoice, a credit
note that was booked and not delivered is a success that carries a
send_error.
The external_id is the credit note. Sending the request again under the
same external_id books nothing twice: a booked credit note is answered as
it stands, 200, and an attempt that did not book is taken up again. An
invoice that is not issued is refused with INVOICE_NOT_ISSUED. While
another credit of the invoice is running the answer is CREDIT_IN_PROGRESS
with Retry-After, and an attempt the accounting system gave no definite
answer to holds every other credit of the invoice back
(CREDIT_ATTEMPT_UNRESOLVED) until the request that made it is sent again.
GET /invoices/{ref}/credit-notes lists the credit notes booked against an
invoice. There is no endpoint that reads a single credit note.
Occurrences of a recurring series
An occurrence of a recurring series is read as an invoice once it is one:
GET /invoices lists it with its series_id, period_from and period_to.
It is written through its series. PUT /invoices/{ref} and
DELETE /invoices/{ref} on an occurrence are refused with
INVOICE_IS_OCCURRENCE (409): change its lines through
PATCH /recurring-series/{ref}/lines/{lineRef} instead.
A series created through the API is active at once, and the occurrences whose period has started are invoices from that moment.
Issuing many invoices
There is no batch endpoint. To issue several invoices, call the issue
endpoint for each: every call is safe to repeat, so a run that stopped
halfway can be started again from the top. The platform_write limit bounds
how fast one key can issue.
For invoices that should go out on a date without a call from you, leave
auto_send on and let the daily run issue them.