Developers

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 — your external_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 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

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).