Create a Session
The request that creates a Hosted Checkout session, field by field, and what the response gives you.
Call this endpoint from your backend each time a customer is ready to pay. It creates one checkout
session — for one payment or one card enrollment — and returns the url to send the customer to.
POST https://api-stage.tonder.io/checkout/v1/sessions # Sandbox
POST https://api.tonder.io/checkout/v1/sessions # ProductionHow it works
- Your server calls this endpoint with the customer, the amount and the items.
- Tonder creates the session and, for payment sessions, the payment. You receive the checkout
urland thepayment_id. - You redirect the customer to the
url, or embed it. - The customer pays on Tonder's page. They can retry in the same session.
- The customer returns to your site, and you confirm the result with webhooks or by reading the session.
A session is single-use and valid for at most 24 hours. Its amount, currency and line items cannot be changed — create a new session instead.
Headers
| Header | Value | Required | Description |
|---|---|---|---|
Authorization | Token <your_api_key> | Yes | Your API key, from Developers in the Dashboard. Call the API only from your backend. |
Content-Type | application/json | Yes | The body is JSON. |
x-idempotency-key | Any unique string | No | Protects against creating the same session twice when you retry a request within 5 seconds. See Idempotency. |
Body
| Field | Type | Required | Values | Description |
|---|---|---|---|---|
session_type | string | No | payment, enrollment | Defaults to payment. enrollment saves a card without charging — see Save a card without charging. |
customer | object | Yes | See Customer | The customer who pays. Saved cards belong to this customer. |
amount_total | number | Payment only | Greater than 0 | Total to charge, in decimal format (150.00). Not checked against the sum of line_items. |
currency | string | Payment only | e.g. MXN | Your business currency. |
line_items | array | Payment only | See Line items | What the customer is paying for. |
payment_method_types | array | No | See Payment methods | Methods to offer. Defaults to ["card"]. |
payment_method_config | object | No | See Saved cards | Options for saved cards and Mercado Pago. |
checkout_ui_version | string | No | v1, v2 | Checkout design. v2 is the current one. If omitted, your business default applies, then v2. |
submit_type | string | No | pay, deposit, subscribe | Wording of the amount and the pay button. A custom button text in ui_config takes precedence. |
locale | string | No | es, en, zh | Language the checkout opens in. The customer can still change it on the page. |
ui_config | object | No | See Customize the checkout | Appearance for this session only. |
ui_config_version | string | No | e.g. V1 | Which saved business appearance to use. Defaults to V1. Unrelated to checkout_ui_version. |
success_url, pending_url, cancel_url | string | No | URL | See Redirect URLs. |
return_url | string | Yes, if no success_url | URL | Fallback redirect. |
expires_at | number | No | Unix time in seconds | Between 30 minutes and 24 hours from now. Defaults to 24 hours. |
external_id | string | No | — | Your order reference. Generated for you if you do not send one. |
metadata | object | No | — | Any data you want back in responses and webhooks. Echo your order reference here as metadata.external_id. |
checkout_type | string | No | hosted, embedded | Defaults to hosted. Use embedded for an iframe. |
post_message_enabled | boolean | No | true, false | Send events to your page. Requires checkout_type: "embedded". |
redirect_on_completion | string | No | always, never | Defaults to always. never keeps the customer on the checkout so your page handles navigation. |
Customer
| Field | Type | Required | Description |
|---|---|---|---|
first_name, last_name, email | string | Yes | Customer identity. |
phone | string | No | Customer phone, E.164. If Tonder configured your account to also require a phone for saved cards, send it on every session that includes saved_cards, always the same value. It doesn't replace email, which always identifies the customer. See Which customer a saved card belongs to. |
address | object | No | country (required inside), state, city, streets, postal_code. |
identification | object | No | type and number (both required inside). |
Line items
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Item name. |
quantity | number | Yes | Whole number, 1 or more. |
unit_price | number | Yes | Price per unit, greater than 0. |
description, product_id | string | No | Item description and your product reference. |
discount, taxes | number | No | Amounts per line, shown in the order summary. |
tax_rate | number | No | Rate shown next to the taxes. Display only. |
Line items are for display. The customer is always charged amount_total. The checkout shows the
full breakdown only when the items add up to amount_total; otherwise it shows the total alone.
Example
curl -X POST 'https://api-stage.tonder.io/checkout/v1/sessions' \
-H 'Authorization: Token YOUR_TEST_API_KEY' \
-H 'Content-Type: application/json' \
-H 'x-idempotency-key: ORD-001-attempt-1' \
-d '{
"customer": { "first_name": "Maria", "last_name": "Garcia", "email": "maria@example.com" },
"amount_total": 150.00,
"currency": "MXN",
"external_id": "ORD-001",
"line_items": [
{ "name": "Premium Plan", "quantity": 1, "unit_price": 150.00, "product_id": "plan-premium-01" }
],
"payment_method_types": ["card", "saved_cards", "oxxopay"],
"submit_type": "pay",
"locale": "en",
"return_url": "https://your-store.com/checkout/complete",
"metadata": { "external_id": "ORD-001" }
}'curl -X POST 'https://api.tonder.io/checkout/v1/sessions' \
-H 'Authorization: Token YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'x-idempotency-key: ORD-001-attempt-1' \
-d '{
"customer": { "first_name": "Maria", "last_name": "Garcia", "email": "maria@example.com" },
"amount_total": 150.00,
"currency": "MXN",
"external_id": "ORD-001",
"line_items": [
{ "name": "Premium Plan", "quantity": 1, "unit_price": 150.00, "product_id": "plan-premium-01" }
],
"payment_method_types": ["card", "saved_cards", "oxxopay"],
"submit_type": "pay",
"locale": "en",
"return_url": "https://your-store.com/checkout/complete",
"metadata": { "external_id": "ORD-001" }
}'{
"id": "cs_97_41528_42434df6c3644b9610df7b64a1fa9925e3b9562b05972097",
"url": "https://stage-payflow.tonder.io/checkout/cs_97_41528_42434df6c3644b9610df7b64a1fa9925e3b9562b05972097",
"status": "pending",
"session_type": "payment",
"checkout_type": "hosted",
"payment_id": 41528,
"checkout_ui_version": "v2",
"submit_type": "pay",
"locale": "en",
"amount_total": 150.00,
"currency": "MXN",
"external_id": "ORD-001",
"return_url": "https://your-store.com/checkout/complete",
"payment_method_types": ["card", "saved_cards", "oxxopay"],
"ui_config": {},
"ui_config_version": "V1",
"created_at": 1751483563558,
"modified_at": 1751483563558,
"expires_at": 1751569963
}What the response gives you
| Field | Description |
|---|---|
id | The session id. Use it with Get a session. |
url | The checkout page to redirect to or embed. It never changes, even if you change your defaults later. |
status | pending, completed or expired. |
payment_id | A number that identifies the payment. It exists from creation, never changes, and stays the same when the customer retries with another card. 0 on enrollment sessions. |
checkout_ui_version, submit_type, locale | The values applied to this session, fixed at creation. |
payment_flow | direct when your account is processed through the Direct API; not present otherwise. |
direct_transaction_id | Direct API accounts only, once the customer has tried to pay: the id of the latest attempt. |
The full list of identifiers and statuses is in the Reference.
Payment attempts
A customer can try to pay more than once in the same session — a card is declined and they retry
with another one. The session keeps only the latest attempt: transaction_status (and
direct_transaction_id on Direct API accounts) is replaced on every new attempt, so a first
Declined can become Success. payment_id stays the same for the whole session, so every
attempt belongs to the same payment.
Errors
| HTTP status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | E004 | The request is invalid: a required field is missing, a value is not allowed, or a payment field was sent on an enrollment session. | Read metadata.errors (one entry per problem), fix the request and send it again. |
| 401 / 403 | — | Missing or invalid API key. | Check the Authorization header and that the key belongs to this environment. |
| 500 | E001 | Unexpected error. | Retry with the same x-idempotency-key; contact support if it persists. |
{
"code": "E004",
"message": "The request was invalid. Please check your input and try again.",
"metadata": {
"errors": [
{
"keyword": "required",
"params": { "missingProperty": "currency" },
"message": "must have required property 'currency'",
"field": "currency"
}
]
},
"name": "TNDR004",
"statusCode": 400
}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).
Which customer a saved card belongs to
Saved cards always belong to the customer identified by the session's customer.email. Tonder
can configure your account to also require customer.phone for saved cards: the customer is
still identified by their email, and the phone is what makes their saved cards available. Ask
Tonder support whether your account requires it — it cannot be read
from the API.
When your account requires a phone:
- Send both
customer.emailandcustomer.phoneon every session that includessaved_cards, including the one where the card is first saved (and every card-enrollment session). Withoutcustomer.phonethere are no saved cards: the customer does not see them. - The phone does not replace the email. The same phone with a different email is a different customer and does not see those cards.
- Tonder compares the phone exactly as you send it. It is not normalized:
+525512345678and5512345678are not the same value. Normalize the number in your own application before you create the session, and always send the same value for the same customer, in E.164 —+, country code and number, for example+525512345678.
"customer": {
"first_name": "Maria",
"last_name": "Garcia",
"email": "maria@example.com",
"phone": "+525512345678"
}The save-card checkbox
Set payment_method_config.saved_cards.show_save_card_checkbox to true to put a "save this
card for future payments" 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 "save this card for future payments" checkbox on the new card form. Not shown when Card-on-File is enabled, because cards are then saved automatically. |
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"
}'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 (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 Card-on-File is enabled for your account, the checkout:
- Runs a 3D Secure check as the card is saved.
- Creates a subscription for that card.
- Accepts payments with it on return visits without asking for the CVV.
If Card-on-File is enabled for your account, the checkout does all of this on its own. The one
thing your integration may need to send is customer.phone, when your account also requires a
phone for saved cards — see
Which customer a saved card belongs to. It is the same
subscription the SDK surfaces as subscription_id — see the
Web SDK reference.
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
Authorization
Authorization Your public API key, with the word Token and a space in front of it. Example: Token 6534bc0a7e1f4d2b9c8e3a5f7b1d2c4e6f8a9b0c — that whole value is the header. Sending the key alone returns 401 'Authentication credentials were not provided'. In this playground you may paste just the key; the prefix is added for you.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
curl -X POST "https://example.com/checkout/v1/sessions" \ -H "Content-Type: application/json" \ -d '{ "customer": { "first_name": "Maria", "last_name": "Garcia", "email": "maria@example.com" }, "amount_total": 150, "currency": "MXN", "external_id": "ORD-001", "line_items": [ { "name": "Premium Plan", "quantity": 1, "unit_price": 150, "product_id": "plan-premium-01" } ], "payment_method_types": [ "card", "saved_cards", "oxxopay" ], "submit_type": "pay", "locale": "en", "return_url": "https://your-store.com/checkout/complete", "metadata": { "external_id": "ORD-001" } }'{
"id": "cs_97_41528_42434df6c3644b9610df7b64a1fa9925e3b9562b05972097",
"url": "https://stage-payflow.tonder.io/checkout/cs_97_41528_42434df6c3644b9610df7b64a1fa9925e3b9562b05972097",
"status": "pending",
"session_type": "payment",
"checkout_type": "hosted",
"payment_id": 41528,
"checkout_ui_version": "v2",
"submit_type": "pay",
"locale": "en",
"amount_total": 150,
"currency": "MXN",
"external_id": "ORD-001",
"return_url": "https://your-store.com/checkout/complete",
"payment_method_types": [
"card",
"saved_cards",
"oxxopay"
],
"ui_config": {},
"ui_config_version": "V1",
"created_at": 1751483563558,
"modified_at": 1751483563558,
"expires_at": 1751569963
}{
"code": "E004",
"message": "The request was invalid. Please check your input and try again.",
"metadata": {
"errors": [
{
"keyword": "required",
"params": {
"missingProperty": "currency"
},
"message": "must have required property 'currency'",
"field": "currency"
}
]
},
"name": "TNDR004",
"statusCode": 400
}{
"code": "E004",
"message": "The request was invalid. Please check your input and try again.",
"metadata": {},
"name": "TNDR004",
"statusCode": 400
}