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.comAPI credentials stopped working on March 31, 2026.
| Dimension | Vipps Checkout | Kustom Checkout |
|---|---|---|
| Auth method | Multiple HTTP headers | HTTP Basic Auth (username:password) |
| Base URL (test) | https://apitest.vipps.no/checkout/v3 | See Merchant Portal for -regionspecific URL |
| Base URL (prod) | https://api.vipps.no/checkout/v3 | https://api.kustom.co/checkout/v3 |
| Unit created | Session (reference) | Order (order_id) |
| Create endpoint | POST /session | POST /orders |
| Read status | GET /session/{reference} | GET /orders/{order_id} |
| Update | PATCH /session/{reference} | POST /orders/{order_id} |
| Abort | Session expires (1 h TTL) | POST /orders/{order_id}/abort |
| Frontend | Load SDK + init with token | Inject html_snippet into DOM |
| User redirect | returnUrl | merchant_urls.confirmation |
| Server callback | callbackUrl | merchant_urls.push |
| Country / market | Implied by MSN header | purchase_country field |
| Locale | SDK language param | locale field in order body |
| Order acknowledgment | Not required | Required — POST /ordermanagement/v1/orders/{id}/acknowledge |
| Capture | Via ePayment API | Via Order Management API |
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.6Kustom 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/jsonGet 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.
Replace the Vipps base URL with the Kustom base URL in your environment config.
| Environment | Vipps | Kustom |
|---|---|---|
| Test (Playground) | https://apitest.vipps.no/checkout/v3 | https://api.playground.kustom.co/ |
| Production | https://api.vipps.no/checkout/v3 | https://api.kustom.co/ |
If you had a previous Klarna Checkout integration, the old
*.klarna.comproduction URLs are still active until March 31, 2026, but the Playground URL must be updated manually. Switch early to avoid disruptions.
This is the largest change. The two payloads have different structures but map closely to each other.
{
"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"
}{
"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.
| Vipps field | Kustom field | Notes |
|---|---|---|
transaction.amount.value | order_amount | Sum of all order_lines[].total_amount |
transaction.amount.currency | purchase_currency | ISO 4217 |
| (implied by MSN) | purchase_country | ISO 3166-1 alpha-2 |
| (SDK param) | locale | BCP 47, e.g. nb-NO, en-GB |
transaction.reference | Not a request field | Kustom generates order_id in response |
transaction.paymentDescription | Not a direct field | Use order_lines[].name for line descriptions |
merchantInfo.callbackUrl | merchant_urls.push | Push = server-to-server notification |
merchantInfo.returnUrl | merchant_urls.confirmation | User browser redirect on success |
merchantInfo.callbackAuthorizationToken | Not used | Kustom does not send an auth token in push calls |
orderSummary.orderLines[].id | order_lines[].reference | Your SKU or line identifier |
orderSummary.orderLines[].name | order_lines[].name | Display name |
orderSummary.orderLines[].totalAmount | order_lines[].total_amount | Minor units (integers) |
orderSummary.orderLines[].totalAmountExcludingTax | (derived) | total_amount - total_tax_amount |
orderSummary.orderLines[].totalTaxAmount | order_lines[].total_tax_amount | Minor units |
orderSummary.orderLines[].taxRate | order_lines[].tax_rate | Basis points (2500 = 25%) |
orderSummary.orderLines[].unitInfo.unitPrice | order_lines[].unit_price | Minor units |
orderSummary.orderLines[].unitInfo.quantity | order_lines[].quantity | Kustom is an integer, not a string |
orderSummary.orderLines[].unitInfo.quantityUnit | order_lines[].quantity_unit | e.g. pcs |
orderSummary.orderLines[].isShipping: true | order_lines[].type: "shipping_fee" | Kustom uses type instead of a flag |
orderSummary.orderLines[].isReturn: true | Handled via Order Management API | Credit/return flow is separate in Kustom |
orderSummary.orderLines[].discount | order_lines[].total_discount_amount | Minor units |
orderSummary.orderLines[].productUrl | (no direct field in basic payload) | Can be set via Order Management API |
| Type | Use for |
|---|---|
physical | Tangible goods |
digital | Digital products / downloads |
shipping_fee | Shipping / delivery charges |
sales_tax | Tax line (if itemised separately) |
discount | Order-level discount |
store_credit | Credit applied to the order |
gift_card | Gift card redemption |
<!-- 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><!-- 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.confirmationon successful payment.
What to remove: The <script src="...vippsCheckoutSDK.js"> tag, VippsCheckout(...) call, and any SDK event listeners.
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 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-7c692e56ddeeThe 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:
- Extract
kustom_order_idfrom the query string. - Fetch the completed order from the Order Management API.
- Create the order in your system.
- Acknowledge the order (mandatory — Kustom will not release funds without this).
- 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-Keyis 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.confirmationURL (user redirect) andmerchant_urls.pushURL (server notification) both include the{checkout.order.id}template placeholder. Kustom substitutes the realorder_idbefore calling them. Use this instead of a session cookie to look up the order.
GET https://api.vipps.no/checkout/v3/session/{reference}
client_id: ...
client_secret: ...
Ocp-Apim-Subscription-Key: ...
Merchant-Serial-Number: ...Response — check sessionState:
sessionState | Meaning |
|---|---|
SessionCreated | Session is active, awaiting payment |
PaymentInitiated | User has started payment |
PaymentSuccessful | Payment authorised ✓ |
PaymentTerminated | Payment failed / cancelled |
SessionExpired | Session TTL (1 h) exceeded |
GET https://api.kustom.co/checkout/v3/orders/{order_id}
Authorization: Basic <base64>Response — check status:
status | Meaning |
|---|---|
checkout_incomplete | Order active, user in checkout |
checkout_complete | Payment authorised ✓ |
created | Order acknowledged and created |
You may need to update the order while the user is still in the checkout (e.g., after they select a shipping option).
// 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();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.
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"
}
}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": [ ... ]
}| Action | Vipps | Kustom |
|---|---|---|
| Cancel | PUT /epayment/v1/payments/{ref}/cancel | POST /ordermanagement/v1/orders/{id}/cancel |
| Refund | POST /epayment/v1/payments/{ref}/refund | POST /ordermanagement/v1/orders/{id}/refunds |
| Read order | GET /checkout/v3/session/{ref} | GET /ordermanagement/v1/orders/{id} |
The user is redirected to your returnUrl. Use the reference you chose at session creation to look up the order in your system.
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 });
});# 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" } }'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": [ ... ]
}'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").
| Operation | Vipps | Kustom |
|---|---|---|
| Create session / order | POST /checkout/v3/session | POST /checkout/v3/orders |
| Read status | GET /checkout/v3/session/{ref} | GET /checkout/v3/orders/{id} |
| Update | PATCH /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 |
| Capture | POST /epayment/v1/payments/{ref}/capture | POST /ordermanagement/v1/orders/{id}/captures |
| Cancel | PUT /epayment/v1/payments/{ref}/cancel | POST /ordermanagement/v1/orders/{id}/cancel |
| Refund | POST /epayment/v1/payments/{ref}/refund | POST /ordermanagement/v1/orders/{id}/refunds |