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_idonPOST /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, notorder-1234-retry-2. - Stable under retries. If your order-create path is retried, the same
external_idmust be used. - Scoped to you, not the platform. You own the namespace — the platform only checks
(merchant_id, external_id)uniqueness. - Short enough — typically
<=128chars. 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_idnow 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#
- Create an invoice via API — where to pass
external_id. - Reconcile with metadata — how to use
metadataalongsideexternal_id.