Skip to content
Last updated

Migrating from Vipps Checkout to Kustom Checkout

This guide walks you through replacing a Vipps MobilePay Checkout integration with Kustom Checkout. It covers every call and data point you need to change, in the order you should change them.

Background: Kustom Checkout is the rebranded Klarna Checkout (KCO). If you previously integrated Klarna Checkout and are now also moving to Kustom-branded endpoints, see the note on base URLs in Step 2 — old *.klarna.com API credentials stopped working on March 31, 2026.


At a glance

DimensionVipps CheckoutKustom Checkout
Auth methodMultiple HTTP headersHTTP Basic Auth (username:password)
Base URL (test)https://apitest.vipps.no/checkout/v3See Merchant Portal for -regionspecific URL
Base URL (prod)https://api.vipps.no/checkout/v3https://api.kustom.co/checkout/v3
Unit createdSession (reference)Order (order_id)
Create endpointPOST /sessionPOST /orders
Read statusGET /session/{reference}GET /orders/{order_id}
UpdatePATCH /session/{reference}POST /orders/{order_id}
AbortSession expires (1 h TTL)POST /orders/{order_id}/abort
FrontendLoad SDK + init with tokenInject html_snippet into DOM
User redirectreturnUrlmerchant_urls.confirmation
Server callbackcallbackUrlmerchant_urls.push
Country / marketImplied by MSN headerpurchase_country field
LocaleSDK language paramlocale field in order body
Order acknowledgmentNot requiredRequiredPOST /ordermanagement/v1/orders/{id}/acknowledge
CaptureVia ePayment APIVia Order Management API

Step 1 — Replace credentials and authentication

Vipps: header-based authentication

Vipps requires five or more headers on every request:

client_id: <your-client-id>
client_secret: <your-client-secret>
Ocp-Apim-Subscription-Key: <your-subscription-key>
Merchant-Serial-Number: <your-msn>
Vipps-System-Name: <your-system-name>
Vipps-System-Version: 3.1.2
Vipps-System-Plugin-Name: <your-plugin-name>
Vipps-System-Plugin-Version: 4.5.6

Kustom: HTTP Basic Auth

Kustom uses standard HTTP Basic Auth. Your credentials are a username and password tied to your Kustom Merchant ID.

# Encode once and store as a constant
echo -n "username:password" | base64
# pwhcueUff0MmwLShJiBE9JHA==
Authorization: Basic pwhcueUff0MmwLShJiBE9JHA==
Content-Type: application/json

Get your credentials from the Kustom Merchant Portal. Generate separate credentials for test and production — never share them.

What to change in your code:

Remove all Vipps-specific headers from your HTTP client. Add a single Authorization: Basic <base64> header. If you set headers centrally in a middleware or HTTP client factory, this is a one-line change.


Step 2 — Update base URLs

Replace the Vipps base URL with the Kustom base URL in your environment config.

EnvironmentVippsKustom
Test (Playground)https://apitest.vipps.no/checkout/v3https://api.playground.kustom.co/
Productionhttps://api.vipps.no/checkout/v3https://api.kustom.co/

If you had a previous Klarna Checkout integration, the old *.klarna.com production URLs are still active until March 31, 2026, but the Playground URL must be updated manually. Switch early to avoid disruptions.


Step 3 — Replace session creation with order creation

This is the largest change. The two payloads have different structures but map closely to each other.

Vipps: POST /checkout/v3/session

{
  "merchantInfo": {
    "callbackUrl": "https://example.com/checkout/callback",
    "returnUrl": "https://example.com/checkout/result?ref={reference}",
    "callbackAuthorizationToken": "538dd1d0-9e7f-4732-8134-dfed7fd0b236"
  },
  "transaction": {
    "amount": {
      "value": 49900,
      "currency": "NOK"
    },
    "reference": "order-abc-123",
    "paymentDescription": "Yellow T-Shirt × 5",
    "orderSummary": {
      "orderLines": [
        {
          "name": "Yellow T-Shirt",
          "id": "SKU-001",
          "totalAmount": 49900,
          "totalAmountExcludingTax": 39920,
          "totalTaxAmount": 9980,
          "taxRate": 2500,
          "unitInfo": {
            "unitPrice": 9980,
            "quantity": "5",
            "quantityUnit": "PCS"
          },
          "discount": 0,
          "productUrl": "https://example.com/products/yellow-tshirt",
          "isReturn": false,
          "isShipping": false
        },
        {
          "name": "Standard delivery",
          "id": "shipping-01",
          "totalAmount": 5000,
          "totalAmountExcludingTax": 5000,
          "totalTaxAmount": 0,
          "taxRate": 0,
          "discount": 0,
          "isReturn": false,
          "isShipping": true
        }
      ],
      "orderBottomLine": {
        "currency": "NOK"
      }
    }
  },
  "configuration": {
    "elements": "PaymentAndContactInfo",
    "showOrderSummary": true
  }
}

Response:

{
  "token": "eyJhbGciOiJSUzI1...",
  "checkoutFrontendUrl": "https://checkout.vipps.no/?token=eyJ...",
  "pollingUrl": "https://api.vipps.no/checkout/v3/session/order-abc-123"
}

Kustom: POST /checkout/v3/orders

{
  "purchase_country": "NO",
  "purchase_currency": "NOK",
  "locale": "nb-NO",
  "order_amount": 54900,
  "order_tax_amount": 9980,
  "order_lines": [
    {
      "type": "physical",
      "reference": "SKU-001",
      "name": "Yellow T-Shirt",
      "quantity": 5,
      "quantity_unit": "pcs",
      "unit_price": 9980,
      "tax_rate": 2500,
      "total_amount": 49900,
      "total_discount_amount": 0,
      "total_tax_amount": 9980
    },
    {
      "type": "shipping_fee",
      "reference": "shipping-01",
      "name": "Standard delivery",
      "quantity": 1,
      "quantity_unit": "pcs",
      "unit_price": 5000,
      "tax_rate": 0,
      "total_amount": 5000,
      "total_discount_amount": 0,
      "total_tax_amount": 0
    }
  ],
  "merchant_urls": {
    "terms": "https://example.com/terms",
    "checkout": "https://example.com/checkout?kustom_order_id={checkout.order.id}",
    "confirmation": "https://example.com/confirmation?kustom_order_id={checkout.order.id}",
    "push": "https://example.com/api/kustom/push?kustom_order_id={checkout.order.id}"
  }
}

Response:

{
  "order_id": "8cf27b55-53e8-6aba-9fb4-7c692e56ddee",
  "status": "checkout_incomplete",
  "purchase_country": "NO",
  "purchase_currency": "NOK",
  "locale": "nb-NO",
  "order_amount": 54900,
  "order_tax_amount": 9980,
  "order_lines": [...],
  "merchant_urls": {...},
  "html_snippet": "<div id=\"klarna-checkout-container\">...</div>",
  "options": {
    "allow_separate_shipping_address": false,
    "date_of_birth_mandatory": false,
    "require_validate_callback_success": false
  }
}

Store the order_id — you will need it for all subsequent operations.


Field mapping reference

Vipps fieldKustom fieldNotes
transaction.amount.valueorder_amountSum of all order_lines[].total_amount
transaction.amount.currencypurchase_currencyISO 4217
(implied by MSN)purchase_countryISO 3166-1 alpha-2
(SDK param)localeBCP 47, e.g. nb-NO, en-GB
transaction.referenceNot a request fieldKustom generates order_id in response
transaction.paymentDescriptionNot a direct fieldUse order_lines[].name for line descriptions
merchantInfo.callbackUrlmerchant_urls.pushPush = server-to-server notification
merchantInfo.returnUrlmerchant_urls.confirmationUser browser redirect on success
merchantInfo.callbackAuthorizationTokenNot usedKustom does not send an auth token in push calls
orderSummary.orderLines[].idorder_lines[].referenceYour SKU or line identifier
orderSummary.orderLines[].nameorder_lines[].nameDisplay name
orderSummary.orderLines[].totalAmountorder_lines[].total_amountMinor units (integers)
orderSummary.orderLines[].totalAmountExcludingTax(derived)total_amount - total_tax_amount
orderSummary.orderLines[].totalTaxAmountorder_lines[].total_tax_amountMinor units
orderSummary.orderLines[].taxRateorder_lines[].tax_rateBasis points (2500 = 25%)
orderSummary.orderLines[].unitInfo.unitPriceorder_lines[].unit_priceMinor units
orderSummary.orderLines[].unitInfo.quantityorder_lines[].quantityKustom is an integer, not a string
orderSummary.orderLines[].unitInfo.quantityUnitorder_lines[].quantity_unite.g. pcs
orderSummary.orderLines[].isShipping: trueorder_lines[].type: "shipping_fee"Kustom uses type instead of a flag
orderSummary.orderLines[].isReturn: trueHandled via Order Management APICredit/return flow is separate in Kustom
orderSummary.orderLines[].discountorder_lines[].total_discount_amountMinor units
orderSummary.orderLines[].productUrl(no direct field in basic payload)Can be set via Order Management API

order_lines[].type values

TypeUse for
physicalTangible goods
digitalDigital products / downloads
shipping_feeShipping / delivery charges
sales_taxTax line (if itemised separately)
discountOrder-level discount
store_creditCredit applied to the order
gift_cardGift card redemption

Step 4 — Replace the frontend rendering

Vipps: load SDK and initialise

<!-- 1. Load the SDK -->
<script src="https://checkout.vipps.no/vippsCheckoutSDK.js"></script>

<!-- 2. Container div -->
<div id="vipps-checkout-frame"></div>

<script>
  // 3. Initialise with token and container
  const checkout = VippsCheckout({
    checkoutFrontendUrl: "https://checkout.vipps.no/?token=eyJ...",
    iFrameContainerId: "vipps-checkout-frame",
    token: "eyJhbGciOiJSUzI1...",
    language: "nb",  // "nb" | "dk" | "fi" | "en"
    on: {
      session_status_changed: (data) => {
        if (data.sessionState === "PaymentSuccessful") {
          window.location.href = "/order-confirmation";
        }
      }
    }
  });
</script>

Kustom: inject the html_snippet

<!-- Container div -->
<div id="kustom-checkout-container"></div>

<script>
  // Inject the html_snippet returned by POST /checkout/v3/orders
  // Do this server-side, or pass it to the frontend securely
  const htmlSnippet = "<div id=\"klarna-checkout-container\">...</div>";
  document.getElementById("kustom-checkout-container").innerHTML = htmlSnippet;

  // Re-execute any scripts embedded in the snippet
  const scripts = document
    .getElementById("kustom-checkout-container")
    .querySelectorAll("script");
  scripts.forEach((s) => {
    const newScript = document.createElement("script");
    newScript.text = s.text;
    document.head.appendChild(newScript);
  });
</script>

Kustom manages its own iframe lifecycle via the injected snippet. You do not load a separate SDK. The Kustom widget will redirect the user to merchant_urls.confirmation on successful payment.

What to remove: The <script src="...vippsCheckoutSDK.js"> tag, VippsCheckout(...) call, and any SDK event listeners.


Step 5 — Update your server-side callback handler

Vipps: callback receives the full session payload

Vipps POSTs to your callbackUrl when the session reaches a terminal state. It includes your callbackAuthorizationToken as the Authorization header.

POST https://example.com/checkout/callback
Authorization: 538dd1d0-9e7f-4732-8134-dfed7fd0b236
Content-Type: application/json
{
  "sessionState": "PaymentSuccessful",
  "reference": "order-abc-123",
  "paymentMethod": "Card",
  "paymentDetails": {
    "state": "AUTHORIZED",
    "amount": { "value": 54900, "currency": "NOK" }
  }
}

Respond with HTTP 200. Vipps retries up to 3 times with exponential backoff on non-2xx responses.


Kustom: push notification + mandatory acknowledgment

Kustom GETs your merchant_urls.push URL when the order is completed. The order_id is appended as a query parameter.

GET https://example.com/api/kustom/push?kustom_order_id=8cf27b55-53e8-6aba-9fb4-7c692e56ddee

The push is a GET, not a POST. There is no authorization header. Treat it as a trigger to fetch order state yourself.

Your push handler should:

  1. Extract kustom_order_id from the query string.
  2. Fetch the completed order from the Order Management API.
  3. Create the order in your system.
  4. Acknowledge the order (mandatory — Kustom will not release funds without this).
  5. Return HTTP 200.
# Step 2 — fetch order
GET https://api.kustom.co/ordermanagement/v1/orders/{order_id}
Authorization: Basic <base64>

# Step 4 — acknowledge
POST https://api.kustom.co/ordermanagement/v1/orders/{order_id}/acknowledge
Authorization: Basic <base64>
Klarna-Idempotency-Key: <your-internal-order-reference>

Klarna-Idempotency-Key is required. Use your own internal order reference. It guarantees the operation is idempotent and is stored as an alternate order identifier in Kustom.

Important: The merchant_urls.confirmation URL (user redirect) and merchant_urls.push URL (server notification) both include the {checkout.order.id} template placeholder. Kustom substitutes the real order_id before calling them. Use this instead of a session cookie to look up the order.


Step 6 — Update order status polling

Vipps

GET https://api.vipps.no/checkout/v3/session/{reference}
client_id: ...
client_secret: ...
Ocp-Apim-Subscription-Key: ...
Merchant-Serial-Number: ...

Response — check sessionState:

sessionStateMeaning
SessionCreatedSession is active, awaiting payment
PaymentInitiatedUser has started payment
PaymentSuccessfulPayment authorised ✓
PaymentTerminatedPayment failed / cancelled
SessionExpiredSession TTL (1 h) exceeded

Kustom

GET https://api.kustom.co/checkout/v3/orders/{order_id}
Authorization: Basic <base64>

Response — check status:

statusMeaning
checkout_incompleteOrder active, user in checkout
checkout_completePayment authorised ✓
createdOrder acknowledged and created

Step 7 — Update order modifications

You may need to update the order while the user is still in the checkout (e.g., after they select a shipping option).

Vipps: lock the SDK, then PATCH

// 1. Lock the SDK to pause user interaction
await checkout.lock();

// 2. PATCH the session
await fetch(`https://api.vipps.no/checkout/v3/session/${reference}`, {
  method: "PATCH",
  headers: { /* Vipps auth headers */ },
  body: JSON.stringify({
    transaction: {
      amount: { value: newTotal, currency: "NOK" }
    }
  })
});

// 3. Unlock
await checkout.unlock();

Kustom: POST to the order

POST https://api.kustom.co/checkout/v3/orders/{order_id}
Authorization: Basic <base64>
Content-Type: application/json

{
  "order_amount": 59900,
  "order_tax_amount": 10980,
  "order_lines": [ ... ]
}

You can only update an order while status === "checkout_incomplete". Kustom's iframe refreshes automatically after a successful update — no SDK lock/unlock is needed.


Step 8 — Update capture and post-payment operations

Vipps: capture via the ePayment API

After the session reaches PaymentSuccessful, funds are reserved. You capture via the ePayment API using the session reference as the payment reference.

POST https://api.vipps.no/epayment/v1/payments/{reference}/capture
Authorization: Bearer <access-token>
Ocp-Apim-Subscription-Key: ...
Merchant-Serial-Number: ...
Content-Type: application/json

{
  "modificationAmount": {
    "value": 54900,
    "currency": "NOK"
  }
}

Kustom: capture via the Order Management API

After acknowledgment, capture via the Order Management API using the order_id.

POST https://api.kustom.co/ordermanagement/v1/orders/{order_id}/captures
Authorization: Basic <base64>
Klarna-Idempotency-Key: <capture-reference>
Content-Type: application/json

{
  "captured_amount": 54900,
  "capture_tax_amount": 9980,
  "description": "Shipped order #12345",
  "order_lines": [ ... ]
}

Other Order Management operations

ActionVippsKustom
CancelPUT /epayment/v1/payments/{ref}/cancelPOST /ordermanagement/v1/orders/{id}/cancel
RefundPOST /epayment/v1/payments/{ref}/refundPOST /ordermanagement/v1/orders/{id}/refunds
Read orderGET /checkout/v3/session/{ref}GET /ordermanagement/v1/orders/{id}

Step 9 — Handle the confirmation page

Vipps

The user is redirected to your returnUrl. Use the reference you chose at session creation to look up the order in your system.

Kustom

The user is redirected to merchant_urls.confirmation with ?kustom_order_id={checkout.order.id} appended. Use the kustom_order_id query parameter to look up the order.

// Express.js example
app.get("/confirmation", async (req, res) => {
  const orderId = req.query.kustom_order_id;

  // Fetch the completed order from Kustom
  const order = await fetch(
    `https://api.kustom.co/checkout/v3/orders/${orderId}`,
    { headers: { Authorization: `Basic ${KUSTOM_CREDENTIALS}` } }
  ).then((r) => r.json());

  res.render("confirmation", { order });
});

Complete before and after

Before (Vipps)

# 1. Create session
curl -X POST https://api.vipps.no/checkout/v3/session \
  -H "Content-Type: application/json" \
  -H "client_id: $VIPPS_CLIENT_ID" \
  -H "client_secret: $VIPPS_CLIENT_SECRET" \
  -H "Ocp-Apim-Subscription-Key: $VIPPS_SUB_KEY" \
  -H "Merchant-Serial-Number: $VIPPS_MSN" \
  -d '{
    "merchantInfo": {
      "callbackUrl": "https://example.com/vipps/callback",
      "returnUrl": "https://example.com/vipps/result",
      "callbackAuthorizationToken": "secret-token"
    },
    "transaction": {
      "amount": { "value": 54900, "currency": "NOK" },
      "reference": "order-abc-123",
      "paymentDescription": "Your order"
    }
  }'

# 2. Poll for status
curl https://api.vipps.no/checkout/v3/session/order-abc-123 \
  -H "client_id: $VIPPS_CLIENT_ID" \
  -H "client_secret: $VIPPS_CLIENT_SECRET" \
  -H "Ocp-Apim-Subscription-Key: $VIPPS_SUB_KEY" \
  -H "Merchant-Serial-Number: $VIPPS_MSN"

# 3. Capture (via ePayment API)
curl -X POST https://api.vipps.no/epayment/v1/payments/order-abc-123/capture \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Ocp-Apim-Subscription-Key: $VIPPS_SUB_KEY" \
  -H "Merchant-Serial-Number: $VIPPS_MSN" \
  -H "Content-Type: application/json" \
  -d '{ "modificationAmount": { "value": 54900, "currency": "NOK" } }'

After (Kustom)

KUSTOM_AUTH="Basic $(echo -n "$KUSTOM_USER:$KUSTOM_PASS" | base64)"

# 1. Create order
curl -X POST https://api.kustom.co/checkout/v3/orders \
  -H "Content-Type: application/json" \
  -H "Authorization: $KUSTOM_AUTH" \
  -d '{
    "purchase_country": "NO",
    "purchase_currency": "NOK",
    "locale": "nb-NO",
    "order_amount": 54900,
    "order_tax_amount": 9980,
    "order_lines": [
      {
        "type": "physical",
        "reference": "SKU-001",
        "name": "Your product",
        "quantity": 1,
        "quantity_unit": "pcs",
        "unit_price": 49900,
        "tax_rate": 2500,
        "total_amount": 49900,
        "total_discount_amount": 0,
        "total_tax_amount": 9980
      },
      {
        "type": "shipping_fee",
        "reference": "shipping-01",
        "name": "Standard delivery",
        "quantity": 1,
        "quantity_unit": "pcs",
        "unit_price": 5000,
        "tax_rate": 0,
        "total_amount": 5000,
        "total_discount_amount": 0,
        "total_tax_amount": 0
      }
    ],
    "merchant_urls": {
      "terms": "https://example.com/terms",
      "checkout": "https://example.com/checkout?kustom_order_id={checkout.order.id}",
      "confirmation": "https://example.com/confirmation?kustom_order_id={checkout.order.id}",
      "push": "https://example.com/api/kustom/push?kustom_order_id={checkout.order.id}"
    }
  }'
# → returns order_id and html_snippet

# 2. Poll for status
curl https://api.kustom.co/checkout/v3/orders/$ORDER_ID \
  -H "Authorization: $KUSTOM_AUTH"

# 3. Acknowledge (mandatory after payment)
curl -X POST https://api.kustom.co/ordermanagement/v1/orders/$ORDER_ID/acknowledge \
  -H "Authorization: $KUSTOM_AUTH" \
  -H "Klarna-Idempotency-Key: internal-ref-12345"

# 4. Capture
curl -X POST https://api.kustom.co/ordermanagement/v1/orders/$ORDER_ID/captures \
  -H "Authorization: $KUSTOM_AUTH" \
  -H "Klarna-Idempotency-Key: capture-ref-12345" \
  -H "Content-Type: application/json" \
  -d '{
    "captured_amount": 54900,
    "capture_tax_amount": 9980,
    "description": "Shipped",
    "order_lines": [ ... ]
  }'

Common gotchas

Amount validation fails Kustom validates that order_amount equals the sum of all order_lines[].total_amount. If these do not match, order creation returns 400. Double-check rounding in your line totals.

Order not released by Kustom Kustom holds the payment until you acknowledge the order. If you do not POST to /ordermanagement/v1/orders/{id}/acknowledge, the payment stays in limbo and eventually times out.

Push URL returning 403 Some WAF or ModSecurity rules block Kustom's push requests because Kustom sends APIs-Kustom as the User-Agent header (not a browser user agent). Add an exception in your WAF for this User-Agent, or for your push endpoint path.

Order update rejected You can only update a Kustom order while status === "checkout_incomplete". If the user has already started payment, the update will fail. Check status before attempting updates.

Shipping as an order line Vipps uses isShipping: true to flag a shipping line. Kustom uses type: "shipping_fee". If you set type: "physical" for a shipping line, it will display and calculate incorrectly.

quantity type difference In Vipps, unitInfo.quantity is a string (e.g., "5"). In Kustom, quantity is an integer (e.g., 5). Passing a string to Kustom will cause a validation error.

No callbackAuthorizationToken equivalent Vipps lets you pass a secret token that it sends back in the Authorization header of callbacks. Kustom does not support this. If you need to verify that a push notification came from Kustom, validate the order_id against your database — if a matching pending order exists, it is legitimate.

Locale format Vipps accepts short locale codes ("nb", "en"). Kustom requires full BCP 47 locale codes ("nb-NO", "en-GB").


Endpoint summary

OperationVippsKustom
Create session / orderPOST /checkout/v3/sessionPOST /checkout/v3/orders
Read statusGET /checkout/v3/session/{ref}GET /checkout/v3/orders/{id}
UpdatePATCH /checkout/v3/session/{ref}POST /checkout/v3/orders/{id}
Abort(session expires)POST /checkout/v3/orders/{id}/abort
Acknowledge(not required)POST /ordermanagement/v1/orders/{id}/acknowledge
CapturePOST /epayment/v1/payments/{ref}/capturePOST /ordermanagement/v1/orders/{id}/captures
CancelPUT /epayment/v1/payments/{ref}/cancelPOST /ordermanagement/v1/orders/{id}/cancel
RefundPOST /epayment/v1/payments/{ref}/refundPOST /ordermanagement/v1/orders/{id}/refunds

Further reading