Tabba.
IntegrationerSådan virker TabbaFeatures

© 2026 Tabba. Alle rettigheder forbeholdes.

Udviklere
Udviklere
Udviklere
Getting startedQuickstartAuthenticationAccounting platformsexternal_id and idempotencyWork registrationIssuance and auto-sendWebhooksPaginationErrors

Andre spor

AI clientsAPI reference

external_id and idempotency

Naming every record by your own key, and why a retried create makes nothing twice.

The API is built so that you name a record by your own key. You stamp the id your system already has, a CRM account id or a project key, on a record as its external_id, and address the record afterwards as ext: and that id. Creating again under an external_id that is taken never makes a second record, which is what makes a retried sync safe.

References

Wherever a request names a record, it takes a reference in one of three forms:

FormNames
ext:<external_id>The record that carries your own key
email:<address>A person, by their address. People only
<id>The record's own id, as the API states it

A reference is accepted in a path (GET /customers/{ref}), in every field of a body that is named for a record (customer, product, user, invoice, series) and in the customer filter of a list. A field that ends in _id states an id, and is what a response answers.

# Read a customer by your own key.
curl https://www.tabba.io/api/v1/customers/ext:crm:acme \
  -H "Authorization: Bearer $TABBA_API_KEY"

# Change one field of it.
curl -X PATCH https://www.tabba.io/api/v1/customers/ext:crm:acme \
  -H "Authorization: Bearer $TABBA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "new-billing@acme.example" }'

URL-encode a reference in a path when it contains a character a path cannot carry, such as /, ?, # or a space. An external_id is text of your own choosing, 1 to 255 characters.

A customer's and a product's id is the accounting system's handle for the record: opaque text, not always a UUID. A person's id, and the id of an invoice, a recurring series and a credit note, is a UUID.

A path that names a record the company does not hold is answered 404 with NOT_FOUND. A customer, a product or a person named in a body that the company does not hold is a 422 with a code under the field: CUSTOMER_NOT_FOUND, PRODUCT_NOT_FOUND or ASSIGNEE_NOT_FOUND.

Create, or update what carries the id

Send external_id in the body of a create:

curl -X POST https://www.tabba.io/api/v1/customers \
  -H "Authorization: Bearer $TABBA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "crm:acme",
    "name": "ACME ApS",
    "country_key": "DK",
    "is_person": false,
    "send_method": "email",
    "email": "billing@acme.example"
  }'

The answer is 201 and the customer, with its external_id. Send the same request again, with another name, and the answer is 200: the customer that carries the id was updated. There is no Idempotency-Key header. The external_id is the one mechanism.

What a repeated create does depends on the record:

CreateUnder an external_id that is taken
POST /customersUpdates that customer. 200
POST /productsUpdates that product. 200
POST /invoicesReplaces that draft: a line that states its id stays the line it was, with the work registered on it. 200. An issued invoice is refused with INVOICE_ALREADY_ISSUED
POST /recurring-seriesSaves the header of that series and leaves its lines as they are. 200
POST /credit-notesIs the same credit note, whatever the body says the second time. Booked, it is answered as it stands, 200, and nothing is booked twice

Where external_id is required

Required on the creates of Tabba's own documents:

  • POST /invoices
  • POST /recurring-series
  • POST /credit-notes

Optional elsewhere:

  • POST /customers and POST /products. A create without one is not safe to send again: it makes a second record.
  • People are not created through the API. Tag one with PATCH /users/{ref}, whose only field is external_id.
  • PUT /work-registrations takes one as a label for the month's registration. The registration is idempotent without it, because it states totals.

Changing and clearing an id

PATCH /customers/{ref}, PATCH /products/{ref}, PATCH /recurring-series/{ref} and PUT /invoices/{ref} take external_id: a value sets it, null clears it, and left out it stays as it is. An id that another record of the same kind already carries is refused with DUPLICATE_EXTERNAL_ID (409).

An external_id is unique per company and kind of record, so a customer and a product can carry the same string. A person's is your organisation's key for them, unique in the organisation: a person who also belongs to another organisation keeps that organisation's id apart from yours, and neither key reads or writes the other's.

Retrying a create

One create runs at a time under an external_id. A second request while the first is still running is answered 409 with CREATE_IN_PROGRESS and a Retry-After header. Nothing was written and the accounting system was not asked. Send the same request again after the interval, and it is answered the record the first one made.

A customer create whose answer was lost can be sent again. When the accounting system could not be reached or did not answer, the request is answered 502 with PLATFORM_API_ERROR: the customer may or may not exist. Send the same request again, with the same body and within the hour, and it is answered the customer the first request made, if it made one. A create with no external_id has no such guard.

A create the accounting system did not finish names its record. The answer is 502 with CREATE_UNFINISHED, and record_id is the id of the customer that was made. It is in the catalog under its external_id. Send the same request again, or a PATCH by that id, to finish it.

A product number names one product. A create whose product_number the accounting system already holds is answered that product, 200, and nothing is written to it. The product is given your external_id unless it carries another one, which is refused with DUPLICATE_PRODUCT_NUMBER (409).

Two creates of one invoice or one series at the same moment make one record. The second is answered that record, or CREATE_IN_PROGRESS while the first is still writing it.

Records that leave the accounting system

The API lists and addresses only what the accounting system still offers.

  • Switched off. A customer or a product that is barred or archived in the accounting system is answered 404. A create under its external_id is refused with RECORD_ARCHIVED (409): it is neither updated nor created again until the accounting system offers it once more.
  • Deleted. A record deleted in the accounting system no longer carries its external_id. The id finds nothing, and the next create under it makes a new record.
  • Restored. A record the accounting system brings back under the handle it had has its external_id again after the next import, unless you have given the id to another record in between. One id names one record.
  • A reused number. Where the accounting system gives a deleted record's number to a new record, the new record does not inherit the external_id.

The customer.updated and product.updated events say when a record was switched off or deleted: see Webhooks.

external_id is not external_reference

A customer and a product have a separate external_reference field. It is the accounting system's own reference field on the record.

  • external_id: your key for the record. It stays in Tabba, and it is what ext: resolves.
  • external_reference: a field of the record in the accounting system. It is not a key, and nothing is addressed by it.

Accounting platforms

One API over Dinero and e-conomic: what is identical, the fields that name a platform's own choice, and what a platform can refuse.

Work registration

Setting a person's hours and completion on a line, as totals.

På denne side

ReferencesCreate, or update what carries the idWhere external_id is requiredChanging and clearing an idRetrying a createRecords that leave the accounting systemexternal_id is not external_reference

Denne side

Vis som MarkdownÅbn i Claude