Idempotency with external_id

Why duplicate invoices happen and how external_id keeps them from charging your customers twice.

Network timeouts, worker retries, and user refreshes all lead to the same POST running twice. The platform solves this with external_id, a merchant-supplied idempotency key.

The contract#

  • You send external_id on POST /api/invoices.
  • The platform checks whether this (merchant_id, external_id) pair already exists.
  • If it does, the existing invoice is returned verbatim. No new invoice is created. No new webhook is emitted.
  • If it does not, the platform creates a new invoice.

Choosing a good external_id#

  • Unique per intent, not per retry. If you are creating an invoice for order #1234, the id is order-1234, not order-1234-retry-2.
  • Stable under retries. If your order-create path is retried, the same external_id must be used.
  • Scoped to you, not the platform. You own the namespace — the platform only checks (merchant_id, external_id) uniqueness.
  • Short enough — typically <=128 chars. The store treats it as an opaque string.

Without external_id#

A client that does not send external_id risks:

  • Creating duplicate invoices on network retries.
  • Showing two QR codes to the same customer for the same order.
  • Double-counting in reconciliation because metadata.order_id now maps to two invoices.

Worked example#

Your checkout flow runs:

create-invoice → show-QR → await-webhook

If the create-invoice POST times out, your client re-issues it. Without external_id you end up with two invoices. With external_id: "order-1234", the second call returns the first invoice unchanged.

Idempotency ≠ retry of failed creates#

external_id is for deduping successful-but-ambiguous creates (you do not know whether the server completed the write). It is not for retrying a 4xx error — fix the payload first.

See also#