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.