Listen for Webhooks
Register your endpoint and process session events idempotently, with a worked handler.
Webhooks are the most reliable way to receive real-time updates about the status of your payment sessions and transactions. Instead of manually polling the API, Tonder sends an HTTP POST request to your server when an event occurs.
Prerequisites
You need a publicly accessible URL on your server — for example
https://your-store.com/webhooks/tonder — that can receive POST requests. This cannot be a
localhost URL.
For local testing, services like ngrok can create a public URL that forwards requests to your local machine.
Configure and listen for webhooks
- Log in to the Dashboard: dashboard-stage.tonder.io (Sandbox) or dashboard.tonder.io (Production).
- Navigate to Developers → Webhooks.
- Click Add Endpoint.
- Paste your public endpoint URL into the Endpoint URL field.
- Click Save.
Your endpoint must accept POST requests with a JSON body. When an event occurs, Tonder sends a request like this:
{
"action": "session.completed",
"type": "checkout.hosted",
"data": {
"id": "cs_97_41521_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d",
"amount_total": 150.00,
"currency": "MXN",
"status": "completed",
"payment_id": 41521,
"transaction_status": "Success",
"metadata": { "external_id": "ORD-001" }
}
}Your external_id does not travel as a top-level payload field. To receive it here, send it
inside metadata when you create the session; it arrives as data.metadata.external_id. See
Reference.
To let Tonder know you received the webhook, your server must respond with a 200 OK status. If
Tonder doesn't receive a 200 OK, it assumes the delivery failed and retries. Respond
immediately, before running any complex business logic, to avoid timeouts.
const express = require('express');
const app = express();
app.post('/webhooks/tonder', express.json(), (req, res) => {
const event = req.body;
// 1. Acknowledge receipt immediately
res.status(200).send();
// 2. Process the event
switch (event.action) {
case 'session.completed':
const session = event.data;
console.log(`Payment successful for session: ${session.id}`);
// TODO: update your database, fulfill the order, etc.
break;
case 'session.expired':
const expiredSession = event.data;
console.log(`Session expired: ${expiredSession.id}`);
// TODO: mark the order as cancelled.
break;
default:
console.log(`Unhandled event type: ${event.action}`);
}
});
app.listen(3000, () => console.log('Listening for webhooks on port 3000'));Don't rely on the received payload alone. Before fulfilling the order, re-fetch the session or
transaction status from the API (by payment_id or metadata.external_id) and respond 200 to
acknowledge receipt.
Always confirm the result server-side (by re-fetching the status) and process webhooks idempotently to avoid duplicate actions on retries.
