The new Payments API. Built for how people pay now.

Tutorials

Your first checkout on the new Payments API.

How to take an order from cart to placed with payment settings, payment sessions and a single loop.

GC
Giuseppe
· October 30, 2026

A checkout on the new Payments API comes down to one sentence:

Open a session for each way the shopper pays, authorize each one, ask the order whether it is placeable, and place it.

Here we build that checkout for the order the first article closed with: €100, paid with a €30 gift card and €70 by credit card. That article introduced the model behind the new Payments API: payment settings, sessions, transactions, wallets and links, and how they combine around an order. This one puts it to work.

The rest of this article walks through that sentence one clause at a time, in TypeScript, with the Commerce Layer auth and SDK packages. We keep the example deliberately small: no retries, no redirects, no saved cards. The goal is to show the shape of the flow, not every road off it. The playground repository covers the rest.

Before you start: API version 2026-05

The new Payments API is available from API version 2026-05. This version covers both the new payment sessions and your existing payment gateways: every request that works with payment settings, gateways, or sessions goes to the versioned endpoint, /api/2026-05/.... Classic orders on the default version are unaffected and keep working where they are.

You don't build those URLs yourself. Version 8 of the Commerce Layer SDK supports 2026-05, and you choose the version once, when you create the client:

import { makeSalesChannel } from "@commercelayer/js-auth";
import { CommerceLayer } from "@commercelayer/sdk";

const salesChannel = makeSalesChannel(
  { clientId: process.env.CL_CLIENT_ID!, scope: "market:code:europe" },
  // Any Storage implementation works; the js-auth README has an in-memory one.
  { storage: memoryStorage() },
);

const { accessToken } = await salesChannel.getAuthorization();

const client = CommerceLayer({ accessToken, apiVersion: "2026-05" });
  

The token comes from a sales channel, the same kind of credential a storefront already uses, and makeSalesChannel keeps it in storage and renews it when it expires. Nothing in this checkout needs an integration key.

Where can the shopper pay?

To keep this article focused on payments, we skip the steps that come before them: the shopper entering their addresses and choosing a shipping method. Those haven't changed, and the existing API covers them exactly as before.

A checkout starts with a question: which payment methods does this order accept?

The order answers it directly, through its available_payment_settings:

const order = await client.orders.retrieve(orderId, {
  include: ["available_payment_settings"],
});

const settings = order.available_payment_settings ?? [];

const paymentSettingGiftCard = settings.find((s) => s.type === "payment_setting_gift_cards");
const paymentSettingStripe = settings.find((s) => s.type === "payment_setting_stripes");
  

Notice what is in that list. The gift card setting sits next to Stripe, with the same shape. In the classic workflow a gift card behaved like a discount, taken off the order total.

On the new model it is a payment method like any other, and the rest of this checkout treats it that way.

Open a session for each way the shopper pays

A payment session is one attempt to pay part, or all, of an order through one setting. Our order is paid two ways, so it gets two sessions.

The shopper enters a gift card code first:

const giftCardSession = await client.payment_sessions.create({
  gift_card_code: "GIFT-30-EUR",
  payment_setting: client.payment_settings.relationship(paymentSettingGiftCard.id),
  order: client.orders.relationship(order.id),
});

giftCardSession.amount_cents; // 3000
  

There is no amount_cents in that request, and that is intentional. A session created without an amount is sized by Commerce Layer to what the order still has to authorize. For a gift card, it is also capped at the card's real balance. The order owes €100, the card holds €30, so the session comes back at €30, which is the amount you show the shopper.

The card covers the rest:

const cardSession = await client.payment_sessions.create({
  amount_cents: order.total_amount_with_taxes_cents - giftCardSession.amount_cents,
  payment_setting: client.payment_settings.relationship(paymentSettingStripe.id),
  order: client.orders.relationship(order.id),
});
  

Here we pass the amount explicitly. Leaving it out works for any payment setting, but the API sizes a new session against what is already authorized, and the gift card session isn't authorized yet. So we tell it: €70.

That is the general rule for splitting a payment. Leave the amount out when one session should cover whatever is left. Set it when you decide how the total is divided. An order can have as many sessions as it needs, and each one carries its own amount.

Hand the session to the gateway

This is the only step that depends on the gateway.

When Commerce Layer creates a session, it also starts the payment on the gateway's side and returns whatever the gateway's frontend needs in the session's response_data. For Stripe, that is the client_secret of a PaymentIntent. You mount Stripe's Payment Element on it, exactly as you would in any Stripe integration:

import { loadStripe } from "@stripe/stripe-js";

const stripeJs = await loadStripe(paymentSettingStripe.public_key!);
const elements = stripeJs.elements({
  clientSecret: cardSession.response_data!.client_secret,
});
elements.create("payment").mount("#payment-element");

// When the shopper clicks Pay:
const { error } = await stripeJs.confirmPayment({
  elements,
  redirect: "if_required",
});
if (error) {
  // Declined or abandoned: show error.message and let the shopper try again.
}
  

Stripe collects the card and, if the bank asks for it, runs the 3-D Secure challenge right there, inside confirmPayment. Adyen, Braintree and Checkout.com each have their own component and their own key in response_data, but the step plays the same role for every one of them: the shopper meets the gateway, and the gateway reserves the funds.

A gift card has no such step. There is no third party to meet, so its session goes straight to authorization.

Authorize each session

In the classic workflow you authorized a payment by flipping a trigger attribute on the order.

On the new model you create the authorization itself, as a payment transaction attached to the session:

const cardAuthorization = await client.payment_authorizations.create({
  payment_session: client.payment_sessions.relationship(cardSession.id),
});
  

For Stripe, Commerce Layer reads the PaymentIntent the shopper just confirmed and records the result. Like every transaction, the authorization starts as pending and settles on its own. If it is still pending when the request returns, retrieve it again until it leaves that state: succeeded means the €70 is reserved.

3-D Secure doesn't change this picture, it only adds a stop. With Stripe the challenge has already happened by the time the authorization is created. Gateways that run it afterwards, such as Adyen's advanced flow, move the authorization to requires_action and attach the challenge for your frontend to show. The shopper completes it, you pass the result back to the same authorization, and it settles like any other. It is the same transaction, waiting on the shopper instead of on the gateway.

Now the gift card:

await client.payment_authorizations.create({
  payment_session: client.payment_sessions.relationship(giftCardSession.id),
});
  

The call is identical, but there is one difference, and it explains the order we used.

How a gift card authorization behaves depends on its payment setting. With auto_capture enabled on the payment_setting_gift_cards, the authorization captures in the same step: there is no "reserved but not yet charged" state, and once it authorizes, the €30 has left the balance.

A card authorization, on the other hand, is a hold. If something goes wrong before the order is placed, the hold can be voided and no money has moved.

So when gift cards auto-capture, authorize the card first and the gift card last. If the card is declined, the shopper's gift card is untouched and they can try another card. If the gift card fails afterwards, the card hold is released with a void. Put the step that moves money at the end, where the fewest things can still go wrong after it. Without auto_capture, the gift card authorization is a hold too, and the order matters less.

Placeable, then place

Two sessions are authorized. The order has followed along on its own: once the card authorized it was partially authorized, and with the gift card it counts as authorized. Is that enough to place it?

You don't have to add it up yourself. Ask the order:

const { placeable } = await client.orders._placeable(order.id);

if (placeable) {
  await client.orders.update({ id: order.id, _place: true });
}
  

_placeable checks the order's sessions against the market's payment rules. By default the rule is the strict one: authorized sessions must cover the total in full. A market can lower that bar, for example by allowing placement once a percentage of the order is authorized, and _placeable reflects whatever the market decided. Your checkout never needs to know the rule; asking _placeable is enough. The rules themselves, and every condition they can express, are documented in the payment rules reference.

Here €30 plus €70 covers €100, so the order is placeable, and _place places it. Capturing the card authorization is a separate step that belongs to fulfillment, not to the checkout.

The whole loop

Put together, the checkout is a loop over sessions followed by one question and one action:

settings = order.available_payment_settings

for each way the shopper pays:
  session = create payment_session(setting, amount_cents?)

for each session, card-like sessions first, gift cards last:
  if the gateway has a client step:
    mount it on session.response_data and let the shopper complete it
  authorization = create payment_authorization(session)
  if authorization requires action (3-D Secure):
    let the shopper complete it and pass the result back
  wait until authorization settles

if order._placeable:
  place the order
else:
  open another session for what is missing, and go round again
  

Read it again with one session instead of two and nothing changes: one setting, one session, one authorization, one question. That is the simplest checkout on the new model, and it is already the general one. Adding a second card, a second gift card or a different gateway adds an iteration to the loop, not a branch to your code.

What we left out

A real checkout has more to handle than this example, and the playground handles it. A few things we skipped on purpose:

  • Redirects and asynchronous methods
    Some payments leave the page, to a bank or a wallet, and come back later. The authorization is created on the way back, and the loop picks up where it left off.
  • Saved cards
    A session can reference a customer's payment wallet, so a returning shopper pays without entering their card details again.
  • A failed place
    If an order can't be placed after a gift card has been charged, the gift card is refunded against its capture.
  • Changing the cart mid-checkout
    A session's amount is fixed when it is created. If the total changes, unauthorized sessions are deleted and opened again at the new amount.

Each of these adds a way into the loop or out of it; the loop itself stays the same.

Try it in the Payments API playground

The Payments API playground has a complete checkout for each gateway, with gift cards, split payments, saved cards, redirects and 3-D Secure, so you can start from the gateway you use and see this loop in real code. The API reference for version 2026-05 documents every resource and attribute we used here.