Return URL handling
Build the page customers return to after checkout, and confirm the result on your server before you fulfil.
After checkout, Teyra Pay redirects the customer to your return_url with the payment ID and a status:
https://shop.example.com/thanks?payment_id=pay_01JBX4T9W2&status=succeeded| Parameter | Values |
|---|---|
payment_id | The pay_… ID of the payment. |
status | succeeded, failed or processing. failed also covers payments that were canceled or expired. |
Never trust the redirect alone
The query string is not proof of payment
Anyone can open https://shop.example.com/thanks?payment_id=pay_123&status=succeeded in a browser. Never fulfil an order, ship goods or unlock content because of the redirect parameters. Always confirm with the API or a verified webhook.
There are two reliable sources of truth:
GET /v1/payments/:idwith your secret key, from your server.- A webhook with a valid
Teyra-Signature.
Use both. The return page gives the customer an immediate answer; the webhook covers customers who close the browser, lose connection or never come back.
A correct return handler
On your return page:
- Read
payment_idfrom the query string. - Fetch the payment from your server with
GET /v1/payments/:id. - Check that the payment belongs to this order (compare
referenceor theidyou stored) and thatamountandcurrencymatch. - Act on the fetched
status, not the query parameter.
app.get('/orders/:id/return', async (req, res) => {
const order = await db.orders.find(req.params.id);
const paymentId = String(req.query.payment_id || '');
// The payment must be the one we created for this order.
if (!paymentId || paymentId !== order.paymentId) return res.status(400).render('payment-error');
const response = await fetch(`https://api.teyra.co/v1/payments/${encodeURIComponent(paymentId)}`, {
headers: { Authorization: `Bearer ${process.env.TEYRA_SECRET_KEY}` },
});
const payment = await response.json();
if (payment.status === 'succeeded' && payment.amount === order.totalMinor && payment.currency === order.currency) {
await markOrderPaid(order.id, payment.id); // must be idempotent: the webhook may get there first
return res.render('thank-you', { order });
}
if (payment.status === 'processing' || payment.status === 'requires_payment') {
return res.render('payment-pending', { order }); // the webhook will finish the job
}
return res.render('payment-failed', { order, reason: payment.failure_message });
});On the failure page, offer a Try again button that sends the customer back to the payment's checkout_url: a failed or canceled payment can still be paid there until expires_at.
When the status is processing
Some methods confirm a few seconds after the customer returns. Show a "confirming your payment" page, and either refresh it every few seconds (by asking your server, which calls GET /v1/payments/:id) or tell the customer you'll email them. The payment.succeeded or payment.failed webhook arrives as soon as the result is final.
Make fulfilment idempotent
The return page and the webhook can arrive in any order, and webhooks can be delivered more than once. Write markOrderPaid so that calling it twice has no extra effect, for example by updating the order only WHERE status = 'pending'.