Bankcard
Charge tokenized bankcards on the Admin API with card_token, including gateway routing, iFrame card tokenization, and 3D Secure (3DS2)
Bankcard is the core payment method on the Admin API. Cards are charged by passing payment_method: card_token with a tokenized card in payment_details. Tokenizing the card with our iFrame payment form keeps sensitive card data off your servers and reduces your PCI compliance scope.
This guide covers the full bankcard flow:
- Gateway routing: route charges across one or more payment gateways.
- Card tokenization: get a
card_tokenwith the iFrame payment form. - 3D Secure (3DS2): process cards through an authentication flow.
Order payment payload
Bankcard orders set payment_method to card_token and pass the tokenized card (and any optional fields) in payment_details:
"payment_method": "card_token",
"payment_details": {
"card_token": "<card token>", // from the iFrame (see below)
"save_card": true, // retain card for future charges (default true)
"statement_descriptor": "BRANDNAME", // optional, up to 22 chars
"payment_gateway": 12, // optional, route to a specific gateway
"payment_gateway_group": 3, // optional, route to a gateway group
"payment_return_url": "<url>" // required for 3DS (see below)
}| Field | Type | Description |
|---|---|---|
card_token | string | Tokenized card produced by the iFrame payment form. |
save_card | boolean | Retain the card for future charges (subscriptions, one-click upsells). Defaults to true. |
statement_descriptor | string | Custom bank-statement descriptor (≤ 22 alphanumeric chars, spaces, and & , . - #). |
payment_gateway | integer | Optional. Charge a specific gateway by id. See Gateway routing. |
payment_gateway_group | integer | Optional. Charge a gateway group by id. See Gateway routing. |
payment_return_url | string (uri) | Required for 3DS. Your endpoint that receives the final order data. See 3D Secure. |
For the complete order request (lines, user, addresses, shipping), see the External Checkout Flow guide. This guide focuses on the bankcard-specific payment detail.
Gateway Routing
By default, bankcard charges route through your store's configured payment gateway. For stores with more than one gateway, you can optionally pin a charge to a specific gateway or gateway group by passing one of these fields in payment_details:
| Field | Type | Routes to |
|---|---|---|
payment_gateway | integer | A single gateway by its id. |
payment_gateway_group | integer | A gateway group by its id. The platform selects a member gateway. |
"payment_method": "card_token",
"payment_details": {
"card_token": "<card token>",
"payment_gateway": 12 // OR "payment_gateway_group": 3
}Pass either payment_gateway or payment_gateway_group, not both. If you pass neither, the store's default routing applies.
Get gateway and group ids
The ids are integers from the Payments API:
- Gateways:
gatewaysListandgatewaysRetrieve - Gateway groups:
gatewayGroupsListandgatewayGroupsRetrieve
A gateway group also exposes its supported available_currencies, available_payment_methods, and card_types, so you can pick the right group for a given order.
When to route
Most stores let the platform route automatically. Pass an explicit gateway or group when you need to:
- Load balance volume across several gateways or processors.
- Send orders to the gateway that supports a given currency or market.
- Fail over to a backup gateway, or use a group so a soft decline can be retried on another gateway in the group.
Group membership, distribution weighting, and soft-decline retries are configured per gateway in your store dashboard. The API only selects among what is already configured.
Card Tokenization (iFrame)
Tokenize cards in our iFrame payment form before you send them to the Admin API. Your checkout never handles raw card data, which keeps it out of PCI scope.
payment.js is a script hosted by Next Commerce. It exposes the NextPayment class, which mounts the card number and security code fields in iFrames, validates them, and returns a token. You can style the fields to match the rest of your form.
Setup
You need your store's Payments Environment Key. Find it under Settings > Payments, or read payments.environment_key from the Store Detail endpoint.
Card tokenization steps
- Add
payment.jswith your environment key. - Add the form. Card number and security code are empty containers, the rest are normal inputs.
- Create a
NextPaymentand assign callbacks. - Call
submit()from your form handler. - Read the token in
onTokenized.
<script src="https://payments.29next.com/js/v1/payment.js?env_key=<STORE PAYMENT ENVIRONMENT KEY>"></script>env_key is required. A key that matches no store still returns the script, but without credentials, so every tokenization fails.
Add the form. NextPayment mounts iFrames into the two empty div elements. The other fields are normal inputs.
<form id="payment-form" onsubmit='submitPaymentForm(); return false;' novalidate>
<label for="id_card_number">Card Number</label>
<div id="id_card_number"></div>
<label for="id_cardholder_name">Cardholder Name</label>
<input id="id_cardholder_name" name="cc-name" autocomplete="cc-name" type="text" placeholder="Full Name on Card">
<label for="id_card_exp_month">Expiry Month</label>
<select id="id_card_exp_month" name="expiry_month" autocomplete="cc-exp-month" required>
<option value="" selected disabled>MM</option>
</select>
<label for="id_card_exp_year">Expiry Year</label>
<select id="id_card_exp_year" name="expiry_year" autocomplete="cc-exp-year" required>
<option value="" selected disabled>YYYY</option>
</select>
<label for="id_card_cvv">Security Code</label>
<div id="id_card_cvv"></div>
<button type="submit" disabled>Pay Now</button>
</form>Create the instance. The constructor starts loading the iFrames at once, so assign callbacks right after it.
const submitButton = document.querySelector('#payment-form button[type="submit"]');
const payment = new NextPayment({
numberEl: "id_card_number",
cvvEl: "id_card_cvv",
});
payment.onReady = () => {
submitButton.removeAttribute('disabled');
payment.setFocus("number");
};numberEl and cvvEl are element ids, not selectors. Keep the submit button disabled until onReady fires.
Configuration options
| Option | Required | Description |
|---|---|---|
numberEl | Yes | Container element id where the card number iFrame is mounted. |
cvvEl | Yes | Container element id where the security code iFrame is mounted. |
numberFormat | No | One of prettyFormat (default), plainFormat, or maskedFormat. |
labels | No | { number, cvv } accessible labels. Defaults to Card Number and Security Code. |
placeholder | No | { number, cvv } placeholder text. Defaults to Card Number and CVV. |
titles | No | { number, cvv } title attributes. Defaults to Enter your card number and Enter your Security Code. |
styling | No | { number, cvv, placeholder } objects of camelCase CSS properties. See Style iFrame Fields. |
Both fields are always required and use input type text. Card type detection is covered below.
Style iFrame Fields
Pass a styling object so the iFrame fields match your native inputs. Give it the same font, size, weight, line height, colour, and padding. Each value is an object of camelCase CSS properties, not a CSS string. placeholder styles the placeholder text in both fields. The demo copies Bootstrap's .form-control values.
const formControlStyle = {
width: '100%',
padding: '.375rem .75rem',
fontSize: '1rem',
fontWeight: '400',
lineHeight: '1.5',
color: '#212529',
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
};
const payment = new NextPayment({
numberEl: "id_card_number",
cvvEl: "id_card_cvv",
styling: {
number: formControlStyle,
cvv: formControlStyle,
placeholder: { color: 'rgba(33, 37, 41, 0.75)' },
},
});Serve the page over HTTP or HTTPS. Opened from disk, the page has a null origin, the SDK cannot reach the iFrames, onReady never fires, and the fields keep the browser's default styling.
CSS :focus cannot reach inside an iFrame. Use onFieldStateChange to style the container on focus and blur. The same payload carries the detected card type, so you can show a brand icon as the customer types.
<img id="id_card_type" height="24" alt="" hidden>
<div id="id_card_number"></div>const cardTypeIcons = {
visa: 'img/cardbrands/visa.svg',
master: 'img/cardbrands/mastercard.svg',
american_express: 'img/cardbrands/amex.svg',
discover: 'img/cardbrands/discover.svg',
diners_club: 'img/cardbrands/diners_club.svg',
jcb: 'img/cardbrands/jcb.svg',
maestro: 'img/cardbrands/maestro.svg',
dankort: 'img/cardbrands/dankort.svg',
};
payment.onFieldStateChange = (payload) => {
const {action, field, cardType} = payload;
const cardTypeEl = document.getElementById('id_card_type');
if (cardTypeIcons[cardType]) {
cardTypeEl.src = cardTypeIcons[cardType];
cardTypeEl.hidden = false;
} else {
cardTypeEl.hidden = true;
}
const fieldEl = document.querySelector(`#id_card_${field}`);
if (!fieldEl) return;
if (action === "focus") {
fieldEl.classList.add('active');
}
if (action === "blur") {
fieldEl.classList.remove('active');
}
}onFieldStateChange payload
| Field | Description |
|---|---|
field | number or cvv. |
action | One of focus, blur, input, mouseover, mouseout, enter, escape, tab, or shiftTab. |
cardType | Detected brand: visa, master, american_express, discover, diners_club, jcb, maestro, dankort, and others. Empty until a brand matches. |
numberLength, cvvLength | Digits entered in each field. |
validNumber, validCvv, luhnValid | Current validity of each field. |
focused, hovered | Whether the field is focused or hovered. hovered is only present on mouse actions. |
The demo's icons are in img/cardbrands. The icon updates on every input action.
Tokenize Card
Call submit() from your form handler with the cardholder values as formData.
function submitPaymentForm() {
submitButton.setAttribute('disabled', true);
submitButton.textContent = 'Processing...';
let full_name = document.querySelector('#id_cardholder_name').value;
let month = document.querySelector('#id_card_exp_month').value;
let year = document.querySelector('#id_card_exp_year').value;
const formData = {
full_name,
month,
year
};
payment.submit(formData);
};submit() returns at once. The result arrives in onTokenized or onError. First, submit() checks month, year, and full_name. If any fail, it reports them in onValidation and stops. Neither onTokenized nor onError fires for that attempt.
Always assign onValidation. Without it, submit() skips the check and sends the data anyway.
formData fields
| Field | Required | Description |
|---|---|---|
full_name | Yes | Cardholder name. first_name and last_name do not replace it. |
month | Yes | Expiry month, 1 to 12. |
year | Yes | Four-digit expiry year, such as 2028. Two-digit years are rejected. |
Retrieve Card Token
onTokenized fires with the result, including the payment method token and card details. Send the token to your backend and pass it to the Admin API as card_token when you create the order.
payment.onTokenized = (result) => {
const payment_method = result.tokenResponse.payment_method;
console.log('Payment Method Data:', payment_method);
document.getElementById("card-token").textContent = payment_method.token;
submitButton.textContent = 'Pay Now';
submitButton.removeAttribute('disabled');
};There are two tokens. tokenResponse.token is the transaction. tokenResponse.payment_method.token is the payment method. Pass the second one as card_token.
{
"message": "Token generated",
"tokenResponse": {
"token": "7Q88WFXX2C91YTWQM24GKAEV4A",
"succeeded": true,
"transaction_type": "AddPaymentMethod",
"state": "succeeded",
"message": "Succeeded!",
"payment_method": {
"token": "01M06YP7T3G2RV5ZJ5H1DSTXBD",
"storage_state": "cached",
"test": true,
"last_four_digits": "1111",
"first_six_digits": "411111",
"card_type": "visa",
"month": 11,
"year": 2026,
"full_name": "a t",
"payment_method_type": "credit_card",
"fingerprint": "474713f866e20b9c7f143ad7643364be37e0",
"number": "XXXX-XXXX-XXXX-1111"
}
}
}Error Handling
Two callbacks report errors. onValidation fires when submit() is blocked by a bad field. onError fires when the tokenization request fails. Nothing fires while the customer is typing.
function fieldSelectorForAttribute(attribute) {
if (attribute === 'number') return '#id_card_number';
if (attribute === 'cvv') return '#id_card_cvv';
if (attribute === 'month') return '#id_card_exp_month';
if (attribute === 'year') return '#id_card_exp_year';
if (attribute === 'full_name') return '#id_cardholder_name';
return null;
}
function setFieldInvalid(selector, message) {
let el = document.querySelector(selector);
if (!el) return;
el.classList.add('is-invalid');
}
payment.onValidation = (payload) => {
document.querySelectorAll('#payment-form .is-invalid').forEach(el => el.classList.remove('is-invalid'));
(payload.errors || []).forEach(err => {
let selector = fieldSelectorForAttribute(err.attribute);
if (selector) setFieldInvalid(selector, err.message);
});
submitButton.removeAttribute('disabled');
submitButton.textContent = 'Pay Now';
};
payment.onError = (error) => {
console.log('on error: ', error);
if (typeof error === 'string') {
alert(error);
} else if (error.errors && Array.isArray(error.errors)) {
error.errors.forEach(err => {
let selector = fieldSelectorForAttribute(err.attribute);
if (selector) setFieldInvalid(selector, err.message);
});
} else if (error.message) {
alert(error.message);
}
submitButton.removeAttribute('disabled');
submitButton.textContent = 'Pay Now';
};onValidation
payload.errors is an array of objects with attribute, key, and message. Two sources feed it, both from submit(). NextPayment checks month, year, and full_name first. If those pass, the iFrames check number and cvv. Each firing covers only its own fields.
attribute | Validated by |
|---|---|
number | The card number iFrame. |
cvv | The security code iFrame. |
month | submit() pre-validation. |
year | submit() pre-validation. |
full_name | submit() pre-validation. |
[
{
"attribute": "month",
"key": "errors.invalid",
"message": "Expiry month is required"
},
{
"attribute": "full_name",
"key": "errors.invalid",
"message": "Cardholder name is required"
}
]onError
The payload varies by cause, so check its type first.
| Error source | Payload |
|---|---|
| Card number or security code rejected, or submit throttled | A string message, for example Invalid CVV. |
| Tokenization API failure | An object with an errors array. Each entry has attribute, key, and message. |
| Other SDK failures | An object with a message field. |
When the iFrame rejects the number or security code, onValidation fires first, then onError repeats it as a string.
Methods
setFocus
Focus one of the iFrame fields, for example the card number on page load or a field with an error. Does nothing before onReady.
payment.setFocus("number");Arguments
| Name | Description |
|---|---|
| field | number, cvv, or iframe. |
destroy
Remove the iFrames. Call it before you discard an instance, such as when a modal closes, so the old instance stops reacting to events.
payment.destroy();Test Cards
Use the Test Gateway card numbers with any name, future expiry, and security code. The demo uses 4111111111111111.
Migrating from Spreedly iFrame v1
Older integrations load iframe-v1.min.js and call Spreedly directly. Replace that script with payment.js from Setup and map the calls:
| Spreedly iFrame v1 | NextPayment |
|---|---|
Spreedly.init(envKey, options) | new NextPayment(options) |
Spreedly.on('ready') | payment.onReady |
Spreedly.setStyle() | styling option, camelCase objects |
Spreedly.setPlaceholder() | placeholder option |
Spreedly.setNumberFormat() | numberFormat option |
Spreedly.setFieldType() | Not configurable, always text |
Spreedly.transferFocus() | payment.setFocus() |
Spreedly.tokenizeCreditCard() | payment.submit() |
Spreedly.on('paymentMethod') | payment.onTokenized, token at payment_method.token |
Spreedly.on('errors') | payment.onValidation and payment.onError |
3D Secure (3DS2)
3DS2 payments are fully supported via the Admin API to process the customer through an authentication flow, with the final transaction information and results provided back to your application.
Your store must have a 3DS2-enabled gateway to process 3DS2 transactions.
API payment redirect flow
Below is a high-level overview of the user flow when creating orders on the Admin API that utilize the payment method redirect flow.
Create the order
When creating an order using a 3DS2-enabled gateway, use payment_method=card_token and provide a payment_return_url. The payment_return_url is your endpoint that will receive a POST request containing the final order data.
"payment_method": "card_token",
"payment_details": {
"card_token": "<card token>",
"payment_return_url": "<external checkout url>",
"payment_gateway": 12, // optional
"payment_gateway_group": 3 // optional
}You can optionally provide a payment_gateway or payment_gateway_group (see Gateway routing) to authenticate against a specific gateway configured in the store.
Redirect to the payment complete URL
The order response provides a payment_complete_url. Redirect the customer to this URL to complete the payment authentication.
{
"reference_transaction_id": null,
"payment_complete_url": "https://<domain>/payments/3ds-auth/?token=<transaction token>"
}Receive order data
After the customer has completed their payment, they will be redirected to your application with a POST request containing data in the response key comprising all of the order information as a string. See examples below.
Order data structure follows Admin Order API and is application/x-www-form-urlencoded in a variable called response. If the order data is an empty dictionary , it means payment collection was unsuccessful and the order was not created.
import json
def order_receiver_view(request):
data = json.loads(request.POST.get("response"))
...
return HttpResponse(status=201)Payment Methods
Capability matrix of Admin API payment methods by flow type, express checkout, upsell, and subscription support, with links to each guide
Apple Pay Admin API Guide
Create Admin API orders paid with Apple Pay using the redirect payment flow, including standard and one-click checkout options