Skip to main content

Expired Invoices

An invoice expires in one of two ways:
  1. The expiresInMinutes time limit is reached without full payment
  2. The invoice is manually cancelled via POST /api/v1/invoices/:invoiceId/cancel

The invoice.expired event

{
  "event": "invoice.expired",
  "eventId": "evt_a1b2c3d4_invoice.expired_1744453800000",
  "timestamp": "2026-04-12T10:30:00.000Z",
  "data": {
    "invoice": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "amount": "49.99",
      "currency": "USD",
      "settlementCurrency": "USDT",
      "settlementChain": "polygon",
      "status": "expired",
      "createdAt": "2026-04-12T10:00:00.000Z",
      "metadata": { "orderId": "order_123" }
    },
    "fees": {
      "platformFee": "0.00",
      "platformFeePercent": "1.5%",
      "networkFee": "0.00",
      "providerFee": "0.00",
      "totalFees": "0.00",
      "currency": "USDT"
    }
  }
}

Handling expiry in your webhook

case 'invoice.expired': {
  const { invoice } = event.data;
  const orderId = invoice.metadata?.orderId;

  await db.orders.update({
    where: { id: orderId },
    data: { paymentStatus: 'payment_expired' },
  });

  await inventory.release(orderId);

  break;
}

Giving the customer a second chance

To let the customer retry, create a new invoice and redirect them to it:
app.post('/orders/:orderId/retry-payment', async (req, res) => {
  const order = await db.orders.findById(req.params.orderId);

  const response = await fetch('https://api.settlx.io/api/v1/invoices', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SETTLX_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: order.total.toString(),
      currency: 'USD',
      description: `Order #${order.id} (retry)`,
      expiresInMinutes: 30,
      webhookUrl: `${process.env.BASE_URL}/webhooks/settlx`,
      metadata: { orderId: order.id },
    }),
  });

  const { data: newInvoice } = await response.json();

  await db.orders.update({
    where: { id: order.id },
    data: {
      settlxInvoiceId: newInvoice.id,
      paymentStatus: 'awaiting_payment',
    },
  });

  res.redirect(newInvoice.paymentUrl);
});

Use caseRecommended expiresInMinutes
E-commerce checkout15–30
B2B / invoice payment1440–10080 (1–7 days)
Bitcoin payment60 minimum
No expiryOmit expiresInMinutes
Bitcoin blocks can take 10–60 minutes. Set a minimum of 60 minutes for any invoice that may be paid with BTC to give customers enough time to complete the transaction.

Manually cancelling an invoice

Cancel a pending invoice before its natural expiry:
await fetch(`https://api.settlx.io/api/v1/invoices/${invoiceId}/cancel`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${process.env.SETTLX_API_KEY}` },
});
Cancellation immediately sets the invoice status to expired and fires the invoice.expired webhook.

Checkout countdown timer

Always show a countdown so customers know how long they have:
function startCountdown(expiresAt) {
  const interval = setInterval(() => {
    const remaining = new Date(expiresAt) - Date.now();

    if (remaining <= 0) {
      clearInterval(interval);
      showExpiredUI();
      return;
    }

    const minutes = Math.floor(remaining / 60000);
    const seconds = Math.floor((remaining % 60000) / 1000);

    document.getElementById('timer').textContent =
      `${minutes}:${seconds.toString().padStart(2, '0')}`;
  }, 1000);
}

Late payments after expiry

If a customer sends funds to an expired invoice’s deposit address:
  • The payment is detected on-chain
  • It is not automatically settled — the invoice is closed
  • Contact Settlx support to manually process the payment