external_id and idempotency
Correlating your world with Invoia without ever holding an Invoia UUID.
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
Send external_id in the create body:
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:
{ "object": "customer", "id": "…", "external_id": "clickup:cust_ACME", "name": "ACME Corp", … }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.
# 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:
Everywhere the API takes a resource ref (in URLs, in bodies), it accepts three forms:
ext:your-id— yourexternal_id.email:someone@example.com— users only.- A raw UUID — Invoia's internal id.
# 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
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 attachexternal_id). - Work registrations (label-only; the row's identity is still the
(line, user, entry-month)composite key).
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
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).