Next Commerce

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:

Order payment payload

Bankcard orders set payment_method to card_token and pass the tokenized card (and any optional fields) in payment_details:

Bankcard Payment Detail
"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)
}
FieldTypeDescription
card_tokenstringTokenized card produced by the iFrame payment form.
save_cardbooleanRetain the card for future charges (subscriptions, one-click upsells). Defaults to true.
statement_descriptorstringCustom bank-statement descriptor (≤ 22 alphanumeric chars, spaces, and & , . - #).
payment_gatewayintegerOptional. Charge a specific gateway by id. See Gateway routing.
payment_gateway_groupintegerOptional. Charge a gateway group by id. See Gateway routing.
payment_return_urlstring (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:

FieldTypeRoutes to
payment_gatewayintegerA single gateway by its id.
payment_gateway_groupintegerA gateway group by its id. The platform selects a member gateway.
Route to a specific gateway or group
"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:

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.

Try the Demo. The source is on Github.

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

  1. Add payment.js with your environment key.
  2. Add the form. Card number and security code are empty containers, the rest are normal inputs.
  3. Create a NextPayment and assign callbacks.
  4. Call submit() from your form handler.
  5. Read the token in onTokenized.
Add payment.js
<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.

Example Payment Form HTML
<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.

Initialize NextPayment
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

OptionRequiredDescription
numberElYesContainer element id where the card number iFrame is mounted.
cvvElYesContainer element id where the security code iFrame is mounted.
numberFormatNoOne of prettyFormat (default), plainFormat, or maskedFormat.
labelsNo{ number, cvv } accessible labels. Defaults to Card Number and Security Code.
placeholderNo{ number, cvv } placeholder text. Defaults to Card Number and CVV.
titlesNo{ number, cvv } title attributes. Defaults to Enter your card number and Enter your Security Code.
stylingNo{ 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.

Example form customization
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.

Card type icon next to the card number field
<img id="id_card_type" height="24" alt="" hidden>
<div id="id_card_number"></div>
Focus styling and card type detection with onFieldStateChange
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

FieldDescription
fieldnumber or cvv.
actionOne of focus, blur, input, mouseover, mouseout, enter, escape, tab, or shiftTab.
cardTypeDetected brand: visa, master, american_express, discover, diners_club, jcb, maestro, dankort, and others. Empty until a brand matches.
numberLength, cvvLengthDigits entered in each field.
validNumber, validCvv, luhnValidCurrent validity of each field.
focused, hoveredWhether 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.

Tokenize Card on Form Submit
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

FieldRequiredDescription
full_nameYesCardholder name. first_name and last_name do not replace it.
monthYesExpiry month, 1 to 12.
yearYesFour-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.

Retrieve Card Token
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.

Example onTokenized payload
{
  "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.

Error Handling
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.

attributeValidated by
numberThe card number iFrame.
cvvThe security code iFrame.
monthsubmit() pre-validation.
yearsubmit() pre-validation.
full_namesubmit() pre-validation.
Example Validation Errors
[
  {
    "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 sourcePayload
Card number or security code rejected, or submit throttledA string message, for example Invalid CVV.
Tokenization API failureAn object with an errors array. Each entry has attribute, key, and message.
Other SDK failuresAn 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.

setFocus
payment.setFocus("number");

Arguments

NameDescription
fieldnumber, 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.

destroy
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 v1NextPayment
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 Details for Order with 3DS2 Payment
"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.

Response with Payment Complete URL
{
    "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.

Example Parsing of Order Data
import json

def order_receiver_view(request):
    data = json.loads(request.POST.get("response"))
    ...
    return HttpResponse(status=201)

On this page