Skip to main content

Confirming a payment

Three signals can tell you a payment happened. They are not equally trustworthy.

SignalTrustWhy
The checkout() promise⚠️ LowRuns in the customer's browser, which they control
The returnUrl redirect⚠️ LowThe customer may close the tab or lose signal first
orders.fetch()✅ HighYour server asking ours
A signed webhook✅ HighestArrives regardless of the browser, and is signed

The rule​

Never unlock a product, ship an item, or mark an invoice paid based on anything the browser told you.

app.get('/api/checkout/status/:orderId', async (req, res) => {
const order = await gateway.orders.fetch(req.params.orderId);

if (order.orderStatus === 'PAID') {
await fulfilOnce(order.orderId); // idempotent — see below
return res.json({ paid: true });
}
res.json({ paid: false, status: order.orderStatus });
});

Make fulfilment idempotent​

Both the status check and the webhook can fire for the same order. Whichever arrives second must do nothing.

async function fulfilOnce(orderId) {
const row = await db.orders.findOne({ orderId });
if (!row || row.status === 'paid') return; // already done
await db.orders.update({ orderId }, { status: 'paid', paidAt: new Date() });
await shipTheThing(row);
}

A unique constraint on orderId in your database is a better guard than an if — two concurrent requests can both pass the check.

Order statuses​

StatusMeaning
ACTIVECreated, not yet paid
PAIDMoney captured. Safe to ship.
EXPIREDThe session ran out
TERMINATEDCancelled