# Documentation (/docs)
## Get started [#get-started]
Your first test payment and how to choose an integration model.
What Tonder is, how a payment flows, and how to choose your integration path.
The models behind every integration: lifecycle, 3DS, webhooks, and idempotency.
## Integrations [#integrations]
Four ways to connect Tonder, from least to most control.
Redirect to a Tonder-hosted payment page — lowest effort, no PCI surface.
Drop Tonder's UI components into your web or mobile app.
Call the API directly for a fully custom, server-driven flow.
Combine a Tonder-managed UI with your own server logic.
## Features [#features]
Payment methods, payouts, and real-time events.
Cards, SPEI, OXXO, Cash vouchers, and Mercado Pago — coverage across MX · CO · CL.
Send payouts to your users via SPEI and debit-card deposits.
Receive real-time payment events and confirm server-side.
## Operate [#operate]
Manage and test your integration, from sandbox to production.
Manage transactions, payment links, disputes, and settlements.
Sandbox test cases and the go-live certification checklist.
# API Reference (/docs/api-reference)
Reference generated from `openapi.yml`. A single `/process/` endpoint covers deposits and
withdrawals; the rest of the operations are grouped by product in the sidebar.
* **Payments** — `POST /process/`, the unified payments and withdrawals endpoint.
* **Hosted Checkout** — sessions, transactions, and business configuration.
* **Tokenization** — secure token, access token, and card tokenization.
* **Transactions** — check the status (source of truth).
* **Customers** — get or create a customer.
Authenticate with `Authorization: Token YOUR_API_KEY`. Always verify the final status at
`GET /api/v1/transactions/{id}/`.
# Analytics (/docs/dashboard/analytics)
**Analytics** digs into your payment performance beyond the Home KPIs. It's organized into two
complementary views: **Acceptance & Conversion** (are your payments approved?) and **Deposit
Analytics** (how do your depositors behave?).
**Acceptance ≠ Conversion.** The **acceptance rate** applies to **cards**: payments approved by the
issuing bank. The **conversion rate** applies to **APMs** (SPEI, Oxxo Pay, Mercado Pago): it measures
how many payment intents result in a completed payment. They have different denominators and are never
averaged into a single number.
## Acceptance & Conversion [#acceptance--conversion]
### KPIs [#kpis]
Percentage of all card transactions approved by the issuing bank.
Acceptance rate for first-time depositors only — new customers making their first payment.
Acceptance rate for returning users who have previously completed a deposit.
Conversion rate for alternative payment methods (SPEI, Oxxo Pay, Mercado Pago). Measures how many
payment intents result in a completed payment.
### Charts & sections [#charts--sections]
Trend lines for Overall, FTD, and Trusted acceptance rates.
Horizontal bars showing acceptance/conversion rate per payment method (Cards, SPEI, OxxoPay, etc.).
Table with acceptance rate, average transaction, volume share, and count per card brand (Visa,
Mastercard, Amex).
Banks with the highest acceptance rates, with their authorization rate.
Banks with the lowest acceptance rates, with their authorization rate.
Side-by-side cards comparing first-time depositors vs. returning users on acceptance rate, average
deposit, top decline reason, and preferred payment methods.
## Deposit Analytics [#deposit-analytics]
### KPIs [#kpis-1]
Total dollar value of all deposits received in the period.
Number of new depositors (first-time customers).
Number of unique users who made at least one deposit.
Average dollar value per deposit.
### Charts & sections [#charts--sections-1]
Bar chart (volume in dollars) with a line overlay (transaction count).
Conversion funnel: Payment initiated → Payment authorized → FTD completed, with drop-off
percentages.
Donut chart + table showing how deposits split across amount ranges ($0–500, $501–2K, $2K–10K,
etc.).
Bar chart comparing deposit and withdrawal volume with a net flow line.
Table of highest-value depositors with total deposited, deposit count, average amount, and preferred
method.
Stacked bar chart showing volume share by method over time, plus a summary table.
**No duplicates.** Each payment intent is counted once. All metrics respect the **date range** selected
at the top (Today, 7D, 30D, 90D, or custom).
## How to use it [#how-to-use-it]
* Change the **date range** and compare against the previous period to see trends.
* Use the network/issuer breakdowns to route better and open cases with providers.
* Compare **FTD vs Trusted** to understand new-customer friction.
## Next steps [#next-steps]
KPIs and live summary.
Guardian decisions and 3DS.
Detail for each payment.
# Developers (/docs/dashboard/developers)
The **Developers** section gathers the credentials you need to integrate with Tonder. From here you get
your public key and your account identifier, and you reach
[Webhooks](/docs/dashboard/webhooks) management. Keep your credentials safe and confidential.
## API Keys [#api-keys]
| Credential | Description |
| -------------- | --------------------------------------------------------------------------------------------------------------------- |
| Public API Key | Your public key to process payments, APMs, and Hosted Checkout sessions. Sent as `Authorization: Token {public_key}`. |
| Account ID | Your business account identifier. |
You can **reveal** and **copy** each credential with the on-screen controls. **Sandbox**
(`dashboard-stage.tonder.io`) and **Production** (`dashboard.tonder.io`) keys are different.
Your **secret key** (used for card tokenization) is **server-side only** — never expose
it in the client. See [Authentication](/docs/get-started/authentication).
## Webhooks [#webhooks]
Webhook management (add, test, view deliveries) lives on its own screen. See
[Webhooks (dashboard)](/docs/dashboard/webhooks) for operation, and the
[Webhooks](/docs/webhooks/how-it-works) section for formats, events, and retries.
## Next steps [#next-steps]
API keys, headers, and tokenization.
Register and manage your endpoints.
Sandbox vs. Production.
# Finances (/docs/dashboard/finances)
**Finances** brings together everything about your money in Tonder: your **balances** (what's
available, in transit, or held), the **settlements** that deposit your funds, and **reconciliation** to
square what you charged against what was settled.
## Balances [#balances]
Indicators of the state of your funds:
| Field | Description |
| ---------- | ----------------------------------------------------------------- |
| In Transit | Funds on their way to your bank account, not yet available. |
| Pending | Funds not yet cleared or made available for disbursement. |
| In Reserve | Funds held to cover risks and liabilities from your transactions. |
### Balance movements [#balance-movements]
| Field | Description |
| ----------------- | ---------------------------------------------------- |
| Date of Operation | The date the financial transaction occurred. |
| Amount | The monetary value of the transaction. |
| Type | The nature of the transaction (e.g. charge, refund). |
| Current Balance | Total available funds after the transaction. |
## Settlements [#settlements]
A **settlement** groups the payments Tonder deposits into your bank account.
| Field | Description |
| ------------------------ | ------------------------------------- |
| Amount | The monetary value of the settlement. |
| Status | Current status (e.g. pending, paid). |
| Settlement ID | Unique identifier for the settlement. |
| Expected Settlement Date | The expected completion date. |
Filter by **Settlement ID**, **Start Date**, **End Date**, and **Status**. Click a row to see its detail
(the included transactions), and export to CSV with **Export** (select the range → **Export**).
### Settlement detail [#settlement-detail]
| Field | Description |
| --------------- | -------------------------------------------------------------------- |
| Account Details | The bank account information where the settlement is deposited. |
| Charges | Total amount of charges included in the settlement. |
| Adjustments | Adjustments applied to the settlement amount (corrections, changes). |
| Refunds | Total amount of refunds processed within the settlement period. |
| Rolling Reserve | Funds held to cover risks and liabilities, released at a later date. |
Each transaction included in the settlement also shows:
| Field | Description |
| --------- | ---------------------------------------------------- |
| Type | The nature of the transaction (e.g. charge, refund). |
| Amount | The monetary value of the transaction. |
| Fees | Fees associated with the transaction. |
| Net Total | The amount after deducting fees. |
| Date | The date the transaction occurred. |
## Reconciliation [#reconciliation]
Reconciliation squares what you charged against what was settled, so you can spot discrepancies. The
canonical correlation key between your systems and Tonder is **`metadata.external_id`**, present in every
webhook and transaction.
Use **`metadata.external_id`** as your order identifier on every operation: it makes reconciling
settlements and payments unambiguous. See [Correlation keys](/docs/core-concepts/correlation-keys).
## Next steps [#next-steps]
Balances and amount representation.
metadata.external\_id for reconciliation.
The detail of each payment.
# Fraud & Risk (/docs/dashboard/fraud-risk)
**Fraud & Risk** is the **Guardian** screen — Tonder's anti-fraud system. Here you see which payments
were blocked or allowed, how 3DS behaves, and which signals (emails, BINs, geography) sit behind
suspicious activity — so you can balance fraud prevention with acceptance.
## KPIs [#kpis]
Percentage of transactions blocked by Tonder's anti-fraud system (Guardian).
Percentage of 3D Secure challenges where the customer abandoned the authentication flow without
completing it.
Acceptance rate for international (non-Mexican) cards.
## 3D Secure [#3d-secure]
Percentage of 3DS authentications that completed successfully.
Percentage of transactions where the bank required an additional authentication step (OTP,
biometric, etc.).
Percentage of 3DS transactions that passed without requiring customer interaction.
Number of transactions where the customer dropped off during the 3DS challenge.
## Charts & sections [#charts--sections]
Stacked bar chart showing Allowed, Issuer Declines, and Guardian Blocks per day.
Table of card BIN ranges with unusual activity — high decline rates, many unique emails, or
rapid-fire attempts.
Table showing transaction count, block rate, acceptance rate, and risk level by country.
Table of email addresses with unusually high transaction attempt rates and high decline rates —
potential fraud signals.
**Guardian works together with Tonder's processing.** When measuring a card's performance, Guardian's
and the acquirer's decisions form a single end-to-end outcome, not two separate metrics.
## Key definitions [#key-definitions]
| Term | Meaning |
| ------------ | ----------------------------------------------------------------------------- |
| **Guardian** | Tonder's anti-fraud system; evaluates risk signals in real time. |
| **3DS** | 3-D Secure: extra issuer authentication that shifts fraud liability. |
| **BIN** | Bank Identification Number: the leading card digits that identify the issuer. |
## How to use it [#how-to-use-it]
* Watch the **block trend** after rule changes or promotions so you don't decline legitimate payments.
* Review **suspicious emails and BINs** to feed lists and rules.
* Cross-reference geographic risk with your target markets.
## Next steps [#next-steps]
Acceptance by network and issuer.
Detail and timeline for each payment.
# Home (/docs/dashboard/home)
The **Home** page is your control center: a live summary of your payment performance with KPIs, volume
trends, and breakdowns by payment method and issuing bank. It's built so that, at a glance, you know
how the business is doing and where to act.
## Primary KPIs [#primary-kpis]
Total value of all transactions processed, across every payment method.
Total value of transactions that were completed successfully.
Number of transactions that completed successfully.
Percentage of transactions that were approved.
## Secondary KPIs [#secondary-kpis]
Total amount refunded and number of refund transactions.
Total amount under dispute (chargebacks) and number of open disputes.
First-time deposits — number of unique customers who paid for the first time.
Average transaction amount across all successful payments.
## Charts & sections [#charts--sections]
Gross volume over time. Toggle between dollar amount, transaction count, or acceptance rate.
Donut chart showing how volume splits across SPEI, Cards, Oxxo Pay, Mercado Pago, and others.
Breakdown of all transactions by outcome: Success, Pending, Declined, Failed.
Acceptance rate for each issuing bank, ranked best to worst. Toggle by count or volume.
30-day heatmap of daily acceptance rate.
Most frequent decline codes with their count.
**Cards ≠ APMs.** The card **acceptance rate** and the APM **conversion rate** (SPEI, Oxxo Pay,
Mercado Pago) are always reported separately — they measure different things and should never be
blended into a single number. See [Analytics](/docs/dashboard/analytics).
**No duplicates.** Every metric is deduplicated: each payment intent is counted once, even if there
were retries behind the scenes. All metrics respect the **date range** selected at the top (Today, 7D,
30D, 90D, or custom).
## How to use it [#how-to-use-it]
* Adjust the **date range** (top right) to focus your analysis.
* Use each KPI's **delta** to spot sudden changes and dig deeper in Analytics.
* Click a recent transaction to open its detail.
## Next steps [#next-steps]
Acceptance, conversion, and deposit analytics.
Full history with filters and detail.
Balances, settlements, and reconciliation.
# Overview (/docs/dashboard/overview)
The Tonder dashboard is where you run your entire payments business: monitor performance in real
time, manage transactions and disputes, control your finances and withdrawals, create payment links,
configure your integrations, and administer your team.
## Sections [#sections]
Real-time KPIs, volume trends, and breakdowns by method and acquirer.
Acceptance, conversion, deposits (FTD), and segments.
Guardian: block decisions, 3DS, BINs, and suspicious emails.
Payment history with filters, detail, and disputes.
Outgoing payouts and balance activity.
Balances, settlements, and reconciliation.
Create and manage customizable payment links.
API keys and integration credentials.
Configure and test notification endpoints.
Invite your team and manage roles and permissions.
# Payment Links (/docs/dashboard/payment-links)
**Payment Links** are unique URLs generated by Tonder to collect payments with no integration: you share
them and your customer pays. This section lists all your links and lets you create them, customize the
checkout (branding), and track them.
## The list [#the-list]
Shows all your generated links:
| Field | Description |
| -------- | --------------------------------------- |
| Product | Name or identifier of the payment link. |
| Amount | Monetary value of the link. |
| Link URL | The unique URL to access and pay. |
| Status | Current status (Active, Inactive). |
| Created | Creation date. |
## Create a payment link [#create-a-payment-link]
Click **+Create** in the top-right corner.
| Field | Required? | Description |
| --------------- | :-------- | ---------------------------------- |
| Product | Yes | Product name or identifier. |
| Description | Yes | Details about the product or link. |
| Amount | Yes | Monetary value. |
| Currency | Yes | Currency. |
| Quantity | Yes | Number of product units. |
| Redirection URL | No | Redirect URL after payment. |
| Expiry Date | No | Link expiration date. |
| Payment Methods | No | Methods accepted beyond cards. |
Click **Create new link**.
On each link's details page you can copy or open its URL and edit the **Redirection URL** and the
**Expiry Date**.
## Lifecycle [#lifecycle]
When created, a payment link is always **Active**, and changes to **Paid** or **Expired** depending on
the customer's actions or the expiration settings.
| Status | Description |
| ----------- | ----------------------------------------------------------------------- |
| **Active** | The link is available and can be used to pay. |
| **Paid** | The payment was completed successfully; the link can no longer be used. |
| **Expired** | The link is no longer valid after reaching its expiration. |
## Branding [#branding]
Customize the checkout experience with your logo and colors from the **Branding** page (button in the
top-right corner of the Payment Links section):
| Field | Description |
| ---------------------- | ---------------------------------------- |
| Company Logo | The company or brand logo. |
| Checkout Color | Background color of the checkout button. |
| Background Color | Background color of the order details. |
| Text in Checkout Color | Text color in the checkout button. |
| Text in Orders Details | Text color in the order details. |
## Next steps [#next-steps]
Track your links' payments.
All sections.
# Team Members (/docs/dashboard/team-members)
**Team Members** is where you administer the people with access to your Tonder account: you see the team
list, send invitations, and assign **roles** that determine what each person can do. Tonder uses
**role-based access control (RBAC)**: each role grants a fixed set of per-module permissions.
## Team list and invitations [#team-list-and-invitations]
* **Team list** — each member with their email, role, and status.
* **Invitations** — invite a person by email and assign them a role; access becomes active when they
accept.
The ability to **invite, edit, delete, and manage permissions** depends on the role (see below): only
**Admin** can manage the team.
## Roles and permissions [#roles-and-permissions]
There are five roles. Each defines the access level per module: **Home, Transactions, Disputes,
Withdrawals, Finances, Payment Links, Connections, Developers, Settings, and Team Members**.
### Admin [#admin]
Full access to all modules, configurations, and team management.
| Module | Permissions |
| ------------- | ------------------------------------------------------- |
| Home | View dashboard and analytics. |
| Transactions | View, search, refund, export. |
| Disputes | View, accept/reject, export. |
| Withdrawals | View, approve/decline/hold, export. |
| Finances | View settlements, export. |
| Payment Links | Create, edit, view, customize checkout. |
| Connections | View & edit connections. |
| Developers | Manage API keys and webhooks (add, edit, delete, test). |
| Settings | Manage profile, password, and payment methods. |
| Team Members | View, edit, delete, and manage permissions/invitations. |
### Dispute Manager [#dispute-manager]
Focused on chargebacks and retrievals — no financial or configuration control.
| Module | Permissions |
| ------------- | --------------------------------- |
| Home | View dashboard and analytics. |
| Transactions | View, search, refund, export. |
| Disputes | View, accept/reject, export. |
| Withdrawals | No access. |
| Finances | No access. |
| Payment Links | No access. |
| Connections | No access. |
| Developers | No access. |
| Settings | Manage profile and password only. |
| Team Members | View team list only. |
### Finance Manager [#finance-manager]
Responsible for settlements, reconciliations, and withdrawal approvals.
| Module | Permissions |
| ------------- | --------------------------------------------------- |
| Home | View dashboard and analytics. |
| Transactions | View, search, export. |
| Disputes | View only, export. |
| Withdrawals | View, approve/decline/hold, export. |
| Finances | View settlements, export. |
| Payment Links | View only. |
| Connections | No access. |
| Developers | No access. |
| Settings | Manage profile, password, and view payment methods. |
| Team Members | View team list only. |
### Developer [#developer]
Manages integrations, webhooks, and API keys — no financial control.
| Module | Permissions |
| ------------- | ---------------------------------------------- |
| Home | View dashboard and analytics. |
| Transactions | View, search, export. |
| Disputes | View only. |
| Withdrawals | View only. |
| Finances | View settlements, export. |
| Payment Links | Create, customize, and manage payment links. |
| Connections | View & edit connections. |
| Developers | Manage API keys; add/edit/test webhooks. |
| Settings | Manage profile, password, and payment methods. |
| Team Members | View team list only. |
### Guest [#guest]
Read-only access to dashboards and modules, with masked data.
| Module | Permissions |
| ------------- | ------------------------------- |
| Home | View dashboard and analytics. |
| Transactions | View and search payments only. |
| Disputes | View only. |
| Withdrawals | View only. |
| Finances | View only. |
| Payment Links | View link details. |
| Connections | No access. |
| Developers | No access. |
| Settings | View profile and password only. |
| Team Members | View team list only. |
## Settings (profile) [#settings-profile]
Each member manages their **profile**, **password**, and — depending on the role — the account's
**payment methods** from Settings. The scope of these actions is defined by the assigned role.
## Next steps [#next-steps]
All sections.
API keys and credentials.
# Transactions (/docs/dashboard/transactions)
**Transactions** is the complete record of your payments: a secure history of every operation that you
can filter, search, export, and open in detail. It also includes the **Disputes** sub-tab for managing
chargebacks.
## The list [#the-list]
Shows all your payments regardless of status. Main columns:
| Field | Description |
| ----------------- | -------------------------------------------- |
| Creation date | Date when the transaction was created. |
| Amount | The monetary value of the transaction. |
| Currency | The currency the amount is expressed in. |
| Type | Type of operation (payment, refund, etc.). |
| Status | Current status (see below). |
| Payment processor | The acquirer/processor used for the payment. |
The **status tabs** at the top let you jump straight to a subset (for example, only `Success` or only
`Failed`).
### Statuses [#statuses]
| Status | Meaning |
| ---------- | ------------------------------------- |
| `Pending` | In progress (includes 3DS in flight). |
| `Success` | Approved by the bank/processor. |
| `Declined` | Declined by the issuer. |
| `Failed` | Failed during processing. |
Transaction synchronization that goes through 3DS can take a few minutes to change from `Pending` to
`Success` in the dashboard, but this does not affect the end user.
## Filters and search [#filters-and-search]
| Filter | Description |
| -------------- | -------------------------------------- |
| Status | Narrow down by current status. |
| Amount | Filter by a monetary value or range. |
| Date | Focus on transactions in a date range. |
| Payment method | Filter by payment method. |
Search accepts identifiers such as `Payment ID` or `Order ID`.
## Data export [#data-export]
Click **Export** (top-right corner), select the date range, and confirm to download a CSV.
## Transaction details [#transaction-details]
Click a row to open the detail panel:
| Field | Description |
| ----------- | --------------------------------------------------- |
| Payment ID | Unique identifier for the payment. |
| Order ID | Unique identifier for the order. |
| Client Info | Information about the associated client. |
| Timeline | Chronological sequence of the transaction's events. |
The **timeline** shows a card for each processing attempt (reference, date, amount, currency, type,
status, processor, and an eye icon to view the processor's raw response). From this page you can also
issue **refunds** with the button in the top-right corner.
The ability to issue partial refunds may vary depending on each processor's policies.
## Disputes [#disputes]
The **Disputes** sub-tab centralizes chargebacks and retrieval requests. From there you can **view**
each dispute, **accept or reject** it, and **export** the list. Access to these actions depends on the
[user's role](/docs/dashboard/team-members).
### Disputes list [#disputes-list]
| Field | Description |
| ----------------- | ------------------------------------------------------------------------------------------------- |
| Amount | The monetary value of the disputed transaction. |
| Status | The current status of the dispute (e.g. open, closed). |
| Card | The card used for the transaction being disputed. |
| Payment Processor | The payment processor involved in the transaction. |
| Time to answer | The available time to respond to the dispute (only available if the payment processor is Tonder). |
### Dispute details [#dispute-details]
Clicking a dispute opens its detail view:
| Field | Description |
| ------------------- | ----------------------------------------------------------------------------- |
| Network reason code | The code indicating the reason for the dispute (assigned by Visa/Mastercard). |
| Dispute ID | Unique identifier for the dispute. |
| Client Info | Information about the client associated with the dispute. |
| Timeline | Chronological sequence of events for the dispute. |
From the detail view you can **accept** the dispute (acknowledge the chargeback without contesting
it) or **dispute** it (upload evidence to challenge the cardholder's claim).
**Dispute process timing**
* Cardholders can initiate a dispute up to **120 days** after the original transaction.
* You have **15 days** to submit evidence after receiving the notification. See
[Handling Disputes](/docs/payments-management) for the full per-market deadlines.
* The issuing bank takes **60-75 days** to assess the evidence and decide.
* The full dispute process typically takes **2-3 months** from start to finish.
## Next steps [#next-steps]
Balances, settlements, and reconciliation.
Acceptance and conversion.
How to issue refunds via the API.
# Webhooks (/docs/dashboard/webhooks)
The **Webhooks** screen (inside Developers) is where you register and manage the endpoints Tonder sends
real-time notifications to when an event occurs (a payment changes status, a settlement completes, etc.),
without your system having to poll the API constantly.
## What you can do here [#what-you-can-do-here]
* **Add** an endpoint (URL) that will receive events.
* **Test** the connection (Tonder sends a verification request on save).
* **View the status** and **delivery** history of each endpoint.
* **Edit or delete** an endpoint.
## Add a webhook [#add-a-webhook]
Go to **Developers → Webhooks**.
Click **+ Add Webhook**, enter your **endpoint URL**, and confirm it.
When you add a webhook URL, Tonder sends a first request to confirm the connection works. You can use
[webhook.site](https://webhook.site/) to test.
This screen **documents dashboard management**. For payload formats, the event catalog, deduplication,
and the retry policy, use the **Webhooks** section.
## Formats, events, and delivery (API) [#formats-events-and-delivery-api]
Webhook model and formats.
Register and manage endpoints.
Which events exist and their payloads.
Retries, deduplication, and idempotency.
## Next steps [#next-steps]
Your API keys and credentials.
Signature verification and security.
# Withdrawals (/docs/dashboard/withdrawals)
**Withdrawals** is the screen for monitoring outgoing payouts and your balance activity. Here you see
each withdrawal, its stage in the processing flow, and how it affects your available balance.
## Tabs [#tabs]
* **Withdrawals** — the list of payouts with amount, beneficiary, method, and status.
* **Balance activity** — the movement of your balance (inflows, outflows, and holds) that gives
context for why funds are available or in transit.
## Withdrawal statuses [#withdrawal-statuses]
A withdrawal moves through a status flow:
| Status | Meaning |
| ------------ | ---------------------------------------------------- |
| `Pending` | Created, waiting to be processed or on hold. |
| `Processing` | Sent to the provider for disbursement. |
| `Success` | Paid in full to the beneficiary. |
| `Declined` | Rejected (check the reason and retry if applicable). |
| `Cancelled` | Cancelled. |
| `Failed` | Failed. |
## How to use it [#how-to-use-it]
* Filter by status or date to audit disbursements.
* Review **balance activity** to reconcile why a withdrawal is `Pending` (funds in transit) or
available.
* Handle `Declined` payouts by checking beneficiary data before retrying.
This screen **documents the dashboard view**. To create withdrawals and see the endpoint, status
flow, and webhooks, use [Withdrawals (API Direct)](/docs/direct-api/withdrawals).
## Create and operate withdrawals via API [#create-and-operate-withdrawals-via-api]
Endpoint, authentication, status flow, and webhooks.
## Next steps [#next-steps]
Balances, settlements, and reconciliation.
What to do with a Declined withdrawal and how to retry.
# Check Status Manually (/docs/hosted-checkout/check-status)
While we recommend [using webhooks](/docs/hosted-checkout/listen-webhooks) for real-time
updates, you can also check the status of a payment at any time by polling the API. This is useful
in several cases:
* When a customer returns to your `success_url` and you need to confirm the final status before
showing an order confirmation.
* For reconciliation scripts that check the status of pending orders.
* As a fallback if your webhook endpoint fails.
## Option 1: session status [#option-1-session-status]
This is the most common method. Use the `session_id` to retrieve the full session object, which
includes the latest `status` and `transaction_status`.
## Option 2: transaction details [#option-2-transaction-details]
If you have a `payment_id` (from the session object or a webhook), you can retrieve details for that
specific transaction.
For order fulfillment, base your logic on the **transaction status** (`transaction_status`, or
the `status` returned by `GET /checkout/v1/payments/{payment_id}`), not on the session `status`.
The session status may not reflect the final payment result. See the meaning of each value in the
[Reference](/docs/hosted-checkout/reference).
## Next steps [#next-steps]
The recommended way to get the status in real time.
All session and transaction status values.
# Create a Payment Session (/docs/hosted-checkout/create-session)
This is the main endpoint for initiating a Hosted Checkout payment. It creates a new payment
session and returns a secure URL where your customer completes their payment.
```http
POST https://api-stage.tonder.io/checkout/v1/sessions # Sandbox
POST https://api.tonder.io/checkout/v1/sessions # Production
```
## How it works [#how-it-works]
1. Your server calls this endpoint with payment details (amount, items, customer info).
2. Tonder creates a secure session and returns a checkout URL.
3. You redirect your customer to this URL.
4. The customer completes the payment on Tonder's hosted page.
5. The customer is redirected back to your `success_url` or `return_url`.
Include the `x-idempotency-key` header to prevent duplicate sessions (see
[Idempotency](/docs/core-concepts/idempotency)) and the `ui_config` object to customize the page
(see [Customize the checkout](/docs/hosted-checkout/customize-ui)).
The `payment_method_types` field defaults to `["card"]`. Accepted values: `card`, `mercadopago`,
`oxxopay`, `spei`, `safetypayCash`, `safetypayTransfer`, `neosurf`, `saved_cards`.
## Saved cards [#saved-cards]
Include `saved_cards` in `payment_method_types` and the checkout shows the customer's previously
saved cards alongside the new card form. Listing, selecting and deleting them is handled for you —
there is no extra endpoint to call.
This is the Hosted Checkout path. If you render your own form with the Web SDK, saved cards work
differently — see [Saved cards (`secure_token`)](/docs/sdks/web#saved-cards-secure_token).
### The save-card checkbox [#the-save-card-checkbox]
Set `payment_method_config.saved_cards.show_save_card_checkbox` to `true` to put a **Guardar
tarjeta para futuros pagos** checkbox on the new card form, so the customer can opt in to saving
their card.
| Field | Type | Description |
| ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `show_save_card_checkbox` | boolean | Shows a "Guardar tarjeta para futuros pagos" checkbox on the new card form, letting the customer opt in to saving their card. |
```bash
curl -X POST 'https://api-stage.tonder.io/checkout/v1/sessions' \
-H 'Authorization: Token YOUR_TEST_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"customer": { "first_name": "Maria", "last_name": "Garcia", "email": "maria@example.com" },
"amount_total": 150.00,
"currency": "MXN",
"line_items": [
{ "name": "Premium Plan", "quantity": 1, "unit_price": 150.00 }
],
"payment_method_types": ["card", "saved_cards"],
"payment_method_config": {
"saved_cards": { "show_save_card_checkbox": true }
},
"external_id": "ORD-001",
"return_url": "https://your-store.com/checkout/complete"
}'
```
```bash
curl -X POST 'https://api.tonder.io/checkout/v1/sessions' \
-H 'Authorization: Token YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"customer": { "first_name": "Maria", "last_name": "Garcia", "email": "maria@example.com" },
"amount_total": 150.00,
"currency": "MXN",
"line_items": [
{ "name": "Premium Plan", "quantity": 1, "unit_price": 150.00 }
],
"payment_method_types": ["card", "saved_cards"],
"payment_method_config": {
"saved_cards": { "show_save_card_checkbox": true }
},
"external_id": "ORD-001",
"return_url": "https://your-store.com/checkout/complete"
}'
```
## Card-on-File [#card-on-file]
Card-on-File (COF) saves cards automatically and lets the customer pay again without re-entering
their CVV. It runs on Tonder's subscription and tokenization infrastructure.
When your business has an active Tonder connection with `active_subscription`, the checkout:
1. Runs 3DS verification as the card is saved.
2. Creates a subscription for that card.
3. Accepts CVV-free payments with it on return visits.
There is nothing to configure. If Card-on-File is enabled for your account, the checkout does all
of this on its own. It is the same subscription the SDK surfaces as `subscription_id` — see the
[Web SDK reference](/docs/sdk-reference/web).
Sending `show_save_card_checkbox: true` while Card-on-File is active will not show the checkbox.
That is expected: Card-on-File takes precedence and cards are saved automatically either way.
## API reference [#api-reference]
The session starts in `pending` and transitions to a final state. See the full values in the
[Reference](/docs/hosted-checkout/reference).
## Next steps [#next-steps]
The full four-step flow.
Retrieve the session or payment status.
Statuses, identifiers, and webhook events.
The full API index.
# Customize the Checkout (/docs/hosted-checkout/customize-ui)
You can customize the Hosted Checkout page to match your brand. There are two ways to apply
customization:
1. Set a default look for all your sessions using the business configuration.
2. Override the defaults for a specific session.
## Method 1: default business configuration [#method-1-default-business-configuration]
This method updates the default UI for your entire business. These settings apply to all new
sessions unless you override them.
Call the Set Business Configuration endpoint with your logo URL and default `ui_config`. The API
returns the saved configuration and all subsequent sessions will use it.
## Method 2: override the UI for a session [#method-2-override-the-ui-for-a-session]
You can override any default setting for a single payment by passing a `ui_config` object when you
[create the payment session](/docs/hosted-checkout/create-session). This is useful for
co-branding, promotions, or if you operate multiple storefronts under one Tonder account.
```bash
curl -X POST 'https://api-stage.tonder.io/checkout/v1/sessions' \
-H 'Authorization: Token YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"external_id": "PROMO-XYZ-456",
"amount_total": 1000,
"currency": "MXN",
"line_items": [
{ "name": "Promo Item", "quantity": 1, "unit_price": 1000 }
],
"customer": { "first_name": "Test", "last_name": "Customer", "email": "test@example.com" },
"success_url": "https://your-store.com/checkout/success",
"ui_config": {
"branding": { "brand_color": "#33CFFF" },
"labels": { "submit_button": "Pay Now (Promo)" }
}
}'
```
This session will use the `#33CFFF` brand color and the custom button text, ignoring the default
business config for those fields.
## `ui_config` fields [#ui_config-fields]
All color fields accept 6-digit hex codes (e.g. `#FFFFFF`).
```json
{
"branding": {
"brand_color": "#0A2540",
"brand_color_text": "#FFFFFF",
"secondary_brand_color": "#F0F0F0",
"secondary_brand_color_text": "#0A2540"
},
"theme": {
"shapes": "rounded",
"background_color": "#FDFDFD",
"form_background_color": "#FFFFFF",
"text_color": "#333333",
"error_color": "#D92D20"
},
"labels": {
"submit_button": "Pay Now",
"card_number": "Card Number",
"expiry_date": "MM/YY",
"cvc": "CVC"
}
}
```
### branding [#branding]
Colors related to your brand, used for buttons and highlights.
| Field | Type | Description |
| ---------------------------- | ------ | -------------------------------------------------------- |
| `brand_color` | string | Main brand color. Used for the submit button. |
| `brand_color_text` | string | Text color to display on top of `brand_color`. |
| `secondary_brand_color` | string | Secondary color. Used for links or minor buttons. |
| `secondary_brand_color_text` | string | Text color to display on top of `secondary_brand_color`. |
### theme [#theme]
General theme of the checkout page, including shapes and background colors.
| Field | Type | Description |
| ----------------------- | ------ | --------------------------------------------------------------------------------------------------- |
| `shapes` | string | Border radius for buttons and form fields. Options: `sharp` (0px) or `rounded` (default, e.g. 8px). |
| `background_color` | string | The main background color of the page. |
| `form_background_color` | string | The background color of the payment form container. |
| `text_color` | string | The primary color for all text labels. |
| `error_color` | string | The color used for form validation errors. |
### labels [#labels]
Custom text for form fields and buttons.
| Field | Type | Description |
| --------------- | ------ | ---------------------------------------------------------------- |
| `submit_button` | string | Text for the main payment button (e.g. "Pay", "Complete Order"). |
| `card_number` | string | Placeholder label for the card number field. |
| `expiry_date` | string | Placeholder label for the card expiration date field. |
| `cvc` | string | Placeholder label for the CVC/CVV field. |
## Next steps [#next-steps]
The request body, including `ui_config`.
The full four-step flow.
# Listen for Webhooks (/docs/hosted-checkout/listen-webhooks)
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 [#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 [#configure-and-listen-for-webhooks]
1. Log in to the Dashboard: [dashboard-stage.tonder.io](https://dashboard-stage.tonder.io) (Sandbox) or [dashboard.tonder.io](https://dashboard.tonder.io) (Production).
2. Navigate to **Developers → Webhooks**.
3. Click **Add Endpoint**.
4. Paste your public endpoint URL into the **Endpoint URL** field.
5. Click **Save**.
Your endpoint must accept POST requests with a JSON body. When an event occurs, Tonder sends a
request like this:
```json
{
"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](/docs/hosted-checkout/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.
```javascript
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.
## Next steps [#next-steps]
The full list of webhook events and their JSON payloads.
Poll the status as a fallback.
# Overview (/docs/hosted-checkout/overview)
Tonder's Hosted Checkout is a secure, pre-built payment page that makes it easy to accept online
payments. You redirect your customer to a Tonder-hosted URL, where they complete their payment. We
handle the UI, security, and payment processing, and then redirect the customer back to your site.
## Benefits [#benefits]
* Get started quickly with a low-code integration that involves just one API call to create a
payment session.
* Securely accept card payments without handling sensitive card data directly on your servers —
Tonder handles PCI DSS compliance.
* Adapt the checkout page to your brand by adding your logo and changing colors, button shapes, and
more.
* Automatically support the variety of payment methods available to your business.
## High-level flow [#high-level-flow]
The process consists of four main parts: session creation, user redirection, payment completion,
and status confirmation.
Your customer clicks "Pay" on your website, triggering your server to create a payment session
via the Tonder API.
Tonder returns a secure URL that you use to redirect your customer to the Hosted Checkout page.
The customer enters their payment details on Tonder's secure checkout page, and the payment is
verified with the acquiring bank or 3DS provider.
Based on the verification result, Tonder processes the payment. If declined, the customer can
retry with a different payment method.
Tonder redirects the customer back to your website with the transaction status, and you display
a confirmation page indicating whether the payment was successful.
## Next steps [#next-steps]
Take your first payment in four steps.
The request body and the session response.
The most reliable way to get payment updates.
The 4 steps with the request building live.
Every Hosted Checkout endpoint and payload.
# Quickstart (/docs/hosted-checkout/quickstart)
Hosted Checkout is the fastest way to get paid: Tonder hosts the payment page, so you never touch
card data. This quickstart takes you from zero to a verified test payment in four steps.
**What you'll build:** a server that creates a payment session, redirects the customer to Tonder,
and confirms the final status after they return.
Use your **test API keys** for this guide — all examples use test environment URLs. Never use
production keys for testing.
## Before you begin [#before-you-begin]
* A Tonder account — sign in at [dashboard-stage.tonder.io](https://dashboard-stage.tonder.io/) (Sandbox).
* Your **test** API key from **Developers → API Keys**.
## Integration flow [#integration-flow]
The diagram shows the full exchange between your customer, your application, and Tonder. Each step
below maps to the diagram.
## Step 1: Create a payment session [#step-1-create-a-payment-session]
From your **server**, call the create-session endpoint. It registers the payment with Tonder and
returns a secure URL to redirect the customer to.
| Field | Description | Example |
| -------------- | ----------------------------------------------------------- | ----------------------------------------------------- |
| `customer` | Customer info: first name, last name, email | `{ first_name, last_name, email }` |
| `amount_total` | Total charge amount, in decimal format | `150.00` (for $150.00) |
| `currency` | ISO currency code | `"MXN"` |
| `line_items` | Products in the cart | Array of `{ name, quantity, unit_price, product_id }` |
| `return_url` | Where to send the customer after payment (or `success_url`) | `"https://your-store.com/checkout/complete"` |
| `external_id` | *(Optional)* Your internal order ID | `"ORD-001"` |
```bash
curl -X POST 'https://api-stage.tonder.io/checkout/v1/sessions' \
-H 'Authorization: Token YOUR_TEST_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"customer": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe@example.com"
},
"amount_total": 150.00,
"currency": "MXN",
"line_items": [
{ "name": "Test Product", "quantity": 1, "unit_price": 150.00, "product_id": "your internal product id" }
],
"return_url": "https://your-store.com/checkout/complete",
"external_id": "ORD-001"
}'
```
```bash
curl -X POST 'https://api.tonder.io/checkout/v1/sessions' \
-H 'Authorization: Token YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"customer": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe@example.com"
},
"amount_total": 150.00,
"currency": "MXN",
"line_items": [
{ "name": "Test Product", "quantity": 1, "unit_price": 150.00, "product_id": "your internal product id" }
],
"return_url": "https://your-store.com/checkout/complete",
"external_id": "ORD-001"
}'
```
Tonder responds with a session object. Keep the `id` (to verify later) and the `url` (to redirect
to):
```json
{
"id": "cs_97_41521_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d",
"url": "https://stage-payflow.tonder.io/checkout/cs_97_41521_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d",
"status": "pending",
"payment_id": 41521,
"amount_total": 150.00,
"currency": "MXN",
"external_id": "ORD-001",
"transaction_status": "Pending",
"created_at": 1751478543567
}
```
## Step 2: Redirect the customer to Tonder [#step-2-redirect-the-customer-to-tonder]
Send the customer to the `url` from the response — typically an HTTP 302 redirect:
```javascript
res.redirect(data.url);
```
## Step 3: Complete the test payment [#step-3-complete-the-test-payment]
On Tonder's hosted page the customer enters their details. Tonder handles verification with the
acquiring bank and 3DS automatically; if a payment is declined, the customer can retry with a
different card on the same page. To complete a test payment:
Fill the form with a test card number from the
[Test cards](/docs/testing/card-test-cases) page.
For example, `12/30`.
For example, `123`.
After submitting, Tonder processes the payment, verifies it with the provider, and redirects the
customer to your `return_url` (or `success_url`).
## Step 4: Verify the payment status [#step-4-verify-the-payment-status]
When the customer lands back on your `return_url`, the URL includes the session ID:
```
https://your-store.com/checkout/success?session_id=cs_97_41521_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d
```
Use that `session_id` to call Get a Session **from your server**. This is more reliable than
assuming the redirect means success:
```bash
curl -X GET 'https://api-stage.tonder.io/checkout/v1/sessions/cs_97_41521_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d' \
-H 'Authorization: Token YOUR_TEST_API_KEY'
```
```bash
curl -X GET 'https://api.tonder.io/checkout/v1/sessions/cs_97_41521_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d' \
-H 'Authorization: Token YOUR_API_KEY'
```
A completed payment returns `status: "completed"` with the `payment_id`:
```json
{
"id": "cs_97_41521_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d",
"status": "completed",
"payment_id": 41521,
"amount_total": 150.00,
"currency": "MXN",
"external_id": "ORD-001",
"transaction_status": "Success",
"paid_at": 1751478550234
}
```
Webhooks are the most reliable way to confirm the result. Reconcile on `metadata.external_id` and
respond `200`. See [Listen for webhooks](/docs/hosted-checkout/listen-webhooks).
## Next steps [#next-steps]
The most reliable way to get payment updates — recommended over manual checks.
Match the hosted page to your brand.
The full request-body reference.
Every Hosted Checkout endpoint and payload.
# Reference (/docs/hosted-checkout/reference)
Reference for Hosted Checkout statuses, identifiers, and webhook events.
## Statuses: session vs. transaction [#statuses-session-vs-transaction]
A common point of confusion is the difference between the `status` of a **session** and that of a
**payment transaction**. They represent two different things:
* **Session status** represents the entire checkout experience — think of it as the "cart" or
"order" waiting to be paid. Its lifecycle is simple: it starts as `pending` and can only end as
`completed` (paid) or `expired` (abandoned).
* **Transaction status** represents a single attempt to pay for that session. A customer might have
multiple transactions within one session if their first attempt fails.
| Type | Purpose | Example values |
| ------------------ | --------------------------------------------------------------------- | ------------------------------------------- |
| Session status | The state of the order (is it paid?) | `pending`, `completed`, `expired` |
| Transaction status | The state of a specific payment attempt (did this charge go through?) | `Pending`, `Success`, `Declined`, `Expired` |
For order fulfillment, base your logic on the **transaction status** (`transaction_status` or the
`payment.transaction` event), not only on the session `status`. A `session.status: "completed"`
or `"expired"` alone is not enough to confirm whether the payment succeeded or was declined.
**Note:** for some merchants, `transaction_status` can come back empty (`""`) to represent a
Declined transaction instead of staying `"Pending"`. If you see this behavior, treat it as
Declined.
### Session status [#session-status]
| Status | Description |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| pending | The session has been created but the customer hasn't completed the payment. The URL is active. |
| completed | The customer completed the payment successfully. Final state. |
| expired | The session wasn't completed in time (customer abandoned the page) and can no longer be paid. Final state. |
### Transaction status [#transaction-status]
| Status | Description |
| ---------------------------------------- | ---------------------------------------------------------------------------- |
| Pending | The payment was initiated but isn't yet confirmed (e.g. awaiting 3D Secure). |
| Success | The payment was successfully authorized and processed. |
| Declined | The card issuer or processor declined the payment. The customer may retry. |
| Expired | The attempt wasn't completed in time (e.g. failed 3D Secure). |
## Key identifiers [#key-identifiers]
| ID | Type | What it is |
| --------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` (Session ID) | string | Unique identifier for the session. Example: `cs_97_41521_d11ba771...`. Appears in the checkout URL and when calling Get a Session. |
| `payment_id` (Payment ID) | number | Identifier for a transaction (a payment attempt). Example: `41521`. A session may have several if the first card is declined. |
| `external_id` (External ID) | string | Your internal order identifier, e.g. `ORD-001`. You send it when creating the session and use it to reconcile. Searchable in the Dashboard. |
Your order (`external_id`) maps 1:1 to a Tonder session, which can have 1:N payment transactions.
## Webhook events [#webhook-events]
All webhook payloads follow this structure:
```json
{
"action": "session.created",
"type": "checkout.hosted",
"data": {
// ... object related to the event
}
}
```
| Field | Type | Description |
| -------- | ------ | -------------------------------------------------------------------------------- |
| `action` | string | The event type (e.g. `session.created`, `session.completed`, `session.expired`). |
| `type` | string | The checkout type, always `checkout.hosted` for Hosted Checkout. |
| `data` | object | Event-specific data containing session or transaction details. |
The `external_id` you send when creating the session appears in the Dashboard as **Order ID** and
in transaction reports as **Business Transaction ID**, but it is **not returned in webhook
payloads**. To receive your order reference in a webhook, also include it inside the `metadata`
object when creating the session; it will be available under `data.metadata`.
### session.created [#sessioncreated]
Sent when a new checkout session is created.
```json
{
"action": "session.created",
"type": "checkout.hosted",
"data": {
"id": "cs_97_41521_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d",
"url": "https://stage-payflow.tonder.io/checkout/cs_97_41521_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d",
"amount_total": 50.00,
"currency": "MXN",
"status": "pending",
"payment_id": 41521,
"transaction_status": "Pending",
"created_at": 1751478543567,
"customer": { "first_name": "New", "last_name": "Customer", "email": "new@example.com" },
"metadata": { "external_id": "ORD-A1B2-NEW" }
}
}
```
### session.completed [#sessioncompleted]
Sent when a session is successfully paid and completed. This is the primary event for confirming a
successful order.
```json
{
"action": "session.completed",
"type": "checkout.hosted",
"data": {
"id": "cs_97_41528_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d",
"url": "https://stage-payflow.tonder.io/checkout/cs_97_41528_d11ba771527b4056c7f85786cfbb980bc105efaf42af113d",
"amount_total": 150.00,
"currency": "MXN",
"status": "completed",
"payment_id": 41528,
"transaction_status": "Success",
"paid_at": 1751478550234,
"customer": { "first_name": "Jane", "last_name": "Doe", "email": "jane.doe@example.com" },
"metadata": { "external_id": "ORD-G7H8-PAID" }
}
}
```
### session.expired [#sessionexpired]
Sent when a pending session expires without a successful payment (e.g. the customer abandoned the
checkout).
```json
{
"action": "session.expired",
"type": "checkout.hosted",
"data": {
"id": "cs_97_41529_k1j2h3g4f5e6d7c8b9a0",
"url": "https://stage-payflow.tonder.io/checkout/cs_97_41529_k1j2h3g4f5e6d7c8b9a0",
"amount_total": 25.00,
"currency": "MXN",
"status": "expired",
"payment_id": 41529,
"transaction_status": "Expired",
"customer": { "first_name": "Tom", "last_name": "Abandoned", "email": "tom@example.com" },
"metadata": { "external_id": "ORD-L9M8-EXPIRED" }
}
}
```
### payment.transaction [#paymenttransaction]
Sent when a payment transaction status changes (Pending, Success, Declined). A session may have
multiple `payment.transaction` events if the user retries. While `session.completed` tells you the
order is paid, `payment.transaction` gives you details about each attempt.
```json
{
"event_type": "payment.transaction",
"data": {
"payment_id": 41522,
"session_id": "cs_97_41522_f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8",
"status": "Declined",
"amount": 75.00,
"currency": "MXN",
"payment_method_type": "card",
"created_at": "2025-10-20T15:00:00Z",
"card_details": { "brand": "mastercard", "last4": "5555" },
"customer": { "name": "Test Retry", "email": "retry@example.com" },
"metadata": { "external_id": "ORD-F1G2-RETRY" }
}
}
```
## Next steps [#next-steps]
Register your endpoint and receive these events.
Short vs Long formats across all modes.
# Mexican Banking Reference (/docs/resources/banking-reference)
Everything you need to implement SPEI transfers and withdrawals in Mexico. Understanding these
concepts helps you build reliable integrations with the Mexican banking system.
## CLABE [#clabe]
CLABE (Clave Bancaria Estandarizada) is Mexico's standardized bank account format for SPEI transfers.
It has **18 digits**:
* **Bank code** (3 digits) — identifies the financial institution.
* **Branch code** (3 digits) — identifies the branch.
* **Account number** (11 digits) — the account identifier.
* **Check digit** (1 digit) — calculated with a weighted algorithm.
Example: `012345678901234567`, where `012` = BBVA México, `345` = branch, `67890123456` = account,
`7` = check digit.
## RFC [#rfc]
RFC (Registro Federal de Contribuyentes) is the Mexican tax identifier, mandatory for financial
transactions. There are two formats:
* **Individual RFC:** 13 characters — 4 letters + 6 digits + 3 alphanumeric. Example: `MAGR850920XY1`.
* **Business RFC:** 12 characters — 3 letters + 6 digits + 3 alphanumeric. Example: `ABC850920123`.
## SPEI operating times [#spei-operating-times]
SPEI operates **24/7** and transfers usually complete within minutes. SPEI itself no longer enforces a
business-day cutoff, but individual institutions may add their own restrictions: maintenance windows,
anti-fraud security validations, and the Mexican holiday calendar.
For reference, these are typical per-institution processing windows:
| Day / period | Processing hours | Notes |
| ---------------- | ----------------------------- | ------------------------------------- |
| Monday to Friday | 6:00 AM – 6:00 PM (CDMX time) | Same-day processing if before 5:00 PM |
| Saturday | 9:00 AM – 2:00 PM | Limited processing |
| Sunday | No processing | |
| Bank holidays | No processing | |
Always confirm current hours and restrictions with your financial institution or your Tonder
integration manager — they can vary by bank and change without notice.
### Holiday calendar (Mexico) [#holiday-calendar-mexico]
The following Mexican holidays may affect SPEI processing:
* New Year's Day (January 1)
* Constitution Day (February 5)
* Benito Juárez's Birthday (March 21)
* Easter Week (date varies)
* Labor Day (May 1)
* Independence Day (September 16)
* Revolution Day (November 20)
* Christmas Day (December 25)
## Institution codes [#institution-codes]
Institution codes are assigned by Banco de México (Banxico) and used in the `institution` field of
the `beneficiary` object when creating withdrawals. Below is the full catalogue of SPEI
participants — 93 institutions.
| Code | Participant |
| ----- | --------------- |
| 2001 | BANXICO |
| 37006 | BANCOMEXT |
| 37009 | BANOBRAS |
| 37019 | BANJERCITO |
| 37135 | NAFIN |
| 37166 | BaBien |
| 37168 | HIPOTECARIA FED |
| 40002 | BANAMEX |
| 40012 | BBVA MEXICO |
| 40014 | SANTANDER |
| 40021 | HSBC |
| 40030 | BAJIO |
| 40036 | INBURSA |
| 40042 | MIFEL |
| 40044 | SCOTIABANK |
| 40058 | BANREGIO |
| 40059 | INVEX |
| 40060 | BANSI |
| 40062 | AFIRME |
| 40072 | BANORTE |
| 40106 | BANK OF AMERICA |
| 40108 | MUFG |
| 40110 | JP MORGAN |
| 40112 | BMONEX |
| 40113 | VE POR MAS |
| 40124 | CITI MEXICO |
| 40127 | AZTECA |
| 40128 | AUTOFIN |
| 40129 | BARCLAYS |
| 40130 | COMPARTAMOS |
| 40132 | MULTIVA BANCO |
| 40133 | ACTINVER |
| 40136 | INTERCAM BANCO |
| 40137 | BANCOPPEL |
| 40138 | ABC CAPITAL |
| 40140 | CONSUBANCO |
| 40141 | VOLKSWAGEN |
| 40143 | CIBANCO |
| 40145 | BBASE |
| 40147 | BANKAOOL |
| 40148 | PAGATODO |
| 40150 | INMOBILIARIO |
| 40151 | DONDE |
| 40152 | BANCREA |
| 40154 | BANCO COVALTO |
| 40155 | ICBC |
| 40156 | SABADELL |
| 40157 | SHINHAN |
| 40158 | MIZUHO BANK |
| 40159 | BANK OF CHINA |
| 40160 | BANCO S3 |
| 40638 | NUBANK |
| 90600 | MONEXCB |
| 90601 | GBM |
| 90602 | MASARI |
| 90605 | VALUE |
| 90608 | VECTOR |
| 90616 | FINAMEX |
| 90617 | VALMEX |
| 90620 | PROFUTURO |
| 90630 | CB INTERCAM |
| 90631 | CI BOLSA |
| 90634 | FINCOMUN |
| 90642 | REFORMA |
| 90646 | STP |
| 90652 | CREDICAPITAL |
| 90653 | KUSPIT |
| 90656 | UNAGRA |
| 90659 | ASP INTEGRA OPC |
| 90661 | ALTERNATIVOS |
| 90670 | LIBERTAD |
| 90677 | CAJA POP MEXICA |
| 90680 | CRISTOBAL COLON |
| 90683 | CAJA TELEFONIST |
| 90684 | TRANSFER |
| 90685 | FONDO (FIRA) |
| 90686 | INVERCAP |
| 90689 | FOMPED |
| 90699 | FONDEADORA |
| 90703 | TESORED |
| 90706 | ARCUS |
| 90710 | NVIO |
| 90722 | Mercado Pago W |
| 90723 | CUENCA |
| 90728 | SPIN BY OXXO |
| 90902 | INDEVAL |
| 90903 | CoDi Valida |
| 91802 | BANAMEX2 |
| 91812 | BBVA BANCOMER2 |
| 91814 | SANTANDER2 |
| 91821 | HSBC2 |
| 91872 | BANORTE2 |
| 91927 | AZTECA2 |
Operational notes:
* **BANXICO (`2001`)** is used for internal operations: it can receive SPEI, but not send.
* **Alternate accounts (`91xxx`)** — BANAMEX2, BBVA BANCOMER2, SANTANDER2, HSBC2, BANORTE2 and
AZTECA2 can only receive payments; they don't send.
* For banks, the last three digits of the code are the CLABE prefix: `40012` BBVA → CLABE starting
with `012`. Don't apply the rule to the `91xxx` alternate accounts, which use their parent bank's
prefix.
**Nu México's code changed on 6 August 2026.** Its SPEI code moved from `90638` to `40638` when it
became a full banking institution. The old code stopped working on 5 August 2026 at 18:00 Mexico
City time, and anything sent with it is rejected. See the [changelog](/docs/changelog).
For Sandbox testing, use institution code `97846`. This list comes from STP's institutions
catalogue dated 12 February 2025, with Nu's code change already applied. Banxico adds and removes
participants regularly, so check with your integration manager if you need a code that isn't listed
here.
## Next steps [#next-steps]
Use the CLABE and institution code.
Accept bank transfers.
# Response & Decline Codes (/docs/resources/codes)
Reference for the codes the Tonder API returns: HTTP codes, card decline codes, and withdrawal status
codes.
## HTTP codes [#http-codes]
### Success [#success]
| Code | Status | Description |
| ---- | ---------- | ------------------------------------------------------------------------------- |
| 200 | OK | The request was successful. Typical for `GET` requests. |
| 201 | Created | The resource was created successfully. Typical for `POST` requests. |
| 202 | Accepted | The request was accepted but isn't yet complete (e.g. a payment requiring 3DS). |
| 204 | No Content | The request was successful but there's no content to return (e.g. a `DELETE`). |
### Client errors [#client-errors]
| Code | Status | Description | Action |
| ---- | -------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| 400 | Bad Request | Malformed request or missing required parameters. | Check the body and parameters. |
| 401 | Unauthorized | Authentication failed: invalid or missing API key. | Check your key and that it carries the `Token ` prefix (with a space). |
| 402 | Payment Required | The processor or issuing bank declined the payment. | Advise the user to try another method. |
| 404 | Not Found | The requested resource doesn't exist. | Check the transaction ID or URL. |
| 422 | Unprocessable Entity | Well-formed request with semantic errors (e.g. invalid email, amount out of range). | Fix the indicated fields. |
| 429 | Too Many Requests | You exceeded the rate limit. | Retry after a delay (exponential backoff). |
### Server errors [#server-errors]
| Code | Status | Description | Action |
| ---- | --------------------- | ------------------------------------------------------ | ----------------------------------- |
| 500 | Internal Server Error | An unexpected error on Tonder's servers. | Retry after a short delay. |
| 502 | Bad Gateway | A dependent service is temporarily down. | Retry after a short delay. |
| 503 | Service Unavailable | The API is temporarily unavailable (e.g. maintenance). | Retry later. Check the status page. |
### Error format [#error-format]
```json
{
"error": {
"code": "validation_error",
"message": "Amount must be greater than 0",
"type": "request_error",
"details": { "field": "amount", "received_value": -10.00 }
},
"request_id": "req_abc123"
}
```
The `error` object contains `code` (machine-readable), `message` (human-readable), `type` (category),
`details` (additional context). The `request_id` identifies the request — include it when contacting
support.
## Card decline codes [#card-decline-codes]
A standardized internal decline-code system to unify error handling across providers. **Format:**
`[Category][Type][Sequence]` — category (letter A-Z), type `H` (hard decline, permanent) or `S` (soft
decline, retry allowed), sequence (two digits).
The detailed catalog of the **raw processor codes** (numeric, alphanumeric, and K codes) is in
[Decline codes](/docs/core-concepts/decline-codes).
### A — Authentication & Security [#a--authentication--security]
| Code | Type | Description |
| ---- | ---- | --------------------------------------------- |
| AH01 | Hard | Invalid card number |
| AH02 | Hard | Invalid CVV/CVC |
| AH03 | Hard | Invalid expiry date |
| AH04 | Hard | 3D Secure authentication failed |
| AH05 | Hard | Fraudulent transaction detected |
| AH06 | Hard | Card reported lost |
| AH07 | Hard | Card reported stolen |
| AH08 | Hard | Suspicious behavior detected |
| AS01 | Soft | 3D Secure authentication required |
| AS02 | Soft | Strong customer authentication (SCA) required |
### B — Bank/Issuer [#b--bankissuer]
| Code | Type | Description |
| ---- | ---- | ----------------------------- |
| BH01 | Hard | Card declined by bank |
| BH02 | Hard | Card expired |
| BH03 | Hard | Card canceled/disabled |
| BH04 | Hard | Card restricted |
| BH05 | Hard | Bank authorization required |
| BH06 | Hard | Card blacklisted |
| BH07 | Hard | Invalid account |
| BH08 | Hard | Bank requested card retention |
| BS01 | Soft | Bank temporarily unavailable |
| BS02 | Soft | Issuer system maintenance |
### C — Credit/Funds [#c--creditfunds]
| Code | Type | Description |
| ---- | ---- | -------------------------- |
| CH01 | Hard | Insufficient funds |
| CH02 | Hard | Credit limit exceeded |
| CH03 | Hard | Over limit |
| CH04 | Hard | Withdrawal limit exceeded |
| CH05 | Hard | Transaction limit exceeded |
| CS01 | Soft | Temporary credit hold |
### D — Data/Configuration [#d--dataconfiguration]
| Code | Type | Description |
| ---- | ---- | ------------------------------------- |
| DH01 | Hard | Invalid merchant configuration |
| DH02 | Hard | Currency not supported |
| DH03 | Hard | Card type not supported |
| DH04 | Hard | Invalid amount |
| DH05 | Hard | Transaction type not permitted |
| DH06 | Hard | Invalid transaction data |
| DS01 | Soft | Validation error — retry allowed |
| DS02 | Soft | Configuration temporarily unavailable |
### E — External/Network [#e--externalnetwork]
| Code | Type | Description |
| ---- | ---- | ---------------------- |
| ES01 | Soft | Network timeout |
| ES02 | Soft | Communication error |
| ES03 | Soft | Processor unavailable |
| ES04 | Soft | Gateway timeout |
| ES05 | Soft | External service error |
### F — Fraud Prevention [#f--fraud-prevention]
| Code | Type | Description |
| ---- | ---- | ------------------------------ |
| FH01 | Hard | High risk transaction blocked |
| FH02 | Hard | Velocity rules violated |
| FH03 | Hard | Geographic restrictions |
| FH04 | Hard | Merchant blacklist match |
| FH05 | Hard | Card blocked for fraud |
| FS01 | Soft | Transaction under fraud review |
### G — General/System [#g--generalsystem]
| Code | Type | Description |
| ---- | ---- | -------------------------------- |
| GS01 | Soft | General system error |
| GS02 | Soft | Processing error — retry allowed |
| GS03 | Soft | Duplicate transaction |
| GS04 | Soft | Transaction expired |
| GS05 | Soft | Service temporarily unavailable |
### U — User/Customer [#u--usercustomer]
| Code | Type | Description |
| ---- | ---- | -------------------------------- |
| UH01 | Hard | Transaction canceled by customer |
| UH02 | Hard | Customer authentication failed |
| UH03 | Hard | Customer verification failed |
| US01 | Soft | Customer action required |
| US02 | Soft | Session timeout |
The decline code arrives in the webhook in the `decline_code` and `decline_reason` fields:
```json
{
"status": "Declined",
"decline_code": "BH01",
"decline_reason": "Card declined by bank",
"event_type": "payment_Declined"
}
```
## Withdrawal status codes [#withdrawal-status-codes]
`Pending` → `Processing` → `Success` (terminal success), or `Declined` / `Cancelled` / `Failed`
(terminal failure). See the full transitions and the mapping from the previous names in
[Withdrawal status workflow](/docs/direct-api/withdrawals).
## Next steps [#next-steps]
Rate limits and handling 429.
Retry safely.
# Coverage by Country (/docs/resources/coverage)
Tonder gives you access to different payment methods across Latin America. Tonder is continuously
working to expand the list of available methods.
## 🇲🇽 Mexico [#-mexico]
### Deposits [#deposits]
### Withdrawals [#withdrawals]
Refunds are available for card payments in Mexico. See [Refunds](/docs/direct-api/refunds).
## 🇨🇴 Colombia [#-colombia]
### Deposits [#deposits-1]
Withdrawals and refunds are not yet available for Colombia.
## 🇨🇱 Chile [#-chile]
### Deposits [#deposits-2]
Withdrawals and refunds are not yet available for Chile.
## Next steps [#next-steps]
How to accept each method.
Minimums and maximums by method.
# Glossary (/docs/resources/glossary)
The terms you'll see across the documentation, each linking to the page where it's explained in
depth.
## 3DS (3-D Secure) [#3ds-3-d-secure]
Extra authentication from the issuing bank (OTP, biometrics) that shifts fraud liability. A payment
can pass frictionlessly or require a challenge. See
[3DS lifecycle](/docs/core-concepts/3ds-lifecycle).
## Acquirer [#acquirer]
The bank or processor that receives the payment on the merchant side. Tonder routes each
transaction to the right acquirer. See [Cards](/docs/payment-methods/cards).
## BIN [#bin]
*Bank Identification Number*: the leading digits of a card, identifying the issuing bank. Used in
risk analysis. See [Fraud & Risk](/docs/dashboard/fraud-risk).
## Cash vouchers [#cash-vouchers]
Cash payment method (formerly *SafetyPay*): the customer gets a reference and pays at affiliated
stores, pharmacies, or supermarkets. See [Cash vouchers](/docs/payment-methods/cash-vouchers).
## Chargeback (dispute) [#chargeback-dispute]
A cardholder's claim with their bank to reverse a charge. Managed from the dashboard's Disputes
sub-tab. See [Transactions](/docs/dashboard/transactions).
## CLABE [#clabe]
Mexico's standardized 18-digit bank account number; validated by structure and check digit.
Essential for SPEI withdrawals. See [Banking reference](/docs/resources/banking-reference).
## Idempotency [#idempotency]
The guarantee that retrying the same operation doesn't duplicate it. See
[Idempotency](/docs/core-concepts/idempotency).
## Issuer [#issuer]
The bank that issued the customer's card and decides whether to approve or decline the charge. See
[Analytics](/docs/dashboard/analytics).
## `metadata.external_id` [#metadataexternal_id]
**The canonical correlation key**: your order identifier, present in every webhook of every format.
Reconcile everything with it. See [Correlation keys](/docs/core-concepts/correlation-keys).
## `payment_id` / `order_id` [#payment_id--order_id]
Identifiers Tonder assigns to the payment and the order; they appear in responses, webhooks, and
the dashboard's transaction detail. See
[Correlation keys](/docs/core-concepts/correlation-keys).
## PCI DSS [#pci-dss]
The security standard for card data. Hosted Checkout and the SDKs keep you out of scope; raw card
data requires PCI L1. See [Security & PCI scope](/docs/get-started/security).
## Sandbox / Production [#sandbox--production]
Tonder's two environments, with separate keys and URLs (`stage` vs production). Test cards only
work in Sandbox. See [Environments](/docs/get-started/environments).
## Settlement [#settlement]
The grouped deposit of your captured payments into your bank account. See
[Finances](/docs/dashboard/finances).
## SPEI [#spei]
Mexico's interbank transfer system. The customer generates a reference and pays from their banking
app. See [SPEI](/docs/payment-methods/spei).
## SPEI Frictionless [#spei-frictionless]
A SPEI variant where the customer deposits straight to a fixed CLABE, with no per-operation
reference. See [Frictionless SPEI](/docs/payment-methods/frictionless-spei).
## Tokenization [#tokenization]
Exchanging card data for a secure token so you can charge without storing sensitive data. See
[Card payments](/docs/direct-api/card-payments).
## Webhook [#webhook]
An HTTP notification Tonder sends to your server when an event occurs (payment status change,
settlement). The source of truth for confirming payments. See
[How it works](/docs/webhooks/how-it-works).
## Next steps [#next-steps]
The models behind each term.
HTTP, decline, and status codes.
# Resources (/docs/resources)
References and supporting material for your Tonder integration.
Countries and payment methods.
CLABE format, RFC, and institution codes.
HTTP, decline, and withdrawal status codes.
API rate limits and per-transaction limits.
Key payments and Tonder platform terms.
Support channels and useful resources.
# Limits (/docs/resources/limits)
This page covers the API **rate limits** (requests per minute). For per-transaction amount limits by
method, see [Payment method limits](/docs/payment-methods/limits).
## Default rate limits [#default-rate-limits]
If you exceed these limits, the API responds with `HTTP 429 Too Many Requests`.
| Endpoint | Limit | Window |
| ------------------------------------- | ------------ | ---------- |
| `POST /process/` | 100 requests | per minute |
| `GET /transactions/{transaction_id}/` | 300 requests | per minute |
If your application needs higher limits, contact support to discuss your use case.
## Rate limit headers [#rate-limit-headers]
The API includes these headers in every response so you can track your status:
| Header | Description |
| ----------------------- | ----------------------------------------------- |
| `X-RateLimit-Limit` | Maximum requests allowed in the current window. |
| `X-RateLimit-Remaining` | Requests remaining in the current window. |
| `X-RateLimit-Reset` | Unix timestamp of when the window resets. |
## Handling the 429 error [#handling-the-429-error]
When you exceed the limit, the API returns `HTTP 429` with a body indicating when to retry:
```json
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests",
"retry_after": 60
}
}
```
The `retry_after` field indicates the seconds to wait before retrying.
### Best practices [#best-practices]
* Wait at least the `retry_after` seconds before retrying after a `429`.
* Implement **exponential backoff** for server errors or rate limits.
* Cache responses to avoid repeating requests for the same information (especially `GET`).
* Control concurrency so parallel requests don't exceed the limit.
## Next steps [#next-steps]
Per-transaction minimums and maximums.
The meaning of each HTTP code.
# Support (/docs/resources/support)
Support channels and useful resources for your Tonder integration.
## Support channels [#support-channels]
| Channel | Response time | Best for |
| ----------------------- | ------------- | ------------------------ |
| Email support | 24 hours | Integration questions |
| Developer documentation | Instant | API reference and guides |
| Status page | Real-time | Service availability |
Contact us at [support@tonder.io](mailto:support@tonder.io). You'll usually get a reply within
24 hours for integration questions.
## Useful resources [#useful-resources]
| Resource | Link | What you'll use it for |
| ------------- | -------------------------------------------------------- | --------------------------------------------- |
| Dashboard | [dashboard.tonder.io](https://dashboard.tonder.io) | Transaction monitoring and API key management |
| API Status | [tonderio.statuspage.io](https://tonderio.statuspage.io) | Real-time service availability |
| Documentation | In the sidebar | Complete API reference and guides |
## When contacting support [#when-contacting-support]
Include:
* Transaction IDs for payment-specific issues.
* Error codes and messages from responses.
* Request IDs from failed calls.
* A description of expected vs. actual behavior.
## Quick reference [#quick-reference]
| Resource | URL |
| -------------- | -------------------------------------------------------- |
| Production API | `https://app.tonder.io/api/v1/` |
| Sandbox API | `https://stage.tonder.io/api/v1/` |
| Dashboard | [dashboard.tonder.io](https://dashboard.tonder.io) |
| Status page | [tonderio.statuspage.io](https://tonderio.statuspage.io) |
## Next steps [#next-steps]
Troubleshoot API responses.
Isolate problems in Sandbox.
# Flutter (/docs/sdk-reference/flutter)
Reference for Tonder's Flutter SDK methods (iOS and Android): the `InlineCheckout` class (Full) and
the `LiteCheckout` class (Lite, `tonder_sdk_lite` package). Requires Skyflow Maven configuration.
## InlineCheckout (Full) [#inlinecheckout-full]
### Core methods [#core-methods]
| Method | Description |
| ------------------------ | --------------------------------------------------------------------------- |
| `new InlineCheckout(..)` | Initializes the SDK with configuration (`apiKeyTonder`, `returnUrl`, etc.). |
| `setPaymentData(data)` | Loads the customer data (name, email, etc.) into the instance. |
| `setCartTotal(total)` | Sets the total amount for the transaction. |
| `injectCheckout()` | Renders the pre-built checkout UI. |
### `InlineCheckout` constructor [#inlinecheckout-constructor]
| Parameter | Type | Description |
| --------------------- | ------ | -------------------------------------------------- |
| `apiKeyTonder` | String | Your Tonder API key for authentication. |
| `returnUrl` | String | Redirect URL after payment completion. |
| `successUrl` | String | URL for successful payment redirects. |
| `renderPaymentButton` | bool | Whether to show the SDK's built-in payment button. |
```dart
InlineCheckout(
apiKeyTonder: "YOUR_API_KEY",
returnUrl: "https://your-app.com/return",
successUrl: "https://your-app.com/success",
renderPaymentButton: false,
)
```
### `setPaymentData(data)` [#setpaymentdatadata]
Configures the customer information used during the payment.
```dart
final customerData = {
'first_name': 'Juan',
'last_name': 'Pérez',
'email': 'juan.perez@example.com'
};
_fullPlugin!.setPaymentData(customerData);
```
## LiteCheckout (Lite) [#litecheckout-lite]
Build your own payment UI while Tonder tokenizes sensitive card data.
| Method | Description |
| ---------------------------- | ------------------------------------------------------------------------ |
| `new LiteCheckout(..)` | Initializes the SDK with configuration (`apiKey`, `returnUrl`). |
| `configureCheckout(..)` | Configures the customer email and the `secureToken` for card management. |
| `setPaymentData(data)` | Loads the customer data (name, email, etc.) into the instance. |
| `setCartTotal(total)` | Sets the total amount for the transaction. |
| `payment(cardData)` | Processes a payment with card data from your custom form. |
| `saveCustomerCard(cardData)` | Tokenizes and saves a customer's card for future use. |
| `getCustomerCards()` | Retrieves all saved cards for the customer. |
| `removeCustomerCard(cardId)` | Removes a saved card by its ID. |
### `LiteCheckout` constructor [#litecheckout-constructor]
| Parameter | Type | Description |
| ----------- | ------ | -------------------------------------------------------- |
| `apiKey` | String | Your Tonder API key for authentication. |
| `returnUrl` | String | URL where users are redirected after payment completion. |
```dart
LiteCheckout(
apiKey: "YOUR_API_KEY",
returnUrl: "YOUR_RETURN_URL",
)
```
### `configureCheckout(..)` [#configurecheckout]
Sets the customer email and the `secureToken` required for card management operations.
```dart
_liteCheckout.configureCheckout(
customerEmail: 'customer@example.com',
secureToken: 'SECURE_TOKEN_FROM_BACKEND',
);
```
### `payment(cardData)` [#paymentcarddata]
```dart
final cardData = {
'card_number': '4111111111111111',
'cardholder_name': 'John Doe',
'expiration_month': '12',
'expiration_year': '25',
'cvv': '123',
};
final response = await _liteCheckout.payment(cardData);
```
### `saveCustomerCard(cardData)` / `getCustomerCards()` / `removeCustomerCard(cardId)` [#savecustomercardcarddata--getcustomercards--removecustomercardcardid]
```dart
// Save a card
final response = await _liteCheckout.saveCustomerCard(cardData);
// Retrieve saved cards
final cards = await _liteCheckout.getCustomerCards();
// Remove a saved card
await _liteCheckout.removeCustomerCard('card_abc123');
```
## Next steps [#next-steps]
Installation and integration flow.
Other platforms.
# SDK Reference (/docs/sdk-reference)
Tonder's SDKs integrate payment processing directly into your web and mobile applications, with
secure data handling and PCI DSS compliance without sensitive data touching your servers.
## Integration flow [#integration-flow]
Your app requests a secure token from your backend to initialize the SDK.
You configure the SDK with customer and cart information.
The SDK renders the payment fields or a full checkout in your app.
The user submits their payment information and the SDK securely sends it to Tonder.
If required, the SDK automatically handles the 3D Secure challenge.
Tonder returns the final transaction status to your app.
## Platforms [#platforms]
`createTonder` with secure card fields, saved cards, and APMs (`@tonder.io/web-sdk`).
`TonderProvider`, `TonderPayment`, and the `useTonder` hook.
The `InlineCheckout` class for iOS and Android.
The `InlineCheckout` class for hybrid apps.
For the step-by-step integration guide, see the [SDKs](/docs/sdks/overview) section.
# Ionic (/docs/sdk-reference/ionic)
Reference for Tonder's Ionic SDK methods for hybrid mobile apps: the `InlineCheckout` class
(Full, `@tonder.io/ionic-full-sdk` package) and the `LiteCheckout` class (Lite,
`@tonder.io/ionic-lite-sdk` package). Requires the Skyflow script tag.
## InlineCheckout (Full) [#inlinecheckout-full]
### `InlineCheckout` constructor [#inlinecheckout-constructor]
| Parameter | Type | Required | Description |
| --------------------- | ------- | :------: | -------------------------------------------------------------------------------------- |
| `apiKey` | String | Yes | Your Tonder API key for authentication. |
| `returnUrl` | String | Yes | URL where the checkout is mounted (to complete the 3DS redirect). |
| `mode` | String | No | Environment mode. Options: `product` or `stage`. |
| `renderPaymentButton` | Boolean | No | If `true`, the SDK renders a default payment button (you must use `setPaymentData()`). |
```javascript
const inlineCheckout = new InlineCheckout({
apiKey: "YOUR_API_KEY",
returnUrl: "YOUR_RETURN_URL",
mode: "stage",
renderPaymentButton: false
});
```
### Methods [#methods]
| Method | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
| `configureCheckout(data)` | Configures customer data (at least the email; include `secureToken` to save cards). |
| `injectCheckout()` | Renders the checkout UI into your container. |
| `setPaymentData(data)` | Sets the payment data (customer, cart, metadata); useful with the default payment button. |
| `payment(checkoutData)` | Initiates a payment transaction securely using the provided data. |
| `verify3dsTransaction()` | Verifies a 3D Secure transaction's status after redirection. |
| `saveCard()` | Saves a new card. Useful when the SDK is configured as an enrollment card (`isEnrollmentCard: true`). |
| `removeCheckout()` | Removes the checkout UI from the DOM and cleans up resources. |
```javascript
inlineCheckout.configureCheckout({
customer: {
email: "example@email.com",
secureToken: "e89eb18.." // Required for saving cards
}
});
```
## LiteCheckout (Lite) [#litecheckout-lite]
Build your own UI while Tonder tokenizes sensitive data. Exposes the `LiteCheckout` class from the
`@tonder.io/ionic-lite-sdk` package.
| Method | Description |
| ---------------------------- | --------------------------------------------------------------------- |
| `new LiteCheckout(config)` | Creates and initializes the instance (`apiKey`, `returnUrl`, `mode`). |
| `configureCheckout(config)` | Sets the customer email and the `secureToken` for card management. |
| `payment(checkoutData)` | Processes a payment securely using data from your custom UI. |
| `getCustomerCards()` | Retrieves the configured customer's saved cards. |
| `saveCustomerCard(cardData)` | Securely tokenizes and saves a new card. |
| `removeCustomerCard(cardId)` | Deletes a saved card using its unique ID. |
| `verify3dsTransaction()` | Verifies a 3D Secure transaction's status after redirection. |
### `LiteCheckout` constructor [#litecheckout-constructor]
| Parameter | Type | Required | Description |
| ----------- | ------ | :------: | ----------------------------------------------------------------- |
| `apiKey` | String | Yes | Your Tonder API key for authentication. |
| `returnUrl` | String | No | URL where the checkout is mounted (to complete the 3DS redirect). |
| `mode` | String | No | Environment mode. Options: `product` or `stage`. |
```javascript
const liteCheckout = new LiteCheckout({
apiKey: "YOUR_API_KEY",
returnUrl: "YOUR_RETURN_URL",
mode: "stage"
});
```
### `payment(checkoutData)` [#paymentcheckoutdata]
```javascript
const response = await liteCheckout.payment({
customer: { firstName: "Juan", email: "juan.hernandez@mail.com" },
cart: { total: "399.00", items: [ /* ... */ ] },
currency: "MXN",
card: {
card_number: "4111111111111111",
cvv: "123",
expiration_month: "12",
expiration_year: "25",
cardholder_name: "John Doe"
}
});
```
## Next steps [#next-steps]
Installation and integration flow.
Other platforms.
# React Native (/docs/sdk-reference/react-native)
Reference for Tonder's React Native SDK (`@tonder.io/rn-sdk` package). The SDK uses a Provider, hooks,
and components to manage state and render the UI. Requires RN 0.70+ and React 16.8+.
## TonderProvider [#tonderprovider]
A wrapper component that initializes the SDK and provides its context. It must be placed at the root
of your checkout flow.
| Parameter | Type | Description |
| ------------------ | ------- | ------------------------------------------------------------------------------------------------ |
| `config` | Object | SDK configuration object. |
| `config.type` | SDKType | SDK type: `SDKType.INLINE` (Full), `SDKType.LITE` (Lite), or `SDKType.ENROLLMENT` (card saving). |
| `config.mode` | String | Mode: `'development'`, `'production'`, or `'sandbox'`. |
| `config.apiKey` | String | Your Tonder public key. |
| `config.returnURL` | String | Optional URL to complete the 3DS redirect. |
```jsx
import { TonderProvider, SDKType, Environment } from '@tonder.io/rn-sdk';
function App() {
return (
);
}
```
## TonderPayment [#tonderpayment]
Renders the complete, pre-built payment UI. Used when `SDKType` is `INLINE` (Full). Takes no props.
```jsx
import { TonderPayment } from '@tonder.io/rn-sdk';
export default function FullPaymentScreen() {
return (
);
}
```
`TonderPayment` ships with its own pay button. If you'd rather use your own, `INLINE` also exposes
`payment()` through the hook:
```jsx
const { create, payment } = useTonder();
const handlePayment = async () => {
const { response, error } = await payment();
if (error) {
console.error('Error payment: ', error);
return;
}
console.log('Response payment: ', response);
};
```
## Lite (custom UI) [#lite-custom-ui]
To build your own UI, set `SDKType.LITE` in the `TonderProvider` and use the `useTonder` hook to
access the payment and card-management methods, along with the SDK's secure input components.
### Secure input components [#secure-input-components]
For `SDKType.LITE` integrations, the SDK provides individual, secure components for building your
own UI. Each one accepts style props.
| Component | Description |
| -------------------------- | ------------------------------------------ |
| `CardHolderInput` | Secure input for the cardholder name. |
| `CardNumberInput` | Secure input for the card number. |
| `CardCVVInput` | Secure input for the CVV/CVC. |
| `CardExpirationDateInput` | Secure input for the full expiration date. |
| `CardExpirationMonthInput` | Secure input for the expiration month. |
| `CardExpirationYearInput` | Secure input for the expiration year. |
```jsx
import {
CardHolderInput,
CardNumberInput,
CardExpirationMonthInput,
CardExpirationYearInput,
CardCVVInput
} from '@tonder.io/rn-sdk';
```
#### CVV for a saved card [#cvv-for-a-saved-card]
When the buyer pays with an already-saved card and the issuer asks for the CVV again, pass that
card's `cardId` so the field updates that card's CVV rather than a new card's:
```jsx
```
Three constraints:
* It only renders when a card is selected.
* Only one `CardCVVInput` with a `cardId` at a time.
* It is mutually exclusive with the full new-card form.
### `useTonder` hook [#usetonder-hook]
A React hook that provides access to the SDK's methods. Use it in `LITE` and `ENROLLMENT` modes, and
in `INLINE` when you want to trigger the payment from your own button.
```jsx
import { useTonder, SDKType } from '@tonder.io/rn-sdk';
// For LITE mode (custom payment forms)
const { create, payment, saveCustomerCard, getCustomerCards, ... } = useTonder();
// For ENROLLMENT mode (card saving)
const { create, saveCustomerCard, getCardSummary, reset } = useTonder();
```
#### Common methods (LITE and ENROLLMENT) [#common-methods-lite-and-enrollment]
| Method | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `create(config)` | Initializes the SDK with configuration. Must be called before rendering components or calling other methods. |
| `reset()` | Resets the SDK state to its initial values and cleans up resources. Useful for re-initializing the flow after a transaction. |
`create(config)` accepts:
| Parameter | Type | Description |
| ---------------------- | ------ | ----------------------------------------------------------------------- |
| `config.secureToken` | String | Secure token obtained from your backend (required for card operations). |
| `config.paymentData` | Object | Payment information (for LITE mode). |
| `config.customer` | Object | Customer information (for ENROLLMENT mode). |
| `config.callbacks` | Object | Optional callback functions (e.g. `onFinishPayment`, `onFinishSave`). |
| `config.customization` | Object | Optional UI customization options. |
| `config.events` | Object | Optional event handlers for the card form input fields. |
```jsx
// LITE mode
const { create } = useTonder();
const paymentData = {
customer: { email: 'test@example.com', firstName: 'John', lastName: 'Doe' },
cart: {
total: 399,
items: [{ name: 'Product', amount_total: 399, description: 'Description', price_unit: 399, quantity: 1 }]
},
};
const initializePayment = async () => {
const { error } = await create({
secureToken: 'your-secure-token',
paymentData,
callbacks: { onFinishPayment: handlePaymentFinish }
});
};
```
#### LITE mode methods [#lite-mode-methods]
Available when `type` is `SDKType.LITE`, giving full control over the payment flow with custom
components.
| Method | Description |
| ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `payment()` | Processes a payment using the configured payment data and the values from the LITE input components. |
| `saveCustomerCard()` | Tokenizes and saves the current card from the LITE input components. |
| `getCustomerCards()` | Retrieves the list of saved cards for the customer. |
| `getCardSummary(skyflowId)` | Retrieves detailed information about a saved card using its Skyflow ID. |
| `removeCustomerCard(skyflowId)` | Deletes a saved card. |
| `getPaymentMethods()` | Retrieves the available payment methods. |
```jsx
const { payment, saveCustomerCard, getCustomerCards, getCardSummary, removeCustomerCard, getPaymentMethods } = useTonder();
const { response, error } = await payment();
```
`getCardSummary(skyflowId)` returns:
```ts
interface ICardsSummaryResponse {
user_id: number;
card: ICardSkyflowFields;
}
interface ICardSkyflowFields {
card_number: string; // masked
expiration_month: string;
expiration_year: string;
skyflow_id: string;
card_scheme: string;
cardholder_name: string;
}
```
### Enrollment (card saving) [#enrollment-card-saving]
Set `SDKType.ENROLLMENT` in the `TonderProvider` for card-saving flows. You can build your own UI
with the secure input components, or use the pre-built `TonderEnrollment` component.
```jsx
import { TonderProvider, SDKType, Environment } from '@tonder.io/rn-sdk';
```
#### `TonderEnrollment` (pre-built) [#tonderenrollment-pre-built]
A component with a ready-to-use enrollment UI. Takes no props.
```jsx
import { TonderEnrollment } from '@tonder.io/rn-sdk';
export default function EnrollmentScreen() {
return (
);
}
```
#### ENROLLMENT mode methods [#enrollment-mode-methods]
| Method | Description |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `saveCustomerCard()` | Tokenizes and saves the current card. Only necessary when building a custom enrollment form or a custom save button. |
| `getCardSummary(skyflowId)` | Retrieves detailed information about a saved card using its Skyflow ID. |
```jsx
const { saveCustomerCard, getCardSummary } = useTonder();
const { response, error } = await saveCustomerCard();
```
## Next steps [#next-steps]
Installation and integration flow.
Other platforms.
# Web (/docs/sdk-reference/web)
Reference for Tonder's Web SDK ([`@tonder.io/web-sdk`](https://www.npmjs.com/package/@tonder.io/web-sdk),
TypeScript, types included). The step-by-step guide is at [Web SDK](/docs/sdks/web); source
code and README at [github.com/tonderio/web-sdk](https://github.com/tonderio/web-sdk).
```ts
import { createTonder, AppError, ErrorKeyEnum } from '@tonder.io/web-sdk';
```
Prefer loading it with a `` to the `` of
your `index.html`.
* **Android** (Flutter and Ionic): add the `INTERNET` permission in `AndroidManifest.xml`:
```xml
```
* **iOS / macOS** (Flutter and Ionic): add the network client entitlement to your
`.entitlements` files:
```xml
com.apple.security.network.client
```
## Integration flow [#integration-flow]
The flow is the same across all three platforms:
`POST /api/secure-token/` with your secret key, from your backend.
Create the checkout instance with your `apiKey` and `returnUrl`.
Set the payment and customer data.
Mount the form (Full) or render your own UI (Lite).
Start the payment when the user confirms.
SDKs receive webhooks in **Long** format and use the **public API key**. There is no top-level
`orderReference` — reconcile on `metadata.order_id` or `metadata.external_id`.
## Next steps [#next-steps]
Flutter SDK methods and parameters.
Ionic SDK methods and parameters.
React Native SDK methods and parameters.
Full vs Lite and the integration flow.
# Overview — Full vs Lite (/docs/sdks/overview)
Tonder's SDKs offer payment-processing tools and a seamless checkout experience for your customers.
They simplify integration with clear documentation and add robust security measures for sensitive
data.
## Choose your SDK type [#choose-your-sdk-type]
Tonder offers two variants to match your integration needs:
**Full SDK (Inline)**
* Pre-built payment UI — ready to use out of the box.
* Built-in form validation (card, expiry, CVV).
* Automatic 3DS handling.
* PCI compliant — sensitive data never touches your servers.
* **Best for:** quick launches, teams with limited frontend resources.
**Lite SDK**
* Build your own custom payment UI from scratch.
* Complete design control — fits any design system.
* Card management: save, retrieve & delete payment methods.
* Secure tokenization — no sensitive data on your servers.
* **Best for:** custom UI, complex workflows, existing design systems.
## Platform support [#platform-support]
**The Web SDK got a major upgrade.** For web, a single package —
[`@tonder.io/web-sdk`](https://www.npmjs.com/package/@tonder.io/web-sdk) (npm or CDN) — covers
both approaches: the default full form (Full-style) and individual secure fields in your own UI
(Lite-style). The Full/Lite split still applies to the mobile SDKs. See
[Web SDK](/docs/sdks/web). Coming from the previous SDK or from Direct API? Follow the
[legacy SDK migration guide](/docs/sdks/migrate-legacy-sdk) or the
[Direct API one](/docs/sdks/migrate-direct-api).
| Platform | Full SDK | Lite SDK | Notes / Requirements |
| ----------------------------- | :------: | :------: | ---------------------------------------------------------------------- |
| Web (JavaScript / TypeScript) | ✅ | ✅ | `@tonder.io/web-sdk` · npm or CDN · one package covers both approaches |
| Flutter | ✅ | ✅ | iOS & Android · Skyflow Maven config required |
| Ionic | ✅ | ✅ | Hybrid mobile · Skyflow script tag required |
| React Native | ✅ | ✅ | RN 0.70+ · React 16.8+ · iOS & Android |
## Integration flow (mobile SDKs) [#integration-flow-mobile-sdks]
On web, the flow is `createTonder()` → `init()` → `create('card_fields')` → `mount()` → `pay()` —
see the [Web SDK quick start](/docs/sdks/web#quick-start-card-payment). On the mobile SDKs the
flow is:
`POST /api/secure-token/` with your secret key. The resulting `secureToken` is valid for
**1 hour**.
Instantiate `new TonderSdk.InlineCheckout({...})`.
Call `configureCheckout({ email, secureToken })`.
Call `injectCheckout()` to mount the form into your page.
Call `payment(checkoutData)` on user click.
## Webhooks [#webhooks]
All SDKs use the **public API key**. The **mobile** SDKs receive webhooks in **Long** format;
**Lite SDK 2.0** (the Web SDK) uses the **Short** format — top-level fields, with
`client_reference` and `event_type` such as `payment_Success` — the same one API Direct sends. See
the [events catalog](/docs/webhooks/events-catalog).
Mobile-SDK Long webhooks have no top-level `orderReference`. Use `metadata.order_id` or
`metadata.external_id` for reconciliation.
## Next steps [#next-steps]
The new `@tonder.io/web-sdk`: secure fields, saved cards, and APMs.
Flutter, Ionic, and React Native.
Methods, parameters, and options per platform.
The Long format in depth.
Integrate the Web SDK with Claude Code, Claude Desktop, or Codex using recipes and versioned docs.
Interactive demos of the new Web SDK (card, saved cards, APMs, SafetyPay) + legacy demos.
# Web (/docs/sdks/web)
Tonder's Web SDK ([`@tonder.io/web-sdk`](https://www.npmjs.com/package/@tonder.io/web-sdk)) is a
browser TypeScript SDK for accepting payments: secure card fields, new-card and saved-card
payments, hosted/3DS presentation, payment-method discovery, transaction lookup, and
webhook-friendly transaction responses.
Want an AI agent to do the integration for you? Install the
[Tonder Web SDK plugin](/docs/mcp/overview) in Claude Code, Claude Desktop, or Codex.
## Before you start [#before-you-start]
You need:
* Your Tonder **public API key**. Never put secret keys in browser code.
* A modern browser: Chrome, Safari, Firefox, or Edge.
* A server endpoint that can create a short-lived `secure_token`, if you'll use saved
cards / Card-on-File.
* A webhook endpoint for reliable payment fulfillment.
## Install [#install]
```bash
npm install @tonder.io/web-sdk
```
```ts
import { createTonder, AppError, ErrorKeyEnum } from '@tonder.io/web-sdk';
```
No bundler? Load the browser global build from the environment CDN:
| Environment | CDN URL |
| ----------- | --------------------------------------------------------------------------------- |
| Stage | `https://zplit-stage.s3.us-east-1.amazonaws.com/web-sdk/v1/tonder-web-sdk.min.js` |
| Production | `https://zplit-prod.s3.us-east-1.amazonaws.com/web-sdk/v1/tonder-web-sdk.min.js` |
```html
```
In a TypeScript app that uses the CDN, you can install `@tonder.io/web-sdk` as a devDependency
**for types only** (`npm install -D @tonder.io/web-sdk` + `import type`), keeping the runtime on
the CDN. Do not import runtime code from the package in that setup.
## Quick start: card payment [#quick-start-card-payment]
### Add containers for the card fields [#add-containers-for-the-card-fields]
```html
```
Cap each container's height so the secure iframe does not visually grow before it settles into
the input layout:
```css
.card-field {
width: 100%;
max-height: 90px;
}
```
### Initialize, mount, and pay [#initialize-mount-and-pay]
```ts
import { createTonder } from '@tonder.io/web-sdk';
const tonder = createTonder({
api_key: 'pk_test_...',
environment: 'sandbox',
session: {
customer: {
email: 'ada@example.com',
first_name: 'Ada',
last_name: 'Lovelace',
},
},
});
await tonder.init();
const card_fields = tonder.create('card_fields');
await card_fields.mount();
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
metadata: { cart_id: 'cart_789' },
payment_method: { type: 'card' },
});
```
### Handle the result [#handle-the-result]
```ts
if (transaction.status === 'Success' || transaction.status === 'Authorized') {
// Show confirmation.
} else if (transaction.status === 'Pending') {
// The customer may need to complete 3DS or an asynchronous payment method.
// Confirm final state with webhooks or getTransaction().
} else {
// Show a recoverable payment message.
console.warn(transaction.decline_code, transaction.decline_reason);
}
```
Declines are **not** thrown as errors — they're returned as transactions: read
`transaction.status`. SDK failures are thrown as `AppError` (see
[Errors](/docs/sdk-reference/web#errors)).
Web SDK amounts are **decimal units** (`150` = MXN 150.00). See
[Money, currencies, and amounts](/docs/core-concepts/money).
## Configuration [#configuration]
`createTonder(config)` creates one SDK instance for one shopper/session. Recreate the SDK if the
customer, `secure_token`, or environment changes.
| Field | Required | Description |
| --------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------- |
| `api_key` | Yes | Public Tonder key for browser integrations. |
| `environment` | Yes | `'sandbox'`, `'stage'`, or `'production'`. |
| `session.customer` | For `pay()` and saved-card operations | Customer identity. Omit for read-only return pages that only call `getTransaction()`. |
| `session.secure_token` | For saved-card operations | Short-lived token minted by your backend. |
| `presentation_mode` | No | `'redirect'` by default, or `'embedded'` for SDK-owned modal presentation. |
| `events.presentation` | No | `on_open` / `on_close` callbacks for the embedded hosted view. |
| `customization.card_fields` | No | Labels, placeholders, styles, and validation-message overrides for the secure fields. |
The full customization tables (per-field styles, error messages, card icon) are in the
[Web SDK Reference](/docs/sdk-reference/web#createtonderconfig).
### Presentation mode [#presentation-mode]
When a payment requires a hosted step (3DS, APM instructions), the SDK uses
`presentation_mode`:
| Mode | Behavior |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `redirect` | The browser navigates to the hosted page. Use `return_url`, `getTransaction()`, and webhooks to confirm final status. |
| `embedded` | The SDK opens a full-screen modal. Card 3DS waits for a final transaction; APM/SPEI hosted instructions may return `Pending` immediately. |
## Saved cards (secure\_token) [#saved-cards-secure_token]
Saved-card operations (`getCustomerCards()`, `enrollCard()`, `removeCustomerCard()`, paying with
`saved_card`) require `session.customer` **and** `session.secure_token`. Mint the token by
calling `/api/secure-token/` **from your backend** with your secret key:
```javascript
fetch("https://stage.tonder.io/api/secure-token/", {
method: 'POST',
headers: {
'Authorization': 'Token YOUR_SECRET_KEY',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(result => {
const secureToken = result.access;
// Pass it to the frontend as session.secure_token
});
```
```javascript
fetch("https://app.tonder.io/api/secure-token/", {
method: 'POST',
headers: {
'Authorization': 'Token YOUR_SECRET_KEY',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(result => {
const secureToken = result.access;
});
```
The generated `secure_token` is valid for **1 hour**. Use it within that window; if you cache or
reuse it after that, mint a new one.
### Pay with a saved card [#pay-with-a-saved-card]
```ts
const tonder = createTonder({
api_key: 'pk_test_...',
environment: 'sandbox',
session: {
customer: { email: 'ada@example.com' },
secure_token: await getSecureTokenFromYourBackend(),
},
});
await tonder.init();
const cards = await tonder.getCustomerCards();
const selected_card = cards[0];
// Mount saved-card CVV only when the card cannot be charged through an
// existing Card-on-File subscription.
if (!selected_card.subscription_id) {
const cvv = tonder.create('card_fields', {
card_id: selected_card.card_id,
fields: ['cvv'],
});
await cvv.mount();
}
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: { type: 'saved_card', card_id: selected_card.card_id },
});
```
### Save a new card [#save-a-new-card]
```ts
const card_fields = tonder.create('card_fields');
await card_fields.mount();
const enrollment = await tonder.enrollCard();
// { card_id: 'card_123', subscription_id: 'sub_123' }
```
### API reference [#api-reference]
## Alternative payment methods [#alternative-payment-methods]
If your checkout already knows which method to offer, pass the method code directly to `pay()`:
```ts
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: { type: 'oxxopay' },
});
```
Use `getPaymentMethods()` (optional) to render the methods enabled for your business, and
`getPaymentMethodBanks()` for bank-backed SafetyPay methods:
```ts
const banks = await tonder.getPaymentMethodBanks();
const bank = banks.cash[0];
const transaction = await tonder.pay({
amount: 150,
currency: 'MXN',
return_url: 'https://yourstore.example/checkout/return',
client_reference: 'order_1001',
payment_method: {
type: 'safetypayCash',
config: {
country: bank.country, // e.g. 'Mexico'
channel: bank.channel, // 'WP' cash, 'OL' transfer
bank_ids: [{ id: bank.code }], // e.g. [{ id: '8186' }]
},
},
});
```
APM/SPEI methods often settle asynchronously. Use
[webhooks](/docs/webhooks/how-it-works) for fulfillment.
## Demos [#demos]
Try every flow in the [SDK demos portal](https://main.d3ufdi32xd85gs.amplifyapp.com/introduction):
| Flow | Demo |
| --------------- | ------------------------------------------------------------------------------------- |
| Card payment | [web/card-payment](https://main.d3ufdi32xd85gs.amplifyapp.com/web/card-payment) |
| Card enrollment | [web/enroll-card](https://main.d3ufdi32xd85gs.amplifyapp.com/web/enroll-card) |
| Saved cards | [web/saved-cards](https://main.d3ufdi32xd85gs.amplifyapp.com/web/saved-cards) |
| Payment methods | [web/payment-methods](https://main.d3ufdi32xd85gs.amplifyapp.com/web/payment-methods) |
| SafetyPay banks | [web/safetypay-banks](https://main.d3ufdi32xd85gs.amplifyapp.com/web/safetypay-banks) |
The [legacy demos](https://main.d3ufdi32xd85gs.amplifyapp.com/legacy) (Web SDK Lite and Inline)
use previous SDK versions and are kept for reference only — use the demos above for new
integrations. If you are still on the previous SDK, follow the
[migration guide](/docs/sdks/migrate-legacy-sdk).
## Reconciliation [#reconciliation]
* `client_reference` is **required**: it's your order reference and appears in dashboards,
exports, webhooks, and transaction reports.
* Use a stable `idempotency_key` per checkout attempt so retries don't create duplicate charges.
Do not reuse `client_reference` as the idempotency key.
* Web SDK webhooks use the **flat** payload (top-level fields) with
`event_type: payment_Success` / `payment_Pending`. See the
[events catalog](/docs/webhooks/events-catalog).
## Next steps [#next-steps]
Every method, type, error, and payment status.
Interactive demos per flow + legacy demos (Lite and Inline).
Let Claude Code or Codex do this integration for you.
Confirm payments reliably on your backend.
# Best Practices & Deduplication (/docs/webhooks/best-practices)
These are the essential security and implementation practices for building secure, reliable webhook
integrations.
## Security [#security]
* **Always use a secure (HTTPS) URL.** HTTP connections are unencrypted and vulnerable to
man-in-the-middle attacks.
* **Use an authentication method** (`BEARER`, `API_TOKEN`, or `BASIC_AUTH`) when creating your
webhook, to prevent unauthorized requests.
* **Validate the event structure and content** before processing: check that required fields are
present and have the expected type.
## Implementation [#implementation]
* **Acknowledge receipt immediately with a `200 OK`.** Tonder has a 30-second timeout; respond first,
then process business logic in the background.
* **Use `metadata.external_id` to deduplicate.** Store processed IDs temporarily (24-48 hours) and
check against them before processing new events — the retry mechanism may send the same event
multiple times.
* **Keep detailed logs** of the full payload, processing status, and errors, with timestamps and
event IDs.
* **Handle errors gracefully.** Wrap processing in try-catch blocks so a failed webhook doesn't bring
down your whole application.
## Deduplication checklist [#deduplication-checklist]
## Next steps [#next-steps]
Retry policy and DLQ.
Reconcile with `metadata.external_id`.
# Delivery & Retry (/docs/webhooks/delivery-retry)
Tonder's webhook system is designed for reliability. If your endpoint is temporarily unavailable, we
automatically retry the delivery.
## How delivery works [#how-delivery-works]
A transaction status change or event occurs.
Tonder sends a POST request to your endpoint.
Your endpoint's response determines the next step.
Based on the response, the event is marked complete or scheduled for retry.
## Retry policy [#retry-policy]
| Setting | Value | Description |
| ------------------ | ---------------------- | ----------------------------------------------- |
| Max retry attempts | 3 | We try to deliver the webhook up to 3 times. |
| Processing timeout | 30 seconds per attempt | Your endpoint must respond within 30 seconds. |
| Retry interval | 60 seconds | We wait 60 seconds before retrying. |
| Total retry window | \~3 minutes | The maximum time spent retrying a single event. |
## Success and failure criteria [#success-and-failure-criteria]
A delivery is **successful** if your endpoint responds with a `2xx` code within 30 seconds. Any other
response is a **failure** and a retry is scheduled. Scenarios that trigger retries:
* `4xx` errors (400, 401, 404, etc.) — client errors.
* `5xx` errors (500, 502, 503, etc.) — endpoint errors.
* Timeouts when no response is received after 30 seconds.
## Dead Letter Queue (DLQ) [#dead-letter-queue-dlq]
After 3 failed attempts, the event is moved to a Dead Letter Queue for manual inspection. Failed
events are stored for 30 days, accessible through the dashboard or via support, and can be retried
manually after fixing the endpoint.
## Implementation considerations [#implementation-considerations]
* Acknowledge receipt immediately with a `200 OK` to prevent timeouts.
* Use `metadata.external_id` to deduplicate and avoid processing the same event twice.
* Keep detailed logs of every webhook for debugging.
* Ensure a malformed payload or a processing error doesn't crash your system.
## Next steps [#next-steps]
Optimal implementation patterns.
Register and manage your endpoints.
# Events Catalog (Short + Long) (/docs/webhooks/events-catalog)
Tonder sends two webhook formats depending on your integration mode. This page catalogs the events by
format; the conceptual model is in the [Webhooks model](/docs/core-concepts/webhooks-model).
## Formats [#formats]
| Characteristic | Short (API Direct · Lite SDK 2.0 · Frictionless) | Long (Hosted · mobile SDK · Legacy) |
| --------------- | ------------------------------------------------ | --------------------------------------- |
| Root structure | Top-level fields | Everything inside `data{}` |
| Status | `status: Pending / Success` | `transaction_status: Pending / Success` |
| Event indicator | `event_type: payment_Pending` | `event: created / confirmed` |
| Order reference | `client_reference` | `metadata.external_id` / `order_id` |
## Events by integration [#events-by-integration]
### API Direct (Short) [#api-direct-short]
Events arrive in `event_type`:
| `event_type` | Meaning |
| ----------------- | -------------------------------------------- |
| `payment_Pending` | The payment was initiated and is processing. |
| `payment_Success` | The payment completed successfully. |
| `payment_Failed` | The payment failed. |
On SPEI Success, the payload adds sender info: `sender_name`, `sender_clabe`, `sender_bank`, among
others.
```json
{
"id": "78eb98ef-65a8-4038-a2a0",
"operation_type": "payment",
"amount": "500",
"currency": "MXN",
"client_reference": "ORD-001",
"status": "Pending",
"payment_method_type": "SPEI",
"metadata": { "external_id": "ORD-001", "order_id": "ORD-001" },
"event_type": "payment_Pending",
"action": "MODIFY"
}
```
### Hosted Checkout (Long) [#hosted-checkout-long]
Session events arrive in `action`:
| `action` | Meaning |
| ------------------- | ------------------------------------------------------- |
| `session.created` | A new checkout session was created. |
| `session.completed` | The session was successfully paid (confirms the order). |
| `session.expired` | The session expired without payment. |
`payment.transaction` is the exception: this event arrives in the `event_type` field, not `action`.
It fires when a payment attempt's status changes. Check `payload.event_type ===
'payment.transaction'`, not `payload.action`.
Full payloads are in the [Hosted Checkout reference](/docs/hosted-checkout/reference).
### Withdrawals (Short) [#withdrawals-short]
API Direct withdrawals use the same flat (Short) payload as payments, with `event_type` using the
`withdrawal_` prefix (`withdrawal_Pending`, `withdrawal_Success`, `withdrawal_Declined`,
`withdrawal_Failed`) and `transfer_method_type` instead of `payment_method_type`. Full details and
examples in [Withdrawals](/docs/direct-api/withdrawals).
Use `metadata.external_id` as your correlation key — it's present in both formats and across all
events.
## Next steps [#next-steps]
The payload and delivery reliability.
Full session-event payloads.
Withdrawal payload structure.
# How Webhooks Work (/docs/webhooks/how-it-works)
Webhooks are automated messages Tonder sends when transaction events occur. Instead of repeatedly
polling the API for status changes, webhooks notify you in real time the moment something happens — a
payment completing, failing, or requiring additional authentication.
## Key events [#key-events]
| Event type | Description | Example flow |
| -------------------------- | ----------------------------------------------- | ------------------------------------------ |
| Payment status changes | Real-time updates as the payment progresses | `pending` → `authorized` → `success` |
| Payment failures | When a payment is declined or fails | `pending` → `declined` or `failed` |
| 3DS authentication | When the user completes the 3D Secure challenge | `pending_3ds` → `success` or `failed` |
| Cash payment confirmations | When the customer pays an OXXO voucher | `pending` → `success` (when paid at store) |
| Withdrawal updates | Status changes for payout transactions | `processing` → `success` or `failed` |
## Payload structure (API Direct) [#payload-structure-api-direct]
API Direct webhooks use a **flat payload** — all fields are at the top level, with no `data` wrapper.
Key fields:
| Field | Type | Description |
| --------------------- | ------ | ----------------------------------------------------------------------------- |
| `id` | string | Unique webhook event identifier. |
| `operation_type` | string | Operation type (e.g. `payment`). |
| `amount` | string | Transaction amount (as a string). |
| `currency` | string | ISO 4217 code (e.g. `MXN`). |
| `client_reference` | string | Your transaction reference. |
| `status` | string | Current status (`Success`, `Pending`, `Failed`). |
| `provider` | string | Processor that handled the transaction. |
| `transaction_id` | string | Tonder's internal identifier. |
| `payment_method_type` | string | Method used (`SPEI`, `CARD`, `OXXO`). |
| `created` | string | ISO 8601 timestamp. |
| `metadata` | object | Key-value pairs sent when creating the payment. |
| `event_type` | string | Event that triggered the notification (`payment_Success`, `payment_Pending`). |
| `action` | string | Action associated with the event (e.g. `MODIFY`). |
```json
{
"id": "fc38522e-3e5d-45b8-ba6a-ece72caee71f",
"operation_type": "payment",
"amount": "70",
"currency": "MXN",
"client_reference": "ORD-001",
"status": "Success",
"provider": "tonder",
"transaction_id": "e9340a04-6d68-4afc-86c5-79f8b7c87de4",
"payment_method_type": "SPEI",
"created": "2026-05-21T19:15:32.029134Z",
"metadata": {
"order_id": "ORD-001",
"external_id": "ORD-001",
"transaction_type": "deposit"
},
"event_type": "payment_Success",
"action": "MODIFY"
}
```
Two webhook formats exist — **Short** (API Direct, Lite SDK 2.0) and **Long** (Hosted Checkout, mobile SDKs) — depending on
your integration mode. See the [Webhooks model](/docs/core-concepts/webhooks-model).
## Getting started with webhooks [#getting-started-with-webhooks]
1. Create a public HTTPS URL that receives POST requests.
2. Secure your endpoint to verify requests come from Tonder.
3. Register your endpoint — see [Set up webhooks](/docs/webhooks/setup).
4. Process the notifications in your application.
Endpoint requirements: use HTTPS (not HTTP), respond within 30 seconds, return a `2xx` code to
acknowledge receipt, and authenticate requests to verify they come from Tonder (see
[Set up webhooks](/docs/webhooks/setup) for the `BEARER`, `API_TOKEN`, and `BASIC_AUTH` methods).
## Common use cases [#common-use-cases]
Handle payment status changes to automatically fulfill orders when they complete or fail. The API
Direct payload is flat — all fields are at the top level.
```python
@app.route('/webhook', methods=['POST'])
def handle_payment_webhook():
payload = request.get_json()
if payload['event_type'] == 'payment_Success':
# Payment completed successfully
fulfill_order(payload['metadata']['external_id'])
elif payload['event_type'] == 'payment_Failed':
# Payment failed
cancel_order(payload['metadata']['external_id'])
elif payload['event_type'] == 'payment_Pending':
# Payment is awaiting confirmation (e.g. SPEI transfer in progress)
mark_order_pending(payload['metadata']['external_id'])
return jsonify({'status': 'received'}), 200
```
Process 3D Secure challenge completions to finalize payments that required additional customer
verification.
```python
@app.route('/webhook', methods=['POST'])
def handle_3ds_webhook():
payload = request.get_json()
if payload['event_type'] == 'payment_Success':
# 3DS authentication succeeded, payment completed
complete_order(payload['metadata']['external_id'])
elif payload['event_type'] == 'payment_Failed':
# 3DS authentication failed
cancel_order(payload['metadata']['external_id'])
return jsonify({'status': 'received'}), 200
```
## Reliability and delivery [#reliability-and-delivery]
| Mechanism | Detail |
| ----------------- | --------------------------------------------------------- |
| Automatic retries | Up to 3 delivery attempts, with 60-second intervals. |
| Response timeout | 30 seconds per attempt. |
| Dead Letter Queue | Failed events stored for 30 days for manual reprocessing. |
| Success criteria | Any `2xx` HTTP code within the timeout. |
Full details in [Delivery and retry](/docs/webhooks/delivery-retry).
## Next steps [#next-steps]
Register and manage your endpoints.
Every webhook event and its payload.
Retry policy and DLQ.
Security and implementation patterns.
# Setup & Management (/docs/webhooks/setup)
This guide explains how to manage your webhook endpoints through the Tonder API: create, update, and
manage endpoints to receive real-time notifications.
**Security requirements:** use a secure (HTTPS) URL, configure an authentication method (`BEARER`,
`API_TOKEN`, or `BASIC_AUTH`) to verify requests come from Tonder, and validate the event structure
and content before processing.
## Step 1: create an endpoint [#step-1-create-an-endpoint]
Register a new URL with the `/webhooks/` endpoint. You can configure authentication to secure your
endpoint:
```bash
curl -X POST https://stage.tonder.io/api/v1/webhooks/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-site.com/webhook-handler",
"auth_method": "BEARER",
"credentials": {
"token": "a-secure-bearer-token-you-generate"
}
}'
```
The response confirms the created endpoint:
```json
{
"id": 123,
"url": "https://your-site.com/webhook-handler",
"status": "active",
"auth_method": "BEARER",
"credentials": {
"token": "a-secure-bearer-token-you-generate"
}
}
```
| Field | Type | Description |
| ------------- | ------- | ------------------------------------------------------------ |
| `id` | integer | Unique identifier for the endpoint. |
| `url` | string | The URL you registered. |
| `status` | string | Webhook status (`active`, `inactive`). |
| `auth_method` | string | Authentication method (`BEARER`, `API_TOKEN`, `BASIC_AUTH`). |
| `credentials` | object | Associated authentication credentials. |
Save the webhook `id` for management operations.
## Step 2: manage webhooks [#step-2-manage-webhooks]
| Operation | Method | Endpoint | Purpose |
| --------- | -------- | ------------------------- | ------------------------------------------------------- |
| List | `GET` | `/webhooks/` | Retrieves all configured endpoints. |
| Update | `PUT` | `/webhooks/{webhook_id}/` | Updates an endpoint's configuration. |
| Delete | `DELETE` | `/webhooks/{webhook_id}/` | Removes an endpoint (it stops receiving notifications). |
```bash
curl -X GET https://stage.tonder.io/api/v1/webhooks/ \
-H "Authorization: Token YOUR_API_KEY"
```
```bash
curl -X PUT https://stage.tonder.io/api/v1/webhooks/123/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-updated-site.com/webhook-handler" }'
```
```bash
curl -X DELETE https://stage.tonder.io/api/v1/webhooks/123/ \
-H "Authorization: Token YOUR_API_KEY"
```
## Troubleshooting [#troubleshooting]
* **Authentication errors when creating webhooks:** verify your credentials are correct and have
permissions; test authentication locally before registering the webhook.
* **Endpoint not receiving events after setup:** verify it's publicly accessible via HTTPS and
responds with a `2xx` within 30 seconds.
## Next steps [#next-steps]
Security and implementation patterns.
Retry policy and DLQ.
# Test Cases — APMs (/docs/testing/apm-test-cases)
Test each alternative payment method end-to-end in Sandbox, receiving and validating the webhook for
each one.
| Method | What to test | Amount | Expected status | Notes |
| ---------------------------------------------- | -------------------------------------------------------------------------------------- | ------------ | --------------- | ------------------------------------------------------------------------------------ |
| **SPEI Standard** | Create the deposit → simulate in the checkout (click 'Simular Depósito', exact amount) | `98.00 MXN` | ✅ Success | Click 'Simular Depósito' inside the checkout. |
| **SPEI Frictionless** *(if enabled)* | Use case 1: mismatched amount · Use case 2: direct transfer (no checkout) | Any | ✅ Success | Simulator: `tonder.live/simulatedeposits/`. |
| **OXXO Pay** | Create the payment → wait for the webhook (do NOT simulate — it arrives automatically) | `99.00 MXN` | ✅ Success | No manual simulation needed. |
| **Cash vouchers — Cash** | `type: safetypayCash` · `channel: WP` · Bank ID in `apm_config` | `150.00 MXN` | ✅ Success | `channel` must be `'WP'`, not `'web'`. |
| **Cash vouchers — Transfer** | `type: safetypayTransfer` · `channel: OL` · Bank ID in `apm_config` | `150.00 MXN` | ✅ Success | `channel` must be `'OL'`. |
| **MercadoPago** | Create → redirect the user → complete with MP's official test cards | Any | ✅ Success | Email with the `@testuser.com` domain required; cardholder name = `APRO` to approve. |
**SPEI Frictionless webhook fields to validate:**
* `mismatched_deposit: 'True'` + `original_expected_amount` (use case 1).
* `concept: 'Frictionless deposit - auto-created'` (use case 2).
## Next steps [#next-steps]
The request and response flow.
Mismatched amounts and direct transfers.
Full validation before production.
# Test Cases — Cards (/docs/testing/card-test-cases)
Use these cards to simulate payment scenarios in the Stage environment before going live. They work
exclusively in `https://stage.tonder.io` — they will not work in production.
For a better experience, we recommend getting in touch with your Tonder representative to obtain
test cards specific to your integration and region.
## Test cards [#test-cards]
| Card number | Expected result |
| --------------------- | ---------------------------------------------------- |
| `4000 0000 0000 0077` | Successful payment |
| `5451 9515 7492 5480` | Card declined |
| `4456 5280 8038 9860` | 3DS authentication required |
| `4456 5400 0000 0063` | Non-3DS flow |
| `4574 4412 1519 0335` | Processing error (does not generate a log in Tonder) |
| `4349 0012 1084 6432` | Insufficient funds |
The CVV should be **3 digits** and the expiration date can be **any future date** in MM/YY format.
## Certification scenarios [#certification-scenarios]
All scenarios must be tested end-to-end, receiving and validating the webhook for each one.
| Scenario | Card | CVV | Expiry | Amount | Expected result |
| --------------------- | --------------------- | ----- | ------- | ------- | ------------------------------------- |
| ✅ Success | `4000 0000 0000 0077` | `123` | `11/26` | `94.00` | Status Success · `payment_id` present |
| 💳 Refund | `4000 0000 0000 0077` | `123` | `11/26` | `94.00` | Status Success → then initiate refund |
| ❌ Decline | `5451 9515 7492 5480` | `123` | `11/26` | `95.00` | Status Declined |
| 💸 Insufficient funds | `4349 0012 1084 6432` | `123` | `11/26` | `96.00` | Declined — insufficient funds |
| 🔒 3DS | `4456 5280 8038 9860` | `123` | `11/26` | `97.00` | 3DS redirect → Status Success |
**Additional validations:** receive the webhook with the final status for each scenario, validate
that `payment_id` and `order_id` are present, and confirm webhook deduplication works.
## Next steps [#next-steps]
The card flow and 3D Secure.
SPEI, OXXO, Cash vouchers, and MercadoPago.
Full validation before production.
Move from Sandbox to production.
# Certification Checklist (/docs/testing/certification-checklist)
Before going to production, all scenarios must be tested end-to-end, receiving and validating the
webhook for each one.
## Scenarios to test [#scenarios-to-test]
* **Cards:** success, refund, decline, insufficient funds, and 3DS. See
[Card test cases](/docs/testing/card-test-cases).
* **APMs:** SPEI Standard, SPEI Frictionless, OXXO Pay, Cash vouchers — Cash, Cash vouchers — Transfer, and
MercadoPago. See [APM test cases](/docs/testing/apm-test-cases).
* **Withdrawals:** create → process in the Dashboard → simulate `LQ` / `D` / `CN`. See
[Withdrawals test cases](/docs/testing/withdrawals-test-cases).
## Per-scenario validations [#per-scenario-validations]
* Receive the webhook with the final status for each scenario.
* Validate that `payment_id` and `order_id` are present.
* Confirm webhook deduplication works correctly.
## Environment notes [#environment-notes]
* Test cards only work in **Sandbox** (`stage.tonder.io`).
* For **MercadoPago**, use only MP's official test cards and an email with the `@testuser.com` domain.
* For **withdrawals**, use the test institution code `97846`.
## Production configuration [#production-configuration]
Before the final deployment, confirm:
* Production credentials configured securely, separated from staging.
* Your code uses the production base URL and you have a process to rotate keys.
* HTTPS on all communications; keys and secrets stored securely (not in code).
* Error handling that doesn't expose sensitive data in logs or to the end user.
* Webhook endpoint configured and tested.
## Next steps [#next-steps]
Sandbox and production URLs.
The go-live checklist.
# Environment & Test Data (/docs/testing/environment)
Use the **Sandbox** (Stage) environment for all testing: transactions are simulated rather than
processed with real money. Test cards and data work **only in Sandbox** — never in production.
## URLs by environment [#urls-by-environment]
## Test environment notes [#test-environment-notes]
* Test cards only work in **Sandbox** (`stage.tonder.io`).
* For **MercadoPago**, use only MP's official test cards and an email with the `@testuser.com` domain
(e.g. `testuser@testuser.com`).
* For **withdrawals**, use the test institution code `97846` and the test originator CLABE
`646180567300000006`.
* For **Frictionless SPEI**, use the simulator `https://tonder.live/simulatedeposits/`.
## Next steps [#next-steps]
Test cards and expected results.
SPEI, OXXO, Cash vouchers, and MercadoPago.
Simulate LQ / D / CN.
End-to-end validation before production.
# Test Cases — Withdrawals (/docs/testing/withdrawals-test-cases)
Test the withdrawal flow in Sandbox: send the request to the unified `/process/` endpoint with
`operation_type: "withdrawal"` and use the test institution code `97846`, which simulates both
SPEI and debit card withdrawals.
The full request, response, status flow, and webhooks are covered in
[Withdrawals](/docs/direct-api/withdrawals).
## Sandbox test data [#sandbox-test-data]
| Field | Value |
| ----------------- | -------------------- |
| Institution code | `97846` |
| Beneficiary CLABE | `846180000400000001` |
Test CLABEs use bank code `846` — they only work in Sandbox.
## Next steps [#next-steps]
Endpoint, parameters, status flow, and webhooks.
Full validation before production.
# Changelog (/docs/changelog)
What's new in the Tonder platform. The most recent entries appear first.
## August 17, 2026 [#august-17-2026]
### Settlements — cycles, rolling reserve, and the report [#settlements--cycles-rolling-reserve-and-the-report]
When your money arrives was undocumented: `T+1` and `T+2` appeared nowhere in the docs, and the
rolling reserve was a single line. Two new pages under Payments Management:
* **[Settlements](/docs/payments-management/settlements)** — the T+1, T+2 and batch cycles and
where each is available, the twice-weekly batch schedule, and how the rolling reserve is withheld
and released after 90 days.
* **[Settlement report](/docs/payments-management/settlement-report)** — every section and column
of the report, plus a worked reconciliation from processed volume down to the amount deposited.
## August 17, 2026 [#august-17-2026-1]
### Payments Management — Handling Disputes [#payments-management--handling-disputes]
A new top-level section for the operational side of running payments. It opens with a chargeback
reference per market:
* **[Handling Disputes — Mexico](/docs/payments-management/handling-disputes-mexico)**
* **[Handling Disputes — Chile](/docs/payments-management/handling-disputes-chile)**
Each covers the response windows published by Visa, Mastercard and American Express, what changes
between domestic and cross-border cases, and what's expected of you at each stage. Brand deadlines,
pre-arbitration and arbitration were previously undocumented.
The dispute response window is now stated as **15 days** to submit evidence, corrected on
[Transactions](/docs/dashboard/transactions) so the site holds one figure.
## August 17, 2026 [#august-17-2026-2]
### Saved cards and Card-on-File in Hosted Checkout [#saved-cards-and-card-on-file-in-hosted-checkout]
Both already worked; neither was documented outside the SDK path. **[Create a Payment
Session](/docs/hosted-checkout/create-session)** now covers them:
* **Saved cards** — add `saved_cards` to `payment_method_types` and the checkout lists the
customer's stored cards beside the new-card form. `payment_method_config.saved_cards.show_save_card_checkbox`
adds the opt-in checkbox.
* **Card-on-File** — with an active Tonder connection, cards are saved automatically (3DS on save,
a subscription per card) and repeat payments skip the CVV. Nothing to configure, and it takes
precedence over the checkbox.
`saved_cards` and `payment_method_config` are in the OpenAPI spec too, so the request schema on
that page lists them.
## August 7, 2026 [#august-7-2026]
### Web SDK migration guides + Apple Pay [#web-sdk-migration-guides--apple-pay]
Two new guides to reach [`@tonder.io/web-sdk`](/docs/sdks/web) from wherever you are today:
* **[Migrating from the legacy SDK](/docs/sdks/migrate-legacy-sdk)** — from `tonder-web-sdk`
v2 (`InlineCheckout` / `LiteInlineCheckout`), with the method-by-method mapping and the webhook
change to the Short format — the part to budget for.
* **[Migrating from Direct API](/docs/sdks/migrate-direct-api)** — move your checkout to the
browser without changing your backend: same `/process/`, same webhooks, same reconciliation.
And **[Apple Pay](/docs/payment-methods/apple-pay)** gets its own payment-method page: the Web
SDK button, domain registration, availability, and events.
## August 6, 2026 [#august-6-2026]
### Check your withdrawal balance by API [#check-your-withdrawal-balance-by-api]
You can now check the available balance of your Mexican withdrawals account with the
[**Get Balance**](/docs/api-reference/withdrawals/getWithdrawalsBalance) API
(`GET /withdrawals/balance`): see how much you can disperse before creating withdrawals via
`POST /process/`. The [Withdrawals](/docs/direct-api/withdrawals) guide includes the balance
section with per-environment examples.
### Nu México's SPEI code changes to `40638` [#nu-méxicos-spei-code-changes-to-40638]
By order of Banco de México, Nu México now operates as **NUBANK, S.A., Institución de Banca
Múltiple**, and its SPEI code changed with it. This affects you if you send withdrawals to Nu
accounts or receive transfers from them.
| Field | Before | Now |
| ---------- | --------- | ------- |
| SPEI code | `90638` | `40638` |
| Short name | NU MEXICO | NUBANK |
What to update:
* **Payouts** — send `40638` in `institucionContraparte`.
* **Deposits** — your system and your H2H connection must recognise `40638` in
`institucionOrdenante`.
Code `90638` stopped working on **5 August 2026 at 18:00** Mexico City time. Anything sent with it
is rejected.
* [Banking reference](/docs/resources/banking-reference) — the institution code table.
## July 28, 2026 [#july-28-2026]
### New Analytics module in the Dashboard [#new-analytics-module-in-the-dashboard]
We launched **Analytics**, a new Dashboard section that goes deeper into payment performance than
the Home KPIs. It's organised into two views:
* **Acceptance & Conversion** — Overall, FTD and Trusted acceptance rate, APM conversion rate,
acceptance rate over time, acceptance by method, card network performance, best and worst issuing
banks, and the FTD vs. Trusted comparison.
* **Deposit Analytics** — total deposits, FTDs, active depositors and average deposit, plus deposit
volume and count, the first-time deposit funnel and the deposit amount distribution.
Both views filter by period (Today, 7D, 30D, 90D or a custom range).
* [Analytics](/docs/dashboard/analytics) — every KPI, chart and section explained.
## July 9, 2026 [#july-9-2026]
### New withdrawal statuses [#new-withdrawal-statuses]
Withdrawals now use the **same status vocabulary as payments**. If your integration expected the
previous names, update it with this mapping:
| Previously | Now |
| ------------------ | --------------------------------------------- |
| `PENDING` | `Pending` |
| `SENT_TO_PROVIDER` | `Processing` (previously surfaced as Pending) |
| `ON_HOLD` | `Pending` |
| `REJECTED` | `Declined` |
| `PAID_FULL` | `Success` |
| `CANCELED` | `Cancelled` (double L) |
| `FAILED` | `Failed` |
See the full flow in [Withdrawals](/docs/direct-api/withdrawals).
## July 8, 2026 [#july-8-2026]
### New Web SDK (`@tonder.io/web-sdk`) [#new-web-sdk-tonderioweb-sdk]
We launched the new **Tonder Web SDK** on npm:
[`@tonder.io/web-sdk`](https://www.npmjs.com/package/@tonder.io/web-sdk) — a browser TypeScript
SDK that replaces the Full/Lite variants for web with **a single package**:
* **Secure card fields** mounted in your own UI (`createTonder` → `init` →
`create('card_fields')` → `mount` → `pay`).
* **Saved cards and Card-on-File**: `enrollCard()`, `getCustomerCards()`,
`removeCustomerCard()` with a `secure_token`.
* **Alternative methods**: SPEI, OXXO Pay, and SafetyPay banks (`getPaymentMethodBanks()`).
* **Hosted/3DS presentation** via `redirect` or `embedded` (SDK modal).
* **TypeScript types included**, structured errors (`AppError`), and webhook-aligned responses.
Also available via **CDN** (`window.Tonder`).
* [Web SDK (guide)](/docs/sdks/web) — quick start and payment flows.
* [Web SDK Reference](/docs/sdk-reference/web) — every method, type, and error.
### Tonder Web SDK plugin for AI agents [#tonder-web-sdk-plugin-for-ai-agents]
We launched [`tonder-ai-integrations`](https://github.com/tonderio/tonder-ai-integrations):
plugins that help AI agents integrate Tonder SDKs correctly. The first plugin is
**Tonder Web SDK** (v0.1.7), for **Claude Code, Claude Desktop, Codex CLI, and Codex Desktop**.
It packages a framework-aware integration skill (HTML, React, Next.js, Angular) and a local MCP
server (`tonder-docs`) with versioned Web SDK documentation — recipes, API reference, errors, and
payment statuses.
Supported flows: card payment, enrollment, saved cards, payment methods, and SafetyPay banks. It
installs today from the GitHub repository marketplace.
* [MCP Integration](/docs/mcp/overview) — what it is and how it works.
* [Installation](/docs/mcp/setup) — step by step per host.
* [Usage](/docs/mcp/usage) — example prompts and the MCP tools.
## June 10, 2026 [#june-10-2026]
### Documentation redesign [#documentation-redesign]
This documentation shipped a full redesign:
* **Two-row header** with centered search and tabs (Docs · API Reference · SDK Reference ·
Resources · Changelog).
* **Sidebar with icon chips**, breadcrumbs above every title, and a refreshed table of contents.
* **Inset-style code blocks**, minimal tables, and Tonder-branded share (OG) cards.
* Native diagrams (flows and sequences) in the Hosted Checkout guides.
## June 8, 2026 [#june-8-2026]
### New Tonder dashboard [#new-tonder-dashboard]
We launched a **redesigned dashboard** — faster, clearer, and with native analytics: Home,
Analytics, Fraud & Risk, Transactions, Withdrawals, Finances, Payment Links, Developers, Webhooks,
and Team Members.
**Dashboard URL change.** The dashboard moved to a new domain:
* **Production:** `app.tonder.io` → **`dashboard.tonder.io`**
* **Sandbox:** `stage.tonder.io` → **`dashboard-stage.tonder.io`**
Update your bookmarks. The **API hosts do not change**: production stays on
`app.tonder.io/api/v1/...` and sandbox on `stage.tonder.io/api/v1/...`.
**Also in this release**
* **Cash vouchers** — the cash payment method formerly called *SafetyPay* is now **Cash vouchers**.
The integration is unchanged (same API `type`). See
[Cash vouchers](/docs/payment-methods/cash-vouchers).
# 3DS Payment Lifecycle (/docs/core-concepts/3ds-lifecycle)
The 3DS payment flow ensures an additional layer of security for online transactions. This page
explains the 3DS payment flow in Tonder, detailing each step in the process so you understand how
transactions are securely processed within the Tonder payment system.
See [card payments](/docs/payment-methods/cards) for how 3D Secure applies within the card
flow.
## The 3DS payment flow [#the-3ds-payment-flow]
The steps below break down the 3DS flow to better demonstrate how it works.
The process starts by setting up the environment. Initialize Tonder's SDK, adding the
necessary data:
* `returnUrl`: the URL to which the user is redirected after the checkout process, regardless
of success or failure.
* `apiKey`: the API key used for authentication and authorization.
Display the payment form to the user. Here the user adds their payment details and starts the
payment process.
When the user clicks the payment button, the payment process returns a status defining the next
step:
* **Pending**: the user is automatically redirected to the 3DS authentication page.
* **Success**: the payment is successfully processed, and the user is notified.
* **Failed/Declined**: the payment process is halted, and the user is informed of the failure
or decline.
The status of the 3DS transaction is validated after the user is redirected back to the return
URL. Depending on the response status, appropriate actions are taken:
* **Success**: the transaction proceeds normally.
* **Failed**: the user is notified, and necessary steps are taken to handle the failed
transaction.
## Next steps [#next-steps]
The statuses a payment, refund, or dispute moves through.
Cards, tokenization, and the 3DS challenge in depth.
# Correlation Keys & Metadata (/docs/core-concepts/correlation-keys)
Reference field names differ across integration modes. Use this map to keep your reconciliation
logic consistent, no matter how you integrate.
## Root reference field (you send it in the request) [#root-reference-field-you-send-it-in-the-request]
This is the field that appears in the Dashboard and in Excel reports so you can recognize your
orders.
| Field | Hosted Checkout | SDK | API Direct |
| ----------------------- | ----------------- | ----------------- | --------------------------- |
| **Root field name** | `external_id` | `orderReference` | `client_reference` |
| **Appears in** | Dashboard · Excel | Dashboard · Excel | Dashboard · Excel · Webhook |
| **Arrives in webhook?** | ❌ No | ❌ No | ✅ Yes — top-level |
## Webhook reconciliation (use the `metadata` fields) [#webhook-reconciliation-use-the-metadata-fields]
The `metadata` fields arrive in **all** formats for **all** modes.
| Field | Hosted Checkout | SDK | API Direct |
| ----------------------- | ----------------------------------------------------------------------- | ---------------------- | ---------------------- |
| **Recommended name** | `metadata.external_id` | `metadata.external_id` | `metadata.external_id` |
| Other fields | `metadata.order_id` · `metadata.customer_id` · `metadata.business_user` | (same) | (same) |
| **Arrives in webhook?** | ✅ Yes — always | ✅ Yes — always | ✅ Yes — always |
API Direct is the only mode where the root reference field (`client_reference`) also arrives in the
webhook. For all modes, **`metadata.external_id` is the safest reconciliation key.**
## Quick reference [#quick-reference]
## Next steps [#next-steps]
Short vs Long formats and where each key appears.
The payload, the fields, and delivery reliability.
# Decline codes (/docs/core-concepts/decline-codes)
When a card payment is declined, the transaction comes back with `status: "Declined"`. The
decline code and description come **nested inside the `response` object**, not as top-level
fields:
* `responseCode` and `responseText`: the general decline code and description. Always present.
* `processorError` and `processorMessage`: appear when the decline comes directly from the
processor (for example, insufficient funds).
* `rules[]`: appears instead of `processorError`/`processorMessage` when the decline comes from a
validation rule (for example, a 3DS authentication failure), as an array of `{code, message}`
objects.
This page is the catalog of the decline codes returned by the card processor.
**Example: insufficient-funds decline**
```json
{
"status": "Declined",
"transaction_status": "Declined",
"response": {
"responseCode": "021",
"responseText": "Tarjeta sin fondos",
"processorError": "K006",
"processorMessage": "Transacción declinada.",
"transactionStatus": "DECLINED"
}
}
```
**Example: validation-rule decline**
```json
{
"status": "Declined",
"transaction_status": "Declined",
"response": {
"responseCode": "K322",
"responseText": "Transacción rechazada.",
"rules": [
{ "code": "K325", "message": "Autenticación fallida - Error de autenticación" }
],
"transactionStatus": "DECLINED"
}
}
```
**MercadoPago:** declines have a different structure (`response.payment` with its own internal
detail). This format does not apply to that payment method.
## Soft vs hard declines [#soft-vs-hard-declines]
Every code has a type that determines whether retrying makes sense:
| Type | What it means | What to do |
| -------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| **Soft** | Temporary decline — the condition can change (issuer unavailable, timeout, daily limit). | You can retry with the same card, later or after fixing the input. |
| **Hard** | Final decline — the condition will not change (stolen card, no account, insufficient funds). | **Do not retry** with the same card: ask for another card or payment method. |
When retrying a soft decline, keep your idempotency key stable per payment attempt so retries are
safe — see [Idempotency](/docs/core-concepts/idempotency). Never loop-retry a hard decline:
besides failing, it degrades your acceptance rate with the issuer.
## Numeric codes [#numeric-codes]
2- and 3-digit codes:
| Code | Description | Type |
| ----- | -------------------------------------------------- | ---- |
| `01` | Refer to issuer | Soft |
| `02` | Refer to issuer | Soft |
| `03` | Invalid merchant | Soft |
| `04` | Pickup card | Soft |
| `05` | Do not honor | Soft |
| `006` | Abandoned transaction | Soft |
| `06` | Processing error | Soft |
| `07` | Pickup card | Soft |
| `08` | Declined honor | Soft |
| `09` | Duplicate transaction | Soft |
| `12` | Invalid transaction | Soft |
| `13` | Invalid amount | Soft |
| `14` | Invalid card number | Hard |
| `15` | Invalid issuer | Soft |
| `017` | Invalid card | Soft |
| `17` | Customer cancelled | Soft |
| `19` | Processing error (retry now) | Soft |
| `21` | Do not honor | Soft |
| `22` | Processing error | Soft |
| `25` | No account | Soft |
| `28` | Issuer unavailable | Soft |
| `30` | Processing error (fix request) | Hard |
| `31` | Bank error | Soft |
| `33` | Expired card | Hard |
| `34` | Suspected fraud | Soft |
| `35` | Invalid merchant (ops) | Soft |
| `36` | Restricted card | Hard |
| `37` | Suspected fraud | Hard |
| `38` | PIN error | Hard |
| `39` | No account | Soft |
| `40` | Invalid transaction | Soft |
| `41` | Lost card | Hard |
| `43` | Stolen card | Hard |
| `46` | No account | Hard |
| `51` | Insufficient funds | Hard |
| `52` | No account | Hard |
| `53` | No account | Hard |
| `54` | Expired card | Hard |
| `55` | PIN error | Hard |
| `56` | Invalid card number | Hard |
| `57` | Transaction not permitted | Soft |
| `58` | Terminal not permitted | Hard |
| `59` | Suspected fraud | Soft |
| `61` | Limit exceeded | Soft |
| `62` | Restricted card | Hard |
| `63` | Security violation | Soft |
| `64` | Visa: compliance violation / Prosa: invalid amount | Soft |
| `65` | Limit exceeded | Soft |
| `68` | Issuer unavailable | Soft |
| `70` | PIN error | Hard |
| `71` | PIN error | Hard |
| `74` | PIN error | Hard |
| `75` | PIN error | Hard |
| `76` | No account | Hard |
| `77` | No account | Hard |
| `78` | No account | Hard |
| `79` | Invalid transaction | Hard |
| `80` | Invalid transaction | Hard |
| `81` | Transaction not permitted | Hard |
| `82` | Do not honor | Soft |
| `83` | Suspected fraud | Hard |
| `84` | Invalid transaction | Hard |
| `85` | Processing error | Hard |
| `86` | PIN error | Hard |
| `87` | Card authentication failed | Soft |
| `88` | Processing error | Soft |
| `89` | PIN error | Hard |
| `91` | Issuer unavailable | Soft |
| `92` | Routing error | Soft |
| `93` | Compliance violation | Hard |
| `94` | Duplicate transaction | Hard |
| `95` | Processing error | Soft |
| `96` | Processing error | Soft |
| `201` | Invalid merchant ID | Hard |
| `228` | Bank declined | Soft |
| `577` | The transaction token is invalid | Hard |
| `621` | Transaction canceled by user | Soft |
| `622` | Session timeout | Soft |
| `703` | Transaction amount is required | Hard |
## Alphanumeric codes [#alphanumeric-codes]
| Code | Description | Type |
| ---- | ----------------------------------------- | ---- |
| `1A` | Authentication required (3DS) | Soft |
| `6P` | CVV failure | Hard |
| `B1` | Terminal not permitted | Hard |
| `N0` | Do not honor | Soft |
| `N1` | Invalid card number | Hard |
| `N2` | Processing error | Soft |
| `N3` | Transaction not permitted | Soft |
| `N4` | Limit exceeded | Soft |
| `N5` | Refund limit exceeded | Soft |
| `N6` | Refund limit exceeded | Soft |
| `N7` | CVV failure | Hard |
| `N8` | Invalid amount | Soft |
| `N9` | Refund limit exceeded | Soft |
| `O0` | Processing error | Soft |
| `O1` | Processing error | Soft |
| `O2` | Invalid amount | Soft |
| `O3` | Do not honor | Soft |
| `O4` | Limit exceeded | Soft |
| `O6` | Invalid card number | Hard |
| `O7` | Processing error | Soft |
| `O8` | Processing error | Soft |
| `O9` | Processing error | Soft |
| `OS` | PIN error | Hard |
| `P0` | Processing error | Soft |
| `P1` | Limit exceeded | Soft |
| `P2` | Processing error | Soft |
| `P3` | Invalid amount | Soft |
| `P4` | Limit exceeded | Soft |
| `P5` | PIN error | Hard |
| `P6` | Visa: PIN error / Prosa: limit exceeded | Hard |
| `P7` | Invalid amount | Soft |
| `P8` | Invalid transaction | Soft |
| `P9` | Invalid amount (lower amount may succeed) | Soft |
| `PS` | Do not honor | Soft |
| `Q0` | Invalid transaction | Soft |
| `Q1` | Card authentication failed | Soft |
| `Q2` | Invalid transaction | Soft |
| `Q3` | Invalid amount | Soft |
| `Q4` | Limit exceeded | Soft |
| `Q5` | Do not honor | Soft |
| `Q6` | Limit exceeded | Soft |
| `Q7` | Limit exceeded | Soft |
| `Q8` | Invalid transaction | Soft |
| `Q9` | Invalid transaction | Soft |
| `R0` | Do not honor | Soft |
| `R1` | Do not honor | Soft |
| `R2` | Transaction not permitted | Soft |
| `R3` | Do not honor | Soft |
| `R4` | Chargeback admin | Hard |
| `R5` | Chargeback admin | Hard |
| `R6` | Chargeback admin | Hard |
| `R7` | Invalid transaction | Soft |
| `R8` | Suspected fraud | Soft |
| `R9` | Issuer unavailable | Soft |
| `S0` | Reversal admin | Soft |
| `S1` | Reversal admin | Soft |
| `S2` | Reversal admin | Soft |
| `S4` | Processing error | Soft |
| `S5` | Chargeback admin | Hard |
| `S6` | Chargeback admin | Hard |
| `S7` | Chargeback admin | Hard |
| `S8` | Processing error | Soft |
| `S9` | Processing error | Soft |
| `T1` | Invalid amount | Soft |
| `T2` | Invalid transaction | Soft |
| `T3` | Restricted card | Soft |
| `T4` | Limit exceeded | Soft |
| `T5` | No account | Soft |
| `T6` | Processing error | Soft |
| `T7` | Limit exceeded | Soft |
| `T8` | No account | Soft |
| `U0` | Card authentication failed | Soft |
| `U1` | Processing error | Soft |
| `U2` | Processing error | Soft |
| `U3` | Processing error | Soft |
| `U4` | Card authentication failed | Soft |
| `U5` | Card authentication failed | Soft |
| `U6` | Card authentication failed | Soft |
| `U7` | Card authentication failed | Soft |
| `U8` | Card authentication failed | Soft |
| `V0` | Refer to issuer | Soft |
| `V1` | Refer to issuer | Soft |
| `V2` | Refer to issuer | Soft |
| `V3` | Refer to issuer | Soft |
| `V4` | Refer to issuer | Soft |
| `V7` | Card authentication failed | Soft |
| `V8` | Card authentication failed | Soft |
| `V9` | Card authentication failed | Soft |
| `Z3` | Issuer unavailable | Soft |
## K codes [#k-codes]
| Code | Description | Type |
| ------ | ---------------------------------------------------------------------------- | ---- |
| `K001` | Invalid request body | Soft |
| `K002` | An unexpected error has occurred | Soft |
| `K003` | Processor does not exist | Soft |
| `K004` | Invalid merchant ID | Soft |
| `K005` | Invalid processor ID | Soft |
| `K007` | Card blocked by the issuer | Soft |
| `K008` | Incorrect token | Soft |
| `K009` | Check SSM variables | Hard |
| `K011` | Invalid BIN | Hard |
| `K012` | Invalid capture amount | Soft |
| `K013` | Transaction tokenized as deferred | Soft |
| `K015` | CVV2\_REJECTED\_TRANSACTION | Hard |
| `K016` | Method not implemented | Soft |
| `K020` | ERROR\_REJECTED\_TRANSACTION | Soft |
| `K021` | ERROR\_REJECTED\_TRANSACTION | Soft |
| `K023` | Void amount higher than sale | Soft |
| `K025` | Invalid card | Soft |
| `K026` | Processor Declined | Soft |
| `K027` | The transaction went into timeout, please try again | Soft |
| `K028` | The merchant does not have the deferred option enabled | Hard |
| `K029` | Invalid card BIN | Hard |
| `K030` | Processor unreachable | Soft |
| `K038` | Partial void cannot be performed without specifying the value to be deducted | Soft |
| `K039` | The sum of the values of the amount property must be greater than 0 | Hard |
| `K040` | The merchant ID does not match the credential submitted | Hard |
| `K041` | Transaction not allowed | Soft |
| `K042` | The currency code is different from that of the initial transaction. | Hard |
| `K047` | An unexpected error has occurred | Soft |
| `K048` | Token expired | Soft |
| `K049` | Previously used token | Soft |
| `K220` | The transaction amount is different to the initial sale amount | Soft |
| `K322` | Rejected Transaction | Soft |
| `K325` | Autentication failed | Soft |
| `K326` | Autentication failed | Soft |
| `K328` | Autentication failed - Without security validation | Soft |
| `K505` | The transaction was declined by the processor or issuer. | Soft |
| `K506` | The transaction was temporarily declined by the processor or issuer. | Soft |
## Next steps [#next-steps]
HTTP codes, the internal decline-code system, and withdrawal statuses.
The states a payment moves through, including `Declined`.
# Idempotency (/docs/core-concepts/idempotency)
Idempotency ensures that a payment request is processed only once, even if the same request is sent
multiple times. When a network timeout or server error occurs, you can safely retry the request
without risking a duplicate charge.
## How it works [#how-it-works]
When Tonder receives a request with an idempotency key, it checks whether it has already processed
a request with that key and an identical body:
* **First request** — Tonder processes the payment and stores the response against the key.
* **Subsequent request with the same key and body** — Tonder returns the stored response without
creating a new transaction.
* **Request with the same key but a different body** — Tonder rejects the request. Generate a new
key for any modified payload.
## By integration mode [#by-integration-mode]
The header differs by integration type:
| Integration | Idempotency header | Format | Window |
| ------------------- | ------------------- | ----------------------------------------------------- | --------------------------- |
| **Hosted Checkout** | `x-idempotency-key` | free string (e.g. `test-001`) | 5 seconds |
| **API Direct** | `X-Request-Id` | UUID v4 (e.g. `550e8400-e29b-41d4-a716-446655440000`) | per request |
| **SDK** | Handled by the SDK | — | Contact support for details |
### API Direct [#api-direct]
Include the `X-Request-Id` header on every `POST` request to `/process/`. Use a UUID v4 for each
distinct payment operation.
```http
POST /api/v1/process/
Authorization: Token
X-Request-Id:
Content-Type: application/json
```
See [Authentication](/docs/get-started/authentication) for full details on the `Authorization`
header.
#### Generating and sending the key [#generating-and-sending-the-key]
Use a UUID v4 for every distinct payment operation. UUIDs are globally unique, easy to generate in
any language, and safe to store for debugging.
```javascript
import { v4 as uuidv4 } from 'uuid';
const idempotencyKey = uuidv4();
// Example: "550e8400-e29b-41d4-a716-446655440000"
const response = await fetch('https://stage.tonder.io/api/v1/process/', {
method: 'POST',
headers: {
'Authorization': 'Token YOUR_API_KEY',
'X-Request-Id': idempotencyKey,
'Content-Type': 'application/json'
},
body: JSON.stringify(paymentData)
});
```
```python
import uuid
import requests
idempotency_key = str(uuid.uuid4())
# Example: "550e8400-e29b-41d4-a716-446655440000"
response = requests.post(
'https://stage.tonder.io/api/v1/process/',
headers={
'Authorization': 'Token YOUR_API_KEY',
'X-Request-Id': idempotency_key,
'Content-Type': 'application/json'
},
json=payment_data
)
```
```bash
curl -X POST https://stage.tonder.io/api/v1/process/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "X-Request-Id: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{
"operation_type": "payment",
"amount": 100.00,
"currency": "MXN"
}'
```
### Hosted Checkout [#hosted-checkout]
When creating a session with `POST /checkout/v1/sessions` you can include the `x-idempotency-key`
header (optional but recommended) to prevent duplicate sessions on network errors or retries. The
default protection window is **5 seconds**:
| Scenario | Result |
| -------------------------- | ----------------------------------------------- |
| Same key, within 5 seconds | Returns the same session — no duplicate created |
| Same key, after 5 seconds | Creates a new session |
| Different key | Always creates a new session |
## Reuse vs. regenerate the key [#reuse-vs-regenerate-the-key]
The rule is simple: the key must match the intent. Keep the **same key** when retrying the exact
same payment after a failure. Generate a **new key** whenever any field in the body changes —
amount, currency, payment method, or customer data.
```javascript
const idempotencyKey = uuidv4();
async function processWithRetry(paymentData, key, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await processPayment(paymentData, key); // same key every attempt
} catch (error) {
if (attempt === maxAttempts) throw error;
// Exponential backoff before next retry
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
}
}
}
```
Generating a new key on each retry defeats idempotency protection and can result in duplicate
charges. Store the key alongside your order record **before** sending the first request so you can
retrieve it on retry.
## Error handling [#error-handling]
**Cause**: a new key was generated on retry instead of reusing the original.
**Solution**: store the idempotency key in your database before making the first request. Retrieve
and reuse it on every subsequent retry of the same operation.
**Cause**: the same `X-Request-Id` was sent with a different request body.
**Solution**: generate a new key whenever any field in the body changes — including amount,
currency, payment method, or customer data.
**Cause**: the stored response from the first attempt is being returned, which may show a
`Pending` or `Failed` status.
**Solution**: this is expected. Check the transaction status with
[Get Transaction Status](/docs/api-reference/transactions/getTransactionStatus) and act on the
current state rather than assuming the retry succeeded.
## Best practices [#best-practices]
* Include `X-Request-Id` on **every** `POST` request to `/process/`, not just in retry logic — it
protects against silent network failures.
* Store idempotency keys in your database alongside the order before sending the request.
* Use **exponential backoff** between retries (start at 1 second and double it).
* Don't use predictable values (sequential integers, order ID alone, or timestamps) as keys — they
increase the risk of collisions.
* On a retry, the stored response may show `Pending` or `Failed`: verify the current status with
[Get Transaction Status](/docs/api-reference/transactions/getTransactionStatus) rather than
assuming the retry succeeded.
## Next steps [#next-steps]
Set up the full set of required headers.
Handle success and error responses from the API.
Request limits and how to implement backoff.
Receive reliable status updates without polling.
# Money, Currencies & Amounts (/docs/core-concepts/money)
Amounts in Tonder use **decimal format** (currency units) across all surfaces: `150.00` means
MXN 150.00.
## Currency [#currency]
Amounts are paired with an **ISO 4217** currency code, for example `MXN`. Tonder operates in
Mexico, Colombia, and Chile.
## How amounts are represented [#how-amounts-are-represented]
| Integration | Field | Unit | Example |
| ------------------- | -------------------------------------- | ------------------------ | --------------------- |
| **Hosted Checkout** | `amount_total`, `unit_price` | Decimal (currency units) | `150.00` = MXN 150.00 |
| **API Direct** | `amount`, `price_unit`, `amount_total` | Decimal (currency units) | `500` = MXN 500.00 |
Amounts are **not** in cents: `150.00` equals MXN 150.00 (sending `15000` would charge
MXN 15,000.00). Always confirm the unit the endpoint you're calling expects before charging.
## Per-transaction limits [#per-transaction-limits]
Minimum and maximum limits are **per transaction** (not daily) and depend on the payment method.
For example, cards have a $10 MXN minimum and SPEI a $1 MXN minimum. See the full table in
[Limits](/docs/payment-methods/limits).
## Balances and settlements [#balances-and-settlements]
Once payments are processed, your money moves through balances (**In Transit**, **Pending**, **In
Reserve**) until it settles. See [Finances](/docs/dashboard/finances) in the dashboard for
balance and settlement details.
## Next steps [#next-steps]
Per-transaction minimums and maximums by method.
The statuses a payment moves through.
Balances, settlements, and exports.
# Payment Lifecycle (/docs/core-concepts/payment-lifecycle)
Every transaction in Tonder starts as a **payment**. A successful payment can later be
**refunded** or contested as a **dispute**. Each of these — payment, refund, dispute — moves
through its own set of statuses that tell you where it is in processing.
Understanding these statuses is the foundation for everything else: webhooks report status
changes, your order fulfillment keys off terminal statuses, and your dashboard groups
transactions by them. This page is the canonical reference for all three.
**Terminal vs. non-terminal.** `Pending` / `Authorized` / `In review` mean processing is still
underway — don't fulfill the order yet. `Success` / `Declined` / `Failed` / `Cancelled` / `Won` /
`Lost` are terminal. Only act on terminal statuses, and always confirm them server-side via
[webhooks](/docs/webhooks/how-it-works) rather than a client redirect.
## Payments [#payments]
A payment transfers funds from the payer to the payee for goods or services. When you create a
payment it always starts in the **Pending** status, then moves through the lifecycle below.
| Status | Terminal? | Description |
| ------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------- |
| Pending | No | A payment attempt is processing. Common with asynchronous processors (e.g. SPEI). The default status right after creation. |
| Authorized | No | The payment processor has authorized the attempt — funds are held but not yet captured. |
| Declined | Yes | The payment processor declined the attempt. See [decline codes](/docs/resources/codes). |
| Failed | Yes | The attempt failed. Tonder shows the failure details in the payment details dashboard. |
| Success | Yes | The attempt was captured successfully. Safe to fulfill the order. |
| Cancelled | Yes | Cancelled before settlement, often by the customer prior to completion. |
## Refunds [#refunds]
A refund is a **merchant-initiated** reimbursement to a customer — for returns, cancellations, or
dissatisfaction. You initiate it from the Tonder dashboard. A refund also starts in **Pending**.
| Status | Terminal? | Description |
| ---------------------------------------- | --------- | -------------------------------------- |
| Pending | No | The refund is awaiting processing. |
| Failed | Yes | The refund attempt was unsuccessful. |
| Success | Yes | The refund was processed successfully. |
## Disputes [#disputes]
A dispute is a **customer-initiated** complaint contesting a transaction — for example an
unauthorized charge, non-receipt of goods, or dissatisfaction. When a dispute is created it
starts in **Needs response**, and you must submit evidence to contest it.
| Status | Terminal? | Description |
| ----------------------------------------------- | --------- | ----------------------------------------------------------------- |
| Needs response | No | Your customer raised a dispute with their bank; you must respond. |
| In review | No | Evidence was submitted and is awaiting the processor's decision. |
| Lost | Yes | You lost the dispute, or accepted it by not responding in time. |
| Won | Yes | You won the dispute. |
## Next steps [#next-steps]
How 3D Secure authentication fits into the card payment flow.
Receive status changes in real time instead of polling.
Respond to disputes and submit evidence.
Sandbox vs. production, and how to test each status.
# Webhooks Model (/docs/core-concepts/webhooks-model)
Webhooks are automated messages Tonder sends when transaction events occur. Instead of repeatedly
polling our API for status changes, webhooks notify you in real time the moment something happens —
a payment completing, failing, or requiring additional authentication.
## Two formats: Short and Long [#two-formats-short-and-long]
Tonder sends **two different** webhook formats. Which one you receive depends on your integration
mode:
* **API Direct** and **Lite SDK 2.0** → **Short** format
* **Hosted Checkout / mobile SDKs / Legacy** → **Long** format
| Characteristic | Short format | Long format |
| -------------------------- | --------------------------------------------- | --------------------------------------------- |
| Root structure | Top-level fields | Everything inside `data{}` |
| Status field | `status: Pending / Success` | `transaction_status: Pending / Success` |
| Event indicator | `event_type: payment_Pending` | `event: created / confirmed` |
| Order reference | `client_reference` | `metadata.external_id` / `order_id` |
| SPEI CLABE | Not included | `data.clabe` |
| Sender info (SPEI Success) | Not included | `sender_name`, `sender_clabe`, `sender_bank`… |
| `_incoming_request` | Not included | `metadata._incoming_request` (full object) |
| Integration modes | API Direct · Lite SDK 2.0 · Frictionless SPEI | Hosted Checkout · mobile SDKs · Legacy |
## Same transaction, two structures [#same-transaction-two-structures]
The example below is a SPEI event in both formats:
```json
{
"id": "78eb98ef-65a8-4038-a2a0",
"operation_type": "payment",
"amount": "500",
"currency": "MXN",
"client_reference": "ORD-001",
"status": "Pending",
"provider": "tonder",
"transaction_id": "26292298-de5e-4edc",
"payment_method_type": "SPEI",
"created": "2026-05-21T19:51:18Z",
"metadata": {
"order_id": "ORD-001",
"external_id": "ORD-001",
"transaction_type": "deposit"
},
"event_type": "payment_Pending",
"action": "MODIFY"
}
```
```json
{
"operation_type": "payment",
"action": "payment",
"event": "created",
"type": "deposit",
"data": {
"id": "3195511b-fd2e-4844-bf02",
"transaction_status": "Pending",
"amount": 500.0,
"currency_code": "MXN",
"payment_id": 5117665,
"payment_method_name": "spei",
"clabe": "710969000312511566",
"concept": "SPEI deposit for payment...",
"metadata": {
"external_id": "ORD-001",
"order_id": "ORD-001",
"transaction_type": "deposit"
}
}
}
```
It's the same transaction with two completely different structures. **Confirm your format with your
Tonder integration manager before building your webhook handler.**
## Correlation key [#correlation-key]
Reference field names differ across modes, but **`metadata.external_id`** arrives in **every**
format and event. Use it as your correlation key.
## Next steps [#next-steps]
The payload, the fields, and delivery reliability.
Every webhook event and its payload.
Register and manage your endpoints.
Reconcile payments across modes with `metadata.external_id`.
# Create Customer (/docs/api-reference/customers/createCustomer)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a Payment Session (/docs/api-reference/hosted-checkout/createCheckoutSession)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a Session (/docs/api-reference/hosted-checkout/getCheckoutSession)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a Payment Transaction (/docs/api-reference/hosted-checkout/getPaymentTransaction)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Hosted Checkout Guide (/docs/api-reference/hosted-checkout/guide)
With Hosted Checkout you create the session and Tonder gives you a URL; you send your customer
there to pay, then you check the result. Four steps — and the first one happens **once**.
🇲🇽 🇨🇱 **Mexico and Chile use the same flow.** Nothing changes except `currency`: `"MXN"` for
Mexico, `"CLP"` for Chile.
Here you define the logo and colors shown across **all** your checkout sessions. It isn't repeated
per sale: normally you do this once when you integrate.
This endpoint also accepts a `logo` field to upload the file directly (multipart) instead of a
hosted URL. This guide uses `logo_url` because it's the common case.
This is the step you repeat on every sale. You tell Tonder who the customer is, what they're buying
and how much they pay. Tonder returns a `url`: that's where you send your customer to enter payment
details — you never see or touch the card in this flow.
`metadata` is free-form: send whatever fields help you identify the sale in your own systems. Keep
`metadata.external_id` — it's the key you'll reconcile the payment by in webhooks.
If it succeeds, open the response's `url` field in a browser: that's where your customer would pay.
Store the session `id` for step 3.
Your customer can close the window, lose connectivity, or take their time. Instead of relying on
`return_url` alone, check the session status by its `id`.
For order fulfillment, base your logic on the **transaction status** (`transaction_status`), not on
the session `status`: `completed` or `expired` alone don't confirm whether the payment succeeded or
was declined. See [Hosted Checkout reference](/docs/hosted-checkout/reference).
The session tells you whether it was paid; this endpoint gives you the fine detail of the attempt:
which method was used, last 4 digits if it was a card, the brand. Use the `payment_id` from step 3.
## Next steps [#next-steps]
The narrative guide, from zero to a test payment.
Confirm the payment server-side.
Every `ui_config` field.
Session vs. transaction statuses.
# Set Business Configuration (/docs/api-reference/hosted-checkout/setBusinessConfig)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Card Payment Guide (/docs/api-reference/payments/card-payment-guide)
Pick the flow that matches your integration and build the request live. Most businesses use
**tokenized card**.
🇲🇽 🇨🇱 **Mexico and Chile use exactly the same flow.** The endpoints, the headers and the way you
tokenize don't change. The only difference is `currency`: `"MXN"` for Mexico, `"CLP"` for Chile.
You'll turn the real card data into random codes called **tokens**, so you never store or send the
real number from your server. Tonder tokenizes **each field separately**: you get 5 distinct
tokens, one per field — not a single token for the whole card.
Before tokenizing, Tonder needs to confirm it's you. Your API key buys a short-lived permission — a
JWT — so it can't be reused indefinitely if it ever leaks.
The response carries the JWT you'll use in step 2:
```json
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
```
You send the real data to the tokenization service, authorized with the `access_token` from step 1
(`X-Skyflow-Authorization` header). It returns a random code per field; that code means nothing on
its own — only Tonder can translate it back when processing the payment.
The `route_id` **differs per environment**. The service lives on its own domain
(`token-sandbox.tonder.io` / `token.tonder.io`), not under Tonder's `/api/v1/`.
The service responds with one token per field. They are **not interchangeable** between fields.
Build the payment like any other, but use the step-2 tokens instead of the real number. Tonder
detokenizes them server-side to charge the real card.
Always validate both `id` (store it) and `status`. Never rely on the HTTP code alone — verify the
final status at `GET /api/v1/transactions/{id}/`.
**PCI DSS Level 1 only.** This flow requires a current certification and that you've shared your
Attestation of Compliance (AOC) with Tonder before production access is enabled. Here the real card
number does travel to Tonder, which is why the bar is far higher.
Same as the tokenized flow: you need a short-lived permission before using the raw-data endpoint.
This JWT is in addition to your API key.
Unlike the tokenized flow, here you send the real number, CVV and expiry as-is, with both
authorization headers. That's why the endpoint is different — `/raw-data`, not `/process/` — and
only enabled for already-certified businesses.
For security this endpoint **does not accept browser calls**. Copy the code and run it from your
terminal, Postman or your own backend.
## Next steps [#next-steps]
The full schema for the `/process/` endpoint.
The narrative guide: flow comparison and 3D Secure.
Card numbers for each sandbox scenario.
How to deduplicate retries with `X-Request-Id`.
# Process Transaction (/docs/api-reference/payments/processTransaction)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## Examples by operation type [#examples-by-operation-type]
The example above is a **tokenized card payment**. The same endpoint covers the other two cases:
change `operation_type` and the method-specific fields.
### SPEI payment [#spei-payment]
```json
{
"operation_type": "payment",
"amount": 500.00,
"currency": "MXN",
"customer": { "name": "Carlos Eduardo López", "email": "carlos.lopez@empresa.mx" },
"payment_method": { "type": "SPEI" },
"client_reference": "invoice-456"
}
```
The full flow — payment reference, expiry and reconciliation — is in
[APM payments](/docs/direct-api/apm-payments).
### SPEI withdrawal [#spei-withdrawal]
```json
{
"operation_type": "withdrawal",
"amount": 750.00,
"currency": "MXN",
"reference": "payout-001",
"transfer_method": "SPEI",
"description": "Commission payment",
"beneficiary": {
"account": "012345678901234567",
"name": "Roberto Martínez García",
"rfc": "MAGR850920XY1",
"institution": "40012",
"email": "roberto.martinez@email.com"
}
}
```
Statuses, webhooks and declined-payout handling are in
[Withdrawals](/docs/direct-api/withdrawals).
# Raw Card Processing (/docs/api-reference/payments/raw-card-processing)
Process a payment by sending **untokenized** card data. It lives on its own domain and on a
different route from `/process/`, and is only enabled for certified businesses.
**Requires PCI DSS Level 1.** You must hold a current certification and share your Attestation of
Compliance (AOC) with Tonder before production access is enabled. If you don't have it, use the
[tokenized flow](/docs/api-reference/payments/card-payment-guide) — it covers the same use case
with a reduced PCI scope (SAQ A / SAQ A-EP).
## Endpoint [#endpoint]
| Environment | URL |
| ----------- | ------------------------------------------------- |
| Sandbox | `POST https://process-sandbox.tonder.io/raw-data` |
| Production | `POST https://process.tonder.io/raw-data` |
It is not under Tonder's `/api/v1/` and **does not accept browser calls**. Test it from your
terminal, Postman or your backend.
## Headers [#headers]
| Header | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `Authorization` | Your API key, prefixed with `Token`. |
| `X-Skyflow-Authorization` | The `access_token` (JWT) from `GET /api/v1/tokenization/auth/`. It's short-lived — request it right before processing. |
| `Content-Type` | `application/json`. |
## Request body [#request-body]
| Field | Type | Description |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------------- |
| `operation_type` | string | `"payment"` to process a payment. |
| `amount` | number | Amount in decimal format (e.g. `150.00`). |
| `currency` | string | `MXN` or `CLP`. Mexico and Chile use the same flow; only this value changes. |
| `customer` | object | The customer's `name` and `email`. |
| `payment_method` | object | `type: "CARD"` plus raw `card_number`, `cardholder_name`, `cvv`, `expiration_month`, `expiration_year`. |
| `client_reference` | string | Your unique reference for this transaction. |
| `return_url` | string | Where the customer returns after finishing (for example, after a 3DS challenge). |
## Example [#example]
## Response [#response]
Same shape as a normal `/process/` payment: `id`, `status`, `amount`, `currency`,
`client_reference` and the timestamps.
Always verify the final status at `GET /api/v1/transactions/{id}/` — it's the source of truth.
Don't rely on the HTTP code or the initial response alone.
## Next steps [#next-steps]
Compare the tokenized flow with this one, step by step.
PCI scope, 3D Secure and the full comparison.
The status lookup — the source of truth.
# Get Balance (/docs/api-reference/withdrawals/getWithdrawalsBalance)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Transaction Status (/docs/api-reference/transactions/getTransactionStatus)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Non-Card Payments (APMs) (/docs/direct-api/apm-payments)
This guide shows how to create payments using alternative payment methods (APMs) through the unified
`/process/` endpoint. You process local options like SPEI bank transfers and OXXO Pay cash payments
with a single, consistent call.
## The core request [#the-core-request]
All payments are created with a `POST` to `/process/`. The body contains the fields common to all
methods, plus a `payment_method` object with method-specific fields.
```http
POST https://stage.tonder.io/api/v1/process/ # Sandbox
POST https://app.tonder.io/api/v1/process/ # Production
```
| Field | Type | Description |
| ------------------ | ------- | ------------------------------------------------------------------------------------------ |
| `operation_type` | string | Must be `"payment"` to process a payment. |
| `amount` | decimal | Amount using a dot as the decimal separator (e.g. `100.00`). |
| `currency` | string | Currency: `"MXN"` or `"CLP"`. Mexico and Chile use the same flow; only this value changes. |
| `customer` | object | Customer info, with at least `name` and `email`. |
| `payment_method` | object | Method config, with `type` and method-specific parameters. |
| `client_reference` | string | Your unique transaction identifier for reconciliation. |
Depending on the method, additional fields may be required inside `payment_method`. See each
method's details in [Payment methods](/docs/payment-methods/overview).
## Examples by method [#examples-by-method]
```json
{
"operation_type": "payment",
"amount": 500.00,
"currency": "MXN",
"customer": { "name": "Carlos Eduardo López", "email": "carlos.lopez@empresa.mx" },
"payment_method": { "type": "SPEI" },
"client_reference": "ORD-001"
}
```
A successful SPEI response has status `Pending` and includes payment instructions for the customer:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"operation_type": "payment",
"status": "Pending",
"amount": 500.00,
"currency": "MXN",
"client_reference": "ORD-001",
"payment_id": 12346,
"transaction_id": "txn_spei456",
"provider": "spei_provider",
"created_at": "2024-07-26T10:35:00Z",
"status_code": 201,
"payment_instructions": {
"clabe": "646180157000000004",
"reference": "7812345678",
"expires_at": "2024-07-27T10:35:00Z",
"bank_name": "STP"
}
}
```
```json
{
"operation_type": "payment",
"amount": 250.00,
"currency": "MXN",
"customer": { "name": "María Isabel Fernández", "email": "maria.fernandez@email.com" },
"payment_method": { "type": "oxxopay" },
"client_reference": "ORD-001"
}
```
A successful OXXO Pay response has status `Pending` and includes a URL with the payment instructions
and reference for the customer:
```json
{
"id": "887e3ff0-4f28-456d-bf33-857de2cdf788",
"operation_type": "payment",
"status": "Pending",
"amount": 34.0,
"currency": "MXN",
"client_reference": "ORD-001",
"provider": "tonder",
"created_at": "2026-02-17T21:32:28.887557Z",
"status_code": 201,
"next_action": {
"redirect_to_url": {
"url": "https://stage-payflow.tonder.io/oxxo-pay?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
},
"verify_transaction_status_url": "/transactions/887e3ff0-4f28-456d-bf33-857de2cdf788/"
}
```
## Response statuses [#response-statuses]
The `status` field can have one of these values:
| Status | Description |
| ------------ | ---------------------------------------------------------------------- |
| `Pending` | The transaction is being processed (common for asynchronous payments). |
| `Processing` | Being processed by the provider. |
| `Authorized` | Authorized, pending capture or settlement. |
| `Success` | Completed successfully. |
| `Declined` | Declined by the provider or issuing bank. |
| `Cancelled` | Cancelled before completion. |
| `Failed` | Failed to process. |
| `Expired` | The reference expired unpaid. |
Always validate `id` (the unique identifier — store it) and `status` (the current state). Never rely
on the HTTP status code alone.
## Asynchronous flow [#asynchronous-flow]
SPEI and OXXO are **asynchronous**: the initial status is always `pending` because they require a
customer action (completing the transfer or paying at a store).
`POST /process/` with the chosen method → `201 Pending` + `payment_instructions`.
Show the customer the CLABE/reference (SPEI) or the voucher (OXXO).
Completes the bank transfer or pays cash at the store.
Receive the [webhook](/docs/webhooks/how-it-works) (`status: success`) or poll
`GET /api/v1/transactions/{id}/`.
## Next steps [#next-steps]
Specific requirements for SPEI, OXXO, Cash vouchers, and MercadoPago.
Implement fallback methods.
Process credit and debit cards.
Receive real-time notifications.
# Authentication & Tokenization (/docs/direct-api/authentication)
All requests to the Tonder API must be authenticated. We use an API key to ensure that all
communication comes from a trusted source. An unauthenticated request returns `401 Unauthorized`.
## Authentication methods [#authentication-methods]
| Credential | Sandbox | Production |
| ---------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| API key | [dashboard-stage.tonder.io](https://dashboard-stage.tonder.io) → Developers → API Keys | [dashboard.tonder.io](https://dashboard.tonder.io) → Developers → API Keys |
## Required headers [#required-headers]
| Header | Description |
| --------------- | ---------------------------------------------------- |
| `Authorization` | Your API key, prefixed with `Token`. |
| `Content-Type` | Must be `application/json` for requests with a body. |
```http
Authorization: Token
Content-Type: application/json
```
The scheme is `Token` followed by a space and then your key — not `Bearer`. If the space is
missing, you get `401 Unauthorized`.
Also send `X-Request-Id` with a UUID v4 per request: it isn't an authentication header, it's the
idempotency key that deduplicates retries. See
[Idempotency](/docs/core-concepts/idempotency).
## Complete request example [#complete-request-example]
```python
import requests
API_KEY = "your_api_key_from_dashboard"
BASE_URL = "https://stage.tonder.io/api/v1" # use https://app.tonder.io in production
request_body = {
"operation_type": "payment",
"amount": 100.00,
"currency": "MXN",
"customer": {"name": "Test Customer", "email": "test@example.com"},
"payment_method": {"type": "SPEI"},
"client_reference": "ORD-001"
}
headers = {"Authorization": f"Token {API_KEY}", "Content-Type": "application/json"}
response = requests.post(f"{BASE_URL}/process/", headers=headers, json=request_body)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
```
```bash
curl -X POST https://stage.tonder.io/api/v1/process/ \
-H "Authorization: Token " \
-H "Content-Type: application/json" \
-d '{
"operation_type": "payment",
"amount": 100.00,
"currency": "MXN",
"customer": { "name": "Test Customer", "email": "test@example.com" },
"payment_method": { "type": "SPEI" },
"client_reference": "ORD-001"
}'
```
## Card tokenization [#card-tokenization]
To store or process cards with a smaller PCI scope, exchange your key for a short-lived access
token: `GET /api/v1/tokenization/auth/` (with your API key) returns an `access_token` (JWT). That
token authorizes tokenizing the card data (`X-Skyflow-Authorization` header). See the full flow in
[Card payments](/docs/direct-api/card-payments).
## Security best practices [#security-best-practices]
* **Never expose your API key** in client-side code, mobile apps, or version control.
* Use **environment variables** to store credentials — don't leave them in the code.
* **Rotate keys immediately** if you suspect they leaked.
* Use **separate** keys for sandbox and production.
* Restrict credential access on a need-to-know basis.
## Authentication errors [#authentication-errors]
| Code | Message | Cause | Fix |
| ----- | -------------- | -------------------------------------- | ------------------------------------------------------------------------------ |
| `401` | `Unauthorized` | Invalid or missing API key | Check that your key is correct and includes the `Token` prefix (with a space). |
| `403` | `Forbidden` | The key isn't allowed on this endpoint | Review your account permissions or contact support. |
## Next steps [#next-steps]
Your first test payment in the sandbox.
Tokenized flow and raw card data.
How to deduplicate retries with `X-Request-Id`.
Sandbox and production base URLs.
# Card Payments (/docs/direct-api/card-payments)
Tonder offers two paths for processing card payments through API Direct. The right choice depends on
your PCI DSS compliance status and infrastructure.
* **Tokenized flow** — for most merchants. Tokenize card data first, then process payments using
tokens. Minimizes your PCI scope.
* **Raw card data** — for PCI Level 1 certified merchants only. Process payments with raw card
numbers directly. Requires full compliance infrastructure.
## Comparison [#comparison]
| Aspect | Tokenized flow | Raw card data |
| ---------------------- | ----------------------------------------- | --------------------------------------------- |
| **PCI requirement** | SAQ A or SAQ A-EP | Full PCI DSS Level 1 |
| **Best for** | Most merchants, startups, mid-market | Enterprise, processors, high-volume platforms |
| **Steps** | 3 (get token → tokenize → process) | 2 (get token → process) |
| **Latency** | Slightly higher (extra tokenization call) | Lower (direct processing) |
| **Card data handling** | Never touches your servers | Passes through your servers |
| **Compliance cost** | Lower | Significantly higher |
Raw card data processing requires proof of PCI DSS Level 1 certification. You must share your
Attestation of Compliance (AOC) with Tonder before we activate production access to the raw card
endpoints.
## Endpoints [#endpoints]
| Purpose | Endpoint |
| --------------------------- | -------------------------------------------------------------------------------- |
| Get access token | `GET https://stage.tonder.io/api/v1/tokenization/auth/` |
| Tokenize card data | `POST https://token-sandbox.tonder.io/v1/gateway/inboundRoutes/{route_id}/token` |
| Process payment (tokenized) | `POST https://stage.tonder.io/api/v1/process/` |
| Process payment (raw data) | `POST https://process-sandbox.tonder.io/raw-data` |
| Purpose | Endpoint |
| --------------------------- | ------------------------------------------------------------------------ |
| Get access token | `GET https://app.tonder.io/api/v1/tokenization/auth/` |
| Tokenize card data | `POST https://token.tonder.io/v1/gateway/inboundRoutes/{route_id}/token` |
| Process payment (tokenized) | `POST https://app.tonder.io/api/v1/process/` |
| Process payment (raw data) | `POST https://process.tonder.io/raw-data` |
Your `route_id` is provided in your dashboard after onboarding. Contact support if you don't have
one.
## Tokenized flow [#tokenized-flow]
Tonder tokenizes each card field individually (number, CVV, expiration month, etc.); you use each
individual token in place of the original value when making the payment request.
Call `GET /api/v1/tokenization/auth/` with your API key. It returns a JWT in `access_token`.
```bash
curl -X GET https://stage.tonder.io/api/v1/tokenization/auth/ \
-H "Authorization: Token "
```
```json
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
```
Send the raw card data to the tokenization service, authorized with the access token from the
previous step (`X-Skyflow-Authorization` header). Required fields:
| Field | Type | Description |
| ------------------ | ------ | -------------------------------------- |
| `card_number` | string | The full card number. |
| `cardholder_name` | string | The name on the card. |
| `cvv` | string | Security code (3-4 digits). |
| `expiration_month` | string | Future expiration month (e.g. `"07"`). |
| `expiration_year` | string | Future expiration year (e.g. `"26"`). |
```bash
curl -X POST https://token.tonder.io/v1/gateway/inboundRoutes/{route_id}/token \
-H "X-Skyflow-Authorization: " \
-H "Content-Type: application/json" \
-d '{
"card_number": "4000000000000077",
"cardholder_name": "Ozzy Osbourne",
"cvv": "123",
"expiration_month": "07",
"expiration_year": "26"
}'
```
The service responds with an individual token for each field. They are not interchangeable
between fields.
Make a standard request to `/process/`. In `payment_method`, use the tokenized values instead of
the raw data:
```json
{
"operation_type": "payment",
"amount": 150.00,
"currency": "MXN",
"customer": { "name": "John Doe", "email": "john.doe@email.com" },
"payment_method": {
"type": "CARD",
"card_number": "9230-0892-4469-1474",
"cardholder_name": "c05d89b2-299c-4f93-b49a-42be00d3b64b",
"cvv": "d31f0da3-0ed3-4ad8-8b68-14c2669a99a7",
"expiration_month": "e401a32e-4174-424f-9688-727005f6a80e",
"expiration_year": "bd9ccc23-3d00-4109-9626-fc6581389063"
},
"client_reference": "ORD-001"
}
```
The request is authenticated with your API key, just like any other payment.
Tonder detokenizes the values server-side.
Always validate both `id` (the unique transaction identifier — store it) and `status` (the current
payment state). Never rely on the HTTP status code alone.
## 3D Secure [#3d-secure]
3DS is handled within the card flow when the issuer requires it. See the
[3DS Lifecycle](/docs/core-concepts/3ds-lifecycle).
## API reference [#api-reference]
Endpoints for the tokenized flow (the final payment is made via `POST /process/`, documented in the
[Overview](/docs/direct-api/overview)):
## Next steps [#next-steps]
The `/raw-data` endpoint reference (PCI DSS Level 1).
Test cards and expected results.
SPEI, OXXO, and other methods.
Refund card payments.
Receive the final status in real time.
# Multi-Method Payments (/docs/direct-api/multi-method)
Multi-method payments let you offer customers alternative options when their preferred method fails.
For example, if a customer's card is declined, you can automatically offer a SPEI bank transfer or
an OXXO voucher as a backup.
## How the fallback flow works [#how-the-fallback-flow-works]
The fallback logic builds a prioritized list of payment methods and attempts each one until a
transaction succeeds. This way, if the preferred method fails (insufficient funds, card declined, or
a technical issue), your system automatically tries alternatives without the customer manually
selecting another option.
The flow follows these steps:
Start with the customer's preferred method and call the Tonder API.
If it's `authorized` or `pending`, complete the transaction.
If the payment fails or is declined, check whether alternative methods are available.
If more methods exist, automatically try the next one in your priority list.
If all methods are exhausted, show an error message; otherwise, process the successful payment.
**User experience first.** Always inform users about fallback attempts. Ask for permission before
switching to alternative methods rather than automatically redirecting to methods they didn't choose.
## Step 1: define your fallback strategy [#step-1-define-your-fallback-strategy]
Plan your payment method priority order. A typical sequence:
1. **Card**: the most common and immediate method.
2. **SPEI**: a reliable alternative, especially for larger amounts.
3. **OXXO**: a final fallback for users without a bank account or card.
Your server-side logic controls this flow. The Tonder API processes each request as a standalone
transaction.
## Step 2: implement the fallback loop [#step-2-implement-the-fallback-loop]
Create a function that tries each method sequentially until one succeeds:
```python
import uuid
def process_payment_with_fallback(customer_data, amount, preferred_method="CARD"):
"""Attempts to process a payment with a preferred method, with fallbacks."""
payment_methods_priority = [
preferred_method,
"CARD", # Fallback to card if it wasn't the preferred method
"SPEI", # Next, a bank transfer
"oxxopay" # Final fallback is cash
]
# Avoid duplicate attempts (e.g. if preferred_method is already 'CARD')
unique_methods = list(dict.fromkeys(payment_methods_priority))
for method in unique_methods:
print(f"Attempting payment with method: {method}")
try:
payment_data = {
"operation_type": "payment",
"amount": amount,
"currency": "MXN",
"customer": customer_data,
"payment_method": {"type": method},
"client_reference": f"order-{uuid.uuid4()}"
}
result = tonder_api.process_payment(payment_data)
# A successful initiation has status 'authorized' or 'pending'.
if result.get("status") in ["authorized", "pending"]:
print(f"Payment initiated with {method}. Transaction ID: {result['id']}")
return result
else:
print(f"Payment with {method} failed with status: {result.get('status')}")
except Exception as e:
print(f"API call for method {method} failed: {e}")
continue
raise Exception("All payment methods failed for this transaction.")
```
## Next steps [#next-steps]
Specific requirements for each method.
SPEI, OXXO, and other methods.
Handle asynchronous status updates.
Gracefully manage payment failures.
# Overview (/docs/direct-api/overview)
API Direct (Tonder Direct) is a unified payment-processing API that lets you accept payments and
send withdrawals across Latin America. Instead of managing complex integrations for each method or
country, you handle everything with a single call to the `/process/` endpoint.
## Key features [#key-features]
* **Single endpoint.** All payments and withdrawals go through the unified `/process/` endpoint. You
specify the operation type and Tonder routes it automatically.
* **Instant response.** Real-time processing with immediate status updates.
* **Regional methods.** Native support for SPEI, OXXO Pay, Cash vouchers, MercadoPago, and cards across
MX · CO · CL.
* **Secure by design.** The tokenization service handles sensitive card data. Two flows: tokenized
(lower PCI scope) and raw card data (requires PCI L1).
## How it works [#how-it-works]
The API is built around simplicity: a single `/process/` endpoint that covers all your transaction
needs. When you send a request, you specify what type of operation you want to perform and the
system routes it to the right processor. The same structure processes card, SPEI, OXXO, and other
payments, as well as [withdrawals](/docs/direct-api/withdrawals).
**Source of truth:** `GET /api/v1/transactions/{id}/`. Never rely solely on the initial response;
always verify the status here.
## Endpoints and authentication [#endpoints-and-authentication]
| Verb | Operation | Endpoint | Notes |
| ---- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| POST | Non-card payments (APMs) | `/api/v1/process/` | No tokenization or PCI L1 required — just your API key |
| GET | Get access token (tokenization) | `/api/v1/tokenization/auth/` | Required before tokenizing a card · uses your API key |
| POST | Tokenize card data | Sandbox: `https://token-sandbox.tonder.io/v1/gateway/inboundRoutes/{route_id}/token` · Production: `https://token.tonder.io/v1/gateway/inboundRoutes/{route_id}/token` | Requires the `access_token` from `tokenization/auth` first · the endpoint lives on a different domain, not under Tonder's `/api/v1/` · the `route_id` differs per environment — don't mix stage with production |
| POST | Tokenized card payment | `/api/v1/process/` | Send the tokens in `payment_method` · reduced PCI scope (SAQ A / SAQ A-EP) |
| POST | Raw card data | `process[-sandbox].tonder.io/raw-data` | Requires PCI DSS Level 1 |
| GET | Transaction status | `/api/v1/transactions/{id}/` | Source of truth — always verify here |
| POST | Withdrawal | `/api/v1/process/` | Server-side only — API key required |
Headers for `POST /process/` requests:
```http
Authorization: Token YOUR_API_KEY
X-Request-Id:
Content-Type: application/json
```
`X-Request-Id` is not an authentication header — it's the idempotency key used to deduplicate
retries (see [Idempotency](/docs/core-concepts/idempotency)). Full details on both in
[Authentication](/docs/direct-api/authentication).
## PCI scope [#pci-scope]
## Webhooks [#webhooks]
API Direct receives webhooks in **Short** format. Reconcile on `metadata.external_id`. See the
[Webhooks model](/docs/core-concepts/webhooks-model).
## API reference [#api-reference]
The unified payments and withdrawals endpoint, and the status lookup (source of truth):
## Next steps [#next-steps]
Your first test payment in the sandbox.
Move your checkout to the browser without changing your backend.
API key, headers, and tokenization in depth.
Tokenized flow and raw card data.
SPEI, OXXO, and other methods.
Payouts to beneficiaries with `operation_type: "withdrawal"`.
# Quickstart (/docs/direct-api/quickstart)
API Direct gives you full control over the payment flow by calling Tonder's API straight from your
server. This quickstart walks you through making your first test payment in the sandbox — no real
money moves.
**What you'll build:** a `POST` request to the Process Transaction endpoint that charges a test card
and returns a transaction you can track.
## Before you begin [#before-you-begin]
Log in to [dashboard-stage.tonder.io](https://dashboard-stage.tonder.io) — the sandbox dashboard — with your credentials.
Retrieve it from **Developers → API Keys**. You authenticate every request with it.
## Step 1: Set up your environment [#step-1-set-up-your-environment]
Use the sandbox for all testing — it validates your integration end to end without processing real
money. The base URL for every test request is `https://stage.tonder.io/api/v1/`. Every request must
include these headers:
| Header | Description |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorization` | Your API key, as `Token `. |
| `X-Request-Id` | A UUID v4 generated per request. It's the idempotency key, not an authentication header (see [Idempotency](/docs/core-concepts/idempotency)). |
| `Content-Type` | `application/json`. |
## Step 2: Make your first payment [#step-2-make-your-first-payment]
Send a `POST` to the Process Transaction endpoint with these parameters. This first test payment
sends raw card data (only valid for PCI Level 1 certified merchants); most merchants instead use
the tokenized flow described in [Card payments](/docs/direct-api/card-payments), where these
same fields are replaced with tokens you get from the tokenization service.
| Parameter | Type | Description |
| ------------------ | ------ | -------------------------------------------------------------------------------- |
| `operation_type` | string | Must be `"payment"` to process a payment. |
| `amount` | number | Payment amount (e.g. `100.00`). |
| `currency` | string | Currency code (e.g. `"MXN"`). |
| `customer` | object | Customer info with `name` and `email`. |
| `payment_method` | object | Method details including `type` and card information. |
| `client_reference` | string | Your unique reference for this transaction. |
| `return_url` | string | Where the customer returns after finishing (for example, after a 3DS challenge). |
Test cards **only work in sandbox** (`stage.tonder.io`). In production use a real card — the same
request with a test card will be declined. See
[Test cards](/docs/testing/card-test-cases).
```bash
curl -X POST https://stage.tonder.io/api/v1/process/ \
-H "Authorization: Token " \
-H "X-Request-Id: " \
-H "Content-Type: application/json" \
-d '{
"operation_type": "payment",
"amount": 100.00,
"currency": "MXN",
"customer": { "name": "Test Customer", "email": "test@example.com" },
"payment_method": {
"type": "CARD",
"card_number": "4000000000000077",
"cardholder_name": "Ozzy Osbourne",
"cvv": "123",
"expiration_month": "07",
"expiration_year": "26"
},
"client_reference": "ORD-001",
"return_url": "https://tonder.io"
}'
```
```bash
curl -X POST https://app.tonder.io/api/v1/process/ \
-H "Authorization: Token " \
-H "X-Request-Id: " \
-H "Content-Type: application/json" \
-d '{
"operation_type": "payment",
"amount": 100.00,
"currency": "MXN",
"customer": { "name": "Test Customer", "email": "test@example.com" },
"payment_method": {
"type": "CARD",
"card_number": "",
"cardholder_name": "Ozzy Osbourne",
"cvv": "",
"expiration_month": "07",
"expiration_year": "30"
},
"client_reference": "ORD-001",
"return_url": "https://tonder.io"
}'
```
A successful request returns `201 Created` with a transaction object:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"operation_type": "payment",
"status": "authorized",
"amount": 100.00,
"currency": "MXN",
"client_reference": "ORD-001",
"payment_id": 12345,
"transaction_id": "txn_abc123",
"created_at": "2024-07-26T10:30:00Z",
"status_code": 201
}
```
| Field | Description |
| ------------------ | ------------------------------------------------------------------ |
| `transaction_id` | Unique identifier for this payment in Tonder's system. |
| `status` | Current payment status (e.g. `authorized`, `pending`, `declined`). |
| `payment_id` | Internal payment ID for tracking and reference. |
| `client_reference` | Your original reference for this transaction. |
Use the `transaction_id` to [check status](/docs/api-reference), set up
[webhooks](/docs/webhooks/how-it-works), or view the transaction in your
[dashboard](/docs/dashboard/transactions).
## Next steps [#next-steps]
Keys, headers, and card tokenization.
Tokenized flow and raw card data.
SPEI, OXXO, and other methods.
Accept SPEI, OXXO, MercadoPago, and more.
# Refunds (/docs/direct-api/refunds)
Tonder's refund functionality lets merchants return funds to customers for card transactions. You
use it for returns, cancellations, or any situation requiring a payment reversal. Refunds are
processed through the same unified `/process/` endpoint using `operation_type: "refund"`.
Refunds are only available for card payments. SPEI, OXXO Pay, and MercadoPago transactions cannot be
refunded.
## Prerequisites [#prerequisites]
* The ID of the original transaction you want to refund.
**Important limitations**
* Refunds work exclusively with credit and debit card transactions.
* Each transaction can be refunded only once, and in full.
* Refunds must be processed within 29 days after payment confirmation.
## Step 1: make the refund request [#step-1-make-the-refund-request]
Send your request to the `/process/` endpoint with these four fields:
| Field | Type | Required | Description |
| ------------------------- | ------- | -------- | ---------------------------------------------------- |
| `operation_type` | string | Yes | Must be `"refund"`. |
| `amount` | decimal | Yes | Refund amount (full). |
| `currency` | string | Yes | Currency code (must match the original transaction). |
| `original_transaction_id` | string | Yes | ID of the original payment transaction. |
```bash
curl -X POST https://stage.tonder.io/api/v1/process/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation_type": "refund",
"amount": 50.00,
"currency": "MXN",
"original_transaction_id": "6ada8797-235e-4674-86de-7b23e90e1163"
}'
```
## Step 2: handle the response [#step-2-handle-the-response]
On a successful request, the API returns an immediate acknowledgment:
```json
{
"id": "07230c98-e749-4d27-a90d-6f3e117205aa",
"operation_type": "refund",
"status": "Success",
"amount": 50.0,
"currency": "MXN",
"client_reference": null,
"transaction_id": "20832559",
"provider": "tonder",
"created_at": "2025-09-18T15:38:38.157382Z",
"status_code": 201,
"original_transaction_id": "6ada8797-235e-4674-86de-7b23e90e1163",
"reason": null,
"refund_id": "74"
}
```
| Field | Description |
| ------------------------- | ------------------------------------------------------ |
| `id` | Unique refund transaction identifier. |
| `operation_type` | Type of operation (`"refund"`). |
| `status` | Current refund status (`"Success"`, `"Failed"`, etc.). |
| `amount` | Refunded amount. |
| `currency` | Currency code. |
| `original_transaction_id` | ID of the original payment transaction. |
| `refund_id` | Internal refund identifier. |
| `created_at` | Timestamp when the refund was created. |
| `status_code` | HTTP status code of the response. |
Always validate `id` (the unique identifier — store it) and `status` (the current refund state).
Never rely on the HTTP status code alone.
## Next steps [#next-steps]
Process the payments you can later refund.
Statuses for payments, refunds, and disputes.
Receive the refund status in real time.
# Withdrawals (/docs/direct-api/withdrawals)
Withdrawals (payouts) to beneficiaries are processed through the same unified `/process/` endpoint
as payments, using `operation_type: "withdrawal"`.
**Availability.** Withdrawals via API Direct are currently only available for beneficiaries in
Mexico.
**Available transfer methods**
| Aspect | SPEI | Debit Card |
| --------------- | ------------------------------------------ | ---------------------------------- |
| Processing time | Instant (seconds) | Instant (seconds) |
| Availability | Banking hours only | 24/7 |
| Account type | 18-digit CLABE | 16-digit card number |
| Best for | Beneficiaries with a CLABE, larger amounts | Beneficiaries who only have a card |
**Geolocation required for Mexico.** The `metadata.latitude` and `metadata.longitude` fields are
**mandatory** for processing withdrawals in Mexico. Omitting them or sending erroneous coordinates
results in a **failed transaction**.
## Step 1: make the withdrawal request [#step-1-make-the-withdrawal-request]
Send your request to the `/process/` endpoint with `operation_type` set to `"withdrawal"`:
| Field | Description |
| ------------------ | -------------------------------------------------------------------------- |
| `operation_type` | Always `"withdrawal"` for payout operations. |
| `amount` | The withdrawal amount in the specified currency. |
| `currency` | Currency code (currently only `"MXN"` is supported). |
| `client_reference` | Your internal reference identifier for this withdrawal. |
| `transfer_method` | Either `"SPEI"` or `"DEBIT_CARD"`. |
| `beneficiary` | Complete beneficiary information object. |
| `metadata` | Additional required information (must include `latitude` and `longitude`). |
The `beneficiary` object specifies who receives the funds and how:
| Field | Description |
| ------------- | -------------------------------------------------------------------------------------- |
| `account` | Destination account (18-digit CLABE for SPEI, or 16-digit card number for debit card). |
| `name` | Beneficiary's full legal name, as it appears on their account or card. |
| `rfc` | Beneficiary's RFC. |
| `curp` | Beneficiary's CURP (18-character alphanumeric code). |
| `institution` | Bank institution code. See [Banking reference](/docs/resources/banking-reference). |
| `email` | Beneficiary's email for notifications and record-keeping. |
**RFC/CURP requirement.** You must provide either `rfc` or `curp` on every withdrawal request. If
you don't have one of the two available, send `"ND"` (Not Available) as the value for that field.
**Institution codes for Stage testing.** Use `97846` as the `institution` to simulate both SPEI
and debit card withdrawals in Stage. For production, use the real codes from the
[Banking reference](/docs/resources/banking-reference) (for example, `40012` is already a real
production code — don't use it for testing).
Confirmed, working example — SPEI transfer:
```json
{
"operation_type": "withdrawal",
"amount": 20.00,
"currency": "MXN",
"client_reference": "payout-001",
"transfer_method": "SPEI",
"description": "Commission payment",
"beneficiary": {
"account": "846180000400000001",
"name": "Ana María González",
"rfc": "GOAN850315AB2",
"institution": "97846",
"email": "ana.gonzalez@email.com"
},
"metadata": {
"latitude": "22.8870221",
"longitude": "-109.911775"
}
}
```
```bash
curl -X POST https://stage.tonder.io/api/v1/process/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation_type": "withdrawal",
"amount": 20.00,
"currency": "MXN",
"client_reference": "payout-001",
"transfer_method": "SPEI",
"description": "Commission payment",
"beneficiary": {
"account": "846180000400000001",
"name": "Ana María González",
"rfc": "GOAN850315AB2",
"institution": "97846",
"email": "ana.gonzalez@email.com"
},
"metadata": {
"latitude": "22.8870221",
"longitude": "-109.911775"
}
}'
```
## Step 2: handle the response [#step-2-handle-the-response]
On a successful request, the API returns an immediate acknowledgment:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"operation_type": "withdrawal",
"status": "Pending",
"amount": 20.00,
"currency": "MXN",
"client_reference": "payout-001",
"created_at": "2024-07-26T10:30:00Z",
"status_code": 201
}
```
| Field | Description |
| ------------------ | ------------------------------------------------------------- |
| `id` | Unique transaction identifier — store it to monitor progress. |
| `operation_type` | Confirms this is a withdrawal operation. |
| `status` | Current transaction status (initially `Pending`). |
| `amount` | Withdrawal amount as submitted. |
| `currency` | Currency code. |
| `client_reference` | Your internal reference identifier. |
| `created_at` | ISO 8601 timestamp when the withdrawal was created. |
| `status_code` | HTTP status code (201 for successful creation). |
## Step 3: check the transaction status [#step-3-check-the-transaction-status]
Withdrawals are asynchronous operations. Use the `id` from the response to check the status at the
`GET /api/v1/transactions/{id}/` endpoint, or monitor via webhooks.
As your withdrawal progresses, it moves through different statuses:
| Status | Type | Description | Previously |
| ------------ | ------------ | ----------------------------------------------------- | -------------------- |
| `Pending` | Initial | The request was received and is queued or on hold. | `PENDING`, `ON_HOLD` |
| `Processing` | Intermediate | The request was sent to the bank or payment provider. | `SENT_TO_PROVIDER` |
| `Success` | Success | The transfer completed successfully. | `PAID_FULL` |
| `Declined` | Terminal | The withdrawal was rejected (e.g., invalid account). | `REJECTED` |
| `Cancelled` | Terminal | The withdrawal was cancelled. | `CANCELED` |
| `Failed` | Terminal | The withdrawal failed. | `FAILED` |
**Renamed statuses.** Withdrawals now use the same status vocabulary as payments. If your
integration expected the previous names (`PENDING`, `SENT_TO_PROVIDER`, `PAID_FULL`, `REJECTED`,
`CANCELED`, `FAILED`), update it using the "Previously" column in the table.
## Webhooks [#webhooks]
API Direct withdrawal webhooks follow the same flat payload as payment webhooks, with these
differences:
| Field | Difference |
| ---------------------- | -------------------------------------------------------------------------------- |
| `operation_type` | `"withdrawal"`. |
| `transfer_method_type` | Used instead of `payment_method_type` (`SPEI` or `DEBIT_CARD`). |
| `event_type` | Uses the `withdrawal_` prefix (e.g. `withdrawal_Pending`, `withdrawal_Success`). |
| `provider` | `"STP"` — the banking rail that settles the withdrawal. |
Exactly two webhooks are generated per withdrawal: first a `Pending` notification, then one
terminal notification (`Success`, `Declined`, or `Failed`).
**Pending**
```json
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"operation_type": "withdrawal",
"amount": "20",
"currency": "MXN",
"client_reference": "payout-001",
"status": "Pending",
"provider": "STP",
"transfer_method_type": "SPEI",
"created": "2026-05-21T19:15:32.029134Z",
"metadata": {
"latitude": "22.8870221",
"longitude": "-109.911775",
"external_id": "payout-001"
},
"event_type": "withdrawal_Pending",
"action": "MODIFY"
}
```
**Success**
```json
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"operation_type": "withdrawal",
"amount": "20",
"currency": "MXN",
"client_reference": "payout-001",
"status": "Success",
"provider": "STP",
"transfer_method_type": "SPEI",
"created": "2026-05-21T19:20:47.029134Z",
"metadata": {
"latitude": "22.8870221",
"longitude": "-109.911775",
"external_id": "payout-001"
},
"event_type": "withdrawal_Success",
"action": "MODIFY"
}
```
**Declined**
```json
{
"id": "c7b14546-84bd-4a81-89fb-3dc660f47011",
"operation_type": "withdrawal",
"amount": "60",
"currency": "MXN",
"client_reference": "card-payout-002",
"status": "Declined",
"provider": "STP",
"transfer_method_type": "SPEI",
"created": "2026-05-21T19:20:47.029134Z",
"metadata": {
"latitude": "22.8870221",
"longitude": "-109.911775",
"external_id": "card-payout-002"
},
"event_type": "withdrawal_Declined",
"action": "MODIFY"
}
```
**Failed**
```json
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"operation_type": "withdrawal",
"amount": "75",
"currency": "MXN",
"client_reference": "card-payout-003",
"status": "Failed",
"provider": "STP",
"transfer_method_type": "SPEI",
"created": "2026-05-21T19:20:47.029134Z",
"metadata": {
"latitude": "22.8870221",
"longitude": "-109.911775",
"external_id": "card-payout-003"
},
"event_type": "withdrawal_Failed",
"action": "MODIFY"
}
```
**Recommended monitoring:** use webhooks to receive real-time status updates. Alternatively, you
can poll the transaction status endpoint.
## Check your balance [#check-your-balance]
Before dispersing, check the available balance:
```bash
curl https://stage.tonder.io/api/v1/withdrawals/balance \
-H "Authorization: Token YOUR_API_KEY"
```
```bash
curl https://app.tonder.io/api/v1/withdrawals/balance \
-H "Authorization: Token YOUR_API_KEY"
```
```json
{
"message": "Data retrieved successfully",
"current_balance": "32894.55"
}
```
A withdrawal for more than the available balance is rejected. The endpoint's schema is in the
[API Reference](/docs/api-reference/withdrawals/getWithdrawalsBalance).
## Next steps [#next-steps]
The unified `/process/` endpoint and status lookup.
Receive real-time status change notifications.
# Authentication (/docs/get-started/authentication)
Tonder authenticates every API request with an **API key**, sent in the `Authorization` header.
You get two keys — a **public key** and a **secret key** — for different jobs.
## Your API keys [#your-api-keys]
* **Public key** — processes payments, APMs, and Hosted Checkout sessions. Safe to use anywhere
your backend can reach Tonder.
* **Secret key** — for card tokenization (Save Cards). **Server-side only —
never expose it** in client code, mobile apps, or version control.
## Get your keys [#get-your-keys]
Find them in the dashboard under **Developers → API Keys**. Sandbox and production have **separate**
keys:
* **Sandbox** — [dashboard-stage.tonder.io](https://dashboard-stage.tonder.io)
* **Production** — [dashboard.tonder.io](https://dashboard.tonder.io)
Never use production keys for testing. See [Environments](/docs/get-started/environments) for the
full split.
## Required headers [#required-headers]
Authenticate with the `Authorization` header plus the JSON content type. The idempotency header
differs by integration type.
The scheme is `Token` **followed by a space**, then your key — not `Bearer`. A missing space
returns `401 Unauthorized`.
### Hosted Checkout [#hosted-checkout]
```http
Authorization: Token YOUR_API_KEY
x-idempotency-key:
Content-Type: application/json
```
`x-idempotency-key` is a free string you define (for example `test-001`), with an idempotency
window of **5 seconds**.
### API Direct [#api-direct]
```http
Authorization: Token YOUR_API_KEY
X-Request-Id:
Content-Type: application/json
```
`X-Request-Id` must be a UUID v4 generated per request (not a free string).
| Integration | Idempotency header | Format | Window |
| --------------- | ------------------- | ----------- | ----------- |
| Hosted Checkout | `x-idempotency-key` | free string | 5 seconds |
| API Direct | `X-Request-Id` | UUID v4 | per request |
## Card tokenization [#card-tokenization]
To save cards, exchange your secret key for a short-lived access token: `GET /api/v1/tokenization/auth/`
returns an `access_token` (a JWT) used to tokenize card data. The SDKs handle this for you — see
[SDKs](/docs/sdks/overview) and [API Direct → Authentication](/docs/direct-api/authentication).
## Webhooks [#webhooks]
Always confirm webhooks **server-side** before acting on them: re-fetch the transaction or session
status (by `payment_id` / `external_id`) rather than trusting the received payload alone. See
[Webhooks](/docs/webhooks/how-it-works).
## Security best practices [#security-best-practices]
* **Never expose your secret key** in client code, mobile apps, or version control.
* Store credentials in **environment variables** — don't hardcode them.
* **Rotate keys immediately** if you suspect they're compromised.
* Use **separate keys** for sandbox and production.
* Restrict credential access on a need-to-know basis.
## Next steps [#next-steps]
Sandbox vs production, base URLs, and going live.
Take your first test payment in four steps.
Headers, tokenization, and errors in depth.
Receive and verify payment events.
Let Claude Code or Codex integrate the Web SDK for you, backed by versioned docs.
# Choose your integration (/docs/get-started/choose-integration)
Tonder offers several integration models to accept deposits and send withdrawals. The model you
pick shapes your time to market, engineering effort, checkout UX, and compliance scope — so it's
worth getting right up front.
This guide helps you understand:
* The five ways to **accept payments** — Hosted Checkout, Full SDK, Lite SDK, the recommended
**Hybrid** model, and API Direct (S2S).
* **Withdrawals** via API Direct — pairs with any deposit model.
* The key differences — setup time, customization, and PCI scope — so you can match a model to
your needs.
* Real-world use cases for platforms like iGaming and e-commerce.
## Compare the models [#compare-the-models]
| Model | Deposits | Withdrawals | Setup | Customization | PCI cert |
| -------------------- | :------: | :---------: | -------- | ------------- | :------: |
| **Hosted Checkout** | Yes | No | \~1 day | Low | No |
| **Full SDK** | Yes | No | 2–3 days | Medium | No |
| **Lite SDK** | Yes | No | 5–7 days | High | No |
| **Hybrid** ★ | Yes | Yes | 1–2 wks | High | No |
| **API Direct (S2S)** | Yes | Yes | 1–2 wks | Complete | Yes (L1) |
Withdrawals are always server-to-server via **API Direct** (`operation_type: "withdrawal"` on
`/process/`) — they pair with any deposit model and never run through Hosted Checkout or the SDK.
New to accepting SPEI in Mexico? Use **Frictionless SPEI** — available with API Direct or Hybrid.
## Before you choose [#before-you-choose]
Answer these questions first — they map almost one-to-one onto the models below:
* **Do you have developers to write integration code?** No → Hosted Checkout.
* **How fast do you need to launch?** Days → Hosted or Full SDK. Weeks are fine → Hybrid or API Direct.
* **Is a fully custom, branded checkout UI essential?** Yes → Lite SDK (or a Hybrid built on it).
* **Do you hold a valid PCI DSS certification?** Yes → API Direct unlocks full control. No → stay on Hosted / SDK / Hybrid.
* **Do you need to send payouts, not just accept deposits?** Yes → any model + [Withdrawals via API Direct](/docs/direct-api/withdrawals).
* **Do you want API control over non-card methods (SPEI, OXXO, wallets) without PCI scope?** Yes → Hybrid.
## Pick your model [#pick-your-model]
→ Hosted Checkout. Zero card data touches you.
→ SDK (Lite for full design control, Full for speed).
→ Hybrid. The recommended model for iGaming & fintech. (Refunds are full, once per transaction, cards only.)
### Detailed decision guide [#detailed-decision-guide]
Prefer to walk the full tree? Expand the scenario that matches your goal.
**Do you have developers?**
* **No** → [Hosted Checkout](/docs/hosted-checkout/overview).
* **Yes** → keep going.
**Do you need withdrawals or API control over non-card methods?**
* **Yes** → go to the Hybrid vs. API Direct question below.
* **No** → decide on UI customization.
**Are you PCI certified?**
* **Yes** → [API Direct](/docs/direct-api/overview) (full control).
* **No** → **Hybrid** ★ (recommended — maximum control without PCI).
**Choosing Hybrid — which card UI?**
* **Redirect to a hosted page** → Hybrid A (Hosted Checkout + API Direct).
* **Embedded standard UI** → Hybrid B (Full SDK + API Direct).
* **Fully custom UI** → Hybrid C (Lite SDK + API Direct).
**Not using Hybrid — need a fully custom UI?**
* **No** → [Full SDK](/docs/sdks/overview) (standard, embeddable).
* **Yes** → [Lite SDK](/docs/sdks/overview) (full UX/UI control).
**Are you PCI certified?**
* **Yes** → [API Direct](/docs/direct-api/overview) (all-in-one: deposits, withdrawals, refunds).
* **No** → **Hybrid** ★ (recommended — pick your card UI: A, B, or C).
**Alternative** — pair any deposit model with
[Withdrawals via API Direct](/docs/direct-api/withdrawals) (≈1–2 days):
* Fastest launch: Hosted Checkout + Withdrawals via API Direct.
* Standard embedded UI: Full SDK + Withdrawals via API Direct.
* Custom branded UI: Lite SDK + Withdrawals via API Direct.
Use [Withdrawals via API Direct](/docs/direct-api/withdrawals) — a server-to-server API for
payouts that pairs with any deposit model (≈1–2 days).
## Integration models at a glance [#integration-models-at-a-glance]
A one-line snapshot of each. Follow the link for the full implementation guide.
| Model | What it is | Best for | Setup · PCI |
| ----------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------- |
| **[Hosted Checkout](/docs/hosted-checkout/overview)** | A Tonder-hosted payment page reached by redirect. No card data touches you. | Launching fast with no front-end work. | \~1 day · No PCI |
| **[Full SDK](/docs/sdks/overview)** | A pre-built checkout UI embedded in your web or mobile app. | An embedded, styleable checkout without building forms. | 2–3 days · No PCI |
| **[Lite SDK](/docs/sdks/overview)** | You build the UI; the SDK tokenizes card data in the background. | Full brand and UX control with no PCI scope. | 5–7 days · No PCI |
| **[Hybrid](/docs/hybrid/overview)** ★ | SDK/Hosted for cards + API Direct for other methods and withdrawals. | Deposits **and** payouts, plus API control of non-card methods — without PCI. | 1–2 wks · No PCI |
| **[API Direct (S2S)](/docs/direct-api/overview)** | Full server-to-server control over every money flow. | Maximum control when you're already PCI certified. | 1–2 wks · PCI L1 |
Hybrid is the recommended pick for deposits + payouts without PCI. It has three valid combinations
(A / B / C) depending on your card UI — see the [Hybrid overview](/docs/hybrid/overview) for the
combinations table, refund rules, and dual-webhook handling.
## Sending payouts (API Direct) [#sending-payouts-api-direct]
Payouts are sent via **API Direct** (`operation_type: "withdrawal"` on the unified `/process/`
endpoint) — player payouts, seller settlements, affiliate commissions. It's independent of how you
accept deposits, so you can add it to an existing checkout without changing it.
* **With Hosted Checkout / Full SDK / Lite SDK:** add
[Withdrawals via API Direct](/docs/direct-api/withdrawals) (≈1–2 days).
* **With Hybrid or API Direct:** withdrawals use the same
[API Direct](/docs/direct-api/withdrawals) leg you already have — no separate implementation
needed.
## Feature support across models [#feature-support-across-models]
| Feature | Hosted Checkout | Full SDK | Lite SDK | Hybrid ★ | API Direct |
| ----------------------------- | :-------------: | :---------: | :----------------: | :---------------------: | :------------: |
| Card payments | Yes | Yes | Yes | Yes (SDK/Hosted leg) | Yes |
| Bank transfers (SPEI) | Yes | Yes | Yes | Yes (API Direct leg) | Yes |
| Cash (OXXO / vouchers) | Yes | Yes | Yes | Yes (API Direct leg) | Yes |
| Digital wallets (MercadoPago) | Yes | Yes | Yes | Yes (API Direct leg) | Yes |
| Withdrawals | API Direct | API Direct | API Direct | Yes (API Direct leg) | Yes |
| 3D Secure | Automatic | Automatic | Automatic | Automatic (cards) | Manual control |
| Tokenization | Automatic | Automatic | Automatic | Automatic (cards) | You implement |
| Saved cards | Yes | Yes | Yes | Yes | You implement |
| Webhooks | Long format | Long format | Long · Short (2.0) | Dual (SDK + API Direct) | Short format |
| Test environment | Yes | Yes | Yes | Yes | Yes |
Every model supports all payment methods. See [Payment methods](/docs/payment-methods/overview)
for the full catalog and the [Webhooks model](/docs/core-concepts/webhooks-model) for the format
each integration emits.
## Use cases [#use-cases]
An iGaming platform accepts deposits across many methods and sends player payouts, but isn't PCI
certified and wants programmatic control over SPEI transfers and cash.
**Recommended: Hybrid** ★
* **Cards:** Full SDK for an embedded card form (Tonder handles PCI).
* **Other methods:** API Direct for SPEI, OXXO Pay, MercadoPago — full control, no PCI required.
* **Withdrawals:** API Direct for automated player payouts.
* **Setup:** \~1–2 weeks.
Trade-off: you handle [dual webhooks](/docs/hybrid/dual-webhooks) and different request/response
shapes for cards vs. other methods.
A platform needs secure deposits to start the user journey. Pick by speed vs. control:
* **Hosted Checkout** — fastest, no code (\~1 day).
* **Full SDK / Lite SDK** — embedded checkout; Full is quicker with a standard UI, Lite gives
complete customization.
* **API Direct** — maximum control, but requires PCI certification.
All of these support deposits and card refunds.
To pay out winnings or settlements:
* **API Direct** — pairs with any deposit model, server-to-server (\~1–2 days).
* **API Direct** — one integration for deposits, withdrawals, and refunds, if you're PCI certified.
For an online store the core need is accepting payments securely:
* **Hosted Checkout** — fastest launch, no code.
* **Full SDK / Lite SDK** — embedded checkout; Lite for strong branding and full UX control.
* **API Direct (S2S)** — full backend control, requires PCI certification.
## Authentication, at a glance [#authentication-at-a-glance]
* **Public key** — process payments, APMs, Hosted sessions. Header: `Authorization: Token {public_key}`.
* **Secret key** — card tokenization only, server-side. Never ship it to the client.
Never expose your secret key in client code, public repos, or logs. Rotate immediately if leaked.
## Next steps [#next-steps]
The fastest path: a payment page hosted by Tonder.
UI components in your web or mobile app.
Full server-to-server control.
The recommended pick for deposits + payouts with no PCI scope.
# Environments (/docs/get-started/environments)
Tonder provides two environments for your integration:
* **Sandbox** — for development and testing.
* **Production** — for processing live transactions.
Each environment has its own set of base URLs and API credentials.
Use the correct base URL and keys for each environment. Using production keys in sandbox (or
vice-versa) results in authentication errors.
## Overview [#overview]
| Environment | Purpose | Dashboard |
| ----------- | ------------------------------------------------ | -------------------------------------------------------------- |
| Sandbox | Development, testing, and integration validation | [dashboard-stage.tonder.io](https://dashboard-stage.tonder.io) |
| Production | Live transactions with real funds | [dashboard.tonder.io](https://dashboard.tonder.io) |
## Base URLs [#base-urls]
## Base domains [#base-domains]
| Domain | Environment | Usage |
| --------------------------- | ----------- | ------------------------------------------------------------------------------------------- |
| `stage.tonder.io` | Sandbox | Standard API, auth |
| `app.tonder.io` | Production | Standard API, auth |
| `api-stage.tonder.io` | Sandbox | Hosted Checkout (checkout sessions) — see [Hosted Checkout](/docs/hosted-checkout/overview) |
| `api.tonder.io` | Production | Hosted Checkout (checkout sessions) |
| `token-sandbox.tonder.io` | Sandbox | Card tokenization service |
| `token.tonder.io` | Production | Card tokenization service |
| `process-sandbox.tonder.io` | Sandbox | Raw card data processing (PCI L1) |
| `process.tonder.io` | Production | Raw card data processing (PCI L1) |
## API credentials [#api-credentials]
Each environment uses a separate set of credentials (API key and secret key), available in the
corresponding dashboard under **Developers**.
* Never use production keys in your testing environment.
* Never expose your keys in client-side code.
* Store credentials securely on your server.
* Rotate keys immediately if compromised.
## Next steps [#next-steps]
API keys and required headers.
Best practices and PCI scope.
Test cards and data.
# Go-live Checklist (/docs/get-started/go-live-checklist)
Before processing real transactions, make sure your integration is robust, secure, and well-tested.
Use this list for your production deployment.
## Pre-production checklist [#pre-production-checklist]
### API configuration [#api-configuration]
* You configured your production credentials securely.
* Your application clearly separates staging and production environments.
* Your code uses the production base URL.
* You have a process to rotate keys if needed.
### Security [#security]
* You use HTTPS for all API communications.
* Keys and secrets are stored securely (environment variables or a secrets manager), not in code.
* Your integration validates and sanitizes all user input before sending it to the API.
* Your error handling doesn't expose sensitive data in logs or to the end user.
### Integration testing [#integration-testing]
* You tested payments with cards, SPEI, and OXXO Pay.
* You tested withdrawals with SPEI and debit cards.
* You tested common error scenarios: declines, validation errors (`422`), and authentication
failures (`401`).
* You tested the 3D Secure flow for cards.
* You set up and tested a webhook endpoint.
## Final deployment checklist [#final-deployment-checklist]
* A security audit was completed.
* Performance and load testing were conducted at expected volumes.
* Monitoring and alerting systems are active for success rates and errors.
* Your team is trained on operational procedures for payment incidents.
* You have a clear rollback plan for critical post-deployment issues.
* You verified compliance with applicable regulatory requirements (e.g. PCI DSS).
## Post-deployment monitoring [#post-deployment-monitoring]
* Continuously monitor all payment flows.
* Check error rates every 15 minutes and investigate any spikes immediately.
* Ensure webhook deliveries are succeeding.
* Track key performance metrics (CPU, memory, API latency).
* Have an on-call team member ready to respond to alerts.
* Review daily metrics and logs for anomalies.
* Collect customer feedback on the payment experience.
* Analyze error patterns to identify and fix recurring issues.
* Begin optimizing your system based on real traffic.
## Next steps [#next-steps]
The test scenarios to validate.
Switch from Sandbox to production.
# Quickstart (/docs/get-started/quickstart)
**Hosted Checkout** is the fastest way to get paid: Tonder hosts the payment page, so card data
never touches your servers (no PCI scope). This quickstart takes you from zero to a verified
**test** payment in four steps.
## Before you begin [#before-you-begin]
* A Tonder account — sign in at [dashboard.tonder.io](https://dashboard.tonder.io) (Production) or
[dashboard-stage.tonder.io](https://dashboard-stage.tonder.io) (Sandbox).
* Your **test** API key from **Developers → API Keys**. Authenticate every request with your
public key: `Authorization: Token YOUR_TEST_API_KEY`.
Amounts are in **decimal format**: `150.00` = MXN 150.00. Set `external_id` to your order
reference — it's the key you'll reconcile payments by.
## Steps [#steps]
### Create a checkout session [#create-a-checkout-session]
From your server, create a session with your public key. The response returns a `url` to send the
customer to.
```bash
curl -X POST 'https://api-stage.tonder.io/checkout/v1/sessions' \
-H 'Authorization: Token YOUR_TEST_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"customer": {
"first_name": "Vicente",
"last_name": "Quintero",
"email": "vquintero@testuser.com"
},
"amount_total": 150.00,
"currency": "MXN",
"line_items": [
{
"name": "Deposit",
"quantity": 1,
"unit_price": 150.00,
"product_id": "your internal product id"
}
],
"payment_method_types": [
"card",
"mercadopago",
"oxxopay",
"spei",
"safetypayCash",
"safetypayTransfer",
"neosurf"
],
"return_url": "https://tonder.io",
"external_id": "ORD-12345-4",
"metadata": { "external_id": "ORD-12345-4" }
}'
```
```bash
curl -X POST 'https://api.tonder.io/checkout/v1/sessions' \
-H 'Authorization: Token YOUR_LIVE_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"customer": {
"first_name": "Vicente",
"last_name": "Quintero",
"email": "vquintero@testuser.com"
},
"amount_total": 150.00,
"currency": "MXN",
"line_items": [
{
"name": "Deposit",
"quantity": 1,
"unit_price": 150.00,
"product_id": "your internal product id"
}
],
"payment_method_types": [
"card",
"mercadopago",
"oxxopay",
"spei",
"safetypayCash",
"safetypayTransfer",
"neosurf"
],
"return_url": "https://tonder.io",
"external_id": "ORD-12345-4",
"metadata": { "external_id": "ORD-12345-4" }
}'
```
The response (`200 Created`) includes the redirect `url`, the session `id`, and a `pending` status:
```json
{
"id": "cs_97_41521_d11ba771527b4056c7f85786cfbb980b",
"url": "https://stage-payflow.tonder.io/checkout/cs_97_41521_d11ba771527b4056c7f85786cfbb980b",
"status": "pending",
"payment_id": 41521,
"amount_total": 150.00,
"currency": "MXN",
"external_id": "ORD-12345-4",
"transaction_status": "Pending"
}
```
### Redirect the customer [#redirect-the-customer]
Send the customer to the `url` from the response — that's Tonder's hosted payment page.
```js
res.redirect(session.url);
```
### Complete a test payment [#complete-a-test-payment]
On the hosted page, pay with a **test card**: `4000 0000 0000 0077` (approved), any future expiry
(e.g. `12/30`), and any 3-digit CVC (e.g. `123`). Find the full set of test cards under
**Developers → Test Data** in the dashboard.
When done, Tonder returns the customer to your `return_url` with the session id appended:
`https://your-store.com/checkout/complete?session_id=cs_97_41521_…`.
### Confirm the result [#confirm-the-result]
Always confirm **server-side** — don't assume the redirect means success.
* **Webhook (recommended).** Tonder sends a `session.completed` event to your endpoint
(**Developers → Webhooks**). Match it to your order via `metadata.external_id` and respond `200`.
* **Status check.** Or fetch the session and read `status`:
```bash
curl -X GET 'https://api-stage.tonder.io/checkout/v1/sessions/cs_97_41521_d11ba771527b4056c7f85786cfbb980b' \
-H 'Authorization: Token YOUR_TEST_API_KEY'
```
```bash
curl -X GET 'https://api.tonder.io/checkout/v1/sessions/cs_97_41521_d11ba771527b4056c7f85786cfbb980b' \
-H 'Authorization: Token YOUR_LIVE_API_KEY'
```
A successful payment returns `status: "completed"` and `transaction_status: "Success"`:
```json
{
"id": "cs_97_41521_d11ba771527b4056c7f85786cfbb980b",
"status": "completed",
"transaction_status": "Success",
"amount_total": 150.00,
"currency": "MXN",
"external_id": "ORD-12345-4",
"paid_at": 1751478550234
}
```
Webhooks are the most reliable way to confirm payments — prefer them over polling. Acknowledge
each event with a `200` and reconcile using `metadata.external_id`.
## Next steps [#next-steps]
Customize the page, sessions, and status handling in depth.
Receive `session.completed` and other events reliably.
Need payouts, custom UI, or full control? Compare the models.
Every endpoint and payload, testable inline.
# Security & PCI Scope (/docs/get-started/security)
Tonder is **PCI DSS Level 1** certified on AWS infrastructure. Your PCI scope depends on the
integration mode you choose, and a few security practices protect your credentials and your customers.
## PCI scope by mode [#pci-scope-by-mode]
With Hosted Checkout, the SDKs, or Hybrid, Tonder handles the card's PCI compliance. Only API Direct
with raw card data requires PCI Level 1 certification.
## How Tonder protects saved cards [#how-tonder-protects-saved-cards]
Tonder's PCI Vault secures your customers' payment data through tokenization:
* **Isolated network** — card data is stored on a separate network and decryption keys are kept on
distinct machines, with no system able to access it as plain text.
* **Network tokenization** — tokenization replaces sensitive card information with unique tokens
during transmission over the network, reducing fraud risk on saved-card payments.
See [Card payments](/docs/direct-api/card-payments) for the tokenized flow in detail.
## Authentication [#authentication]
Every request authenticates with the `Authorization: Token YOUR_API_KEY` header (the literal `Token `
scheme with a space, not `Bearer`). Also send `X-Request-Id` with a UUID v4 per request as the
idempotency key. See [Authentication](/docs/get-started/authentication).
## Security best practices [#security-best-practices]
* **Never expose your secret key** in client code, mobile apps, or version control.
* Use **HTTPS** for all communications.
* Store credentials in **environment variables** — don't hardcode them.
* **Rotate keys immediately** if you suspect they're compromised.
* Use **separate keys** for sandbox and production.
* Restrict credential access on a need-to-know basis.
## Webhooks [#webhooks]
* **Incoming webhooks:** confirm them server-side by re-fetching the transaction or session status
before trusting the payload. See [Webhooks](/docs/webhooks/how-it-works).
## Next steps [#next-steps]
Keys, headers, and card tokenization.
Sandbox vs production and base domains.
The pre-production checklist.
# What is Tonder (/docs/get-started/what-is-tonder)
Accepting payments across Latin America usually means integrating many different methods,
acquirers, and fraud rules — each with its own API, reconciliation, and maintenance. Tonder is
the payments infrastructure for Latin America: a single integration covers deposits, payouts,
and fraud prevention across Mexico, Colombia, and Chile.
One integration connects you to cards, SPEI bank transfers, cash vouchers (OXXO), and wallets
(Mercado Pago) — with 3D Secure, fraud defense, retries, and reconciliation handled for you.
## Key benefits [#key-benefits]
Connect cards, SPEI, OXXO, and Mercado Pago through one integration for deposits, payouts,
and fraud prevention.
Cards, SPEI, OXXO, Cash vouchers, Mercado Pago, and more across Mexico, Colombia, and Chile.
200+ fraud signals analyzed in real time on every transaction.
AWS infrastructure with end-to-end PCI DSS Level 1 certification.
Accept payments and send payouts to your users from a single platform.
Hosted Checkout, SDKs, API Direct, and Hybrid — pick the one that fits your stack.
## How a payment flows [#how-a-payment-flows]
Every integration follows the same lifecycle. You create a payment, the customer authenticates
and pays, the acquirer authorizes it, and Tonder notifies you via webhook.
### Create the payment [#create-the-payment]
Your server tells Tonder how much to charge and who the customer is — via a hosted checkout
session, an SDK call, or a direct API request.
### Customer pays [#customer-pays]
The customer enters their details and, for cards, completes 3D Secure when required. Tonder
routes the attempt to the best acquirer.
### Authorization & result [#authorization--result]
The acquirer approves or declines. The terminal status depends on the payment method:
| Payment method | Terminal statuses |
| --------------------- | --------------------------------------------------------------------- |
| Card | `Success`, `Declined`, `Failed` |
| SPEI | `Success`, `Expired`, `Failed` |
| Withdrawals / Payouts | `Pending`, `Processing`, `Declined`, `Success`, `Cancelled`, `Failed` |
See the [Payment Lifecycle](/docs/core-concepts/payment-lifecycle) for the full description of
each status.
### Confirmation [#confirmation]
There are two ways to confirm a payment's result:
1. **Webhooks** (recommended) — Tonder notifies your server with the result; it's the most
popular and robust mechanism.
2. **Direct integration response** — the response from the method you used (checkout session,
SDK call, or API request) also carries the payment status.
Always confirm server-side rather than trusting a redirect.
New to payments concepts? Read the [Payment Lifecycle](/docs/core-concepts/payment-lifecycle)
and [3DS Lifecycle](/docs/core-concepts/3ds-lifecycle) first — they explain the statuses and
authentication flows every integration shares.
## Choose your integration [#choose-your-integration]
Tonder offers four integration modes. They trade **control** for **effort** — pick the least
complex one that meets your needs.
**Lowest effort.** Redirect to a Tonder-hosted payment page and get paid. No PCI surface,
no card fields in your app. Best for getting live fast.
**Balanced.** Drop Tonder's UI components into your web or mobile app (Web JS, React Native,
Flutter, Ionic). You own the look; Tonder handles the sensitive parts.
**Recommended for most.** Use SDK/Hosted for cards and API Direct for SPEI, APMs and payouts
— without taking on PCI scope.
**Full control.** Call the API directly for a fully custom flow — raw card, tokenization,
3DS, SPEI and APMs. Requires handling PCI scope yourself.
| If you want to… | Use | Quickstart |
| :-------------------------------------------- | :------------------- | :------------------------------------------------------------- |
| Be live with the least code | **Hosted Checkout** | [Hosted Checkout Quickstart](/docs/hosted-checkout/quickstart) |
| Keep customers in your app with your branding | **SDKs** | [SDK Overview](/docs/sdks/overview) |
| Accept deposits **and** send payouts | **Hybrid** | [Hybrid Overview](/docs/hybrid/overview) |
| Build a fully custom, server-driven flow | **API Direct (S2S)** | [Direct Quickstart](/docs/direct-api/quickstart) |
Still unsure? The [Choose your integration](/docs/get-started/choose-integration) guide
compares them in detail.
## Before you build [#before-you-build]
### Get access to the dashboard [#get-access-to-the-dashboard]
Sign in to the dashboard — [dashboard.tonder.io](https://dashboard.tonder.io) (Production) or
[dashboard-stage.tonder.io](https://dashboard-stage.tonder.io) (Sandbox) — and add your business information.
Walkthrough: [Quickstart](/docs/get-started/quickstart).
### Grab your test API keys [#grab-your-test-api-keys]
Find them under **Developers → API Keys**. Always start in the sandbox — never use production
keys for testing.
### Understand environments [#understand-environments]
Sandbox and production are fully separate. Review
[Environments](/docs/get-started/environments) before going live.
## Next steps [#next-steps]
The models behind every integration: lifecycle, 3DS, environments.
Cards, SPEI, OXXO, Mercado Pago and more — plus country coverage.
Every endpoint and payload, testable inline.
Manage transactions, payment links, disputes and settlements.
# Generate Secure Token (/docs/api-reference/payments/tokenization/generateSecureToken)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Tokenization Access Token (/docs/api-reference/payments/tokenization/getTokenizationToken)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Tokenize Card Data (/docs/api-reference/payments/tokenization/tokenizeCardData)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# MCP Integration (/docs/mcp/overview)
[`tonder-ai-integrations`](https://github.com/tonderio/tonder-ai-integrations) is Tonder's suite
of AI-agent plugins: they help developers integrate Tonder SDKs correctly in their applications,
using versioned documentation instead of the model's memory.
The first available plugin is **Tonder Web SDK** (`tonder-web-sdk`), for **Claude Code, Claude
Desktop, Codex CLI, and Codex Desktop**. It packages two pieces:
* An **integration skill** (`tonder-web-sdk-integrator`) that knows web frameworks and guides the
agent step by step.
* A **local MCP server** (`tonder-docs`) that serves versioned snapshots of the Web SDK
documentation — integration recipes, API reference, errors, and payment statuses. It runs
locally over stdio with Node.js: it opens no ports, never calls the Tonder backend, and needs
no credentials.
**Availability.** The plugin can be installed today from the GitHub repository marketplace
(current version: `0.1.7`). Publication in the official Claude plugin directory and the Codex
directory is pending.
## What it can integrate [#what-it-can-integrate]
| | |
| ------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Supported flows** | Card payment · Card enrollment · Saved-card payment · Payment methods (e.g. SPEI, OXXO Pay) · SafetyPay banks |
| **Frameworks** | Vanilla HTML, React, Next.js, Angular, and similar (browser-based web apps) |
| **Hosts** | Claude Code · Claude Desktop · Codex CLI · Codex Desktop |
## How the plugin works [#how-the-plugin-works]
When you ask for an integration, the plugin guides the agent to:
1. **Inspect your project** before editing (it detects the framework and the checkout page).
2. **Query the `tonder-docs` MCP** as the only source of truth — it never improvises examples or
uses local doc copies.
3. **Ask before deciding**: payment flow, presentation mode (embedded or redirect), CDN or npm
loading, and default or custom UX.
4. **Implement the smallest working integration** for the chosen framework and flow.
5. **Validate PCI safety**: merchant code never collects raw PAN, CVV, or expiration data —
always the SDK-rendered secure fields.
6. **Remind you of the operational side**: API keys, `idempotency_key`, webhooks, environment
values for production, and backend reconciliation.
The `tonder-docs` MCP server is bundled inside the plugin — it isn't installed separately. Once
you install the plugin in your editor, the MCP is available automatically.
## Next steps [#next-steps]
Step by step for Claude Code, Claude Desktop, Codex CLI, and Codex Desktop.
How to invoke it, example prompts, and the MCP tools.
# Installation (/docs/mcp/setup)
The `tonder-web-sdk` plugin installs from the GitHub repository marketplace
([tonderio/tonder-ai-integrations](https://github.com/tonderio/tonder-ai-integrations)) on any of
the four supported hosts. The `tonder-docs` MCP server comes bundled and runs locally with
Node.js — nothing else to install, no credentials to configure.
### Add the Tonder marketplace [#add-the-tonder-marketplace]
```bash
claude plugin marketplace add tonderio/tonder-ai-integrations
```
### Install the plugin [#install-the-plugin]
```bash
claude plugin install tonder-web-sdk@tonder-ai-integrations
```
This installs the plugin for Claude Code (it is not the same as manually uploading a plugin
through Claude Desktop).
### Open the plugin settings [#open-the-plugin-settings]
Open **Settings → Plugins**.
### Add the marketplace [#add-the-marketplace]
Select **Add → Add marketplace** and choose **Add from repository**. Enter:
```text
https://github.com/tonderio/tonder-ai-integrations
```
### Sync and install [#sync-and-install]
Synchronize the marketplace, open the **Code** marketplace tab, and install
**Tonder Web SDK**.
If repository marketplace sync is unavailable in your Claude Desktop build, download the
`.plugin` file from the
[GitHub release](https://github.com/tonderio/tonder-ai-integrations/releases) and upload it
through **Add → Upload plugin**.
### Add the Tonder marketplace [#add-the-tonder-marketplace-1]
```bash
codex plugin marketplace add tonderio/tonder-ai-integrations --ref main
```
### Install the plugin [#install-the-plugin-1]
```bash
codex plugin add tonder-web-sdk@tonder-ai-integrations
```
### Open the plugin marketplace [#open-the-plugin-marketplace]
Open **Plugins → Add marketplace**.
### Configure the origin [#configure-the-origin]
Set **Origin** to:
```text
https://github.com/tonderio/tonder-ai-integrations
```
Set **Git ref** to `main` and leave **Sparse paths** empty (unless your Codex build explicitly
requires otherwise).
### Add and install [#add-and-install]
Add the marketplace and install **Tonder Web SDK** from **Tonder AI Integrations**.
### Restart the thread [#restart-the-thread]
Start a new Codex thread after installation so the plugin becomes active.
**Requirement:** Node.js installed on your machine. The bundled `tonder-docs` MCP runs locally
over stdio — it opens no ports, never calls the Tonder backend, and needs no API keys to install
or to serve documentation.
## Next steps [#next-steps]
Invoke the plugin, example prompts, and the tools reference.
The Web SDK reference the plugin integrates.
# Usage (/docs/mcp/usage)
With the plugin installed, ask your agent for the integration — via the skill command or in
natural language.
## Invoke the plugin [#invoke-the-plugin]
With the skill command (Claude):
```text
/tonder-web-sdk-integrator Add embedded card payment to this checkout. Keep the existing amount input and deposit button.
```
Or in natural language:
```text
Use the Tonder Web SDK plugin to add embedded card payments to this checkout. Keep the existing amount input and deposit button.
```
More examples:
```text
Use Tonder Web SDK to add saved-card payments to this React checkout.
```
```text
Use Tonder Web SDK to add card enrollment to this Angular account settings page.
```
```text
Use Tonder Web SDK to add payment methods and SafetyPay bank selection to this HTML checkout.
```
## What to expect [#what-to-expect]
The plugin doesn't edit blindly: it inspects your project first and then **asks you** anything it
can't infer with certainty, one decision at a time:
| Decision | Options |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Framework | HTML/static · React · Angular (if ambiguous, it asks you to confirm) |
| Target page/component | Where the checkout goes, when several candidates exist |
| Payment flow | Card payment · Enrollment · Saved cards · Payment methods · SafetyPay banks |
| Presentation mode | `embedded` (SDK modal/iframe inside your page) · `redirect` (navigates to the hosted step and returns to your `return_url`) |
| SDK loading | `cdn` (browser script) · `npm` (package via your bundler) |
| UX | Tonder's default configuration · custom options (styles, callbacks, `idempotency_key`, metadata) |
With the decisions made, it implements only the selected flow, validates that your code never
collects raw card data, and closes with the changed files, the validation run, and setup notes.
**PCI safety.** Generated integrations never collect raw PAN, CVV, or expiration data in
merchant-owned inputs — always the secure fields rendered by the Tonder SDK.
## The `tonder-docs` MCP [#the-tonder-docs-mcp]
The plugin bundles a local MCP server the agent uses as its only documentation source. It serves
versioned snapshots of the public Web SDK README.
### Tools [#tools]
| Tool | What it returns |
| ------------------------------ | ---------------------------------------------------------------------------- |
| `get_integration_recipe` | The integration recipe for the chosen framework, flow, and presentation mode |
| `get_sdk_api_reference` | Method signatures, payloads, CDN/npm setup, and customization details |
| `get_error_reference` | The SDK error reference for handling and explanation |
| `get_payment_status_reference` | The payment status reference |
### Prompts [#prompts]
| Prompt | Purpose |
| -------------------------------- | ----------------------------------- |
| `integrate-web-sdk-card-payment` | Guides the card payment integration |
| `integrate-web-sdk-saved-cards` | Guides the saved-cards integration |
**MCP boundaries.** `tonder-docs` does not process payments, store credentials, or access
merchant customer data — it only serves documentation.
## After the integration [#after-the-integration]
The plugin reminds you when it finishes, but this is your operational checklist:
1. Configure your Tonder **public API key** (see [Authentication](/docs/get-started/authentication)).
2. Keep the **`idempotency_key`** stable per checkout attempt so retries are safe.
3. Configure your **webhooks** in the dashboard before fulfilling orders (see [Webhooks](/docs/webhooks/how-it-works)).
4. Update the **environment/CDN values** when moving from sandbox to production (see [Environments](/docs/get-started/environments)).
## Next steps [#next-steps]
Step by step per host, if you haven't installed it yet.
Web SDK methods and parameters.
# Apple Pay (/docs/payment-methods/apple-pay)
Apple Pay is accepted on the web through the **Web SDK button** (`@tonder.io/web-sdk`). The SDK
renders the button, presents Apple's payment sheet, and processes the charge; the transaction
lands on `/process/` with the same shape as a card payment and fires the same webhooks — your
reconciliation does not change.
Apple Pay does not exist in the legacy SDK or as a Direct API `payment_method.type`. The only
integration surface is the Web SDK's button component.
**Try it live:** the
[Apple Pay demo](https://main.d3ufdi32xd85gs.amplifyapp.com/web/apple-pay) runs this page's code —
open it in **Safari** (macOS, iOS or iPadOS) to see the real button and the availability result.
## Step 1: ask Tonder to enable it and register your domains [#step-1-ask-tonder-to-enable-it-and-register-your-domains]
Not a code step, and the most common reason a correct integration fails in production.
* **Tonder handles Apple** — you never contact them and you do not need an Apple developer
account.
* Your part is sending Tonder **the list of domains** that will show the button — subdomains count
separately — and **hosting the verification file** Tonder gives you.
Start this step before writing code: it gates everything else. The exact steps and failure modes
are in
[the SDK README](https://github.com/tonderio/web-sdk/blob/main/README.md#ask-tonder-to-register-your-domain-first).
## Step 2: render the button [#step-2-render-the-button]
Apple Pay is the one flow that does not end in a `pay()` call. Apple requires the payment sheet to
open in the same tick as the tap, so **the SDK owns the click**, and the result comes back on the
`events.payment` callbacks instead of a returned promise.
Those callbacks are not an Apple Pay-only mechanism — they fire for every method the SDK charges,
so one set of handlers covers the whole checkout.
The SDK renders the button into an element you provide. It has to exist before `mount()` runs, and
it stays empty — do not put a button, a label, or an icon inside it:
```html
```
```tsx
const tonder = createTonder({
api_key: tonderPublicConfig.api_key,
environment: 'stage', // switch to 'production' when you go live
session: { customer: { email: 'ada@example.com' } },
// Fires for EVERY payment method, not just Apple Pay. With pay() these run
// alongside the promise it returns; Apple Pay has no promise, so for it
// these callbacks are the only channel.
events: {
payment: {
on_completed: (transaction) => handleResult(transaction),
on_error: (error) => showError(error.code),
on_cancel: () => { /* shopper dismissed the sheet — not an error */ },
},
},
});
await tonder.init();
// Ask before you render. The answer is an OBJECT, never a bare boolean:
// { available: true }, or { available: false, code, message }. Reading the
// object itself as a condition would always be truthy — read .available.
const availability = tonder.isApplePayAvailable();
if (availability.available) {
const button = tonder.create('apple_pay_button', {
// Called SYNCHRONOUSLY when the shopper taps, so it reads whatever the
// cart holds at that moment — amount, currency and references can all
// change after mount without remounting the button. It must not be
// async: Apple requires the sheet to open in the same tick as the tap.
payment: () => ({
amount: 250,
currency: 'MXN',
return_url: 'https://merchant.example.com/return',
client_reference: 'ORD-001',
}),
});
await button.mount();
} else {
// Do not guess at the reason: only APPLE_PAY_UNSUPPORTED_BROWSER means
// "offer another method"; the other two codes are yours to fix.
console.info('Apple Pay hidden:', availability.code, availability.message);
}
```
### When availability is `false` [#when-availability-is-false]
| `code` | Means | What to do |
| ------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------- |
| `APPLE_PAY_UNSUPPORTED_BROWSER` | The browser cannot run Apple Pay | Offer another payment method — the only code that is the shopper's browser |
| `APPLE_PAY_NOT_ENABLED` | Apple Pay is not enabled for your business | Yours to fix — see step 1 |
| `NOT_INITIALIZED` | You called before `init()` finished | Yours to fix — await `tonder.init()` |
### Customize the button [#customize-the-button]
Safari draws the control natively, so Apple only allows these keys in
`customization.apple_pay_button` — anything else is ignored:
| Key | Example |
| ------------------ | ---------------------------------- |
| `type` | `'check-out'` (the call to action) |
| `style` | `'black'` |
| `locale` | `'es-MX'` (the label's language) |
| `width` / `height` | `'100%'` / `'48px'` |
| `border_radius` | `'8px'` |
The SDK reads `customization` once at `createTonder()` — changing it means building a new
instance and mounting again. The same applies to `api_key` and `session.customer`.
**A settled attempt consumes its references, whatever the outcome.** On `on_completed`, mint a
fresh `client_reference` and `idempotency_key` so the next tap is a new order with its own
idempotency scope.
### Where the button goes in your checkout [#where-the-button-goes-in-your-checkout]
Apple Pay earns its conversion lift by letting the shopper **skip the card form entirely** — so
the layout that works is the express one: the button first, a divider, and your other methods
underneath, untouched.
**Apple Pay is one more payment method, not a replacement.** Keep your credit and debit card
button — and every other method — visible alongside it. A shopper whose browser or wallet can't
use Apple Pay still needs a way to pay: that's exactly the
[`APPLE_PAY_UNSUPPORTED_BROWSER`](#when-availability-is-false) case.
The practices below follow Apple's
[Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/apple-pay):
| Practice | Why |
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Offer it **early** — express position, above the card form | Its value is skipping the form; burying it behind a "choose your method" step throws that away |
| Give it **at least the prominence of your primary pay button** | Same width or larger, and never below the fold when the card button isn't |
| Match your checkout's controls with [`customization.apple_pay_button`](#customize-the-button) | `width`, `height` and `border_radius` are the sanctioned way to fit it in |
| Only render it [when it's available](#when-availability-is-false) | Never show a disabled or grayed-out Apple Pay button |
| Never rebuild or decorate it | The container stays empty and Safari's native control is the only permitted rendering |
The mistake to avoid is the replacement: swapping the card button out for Apple Pay strands every
shopper it isn't available for. Measured checkouts keep both.
What this guide leaves out — a custom container id and releasing the button on a route change —
is in
[Apple Pay in the SDK README](https://github.com/tonderio/web-sdk/blob/main/README.md#apple-pay).
`pay({ payment_method: { type: 'apple_pay' } })` is rejected on purpose. Apple Pay cannot be
charged through `pay()` — Apple's gesture requirement is why. Use the button component.
## Step 3: your reconciliation does not change [#step-3-your-reconciliation-does-not-change]
Nothing to do. The Apple Pay charge lands on `/api/v1/process/` like your card charges, produces a
transaction with the same shape, and fires the same webhook. Your existing handler already covers
it.
Send `client_reference` in the button's `payment` data exactly as you do today and your
correlation keeps working. See the [Webhooks model](/docs/core-concepts/webhooks-model).
## Test Apple Pay [#test-apple-pay]
| Check | What proves it worked |
| ------------------------------------ | ----------------------------------------------------------------------- |
| The sheet opens on a **real device** | The iOS Simulator cannot test web Apple Pay |
| A decline | Arrives on `on_completed` with a declined status, **not** on `on_error` |
| `client_reference` | Appears in the webhook and correlates your order |
The decline row is the one that surprises people: `on_completed` means the charge reached a final
answer, not that the answer was yes.
## Next steps [#next-steps]
The real button, live — open it in Safari.
Install, quick start, and alternative methods.
Apple Pay first, without touching your current checkout.
Every method, type, and error.
Confirm the payment server-side.
# Cards (/docs/payment-methods/cards)
Accept credit and debit card payments through the unified endpoint. This guide covers card payments
and handling 3D Secure when required.
Cards supports **Visa, Mastercard, American Express and Carnet** in Mexico, and **Visa and
Mastercard** in Chile. See [Country coverage](/docs/resources/coverage) for the full breakdown by
market.
## Step 1: prepare the request [#step-1-prepare-the-request]
Create the request with the card method. Always use tokenized data when possible.
```json
{
"operation_type": "payment",
"amount": 150.00,
"currency": "MXN",
"customer": { "name": "Ana María Rodríguez", "email": "ana.rodriguez@email.com" },
"payment_method": {
"type": "CARD",
"card_number": "9230-0892-4469-1474",
"cardholder_name": "c05d89b2-299c-4f93-b49a-42be00d3b64b",
"cvv": "d31f0da3-0ed3-4ad8-8b68-14c2669a99a7",
"expiration_month": "e401a32e-4174-424f-9688-727005f6a80e",
"expiration_year": "bd9ccc23-3d00-4109-9626-fc6581389063"
},
"client_reference": "ORD-001",
"return_url": "https://yourstore.com/payment/return"
}
```
Always include a `return_url` when processing cards: it's required for 3D Secure authentication flows.
## Step 2: handle the response [#step-2-handle-the-response]
Without 3DS, the payment is processed immediately (`authorized`). If additional authentication is
required, the response includes redirect information.
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"operation_type": "payment",
"status": "authorized",
"amount": 150.00,
"currency": "MXN",
"payment_id": 12345,
"transaction_id": "txn_abc123",
"created_at": "2024-07-26T10:30:00Z"
}
```
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"next_action": {
"redirect_to_url": {
"url": "https://secure.payment-provider.com/3ds/abc123",
"return_url": "https://yourstore.com/payment/return"
}
}
}
```
## Step 3: handle 3D Secure [#step-3-handle-3d-secure]
Use the URL from `next_action.redirect_to_url.url`.
They complete the 3D Secure challenge on their bank's page.
After authenticating, they return to your `return_url`.
Make a `GET /api/v1/transactions/{id}/` to confirm the result.
```bash
curl -X GET https://stage.tonder.io/api/v1/transactions/550e8400-e29b-41d4-a716-446655440000/ \
-H "Authorization: Token YOUR_API_KEY"
```
## Tokenized vs. raw data [#tokenized-vs-raw-data]
For most merchants, the [tokenized flow](/docs/direct-api/card-payments) (3 steps: get token →
tokenize → process) reduces your PCI scope. Raw card data processing requires PCI DSS Level 1
certification.
## Next steps [#next-steps]
Tokenization and raw card data in depth.
How 3D Secure fits into the flow.
Test cards and expected results.
Per-transaction minimums and maximums.
Available networks and countries.
# Cash vouchers (/docs/payment-methods/cash-vouchers)
**Cash vouchers** (formerly *SafetyPay*) is a cash payment method operating across Latin America.
It's ideal for regions without OXXO coverage or to offer additional cash options.
## When to use Cash vouchers [#when-to-use-cash-vouchers]
* Customers in regions without OXXO coverage.
* Expanding cash payment options beyond convenience stores.
* Markets where the Cash vouchers network has established payment points.
This guide documents Mexico and Chile. Cash vouchers (SafetyPay) also operates in other Latin American
countries; see [Country coverage](/docs/resources/coverage) or contact your integration manager to
confirm availability and local providers in your market.
## Step 1: create the request [#step-1-create-the-request]
Cash vouchers supports two types: **cash** (`safetypayCash`) and **transfer** (`safetypayTransfer`).
Both require an `apm_config` object with `channel` and `bank_ids` (`channel: "WP"` for cash,
`channel: "OL"` for online transfer).
**Cash (`safetypayCash`):**
```bash
curl -X POST https://stage.tonder.io/api/v1/process/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation_type": "payment",
"amount": 180.00,
"currency": "MXN",
"customer": { "name": "Patricia Morales Vega", "email": "patricia.morales@email.com" },
"payment_method": {
"type": "safetypayCash",
"apm_config": {
"country": "Mexico",
"channel": "WP",
"bank_ids": [{ "id": "8178" }]
}
},
"client_reference": "ORD-001",
"return_url": "https://your-site.com/return"
}'
```
**Transfer (`safetypayTransfer`):**
```json
{
"operation_type": "payment",
"amount": 180.00,
"currency": "MXN",
"customer": { "name": "Patricia Morales Vega", "email": "patricia.morales@email.com" },
"payment_method": {
"type": "safetypayTransfer",
"apm_config": {
"country": "Mexico",
"channel": "OL",
"bank_ids": [{ "id": "1020" }]
}
},
"client_reference": "ORD-001",
"return_url": "https://your-site.com/return"
}
```
The API method identifier is still `SAFETYPAY` (and the channels `safetypayCash` /
`safetypayTransfer`). Only the display name changed to **Cash vouchers**.
### Available bank IDs — Mexico [#available-bank-ids--mexico]
Use `channel: "WP"` for cash and `channel: "OL"` for online transfer. Banks in the **Cash + Online**
section support both channels.
**Cash only (`channel: "WP"`):**
| Bank ID | Collection points |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `8178` | Bodega Aurrera, Circle K, OpenPay, Super Farmacia Guadalajara, 7-Eleven, Farmacias del Ahorro, Sam's Club, Walmart, Extra, Waldo's, GestoPago, Kiosko, Pago Rápido |
| `8419` | Calimax, Super Farmacia Santa María, Farmacia La Más Barata, Roma, PayCash, Soriana |
**Cash + Online (`channel: "WP"` or `"OL"`):**
| Bank ID | Bank |
| ------- | ------------- |
| `8186` | Banco Azteca |
| `8395` | Santander |
| `1020` | BBVA Bancomer |
| `1007` | Scotiabank |
### Chile [#chile]
Cash vouchers also operates in Chile (currency `CLP`). Use `channel: "WP"` for cash and
`channel: "OL"` for online transfer.
**Cash (`safetypayCash`, e.g. Walmart, Líder, Express de Líder, Acuenta):**
```json
{
"operation_type": "payment",
"amount": 200.00,
"currency": "CLP",
"customer": { "name": "John Doe", "email": "doe@testuser.com" },
"payment_method": {
"type": "safetypayCash",
"apm_config": {
"country": "Chile",
"channel": "WP",
"bank_ids": [{ "id": "8198" }]
}
},
"client_reference": "ORD-001",
"return_url": "https://your-site.com/return"
}
```
**Transfer (`safetypayTransfer`, e.g. Banco BCI):**
```json
{
"operation_type": "payment",
"amount": 200.00,
"currency": "CLP",
"customer": { "name": "John Doe", "email": "doe@testuser.com" },
"payment_method": {
"type": "safetypayTransfer",
"apm_config": {
"country": "Chile",
"channel": "OL",
"bank_ids": [{ "id": "8296" }]
}
},
"client_reference": "ORD-001",
"return_url": "https://your-site.com/return"
}
```
**Cash only (`channel: "WP"`):**
| Bank ID | Collection points |
| ------- | ----------------------------------------- |
| `8198` | Walmart, Líder, Express de Líder, Acuenta |
**Online / Transfer (`channel: "OL"`):**
| Bank ID | Bank |
| ------- | ---------------------- |
| `8296` | Banco BCI |
| `8416` | Machpay |
| `8488` | Khipu |
| `8490` | Khipu — Banco de Chile |
| `8494` | Khipu — Itaú |
| `8491` | Khipu — Santander |
## Step 2: handle the response [#step-2-handle-the-response]
The payment starts in `Pending`. The response does **not** carry the voucher or payment
instructions — those live on SafetyPay's hosted page, which the response points to via
`next_action.redirect_to_url`:
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"operation_type": "payment",
"status": "Pending",
"amount": 180.00,
"currency": "MXN",
"client_reference": "ORD-001",
"metadata": {},
"provider": "tonder",
"created_at": "2026-08-11T10:00:00.000000Z",
"status_code": 201,
"next_action": {
"redirect_to_url": {
"url": "https://sandbox-gateway.safetypay.com/Express4/Checkout/index?TokenID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&CountryID=MEX&ChannelID=ONLINE",
"verify_transaction_status_url": "/transactions/3fa85f64-5717-4562-b3fc-2c963f66afa6/"
}
}
}
```
## Step 3: redirect the customer [#step-3-redirect-the-customer]
Send the customer to `next_action.redirect_to_url.url` — SafetyPay's page, where they get the
reference and payment instructions (cash) or complete the transfer online.
The payment stays `Pending` until it's paid. Confirm the final status server-side — by
[webhook](/docs/webhooks/how-it-works) or by polling the transaction at
`verify_transaction_status_url` — before releasing goods.
## Limits by store type [#limits-by-store-type]
Cash vouchers has a minimum of **$10 MXN**; the maximum depends on the store type:
| Store | Maximum (MXN) |
| ------------------------ | ------------- |
| 7-Eleven / Circle K | $5,000 |
| Walmart / Sam's / Bodega | $20,000 |
| Soriana / Calimax | $10,000 |
See the full table in [Limits](/docs/payment-methods/limits).
## Next steps [#next-steps]
The full asynchronous flow.
Cash vouchers — Cash and Transfer channels.
# Frictionless SPEI (/docs/payment-methods/frictionless-spei)
Frictionless SPEI automatically processes bank transfers even when they don't match an existing
pending transaction. It enables two key scenarios:
1. **Mismatched amounts:** the customer deposits a different amount than expected.
2. **Direct transfers:** the customer transfers without initiating a checkout.
This feature builds on standard SPEI payments. If you haven't integrated SPEI yet, start with
[SPEI](/docs/payment-methods/spei). It's the recommended option for new SPEI merchants (MX) and is
available with API Direct or Hybrid.
## CLABE + identifier [#clabe--identifier]
Every SPEI deposit uses two identification layers:
* **CLABE (Tonder-managed):** a unique 18-digit account assigned by Tonder for each merchant +
customer. It's the primary deposit identifier, used for lookups and matching.
* **Identifier fields (you provide):** `external_id` (your internal reference) and
`additional_external_id` (an optional second identifier). They enable your reconciliation logic.
## Use case 1: mismatched amount deposits [#use-case-1-mismatched-amount-deposits]
The customer initiates checkout but deposits a different amount. Example: creates a 100 MXN checkout
but deposits 600 MXN. The system matches by CLABE and processes automatically.
```json
{
"data": {
"amount": 600.0,
"metadata": {
"external_id": "ORD-001",
"mismatched_deposit": "True",
"original_expected_amount": "100"
}
}
}
```
## Use case 2: direct transfers (no checkout) [#use-case-2-direct-transfers-no-checkout]
The customer transfers directly to their CLABE without initiating a checkout. Example: uses the saved
CLABE from a previous deposit and transfers 700 MXN from their banking app. The system finds the last
successful transaction for that CLABE, extracts the identifier, and creates a new deposit.
```json
{
"data": {
"amount": 700.0,
"metadata": {
"external_id": "CUSTOMER-12345",
"concept": "Frictionless deposit - auto-created"
}
}
}
```
## Recommended strategy: single identifier [#recommended-strategy-single-identifier]
Use the same identifier for both cases — the simplest approach. For example, a gaming platform using
`player_id`:
```json
{
"metadata": {
"external_id": "PLAYER-12345"
}
}
```
Process the webhook by always crediting the same customer account, and handle mismatched amounts when
`metadata.mismatched_deposit` is `"True"`:
```javascript
const playerId = webhook.data.metadata.external_id;
await creditPlayer(playerId, webhook.data.amount);
if (webhook.data.metadata.mismatched_deposit === "True") {
await notifyPlayer(playerId, "amount_mismatch");
}
```
## Advanced setup (dual identifiers) [#advanced-setup-dual-identifiers]
For merchants who need **different identifiers per use case** — for example, order-level tracking for
checkouts (use case 1) and customer-level tracking for direct transfers (use case 2):
```json
{
"amount": 500,
"currency": "MXN",
"payment_method": "spei",
"metadata": {
"external_id": "ORDER-12345",
"additional_external_id": "PLAYER-98765"
}
}
```
**Use case 1 webhook** (a pending transaction exists):
```json
{
"metadata": {
"external_id": "ORDER-12345",
"additional_external_id": "PLAYER-98765",
"mismatched_deposit": "True"
}
}
```
```javascript
const orderId = webhook.data.metadata.external_id;
await completeOrder(orderId, webhook.data.amount);
```
**Use case 2 webhook** (no pending transaction; `external_id` isn't included since no order exists):
```json
{
"metadata": {
"additional_external_id": "PLAYER-98765"
}
}
```
```javascript
const playerId = webhook.data.metadata.additional_external_id;
await manualTopUp(playerId, webhook.data.amount);
```
| Strategy | Use case 1 returns | Use case 2 returns | Complexity | Best for |
| ----------------- | ------------------ | --------------------- | ---------- | -------------------------- |
| Single identifier | `player_id` | `player_id` (history) | Simple | Customer-centric platforms |
| Dual identifiers | `order_id` | `player_id` (history) | Advanced | Order + customer tracking |
### Custom field aliases [#custom-field-aliases]
Instead of always using `external_id` / `additional_external_id`, you can ask Tonder to use
domain-specific field names instead (for example `order_id` / `player_id`):
```json
{
"field_aliases": {
"order_id": "external_id",
"player_id": "additional_external_id"
}
}
```
With that configuration, your request and the webhook use your own field names:
```json
{
"metadata": {
"order_id": "ORDER-12345",
"player_id": "PLAYER-98765"
}
}
```
Dual identifiers and custom field aliases are agreed on and configured together with your Tonder
integration manager. Start with a single identifier; add dual identifiers only if you need different
tracking per use case, and aliases only if you want domain-specific field names.
## Check a deposit's status (Polling) [#check-a-deposits-status-polling]
Webhooks are the recommended way to receive Frictionless SPEI deposit updates, but you can also
query the status directly with the deposit-specific endpoints. This is useful for reconciliation
or when a webhook delivery is delayed.
```
GET https://02ljs5zoif.execute-api.us-east-1.amazonaws.com/stage/api/v1/deposits/{id}/transaction
```
```
GET https://38dictnz7c.execute-api.us-east-1.amazonaws.com/pdn/api/v1/deposits/{transaction_id}/transaction
```
These endpoints are specific to Frictionless SPEI deposits and are distinct from the general
[Get Transaction Status](/docs/api-reference/transactions/getTransactionStatus). Use
`{transaction_id}` / `{id}` as the deposit's transaction identifier.
## Simulator (Sandbox) [#simulator-sandbox]
In sandbox you can simulate a Frictionless SPEI deposit — including mismatched amounts and direct
transfers — from the simulator:
`https://tonder.live/simulatedeposits/`
The simulator is **Sandbox-only**. Use it to test CLABE matching and your reconciliation logic
before going to production.
## Next steps [#next-steps]
The standard SPEI method.
Test mismatched amounts and direct transfers.
Formats and correlation keys.
# Limits & Business Rules (/docs/payment-methods/limits)
Per-transaction limits for deposit methods (MXN). Withdrawal limits depend on the merchant agreement.
## Limits by method (MXN) [#limits-by-method-mxn]
| Payment method | Min | Max (standard) | Max (gambling/forex) | Notes |
| ------------------------------------ | -------------------------------- | ---------------------------------- | --------------------- | ------------------------ |
| Cards | $10 | Unlimited (depends on user's bank) | $200,000/tx | Visa · MC |
| SPEI (deposit) | $1 | Unlimited | $200,000/tx | Frictionless recommended |
| OXXO Pay | $10 | $10,000 | $10,000 | No gambling exception |
| Cash vouchers — 7-Eleven/Circle K | $10 | $5,000 | $5,000 | Paynet by Openpay |
| Cash vouchers — Walmart/Sam's/Bodega | $10 | $20,000 | $20,000 | Higher-limit stores |
| Cash vouchers — Soriana/Calimax | $10 | $10,000 | $10,000 | PayCash network |
| MercadoPago | Depends on merchant's MP account | Depends on MP account | Depends on MP account | eWallet + cards |
Limits are **per transaction** (not daily). Withdrawal limits depend on the merchant agreement.
Always verify current limits with your Tonder integration manager.
## Next steps [#next-steps]
How amounts are represented per integration.
All available methods.
# MercadoPago (/docs/payment-methods/mercadopago)
MercadoPago is Latin America's most popular digital wallet. It offers instant processing and is
widely used by frequent online shoppers. It uses a redirect flow where the customer authenticates on
MercadoPago's platform.
## When to use MercadoPago [#when-to-use-mercadopago]
* E-commerce customers familiar with digital wallets.
* Markets with strong MercadoPago adoption.
* Repeat customers who value a quick checkout.
## Step 1: create the request [#step-1-create-the-request]
```bash
curl -X POST https://stage.tonder.io/api/v1/process/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation_type": "payment",
"amount": 350.00,
"currency": "MXN",
"customer": { "name": "José Antonio Ramírez", "email": "jose.ramirez@email.com" },
"payment_method": { "type": "MERCADOPAGO" },
"client_reference": "ORD-001",
"return_url": "https://yourstore.com/payment/return"
}'
```
Always include a `return_url` for MercadoPago: it's where the customer is redirected after completing
the payment.
## Step 2: handle the response and redirect [#step-2-handle-the-response-and-redirect]
The response includes redirect information. MercadoPago requires a redirect flow where the customer
authenticates:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"operation_type": "payment",
"status": "pending",
"amount": 350.00,
"currency": "MXN",
"next_action": {
"redirect_to_url": {
"url": "https://mercadopago.com/checkout/123456789",
"return_url": "https://yourstore.com/payment/return"
}
},
"created_at": "2024-07-26T10:30:00Z"
}
```
## Step 3: redirect the customer [#step-3-redirect-the-customer]
Use the URL from `next_action.redirect_to_url.url`.
Logs into their MercadoPago account and confirms the payment details.
Selects their funding source and confirms.
After payment, they return to your `return_url`.
For MercadoPago testing, the customer email must use the `@testuser.com` domain (e.g.
`testuser@testuser.com`). For the payment to be **approved** in sandbox, the cardholder name must
be **`APRO`**.
## Limits [#limits]
MercadoPago limits depend on the merchant's MercadoPago account. See
[Limits](/docs/payment-methods/limits).
## Next steps [#next-steps]
The full asynchronous flow.
MercadoPago testing requirements.
# Overview & Matrix (/docs/payment-methods/overview)
All payment methods in Tonder work through the same unified `/process/` endpoint. You just change
`payment_method.type` and add the method-specific parameters, making it easy to offer multiple
options without managing different integrations.
```json
{
"operation_type": "payment",
"amount": 100.00,
"currency": "MXN",
"customer": { /* customer data */ },
"payment_method": {
"type": "CARD"
},
"client_reference": "ORD-001"
}
```
## Available methods [#available-methods]
| Method | Type | Best for | Processing time |
| ---------------------------------------------------- | ------------- | --------------------------------------------- | --------------- |
| [Cards](/docs/payment-methods/cards) | Instant | E-commerce, instant confirmation | Immediate |
| [Apple Pay](/docs/payment-methods/apple-pay) | Instant | Safari / iOS shoppers, via the Web SDK button | Immediate |
| [MercadoPago](/docs/payment-methods/mercadopago) | Instant | Digital wallet customers | Immediate |
| [SPEI](/docs/payment-methods/spei) | Bank transfer | Higher-value, B2B | Real-time |
| [OXXO Pay](/docs/payment-methods/oxxo) | Cash | Customers without bank accounts | 24-48 hours |
| [Cash vouchers](/docs/payment-methods/cash-vouchers) | Cash | Regions without OXXO coverage | 24-48 hours |
Country and network availability, and each method's support for deposits/withdrawals/refunds, vary by
market. See [Country coverage](/docs/resources/coverage) for the full breakdown.
## Implementation strategy [#implementation-strategy]
They provide immediate confirmation and work for most customers.
Essential in the Mexican market, where cash is still popular.
Bank transfers are trusted for larger transactions.
Add digital wallets and alternative methods based on customer feedback.
Set up [webhooks](/docs/webhooks/how-it-works) to track asynchronous payments (OXXO Pay, SPEI,
Cash vouchers).
## Next steps [#next-steps]
Credit and debit, with 3D Secure.
Real-time bank transfers.
Mismatched amounts and direct transfers.
Minimums and maximums by method.
Countries, networks, and deposit/withdrawal support by method.
# OXXO Pay (/docs/payment-methods/oxxo)
Accept cash payments at OXXO convenience stores in Mexico. OXXO is the country's largest convenience
store chain, ideal for customers who prefer cash or don't have a bank account.
## When to use OXXO [#when-to-use-oxxo]
* Customers without a bank account or card.
* E-commerce customers who prefer cash.
* Areas with high OXXO store density.
## Step 1: create the request [#step-1-create-the-request]
```bash
curl -X POST https://stage.tonder.io/api/v1/process/ \
-H "Authorization: Token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation_type": "payment",
"amount": 250.00,
"currency": "MXN",
"customer": { "name": "María Isabel Fernández", "email": "maria.fernandez@email.com" },
"payment_method": { "type": "oxxopay" },
"client_reference": "ORD-001"
}'
```
## Step 2: handle the response [#step-2-handle-the-response]
OXXO payments start in `pending` and include the payment instructions:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"operation_type": "payment",
"status": "pending",
"amount": 250.00,
"currency": "MXN",
"payment_instructions": {
"reference_code": "99900012345678",
"barcode": "||99900012345678||",
"store_name": "oxxopay",
"expiration_date": "2024-07-29T23:59:59Z",
"instructions": [
"Acude a cualquier tienda OXXO",
"Presenta este código de barras en caja",
"Paga exactamente $250.00 MXN en efectivo"
]
},
"voucher_pdf": "https://api.tonder.io/vouchers/abc123.pdf",
"created_at": "2024-07-26T10:30:00Z"
}
```
| Field | Description |
| -------------------------------------- | ------------------------------------------------------------- |
| `id` | Unique transaction identifier — store it for status checking. |
| `status` | Payment status — initially `pending`. |
| `payment_instructions.reference_code` | OXXO Pay reference code. |
| `payment_instructions.barcode` | Barcode to display to the customer. |
| `payment_instructions.expiration_date` | Voucher expiration date. |
| `voucher_pdf` | URL to download the PDF voucher. |
Confirmation usually arrives within minutes of paying at a store. However, OXXO's system may hold a
payment in `pending` for up to 3 hours while it checks for internal failures or potential refunds.
This is expected behavior. If it remains `pending` beyond 3 hours, contact support.
## Step 3: display the instructions [#step-3-display-the-instructions]
Display the `barcode` so the customer can show it at the register.
Show the `reference_code` as a backup in case the barcode can't be scanned.
Use the `instructions` array to guide the customer.
Indicate when the voucher expires and offer to download the `voucher_pdf`.
## Limits [#limits]
OXXO Pay has a minimum of **$10 MXN** and a maximum of **$10,000 MXN** per transaction. See the full
table in [Limits](/docs/payment-methods/limits).
## Next steps [#next-steps]
The full asynchronous flow.
Receive confirmation when the customer pays.
# SPEI (/docs/payment-methods/spei)
SPEI (Sistema de Pagos Electrónicos Interbancarios) is Mexico's real-time bank transfer system,
ideal for higher-value transactions where the customer prefers paying from their bank.
## When to use SPEI [#when-to-use-spei]
* Higher-value transactions (B2B payments).
* Customers who prefer bank transfers over cards.
* Transactions requiring immediate processing during business hours.
* Reducing card processing fees for large amounts.
## Step 1: create the request [#step-1-create-the-request]
SPEI doesn't require additional customer data beyond the basics. The API generates the payment
instructions.
```json
{
"operation_type": "payment",
"amount": 500.00,
"currency": "MXN",
"customer": { "name": "Carlos Eduardo López", "email": "carlos.lopez@empresa.mx" },
"payment_method": { "type": "SPEI" },
"client_reference": "ORD-001"
}
```
## Step 2: handle the response [#step-2-handle-the-response]
SPEI payments always start in `pending` because they require the customer to complete the transfer.
The response includes redirect information with the CLABE:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"operation_type": "payment",
"status": "pending",
"amount": 500.00,
"currency": "MXN",
"next_action": {
"redirect_to_url": {
"url": "https://stage-payflow.tonder.io/spei?clabe=710969000000629935&...",
"return_url": "https://tonder.io?tndr_payment_id=63772",
"verify_transaction_status_url": "/transactions/21cbe500-b320-4e0d-9186-9fb63c3d9617/"
}
}
}
```
Show the customer the CLABE and reference so they can complete the transfer. Confirm the result with
a [webhook](/docs/webhooks/how-it-works) or by polling the status.
New SPEI merchants (MX) should use [Frictionless
SPEI](/docs/payment-methods/frictionless-spei), which automatically processes mismatched amounts
and direct transfers.
This page covers SPEI deposits. SPEI also supports **withdrawals** (to a bank account or debit card)
via [Withdrawals via API Direct](/docs/direct-api/withdrawals).
## Limits [#limits]
SPEI has a minimum of **$1 MXN** per transaction. See the full table in
[Limits](/docs/payment-methods/limits).
In addition, your business can **configure a custom minimum amount** for SPEI (above the default
minimum). Request it from your Tonder integration manager.
If a customer attempts to pay an amount below the configured minimum, Tonder issues an **automatic
refund** for the amount received and the transaction does not complete.
## Next steps [#next-steps]
Mismatched amounts and direct transfers.
The full asynchronous flow.
Receive the payment confirmation.
Send payouts to a bank account or debit card.
# Dual Webhooks (/docs/hybrid/dual-webhooks)
In the Hybrid model, a single transaction can emit **two** webhooks: one in **Long** format (from
Hosted/SDK, for the card) and one in **Short** format (from API Direct, for non-card methods,
withdrawals, and refunds). This is by design.
## Why duplicate webhooks arrive [#why-duplicate-webhooks-arrive]
Because each leg of your integration (SDK/Hosted for cards, API Direct for everything else) has its
own webhook channel, you'll receive notifications from both. A single payment can generate a
`Pending` event and then a `Success` for each channel involved.
Duplicate webhooks in Hybrid mode are **intentional** (Pending + Success). Always implement
deduplication using `metadata.external_id` on your side.
## What you need to configure [#what-you-need-to-configure]
* **Two webhook endpoints**, one per integration style.
* **Separate handling logic:** Hosted/SDK-style webhooks for card transactions, and API Direct-style
for other methods, withdrawals, and refunds.
## Deduplication checklist [#deduplication-checklist]
## Next steps [#next-steps]
Short vs Long formats in depth.
Format and correlation key per leg.
Reconcile with `metadata.external_id`.
# Overview & When to Use It (/docs/hybrid/overview)
The Hybrid model combines the simplicity of SDK/Hosted for card payments with the power and
flexibility of API Direct for all other methods and withdrawals — without requiring PCI
certification. It's the recommended approach for merchants that need custom UI for some flows and
programmatic control for others (iGaming, fintech, high-volume operations).
## How it works [#how-it-works]
The Hybrid model splits your processing strategically:
1. **For card payments:** use Hosted Checkout, Full SDK, or Lite SDK. Tonder manages PCI compliance;
no certification needed.
2. **For all other methods:** use API Direct (SPEI, OXXO Pay, MercadoPago, cash, and any non-card
method).
3. **For withdrawals:** use API Direct, with full programmatic control and no PCI requirements.
## Valid combinations [#valid-combinations]
Choose **one** combination based on your card UI needs:
| Combination | Cards | Other methods | Withdrawals | Best for |
| ------------ | --------------- | ------------- | ----------- | -------------------------------------------------------- |
| **Hybrid A** | Hosted Checkout | API Direct | API Direct | Fastest launch with maximum control for non-card methods |
| **Hybrid B** | Full SDK | API Direct | API Direct | Embedded card experience with standard UI |
| **Hybrid C** | Lite SDK | API Direct | API Direct | Complete UI customization across all methods |
## PCI scope [#pci-scope]
## Refunds in Hybrid mode [#refunds-in-hybrid-mode]
Card refunds can be processed in two ways:
1. **Dashboard (recommended):** process all card refunds through the Tonder dashboard.
2. **API Direct:** use API Direct for card refunds.
We recommend choosing **one refund method** and using it consistently. Mixing both complicates your
integration.
**Important limitations**
* Refunds are only available for card payments; SPEI, OXXO Pay, and MercadoPago transactions cannot
be refunded.
* Each transaction can be refunded only once, and in full.
* Refunds must be processed within 29 days after payment confirmation.
## Trade-offs to consider [#trade-offs-to-consider]
* **Dual integration patterns.** You'll implement two integration styles at once, with different
request/response formats for cards vs. other methods.
* **Dual webhook handling.** You'll receive Hosted/SDK-style webhooks for cards and API Direct-style
webhooks for everything else. See [Dual webhooks](/docs/hybrid/dual-webhooks).
* **Higher initial development effort** than a pure SDK/Hosted approach, since you build and
maintain two integrations in parallel.
The Hybrid approach is a bridge solution toward the next generation of Hosted Checkout and SDKs,
which will unify request/response/webhook patterns. Current hybrid implementations will remain
supported, with migration paths available.
## Next steps [#next-steps]
Why duplicate webhooks arrive and how to dedupe.
Formats, correlation keys, and PCI scope per leg.
Compare the five models.
Withdrawals in Hybrid are handled via API Direct.
# Reference (/docs/hybrid/reference)
A consolidated reference for the Hybrid model: which leg handles each flow, which webhook format, and
which correlation key to use.
## Format and key per leg [#format-and-key-per-leg]
| Leg | Handles | Webhook format | Correlation key |
| ------------ | --------------------------------------- | -------------- | ------------------------------------------- |
| SDK / Hosted | Card payments | Long | `metadata.external_id` |
| API Direct | Other methods, withdrawals, and refunds | Short | `client_reference` + `metadata.external_id` |
On both legs, `metadata.external_id` is the universal reconciliation key.
## Valid combinations [#valid-combinations]
| Combination | Cards | Other methods | Withdrawals |
| ------------ | --------------- | ------------- | ----------- |
| **Hybrid A** | Hosted Checkout | API Direct | API Direct |
| **Hybrid B** | Full SDK | API Direct | API Direct |
| **Hybrid C** | Lite SDK | API Direct | API Direct |
## Refunds [#refunds]
Refunds apply to cards only and are processed via **Dashboard (recommended)** or **API Direct**.
Choose one method and use it consistently.
**Important limitations**
* Refunds are only available for card payments; SPEI, OXXO Pay, and MercadoPago transactions cannot
be refunded.
* Each transaction can be refunded only once, and in full.
* Refunds must be processed within 29 days after payment confirmation.
## PCI scope [#pci-scope]
## Correlation keys [#correlation-keys]
## Next steps [#next-steps]
How it works and when to use it.
Dedupe the duplicate webhooks.