Invoice lifecycle
The normative status vocabulary — internal/buyer statuses, merchant API payment_status, CP adapter codes, widget callbacks, and IPN trigger points.
An invoice's status is exposed through several surfaces — the buyer-facing status endpoint and widget, the merchant API, the CoinPayments adapter, and the IPN webhook — and each surface uses its own vocabulary. This page is the normative equivalence table between them. If any other page disagrees with this one, this page wins.
Two vocabularies
Internal / buyer vocabulary — used by the database, the public buyer endpoint GET /v1/invoices/:id/status, and the hosted checkout / widget. Six statuses:
pending, partial, paid, overpaid, expired, cancelled
overpaid is a real, persisted status — an invoice is classified as overpaid using per-merchant tolerances (overpayBps, default 500 = +5% over the invoice amount).
Merchant API vocabulary (payment_status) — the NP-style names returned by GET /v1/payment/:id and carried in IPN payloads. Five statuses:
waiting, partially_paid, finished, expired, failed
Equivalence table (normative)
| Internal / buyer / widget | payment_status (merchant API) | CP status | Widget callback | IPN fired? |
|---------------------------|--------------------------------|-----------|-----------------|------------|
| pending | waiting | 0 | (none) | No |
| partial | partially_paid | 0 | onPartial | No — a partial payment currently does not emit an IPN. Detect it by polling; an IPN on partial payment is on the roadmap. |
| paid | finished | 100 | onSuccess | Yes (invoice.paid) |
| overpaid | finished | 100 | onSuccess | Yes — delivered as paid/finished. The IPN never says "overpaid"; detect over-payment by comparing actually_paid > pay_amount. |
| expired | expired | -1 | onExpired | Yes (invoice.expired) |
| cancelled | failed | -1 | onCancel | No — no IPN is sent. Detect cancellation by polling GET /v1/payment/:id, which returns failed. |
In addition to status transitions, the IPN can deliver informational notifications for wrong_token_received, suspicious_token_received, and late_deposit_received. Their payload is the current state of the invoice — they do not represent a status change.
For full IPN payload structure and HMAC verification, see Payment IPN.
Status flow
Using the internal / buyer vocabulary:
pending
│
├─► partial ──► paid | overpaid
│ │
│ └─► expired
│
├─► paid | overpaid
├─► expired (no sufficient payment before deadline)
└─► cancelled (buyer cancels — only possible while pending)
The happy path is pending → paid (or pending → partial → paid if the buyer tops up). Buyer-side cancellation is only possible while the invoice is still pending; once any payment has been detected the cancel option is gone.
Terminal statuses (internal vocabulary): paid, overpaid, expired, cancelled. In the merchant API vocabulary these appear as finished, expired, and failed. Terminal statuses will not change — once an invoice reaches one you can safely archive it.
Over-payment and partial payment
Partial payment (partial / partially_paid)
The buyer sent less than the required amount. The invoice stays open until it either receives a top-up that brings it to the full amount (transitioning to paid/finished) or the deadline passes (transitioning to expired).
No IPN is emitted for a partial payment today — the widget fires onPartial on the buyer's screen, but your backend must detect the partially_paid state by polling GET /v1/payment/:id. Server-side partial-payment IPNs are on the roadmap.
Over-payment (overpaid)
overpaid is a dedicated status on the buyer/widget surface: an invoice whose received total exceeds the invoice amount beyond the merchant's tolerance (overpayBps, default 500 = +5%) is persisted as overpaid. However, the merchant API and the IPN present it as finished/paid — the IPN never reports "overpaid". To detect over-payment in your handler, compare actually_paid against pay_amount in the payload. The excess amount is routed to treasury.
Cancellation
Buyers can cancel a pending invoice from the hosted checkout. Merchants cancel from the dashboard or the app's cancel action — there is no REST cancel endpoint in /v1. A cancelled invoice returns payment_status: "failed" from the merchant API and fires no IPN, so poll GET /v1/payment/:id if you need to observe cancellations server-side.
Polling vs webhooks
Webhook delivery is the recommended approach for the paid, overpaid (delivered as paid), and expired transitions. For partial and cancelled — which fire no IPN today — and for buyer-facing UIs that need live status, poll GET /v1/payment/:id or use the embeddable widget, which handles polling internally.
For more detail on reading invoice state from the API, see Reading invoices.