
Crypto invoices look simple until you run them in production: create an invoice, show an amount, wait for the customer to pay, then mark the invoice as paid.
The problems start around the edges. A customer sends less than requested, sends more than requested, pays after expiry, uses the wrong network, or sends two transfers for one invoice. The blockchain transaction may arrive before your checkout screen updates. A webhook may be retried. A support agent may look at the invoice before confirmations are final.
If your team wants to accept crypto payments in a real product, these cases should be modeled as expected payment states, not treated as random support tickets.
This article covers a practical backend approach to underpayments, overpayments, and expired crypto invoices, with emphasis on payment reconciliation, asynchronous confirmations, idempotency, and event-driven processing.
Why crypto invoices need stricter state handling
Card payments usually sit inside a familiar payment stack. Crypto payment processing is more asynchronous. A transaction is broadcast, detected, confirmed, matched to an invoice, and then reconciled against the expected amount.
That means a crypto invoice should not be treated as just a wallet address plus a note. It behaves more like a small state machine:
- invoice created;
- payment address or payment page shown;
- transaction detected;
- confirmations pending;
- amount matched;
- invoice completed, expired, or sent to review.
The backend should store each transition as an event. That gives support and finance an audit trail, and it gives engineering a way to retry processing without changing the final result twice.
A simple flow can look like this:
invoice_created
↓
waiting_for_payment
↓
transaction_detected
↓
confirmations_pending
↓
amount_reconciled
↓
paid | underpaid | overpaid | late_payment | needs_review
The goal is not to make the integration heavy. The goal is to stop ambiguous cases from leaking into manual reconciliation.
The three common edge cases
| Case | What happened | Backend handling |
|---|---|---|
| Underpayment | Received amount is below the invoice amount | Store the received amount, keep the invoice open or move it to underpaid, and expose the remaining amount if supported |
| Overpayment | Received amount is above the invoice amount | Mark the invoice as payable if policy allows it, record the surplus, and create a refund or balance action |
| Expired invoice | Transaction arrived after the payment window | Do not complete blindly; create a late_payment event and apply the merchant policy |
This table hides the most important part: the API should detect facts, not invent business policy. The business decides whether partial payment is acceptable, whether a late payment can be completed, and how refunds work. The backend should make those decisions enforceable.
Underpayments: do not mark them as complete too early
An underpayment happens when the customer sends less than the requested amount. In stablecoin payments, including USDT payments, this can happen because of manual copying, network fee confusion, wallet UI mistakes, or a split transfer.
A useful underpayment flow should store at least:
- expected amount;
- received amount;
- asset and network;
- transaction hash;
- invoice expiration time;
- reconciliation status.
For most checkout flows, the safer default is not to mark the invoice as paid until the expected amount is received and confirmed. If partial payment is allowed, the customer can be shown the remaining amount. If partial payment is not allowed, the invoice should move to underpaid or needs_review.
Idempotency matters here. If the same transaction event is delivered twice, the backend should not add the received amount twice. A good rule is to deduplicate by transaction hash, network, and invoice ID before updating balances or invoice status.
Overpayments: complete the payment, but track the surplus
An overpayment is usually less urgent for the customer, but it is still a reconciliation issue. If an invoice requests 100 USDT and the customer sends 110 USDT, the backend needs to know what the extra 10 USDT represents.
Common options:
- mark the invoice as paid and create a surplus record;
- create a refund task for the extra amount;
- apply the surplus to an internal balance if the product supports balances.
The surplus should never disappear into a generic wallet balance without context. Finance needs to know what was requested, what was received, which invoice it belongs to, and what happened to the difference.
In an event-driven system, this can be handled by emitting an internal event such as invoice.overpaid. A worker can then create a refund task, notify support, or update the customer balance. The original invoice handler stays focused on payment matching.
Expired invoices: late payment is not always a failed payment
Invoice expiry exists to keep checkout and reconciliation predictable. Even with stablecoin payments, teams often use payment windows to avoid stale invoices, outdated quotes, or long-running pending states.
But expiry does not mean a late blockchain transaction should be ignored. If a customer pays five minutes after the invoice expires, the system still needs to detect the transfer, attach it to the invoice if possible, and apply a policy.
A practical late-payment policy should define:
- invoice payment window;
- confirmation threshold by network;
- whether late payments can be accepted automatically;
- when a late payment goes to review;
- what the customer sees after expiry;
- how refunds are handled if the invoice cannot be completed.
The worst case is a silent mismatch: the customer paid, the invoice says expired, and support has no internal record. Even when the payment cannot be completed automatically, the transaction should be visible in the invoice timeline.
A simple status model for invoice exceptions
You do not need a complicated state machine for the first version. Start with a small set of statuses that product, support, finance, and engineering can all understand.
A practical model might look like this:
-
pending— invoice created, no payment detected yet; -
detected— transaction seen, waiting for confirmations; -
paid— expected amount received and confirmed; -
underpaid— received amount is below the invoice amount; -
overpaid— received amount is above the invoice amount; -
expired— no valid payment arrived inside the payment window; -
late_payment— payment arrived after expiry; -
needs_review— support or finance should check the case.
For backend implementation, keep state transitions explicit. For example, pending → detected → paid is a normal path. expired → late_payment → needs_review is also a valid path. What you want to avoid is jumping from expired to paid without recording why.
Webhook processing should be idempotent as well. Store an event ID or derive a deterministic key from the transaction data. Then process each event once, even if the provider retries webhooks or your worker restarts halfway through. This is one of the places where a crypto payment API should expose stable event identifiers instead of leaving the integration to guess.
Where Cryptoway fits
If your team builds this internally, the work is more than generating addresses. You need invoice creation, payment matching, asynchronous confirmation handling, webhook retries, and reconciliation views for support and finance.
For teams that prefer not to build the invoice layer from scratch, Cryptoway provides crypto payment infrastructure for businesses that need to accept crypto payments. The relevant product layer here is Cryptoway invoices, which can help create payment requests and keep incoming crypto payments easier to track.
That is the whole product mention. The engineering decision is still yours: define the policy first, then choose whether to implement the payment layer internally or use a provider.
Implementation checklist
Before going live with crypto invoices, align these rules:
- supported assets and networks;
- invoice expiry time;
- required confirmation count by network;
- underpayment tolerance, if any;
- overpayment handling policy;
- late-payment handling policy;
- idempotency key design for transaction and webhook events;
- retry behavior for webhook and worker failures;
- refund path and responsible team;
- customer-facing messages for each status;
- internal timeline for support and finance.
A minimal processing loop can be described like this:
onPaymentEvent(event):
key = event.network + ':' + event.tx_hash + ':' + event.invoice_id
if alreadyProcessed(key):
return OK
invoice = loadInvoice(event.invoice_id)
received = sumConfirmedPayments(invoice.id)
if invoice.isExpired() and event.confirmed:
transition(invoice, 'late_payment')
else if received < invoice.expected_amount:
transition(invoice, 'underpaid')
else if received == invoice.expected_amount:
transition(invoice, 'paid')
else:
transition(invoice, 'overpaid')
createSurplusRecord(invoice, received - invoice.expected_amount)
markProcessed(key)
The exact code will differ, but the principle is stable: process events idempotently, store state transitions, and separate payment detection from business policy.
Final thought
Crypto invoice handling is mostly about reducing ambiguity. Underpayments, overpayments, and expired invoices will happen. The question is whether your backend treats them as one-off exceptions or as expected states in the payment lifecycle.
A good crypto payment gateway should make the happy path simple, but the backend value is in the edge cases: reconciliation, retries, asynchronous confirmations, and clean status transitions. That is what separates a wallet address from a payment process a business can actually operate.
Top comments (0)