# 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

You need Kustom credentials for both environments from the
[Kustom Merchant Portal](https://portal.kustom.co), 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

### 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:

```bash
echo -n "username:password" | base64
```

```http
Authorization: Basic <the base64 above>
Content-Type: application/json
```

If 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

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](/api/authentication/).

### 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_amount` must equal the sum of `order_lines[].total_amount`.** Kustom validates it and returns `400` when it doesn't. Vipps didn't check this for you.
- **Amounts are minor-unit integers and `tax_rate` is basis points.** `2500` is 25%.

The [full mapping is below](#field-mapping-from-vipps-session-to-kustom-order). 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](/checkout/integrate/merchant-urls/).

### 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:

```html
<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](/checkout/integrate/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

The two callbacks aren't the same shape, and this is where migrations most often break.

Vipps `POST`s the terminal session state to `callbackUrl`, with your
`callbackAuthorizationToken` in the `Authorization` header, and you can act on the body.

**Kustom `POST`s to your push URL, with no authorization header and no body you should
act on.** It's a trigger, not a payload:

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

Your handler must:

1. Take the order id from the query string.
2. Fetch the order from Order Management.
3. Create the order in your own system.
4. **Acknowledge it.** Kustom holds the payment until you do.
5. Return `200`.

```http
GET /ordermanagement/v1/orders/{order_id}
Authorization: Basic <token>

POST /ordermanagement/v1/orders/{order_id}/acknowledge
Authorization: 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

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](/checkout/integrate/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

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](#endpoint-mapping) below and
[Capture an order](/order-management/capture/).

## 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

| 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

Against Playground, in this order:

1. Create an order and get back an `order_id` and an `html_snippet`. A `400` here is almost always mismatched totals.
2. Render the snippet and pay with [Playground test data](/basics/test-data/).
3. Your push endpoint is hit with the order id in the query string.
4. Your handler fetches the order, acknowledges it and returns `200`.
5. Read the order and confirm `status` is `checkout_complete`.
6. Capture through Order Management and confirm the captured amount.

## 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

- [Create and update an order](/checkout/integrate/create-order/) — the Kustom order in full
- [Confirmation and push](/checkout/integrate/confirmation-and-push/) — the callback pair in detail
- [Capture an order](/order-management/capture/) — post-payment operations
- [Integration models](/checkout/#integration-models) — which Kustom integration model to land on
- [Migrating from Vipps Recurring](/checkout/recurring/migrating-from-vipps-recurring/) — the subscription half, if you also use the Vipps Recurring API
- [Checkout API reference](/api/checkout/) — the order schema you're mapping onto
- [Vipps Checkout API guide](https://developer.vippsmobilepay.com/docs/APIs/checkout-api/checkout-api-guide/) — the integration you're leaving
