easyway
Create account

Handle underpaid and late crypto payments

With crypto the customer sends the money, not you, so the amount and the timing are theirs. Some wallets subtract the network fee from the amount, some customers pay from an exchange hours later, some send twice. This guide shows what easyway does in each case and what your order logic should do about it.

The rules in one table

An invoice's coin amount (pay_amount) is fixed when the coin is chosen. Every payment to its address is compared with it once it has the coin's confirmations. Each confirmed payment is credited to your balance minus the fee — the fee is taken from what arrived, so a short payment pays a smaller fee.

What the customer didInvoice statusWebhookOn your balance
Sent the full amount (or within your tolerance)paidinvoice.paidWhat arrived, minus the fee
Sent lessunderpaidinvoice.underpaidWhat arrived, minus the fee
Sent the rest laterpaidinvoice.paidBoth payments, minus the fee
Sent morepaidinvoice.paidEverything that arrived, minus the fee
Paid again after it was paidpaid (unchanged)NoneCredited; received_amount grows
Paid nothing before expires_atexpiredinvoice.expiredNothing
Paid after expiry, within 7 dayspaid or underpaidinvoice.paid or invoice.underpaidWhat arrived, minus the fee

Each short payment sends its own invoice.underpaid, so one invoice can produce several. Before the confirmations are in, a pending invoice shows confirming and you get invoice.confirming — that is not money yet.

Underpayment tolerance

In Dashboard → Settings, Underpayment tolerance (0 %, 0.5 %, 1 %, 2 %, 3 % or 5 %; 1 % by default) decides how short a payment may be and still count as paid. It exists because many wallets and exchanges deduct their withdrawal fee from the amount the customer typed. With 1 %, a 100 USDT invoice that receives 99.2 USDT is paid; with 0 % it is underpaid and asks the customer for 0.8 USDT more. A paid invoice inside the tolerance keeps a small remaining_amount, so always decide on status.

When a customer underpays

The payment page switches to "send the remaining amount" with the missing figure and the same address (and the same memo or tag on TON and XRP). If Email me about underpayments is on in Settings, you get an email with what arrived and what is still owed, sent to the API key's support address, your support address or your account email. An underpaid invoice never expires: it stays underpaid until it is topped up. Your options:

PolicyWhen it fits
Wait for the top-up, then fulfil on invoice.paidThe default for physical goods and anything you cannot take back.
Fulfil partly (credit the customer what arrived)Account top-ups, prepaid credit, donations.
Return the moneyThere is no refund button: send a payout to an address the customer gives you. Live payouts go only to whitelisted addresses.

Late payments

An invoice expires after its lifetime (60 minutes unless you set it per invoice or in Settings). If the customer had already chosen a coin, its address stays watched until 7 days after expires_at. A payment in that window is credited and the expired invoice turns paid or underpaid, with the normal webhook — so an expired order is not a dead order. Release reserved stock on invoice.expired, but keep the order and accept a later invoice.paid. The coin amount does not change after expiry, even if the price has moved.

The same holds for an invoice you cancelled after the coin was chosen: a payment that still arrives is credited and the invoice turns paid. A payment more than 7 days after expires_at is not detected automatically.

A handler that covers every case

Events can arrive out of order and more than once, so the handler looks at the invoice's current status, never moves a fulfilled order backwards and remembers event ids. Save the invoice id on your order when you create it.

Node.js

// Runs after the signature check (see the webhook signatures guide).
async function handleInvoiceEvent(event) {
  if (await db.seenEvent(event.id)) return;            // a retry repeats the same event id
  const inv = event.data.invoice;
  if (!inv) return;                                     // payout.* and webhook.test carry no invoice
  const order = await db.orderByInvoiceId(inv.id);      // the id you saved when you created it
  if (!order) return;

  // Decide on the status, never on remaining_amount: an invoice paid within your
  // tolerance is "paid" with a small remaining_amount left.
  if (inv.status === "paid" && order.state !== "fulfilled") {
    if (order.state === "cancelled") {
      // Paid after you gave up on it (expired or cancelled order): the money is on
      // your balance. Ship it anyway, or refund it by hand - but never ignore it.
      await db.flagForReview(order.id, "paid_after_cancel", inv.received_amount);
    } else {
      await db.fulfil(order.id);                        // exactly once: state becomes "fulfilled"
    }
  } else if (inv.status === "underpaid" && order.state !== "fulfilled") {
    await db.setOrderState(order.id, "underpaid", { received: inv.received_amount, missing: inv.remaining_amount });
  } else if (event.type === "invoice.expired" && order.state === "open") {
    await db.releaseStock(order.id);                    // keep the order: a late payment may still come
  }
  await db.rememberEvent(event.id);
}

When an old order is in doubt, read the invoice again instead of trusting a stored webhook:

// Re-read an invoice before acting on an old order; received_amount is the total credited
const res = await fetch("https://api.easyway.cash/v1/invoices/inv_7Q2mN4kL8pR1sT9uV3wX0y", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.EASYWAY_API_KEY}`,
  },
});
if (!res.ok) throw new Error(`easyway ${res.status}: ${(await res.json()).error}`);
const data = await res.json();

Test each case before going live

In test mode, Simulate payment on a test invoice accepts an amount: enter less than due for an underpayment, press again with the field empty for the top-up, and simulate on an expired invoice for a late payment. The sandbox guide has the full test plan, the webhook signatures guide the signature check that runs before this handler, and the API reference every status and field.

Create a merchant accountRead the API docs
Handle underpaid and late crypto payments | easyway