# external_id and idempotency

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

Source: https://www.tabba.io/developers/api/external-id

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 [#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.

```bash
# 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 [#create-or-update-what-carries-the-id]

Send `external_id` in the body of a create:

```bash
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 [#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 [#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 [#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 [#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](/developers/api/webhooks).

## external\_id is not external\_reference [#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.
