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:
| Form | Names |
|---|---|
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:
| Create | Under an external_id that is taken |
|---|---|
POST /customers | Updates that customer. 200 |
POST /products | Updates that product. 200 |
POST /invoices | Replaces 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-series | Saves the header of that series and leaves its lines as they are. 200 |
POST /credit-notes | Is 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 /invoicesPOST /recurring-seriesPOST /credit-notes
Optional elsewhere:
POST /customersandPOST /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 isexternal_id. PUT /work-registrationstakes 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 itsexternal_idis refused withRECORD_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_idagain 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 whatext:resolves.external_reference: a field of the record in the accounting system. It is not a key, and nothing is addressed by it.