# external_id and idempotency

> Correlating your world with Invoia without ever holding an Invoia UUID.

Source: https://invoia.io/developers/api/external-id

Invoia's public API is designed so **you never need to store an Invoia
UUID**. Instead you stamp your own id — the ClickUp task id, the Monday
board id, the Jira ticket key — onto every resource via
`external_id`, and address it later by `ext:your-id`.

## Creating a resource [#creating-a-resource]

Send `external_id` in the create body:

```bash
curl -X POST https://invoia.io/api/v1/customers \
  -H "Authorization: Bearer $INVOIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "clickup:cust_ACME",
    "name": "ACME Corp",
    "country_key": "DK",
    "is_person": false,
    "send_method": "email",
    "email": "billing@acme.example"
  }'
```

Response echoes the `external_id`:

```json
{ "object": "customer", "id": "…", "external_id": "clickup:cust_ACME", "name": "ACME Corp", … }
```

## Upsert semantics [#upsert-semantics]

**Repeating a create with the same `external_id` upserts.** A retried
sync is safe by construction — no separate `Idempotency-Key` header, no
duplicate rows.

```bash
# Second call with the same external_id — updates the same row.
curl -X POST https://invoia.io/api/v1/customers \
  -H "Authorization: Bearer $INVOIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "clickup:cust_ACME",
    "name": "ACME Corp (Denmark)",
    "country_key": "DK",
    "is_person": false,
    "send_method": "email",
    "email": "billing@acme.example"
  }'
```

This is the ONE idempotency primitive. Stripe's `Idempotency-Key` header
is **not** supported — `external_id` is the sole mechanism.

## Addressing by ext: [#addressing-by-ext]

Everywhere the API takes a resource ref (in URLs, in bodies), it accepts
three forms:

* `ext:your-id` — your `external_id`.
* `email:someone@example.com` — users only.
* A raw UUID — Invoia's internal id.

```bash
# Read your customer by ext id — no UUID round-trip needed.
curl https://invoia.io/api/v1/customers/ext:clickup:cust_ACME \
  -H "Authorization: Bearer $INVOIA_API_KEY"

# Patch it.
curl -X PATCH https://invoia.io/api/v1/customers/ext:clickup:cust_ACME \
  -H "Authorization: Bearer $INVOIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "new-billing@acme.example" }'
```

## Where external\_id is required [#where-external_id-is-required]

Required on the **money-moving creates**:

* `POST /invoices` — a one-time invoice.
* `POST /recurring-series` — a retainer.
* `POST /credit-notes` — a reversal.

Optional (but recommended) everywhere else:

* Customers, products, users (tag an existing user with `PATCH /users/{ref}`
  to attach `external_id`).
* Work registrations (label-only; the row's identity is still the
  `(line, user, entry-month)` composite key).

## external\_id is NOT external\_reference [#external_id-is-not-external_reference]

Customers and products have a **separate** `external_reference` field
that passes through to Dinero — used for the accounting-side lookup.

* `external_id` — Invoia's correlation id for YOUR world. Never sent to
  Dinero.
* `external_reference` — Dinero's identifier as shown in Dinero.

Keep them separate so a change to your ClickUp id doesn't accidentally
overwrite the Dinero reference (and vice versa).

## Uniqueness [#uniqueness]

`external_id` is unique within `(integration, resource_type)`. Two
integrations can safely use the same string; two resource types on the
same integration can too (a customer and a product may share
`clickup:demo` without conflict).
