Migrating from Vipps Checkout
Replace a Vipps MobilePay Checkout integration with Kustom Checkout — auth, base URLs, the session-to-order payload mapping, the frontend swap, the push handler and the acknowledgment Vipps never required.
Vipps Checkout and Kustom Checkout do the same job with different vocabulary. Vipps has a session you create and then render with an SDK. Kustom has an order you create and then render from an HTML snippet. This guide walks the swap in the order you should make it — credentials, URLs, payload, frontend, callbacks — and flags the two places where Kustom needs something Vipps didn’t: the order acknowledgment, and matching totals.
Before you start
Section titled “Before you start”You need Kustom credentials for both environments from the Kustom Merchant Portal, and a place to put the base URLs that isn’t scattered through your code.
Here is the whole migration in one table. Everything below is detail on one of these rows.
| Vipps Checkout | Kustom Checkout | |
|---|---|---|
| Auth | Five or more custom headers | HTTP Basic — one Authorization header |
| Base URL (test) | https://apitest.vipps.no/checkout/v3 | https://api.playground.kustom.co |
| Base URL (prod) | https://api.vipps.no/checkout/v3 | https://api.kustom.co |
| What you create | A session, keyed by your reference | An order, keyed by Kustom’s order_id |
| Frontend | Load the SDK, init with a token | Inject the html_snippet into the DOM |
| Customer redirect | returnUrl | merchant_urls.confirmation |
| Server callback | callbackUrl, a POST with the payload | merchant_urls.push, a POST with the id in the query string |
| Market | Implied by the MSN header | purchase_country on the order |
| Locale | SDK language parameter | locale on the order, full BCP 47 |
| Acknowledgment | Not a thing | Required. Funds aren’t released without it |
| Capture | ePayment API | Order Management API |
Step by step
Section titled “Step by step”1. Replace the headers with Basic auth
Section titled “1. Replace the headers with Basic auth”Vipps wants client_id, client_secret, Ocp-Apim-Subscription-Key,
Merchant-Serial-Number and a family of Vipps-System-* headers on every request. Kustom
wants one header. Encode your merchant credentials once:
echo -n "username:password" | base64Authorization: Basic <the base64 above>Content-Type: application/jsonIf your HTTP client sets headers in one place — a middleware, a client factory — this is a one-line change. Delete every Vipps header while you’re in there. Leaving them costs nothing but confuses the next reader.
2. Point at the Kustom base URLs
Section titled “2. Point at the Kustom base URLs”Swap the host in your environment config. Production is https://api.kustom.co, and
Playground is https://api.playground.kustom.co — see
Authentication and environments.
3. Turn the session payload into an order payload
Section titled “3. Turn the session payload into an order payload”This is the bulk of the work. POST /checkout/v3/session becomes
POST /checkout/v3/orders, and the body is restructured rather than renamed. Two rules
account for most of the failures:
order_amountmust equal the sum oforder_lines[].total_amount. Kustom validates it and returns400when it doesn’t. Vipps didn’t check this for you.- Amounts are minor-unit integers and
tax_rateis basis points.2500is 25%.
The full mapping is below. Kustom
also needs merchant_urls, which has no Vipps equivalent as a block: terms, checkout,
confirmation and push are required, and confirmation and push must be HTTPS — in
Playground as well as production. See
Merchant URLs.
4. Replace the SDK with the snippet
Section titled “4. Replace the SDK with the snippet”Vipps gives you a token and you initialize its SDK against it. Kustom gives you an
html_snippet in the create-order response — a <div> plus a <script> — and you put it
on your page. Server-side rendering is the simple path:
<div id="checkout">{{ kustom_order.html_snippet }}</div>Client-side, innerHTML won’t execute the script, so you have to recreate the script
tags. Render Checkout snippet has the loop, and the
rules: keep the script tags exactly as received, and never put the snippet inside an
iframe of your own.
5. Rewrite the callback handler, and add the acknowledgment
Section titled “5. Rewrite the callback handler, and add the acknowledgment”The two callbacks aren’t the same shape, and this is where migrations most often break.
Vipps POSTs the terminal session state to callbackUrl, with your
callbackAuthorizationToken in the Authorization header, and you can act on the body.
Kustom POSTs to your push URL, with no authorization header and no body you should
act on. It’s a trigger, not a payload:
POST https://example.com/api/kustom/push?kustom_order_id=8cf27b55-53e8-6aba-9fb4-7c692e56ddeeYour handler must:
- Take the order id from the query string.
- Fetch the order from Order Management.
- Create the order in your own system.
- Acknowledge it. Kustom holds the payment until you do.
- Return
200.
GET /ordermanagement/v1/orders/{order_id}Authorization: Basic <token>
POST /ordermanagement/v1/orders/{order_id}/acknowledgeAuthorization: Basic <token>Kustom-Idempotency-Key: <your internal order reference>Send Kustom-Idempotency-Key on the acknowledgment. The API reference lists it under its
older name, Klarna-Idempotency-Key, and both work. Put your own order reference in it: it makes the call idempotent and is
stored as an alternate identifier on the Kustom order, so you can look the order up by it
later.
There’s no equivalent of callbackAuthorizationToken. To satisfy yourself a push is
genuine, look the order id up in your own database — if you have a pending order for it,
it’s real.
6. Move status polling and updates
Section titled “6. Move status polling and updates”Reading status becomes GET /checkout/v3/orders/{order_id}, and the field to check is
status. Updating becomes POST to the same URL — not PATCH — with the fields you want
to change. Omitted fields keep their values. You can only update while status is
checkout_incomplete. Once the customer has begun paying, the update is rejected.
Where Vipps had you lock the SDK before a PATCH, Kustom has you suspend and resume the
iframe around the update. Same idea, different call — see
Render Checkout snippet.
Aborting is explicit in Kustom: POST /checkout/v3/orders/{order_id}/abort, rather than
waiting out a one-hour session TTL. A Kustom order is valid for 48 hours.
7. Move capture and post-payment operations
Section titled “7. Move capture and post-payment operations”Everything after authorization moves from the ePayment API to Order Management: captures,
cancels and refunds all hang off /ordermanagement/v1/orders/{order_id}. See
the endpoint mapping below and
Capture an order.
Field mapping from Vipps session to Kustom order
Section titled “Field mapping from Vipps session to Kustom order”| Vipps field | Kustom field | Notes |
|---|---|---|
transaction.amount.value | order_amount | Must equal the sum of the line totals |
transaction.amount.currency | purchase_currency | ISO 4217 |
| (implied by the MSN header) | purchase_country | ISO 3166-1 alpha-2 |
(SDK language parameter) | locale | language-COUNTRY, e.g. nb-NO |
transaction.reference | (not a request field) | Kustom generates order_id in the response |
transaction.paymentDescription | (no direct field) | Use order_lines[].name |
merchantInfo.callbackUrl | merchant_urls.push | Server-to-server, and a POST |
merchantInfo.returnUrl | merchant_urls.confirmation | The customer’s browser lands here |
merchantInfo.callbackAuthorizationToken | (no equivalent) | Verify by looking up the order id instead |
orderSummary.orderLines[].id | order_lines[].reference | Your SKU |
orderSummary.orderLines[].name | order_lines[].name | Display name |
orderSummary.orderLines[].totalAmount | order_lines[].total_amount | Minor units |
orderSummary.orderLines[].totalAmountExcludingTax | (derived) | total_amount minus total_tax_amount |
orderSummary.orderLines[].totalTaxAmount | order_lines[].total_tax_amount | Minor units |
orderSummary.orderLines[].taxRate | order_lines[].tax_rate | Basis points |
orderSummary.orderLines[].unitInfo.unitPrice | order_lines[].unit_price | Minor units |
orderSummary.orderLines[].unitInfo.quantity | order_lines[].quantity | Integer in Kustom, string in Vipps |
orderSummary.orderLines[].unitInfo.quantityUnit | order_lines[].quantity_unit | pcs, and so on |
orderSummary.orderLines[].isShipping: true | order_lines[].type: "shipping_fee" | A type, not a flag |
orderSummary.orderLines[].isReturn: true | (Order Management) | Returns are a separate flow |
orderSummary.orderLines[].discount | order_lines[].total_discount_amount | Minor units |
orderSummary.orderLines[].productUrl | order_lines[].product_url | Max 1024 characters |
Kustom’s order_lines[].type values are physical, digital, shipping_fee,
sales_tax, discount, store_credit, gift_card and surcharge.
Endpoint mapping
Section titled “Endpoint mapping”| Operation | Vipps | Kustom |
|---|---|---|
| Create | 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 |
Check that it works
Section titled “Check that it works”Against Playground, in this order:
- Create an order and get back an
order_idand anhtml_snippet. A400here is almost always mismatched totals. - Render the snippet and pay with Playground test data.
- Your push endpoint is hit with the order id in the query string.
- Your handler fetches the order, acknowledges it and returns
200. - Read the order and confirm
statusischeckout_complete. - Capture through Order Management and confirm the captured amount.
Troubleshooting
Section titled “Troubleshooting”Order creation returns 400. order_amount doesn’t equal the sum of
order_lines[].total_amount, or a quantity is a string. Both are Vipps habits.
The payment never settles. You’re not acknowledging. Kustom holds the funds until the acknowledgment arrives, and eventually times out.
Your push endpoint returns 403. A WAF is blocking it. Kustom’s push sends
APIs-Kustom as its User-Agent, which doesn’t look like a browser, so allow that agent
or that path.
A shipping line prices wrongly. It’s typed physical. Vipps used an isShipping
flag. Kustom uses type: "shipping_fee".
Updates rejected. The order is no longer checkout_incomplete. Check status first.
Related
Section titled “Related”- Create and update an order — the Kustom order in full
- Confirmation and push — the callback pair in detail
- Capture an order — post-payment operations
- Integration models — which Kustom integration model to land on
- Migrating from Vipps Recurring — the subscription half, if you also use the Vipps Recurring API
- Checkout API reference — the order schema you’re mapping onto
- Vipps Checkout API guide — the integration you’re leaving
This page is also available as Markdown — append .md to its URL — and is indexed in llms.txt.