Manual payments


Chec/Commerce.js provides the functionality to defer payment to a manual process - meaning it's not required to capture an order. This is referred to as the "manual" gateway. The manual gateway can be used to facilitate business requirements like "cash on delivery", or to support custom payment options like alternative payments gateways or subscription based payments.

In this guide

  • Creating manual gateways in the Chec Dashboard
  • Considerations and caveats with the manual gateway
  • An example implementation that shows how to capture orders with Commerce.js using the manual gateway
  • A high-level overview of how you might support a payment gateway that doesn't have native support with Chec/Commerce.js

Creating a manual gateway in the Chec Dashboard

The manual gateway appears in the Dashboard with the other gateways. When you add the manual gateway you will need to provide a few fields:

  • Method name - Given as a name to display when the customer is choosing a payment method
  • Details - Provided with the name of the payment method that is given when generating a checkout token
  • Payment instructions - Given with the order details after capturing an checkout

The first two options provide some flexibility for updating the display of the payment method from the dashboard, and the last option allows for detailed instructions to be provided and maintained from the dashboard.

Considerations and caveats

Although the manual gateway is the most flexible option for developers to implement payments, it does have important drawbacks that need to be considered. Enabling the manual payment gateway in the Chec Dashboard means that any order may be captured without payment, even if your storefront isn't configured to support the manual payment method. Any user familiar with the Chec API may use your public key (part of the source code of your storefront) to make orders with the manual gateway that doesn't require a physical payment.

Additionally, when trying to tie in a custom gateway, you will need to decide whether capturing the order or taking a payment should come first. The choice you make will affect whether you might have "orphaned" orders or payments.

Example implementation

To capture an order with the manual gateway, you must provide the ID of the manual gateway to use as you may create multiple manual gateways in the Chec Dashboard. Valid IDs for the manual gateway are provided when generating a checkout token.

The examples provided here also assume you are familiar with some modern JavaScript features including async/await, "shorthand property names", and object destructuring assignment.

// Generate a checkout token from an existing cart
const checkout = await commerce.checkout.generateTokenFrom('cart', 'cart_2Jwr9yJAeN4VlP');

// Access available manual gateways - This will be an array of available manual options
const manualOptions = checkout.gateways.manual;

// Each option has an ID, a name, and details.
manualOptions.forEach((option) => console.log(,, option.details));

You may allow the customer to choose which manual option they would like to use, or you may choose the manual option that applies programmatically. Remember that Chec/Commerce.js has no way to enforce any business logic that might make only a specific manual method available, so you will need to enforce the payment method is correct on individual orders directly with your customers.

To choose a manual payment method and continue with capturing the order, you just need to provide the chosen manual payment ID:

// Create a function that can be called when a "Complete order" button is clicked
function captureCheckout() {
  // Using the checkout token we created earlier, we can use Commerce.js to capture the checkout
  commerce.checkout.capture(, {
    // Add in any order details we might have gathered on this page
    payment: {
      gateway: 'manual',
      manual: {
        id: 'gtwy_zURSwkv193kOIY2bfS',
  }).then((order) => {
    // The checkout has been successfully captured and converted to an order using the manual payment method.
    // The payment instructions are provided on the order object. Payments on an order is an array, but capturing
    // checkouts only supports single payments, so we know it'll be the first (and only) payment on the order
  }).catch((error) => {
    // Something went wrong during capture:

Implementing a custom gateway with the manual payment method

The flexibility offered by the manual payment method means that you may do some more technical integrations with Commerce.js to support payment options specific to your business requirements. We can illustrate the flexibility and caveats of this by covering how we might integrate with Klarna for payments.

Note that the examples and code provided in this section are theoretical, and are not tested or ready for production use.

The first requirement with any custom gateway is that you will need to run some code on a server, as we can't trust the client side with all the communication with the gateway. The first step required with Klarna is to create a session. This needs to be completed on the server side. With the JamStack, you might be able do this with a serverless function. The examples of server side code will be provided as JavaScript, intended to be run in a Node.js environment.

You can combine the processes of generating a checkout token and creating a Klarna session into the same call:

// GET request to your server, eg:
// /create-checkout?cart=cart_2Jwr9yJAeN4VlP
export const handler = async (request) => {
  const { cart } = request.params;

  // Create a checkout token from the given cart using Commerce.js
  const token = await commerce.checkout.createFromCart('cart', cart);

  // Create a Klarna session with their API
  const klarnaResponse = await fetch(`${klarnaUrl}/payments/v1/sessions`, {
    method: 'POST',
    headers, // Assuming a `headers` variable is available with content-type and authorization set
    body: JSON.stringify({
      locale: 'en-GB',
      order_lines:* ... */), // Klarna expect a list of items in the order.
  }).then(response => response.json());

  // Assumes a "respond" function is created to help respond with JSON payloads and a status code
  return respond({
    paymentSession: klarnaResponse.client_token,
  }, 200);

This endpoint can be used in place of commerce.checkout.createFromCart on your storefront to generate checkout tokens. Note that because the Klarna session needs to know the true value that needs to be charged, any updates to the token need to update the Klarna session. In cases where you use a Commerce.js checkout helper method that affects the amount due, you will have to ensure the session is updated on the server side. You could do this by having a server side endpoint that fetches the token from Chec, and updates the Klarna session with the current amount due.

Next, the Klarna widget can be shown as described in their documentation. Once the widget is shown, the customer can use it to enter their payment details. The customer can then click on a button to continue with the order, and you will need to authorize the payment with Klarna. Klarna provides a separate "authorize" and "finalize" process for taking payment, which helps to avoid issues where you end up with an order without a payment (and vice versa) when there's an issue during this process. With this flow, the Commerce.js checkout should be captured in between the authorize and finalize calls with Klarna.

Once the payment has been completed and the finalize process has returned successful, you will need to update the payment on the order so that it shows as completed. This needs to be done using a Chec secret key API, so needs to be part of your server side logic:

const querystring = require('querystring');

// POST /register-payment
export const handler = async (request) => {
  // Take the attribute provided in the body of the request for the Chec order ID and the Klarna session ID
  const { orderId, klarnaSession } = querystring.parse(request.body);

  // Fetch the order from Chec
  const order = await fetch(`${checUrl}/v1/orders/${orderId}`, {
    headers: checHeaders,
  }).then(response => response.json());

  // Fetch the session from Klarna
  const klarnaResponse = await fetch(`${klarnaUrl}/payments/v1/sessions/${klarnaSession}`, {
    headers: klarnaHeaders,
  }).then(response => response.json());

  // Assert that the payment is completed with Klarna
  if (klarnaResponse.status !== 'complete') {
    return respond('The payment is not complete', 402);

  // Update the payment transaction on the order with Chec (but don't wait for a response)
  const transactionId = order.payments[0].id;
  fetch(`${checUrl}/v1/orders/${orderId}/transactions/${transactionId}`, {
    headers: checHeaders,
    body: JSON.stringify({
      status: 'complete',
      gateway_transaciton_id: '', // Optionally provide some message or ID from Klarna
  }).then(response => response.json());

  return respond('', 204);

This process isn't completely flawless, as the payment and order aren't being completed by the same system. It's important to understand that payments or orders may be left without the other.