# Migrating from Vipps Recurring

Replace a Vipps MobilePay Recurring API integration with Kustom customer tokens — the sign-up checkout, storing the token, charging it, scheduling and retries you now own, stopping a token and moving existing agreements by April 30, 2027.

The Vipps MobilePay Recurring API (`/recurring/v3`) and Kustom solve the same problem with a
different split of responsibilities. Vipps keeps an *agreement* with a price and an interval
and runs the charges for you. Kustom gives you a **customer token** — the `recurring_token`
on a completed checkout order — and charges it the moment you ask, with the schedule, the
price and the retries living in your system. This guide replaces each Recurring API call with
its Kustom equivalent and then moves the agreements you already have, without asking your
customers to sign up again.

If you also use Vipps Checkout for one-off purchases, follow
[Migrating from Vipps Checkout](/checkout/surfaces/migrating-from-vipps-checkout/) too. This
page covers only the recurring part.

## Migration timeline

| Date | What happens |
|---|---|
| **November 18, 2026** | Vipps goes live as a recurring payment method in Kustom Checkout. You can create new Vipps tokens and charge them through Kustom. |
| **February 21, 2027** | Last day to create new Vipps agreements outside Kustom. From February 22, 2027, every new Vipps token is created through Kustom. |
| **March 8, 2027** | Your existing Vipps agreements become chargeable through Kustom. |
| **April 30, 2027** | All Vipps recurring charges must run through Kustom from this date. |

Your existing agreements stay valid throughout. [Step 9](#9-move-your-existing-agreements)
covers how they move.

**You can build today.** The Kustom recurring API is already live, and you can build and test
the whole flow in Playground against the payment methods a recurring order offers now.

**Vipps isn't a recurring payment method in Kustom yet.** Until November 18, 2026 a recurring
checkout order doesn't offer Vipps or MobilePay, so no Kustom token is backed by Vipps. Some
Vipps-specific details aren't confirmed yet, such as the `payment_method_type` a Vipps token
reads back with. This page will be updated before Vipps recurring goes live.

## Before you start

You need Kustom credentials for Playground and production from the
[Kustom Merchant Portal](https://portal.kustom.co), and **recurring enabled on both merchant
IDs**. Contact Kustom merchant support to turn it on, and ask for Vipps to be included as a
recurring payment method once it's available. Without the entitlement, a create-order call with
`recurring: true` is rejected with `400` and `error_code: BAD_VALUE`.

You also need a working Kustom Checkout integration — [create an
order](/checkout/integrate/create-order/), [render the
snippet](/checkout/integrate/render-checkout-snippet/) and [handle the
push](/checkout/integrate/confirmation-and-push/) — because the sign-up is that flow with one
extra field. If you're coming from Vipps Checkout, the [Vipps Checkout
migration](/checkout/surfaces/migrating-from-vipps-checkout/) gets you there.

Here is the whole migration in one table. Everything below is detail on one of these rows.

| | Vipps Recurring API | Kustom |
|---|---|---|
| **Auth** | Access token plus `Ocp-Apim-*`, `Merchant-Serial-Number` and `Vipps-System-*` headers | HTTP Basic — one `Authorization` header |
| **Base URL (test)** | `https://apitest.vipps.no` | `https://api.playground.kustom.co` |
| **Base URL (prod)** | `https://api.vipps.no` | `https://api.kustom.co` |
| **The subscription is** | An agreement (`agreementId`) | A customer token (`recurring_token`) |
| **Create** | `POST /recurring/v3/agreements` | `POST /checkout/v3/orders` with `recurring: true` |
| **Customer consent** | Redirect to `vippsConfirmationUrl`, approve in the app | Complete the Kustom Checkout |
| **First payment** | `initialCharge` on the agreement | The order lines of the recurring checkout order |
| **Charge** | `POST /recurring/v3/agreements/{agreementId}/charges` | `POST /customer-token/v1/tokens/{recurring_token}/order` |
| **When the charge runs** | On the `due` date Vipps schedules | Immediately, when you call |
| **Retries** | Vipps retries for `retryDays` | You retry on your own schedule |
| **Capture model** | `DIRECT_CAPTURE` or `RESERVE_CAPTURE` | `auto_capture: true`, or capture later in Order Management |
| **Stop** | `PATCH` the agreement to `STOPPED` | `PATCH /customer-token/v1/tokens/{recurring_token}/status` to `CANCELLED` |
| **Idempotency** | `Idempotency-Key` on every call | None on the token charge. `Kustom-Idempotency-Key` on Order Management calls |
| **Amounts** | Minor units | Minor units |

## Step by step

### 1. Replace credentials and base URLs

Every Vipps Recurring call carries an access token from `POST /accesstoken/get` plus a set of
merchant headers. Kustom wants one header and no token exchange. Encode your merchant
credentials once:

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

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

Swap the host at the same time: `https://api.kustom.co` in production and
`https://api.playground.kustom.co` in Playground — see
[Authentication and environments](/api/authentication/). Credentials are per environment, so
generate a pair for each.

### 2. Replace agreement creation with a recurring checkout order

Switch your sign-up flow before **February 21, 2027**, the last day you can create agreements
with `POST /recurring/v3/agreements`.

On Vipps you create a draft agreement and redirect the customer to `vippsConfirmationUrl`:

```json
{
  "pricing": { "type": "LEGACY", "amount": 29900, "currency": "NOK" },
  "interval": { "unit": "MONTH", "count": 1 },
  "initialCharge": {
    "amount": 29900,
    "description": "First month",
    "transactionType": "DIRECT_CAPTURE",
    "orderId": "sub-1001-0"
  },
  "merchantRedirectUrl": "https://example.com/subscription/result",
  "merchantAgreementUrl": "https://example.com/my-subscriptions",
  "productName": "Coffee subscription",
  "externalId": "sub-1001"
}
```

The agreement stays `PENDING` until the customer approves it in the app, then turns `ACTIVE`.

On Kustom the sign-up is an ordinary checkout order with `recurring: true`, and the customer
approves future charges by completing it:

```http
POST /checkout/v3/orders
Authorization: Basic <token>
Content-Type: application/json
```

```json
{
  "purchase_country": "NO",
  "purchase_currency": "NOK",
  "locale": "nb-NO",
  "order_amount": 29900,
  "order_tax_amount": 5980,
  "recurring": true,
  "merchant_reference1": "sub-1001-0",
  "merchant_reference2": "sub-1001",
  "order_lines": [
    {
      "type": "physical",
      "reference": "COFFEE-1KG",
      "name": "Coffee subscription, first month",
      "quantity": 1,
      "quantity_unit": "pcs",
      "unit_price": 29900,
      "tax_rate": 2500,
      "total_amount": 29900,
      "total_discount_amount": 0,
      "total_tax_amount": 5980
    }
  ],
  "options": {
    "subscription_buy_button": true
  },
  "merchant_urls": {
    "terms": "https://example.com/terms",
    "checkout": "https://example.com/checkout?kustom_order_id={checkout.order.id}",
    "confirmation": "https://example.com/subscription/result?kustom_order_id={checkout.order.id}",
    "push": "https://example.com/api/kustom/push?kustom_order_id={checkout.order.id}"
  }
}
```

Render the `html_snippet` from the response exactly as for any other order. What changes:

- **`recurring: true`** makes completing the order create a token. It also removes the financing payment methods, so the customer sees a smaller set of options than on an ordinary order.
- **`options.subscription_buy_button`** puts a subscribe label on the buy button — *Subscribe and pay* rather than *Pay order* in English.
- **The consent text is Kustom's.** The checkout tells the customer they're setting up recurring payments with your store, by name. You don't supply that copy.
- **There is no price or interval on the token.** Your system decides what to charge and when. See [Create a customer token](/checkout/recurring/minting-a-token/).

**Sign-up without a first payment.** Vipps lets you create an agreement without
`initialCharge`. That a recurring checkout order can carry `order_amount: 0` isn't confirmed —
check with Kustom merchant support before you design a free trial around it.

| Vipps field | Kustom field | Notes |
|---|---|---|
| `pricing.amount` | *(none on the token)* | You send the amount on every charge |
| `pricing.currency` | `purchase_currency` | ISO 4217 |
| `pricing.type: VARIABLE`, `suggestedMaxAmount` | *(none)* | There's no customer-chosen maximum. Tell the customer about amount changes yourself |
| `interval.unit`, `interval.count` | *(none)* | The schedule lives in your system |
| `productName` | `order_lines[].name` | |
| `productDescription` | *(none)* | The checkout shows its own consent text |
| `initialCharge.amount` | `order_amount` and `order_lines` | Of the checkout order |
| `initialCharge.description` | `order_lines[].name` | |
| `initialCharge.transactionType` | `options.auto_capture` | Or capture later in Order Management |
| `initialCharge.orderId` | `merchant_reference1` | Your reference for the first payment |
| `externalId` | `merchant_reference2` | Your subscription id |
| `merchantRedirectUrl` | `merchant_urls.confirmation` | The customer's browser lands here |
| `merchantAgreementUrl` | *(none)* | Keep your own "My subscriptions" page and link it from your emails |
| `phoneNumber` | `billing_address.phone` | Optional prefill |
| `scope` | *(none)* | The completed order already carries the billing address, email and phone |
| `campaign` | A `discount` order line | Apply the discounted price on the charge it belongs to |
| `countryCode` | `purchase_country` | `NO`, `DK` or `FI` |
| `isApp`, `skipLandingPage` | *(none)* | The checkout handles the app switch |

### 3. Store the token when the customer completes

On Vipps you poll the agreement or wait for its webhook until it's `ACTIVE`, then store the
`agreementId`. On Kustom the push for the checkout order is the signal. Your push handler:

1. Reads `kustom_order_id` from the query string.
2. [Reads the order](/api/checkout/read-order-merchant/) with `GET /checkout/v3/orders/{order_id}` and takes its `recurring_token`.
3. Stores the token against the customer's subscription.
4. [Acknowledges the order](/checkout/integrate/confirmation-and-push/) with `POST /ordermanagement/v1/orders/{order_id}/acknowledge`.
5. Returns `200`.

```json
{
  "order_id": "8cf27b55-53e8-6aba-9fb4-7c692e56ddee",
  "status": "checkout_complete",
  "recurring": true,
  "recurring_token": "8b7c6e1a-2f3d-4a5b-9c8d-7e6f5a4b3c2d",
  "order_amount": 29900
}
```

**Treat the token as a credential.** It can charge a real person, so store it the way you'd
store a card token: encrypted at rest, never in a log or a URL. It can only be charged under
the merchant ID that created it.

### 4. Replace charge creation

On Vipps you queue a charge at least two days before it's `due` in production, and Vipps
attempts it on the due date and for `retryDays` after:

```json
{
  "amount": 29900,
  "transactionType": "DIRECT_CAPTURE",
  "description": "Coffee subscription, March",
  "due": "2027-03-15",
  "retryDays": 5,
  "orderId": "sub-1001-3"
}
```

On Kustom you [charge the token](/api/customer-tokens/charge-token/) the moment the payment is
due. Each call is one synchronous payment — no queue, no due date, no pending state:

```http
POST /customer-token/v1/tokens/8b7c6e1a-2f3d-4a5b-9c8d-7e6f5a4b3c2d/order
Authorization: Basic <token>
Content-Type: application/json
```

```json
{
  "purchase_currency": "NOK",
  "order_amount": 29900,
  "order_tax_amount": 5980,
  "auto_capture": true,
  "merchant_reference1": "sub-1001-3",
  "merchant_reference2": "sub-1001",
  "order_lines": [
    {
      "type": "physical",
      "reference": "COFFEE-1KG",
      "name": "Coffee subscription, March",
      "quantity": 1,
      "quantity_unit": "pcs",
      "unit_price": 29900,
      "tax_rate": 2500,
      "total_amount": 29900,
      "total_discount_amount": 0,
      "total_tax_amount": 5980
    }
  ]
}
```

A success is small:

```json
{
  "order_id": "a89ec121-1276-419d-882a-c343d58fd1bc",
  "fraud_status": "ACCEPTED",
  "authorized_payment_method": {
    "type": "card"
  }
}
```

- **A `200` means the charge went through.** The order is authorized at once and, with `auto_capture: true`, captured too. `fraud_status` is always `ACCEPTED`, so there's nothing to branch on.
- **`auto_capture` defaults to `false`**, which leaves the charge authorized but not collected — the Kustom equivalent of `RESERVE_CAPTURE`.
- **Store the `order_id`.** It's the only handle you get for capturing, cancelling or refunding the charge.
- **A failed charge is an error response**, not a pending order. See [step 6](#6-take-over-scheduling-and-retries).

[Charge a customer token](/checkout/recurring/charging-a-token/) has the call in full.

| Vipps field | Kustom field | Notes |
|---|---|---|
| `agreementId` (path) | `recurring_token` (path) | |
| `amount` | `order_amount` | Must equal the sum of `order_lines[].total_amount` |
| *(currency on the agreement)* | `purchase_currency` | Required on every charge |
| `description` | `order_lines[].name` | Visible to the customer |
| `DIRECT_CAPTURE` | `auto_capture: true` | |
| `RESERVE_CAPTURE` | `auto_capture: false` | The default. Capture in Order Management |
| `due` | *(none)* | Call on the day you want to charge |
| `retryDays` | *(none)* | See step 6 |
| `orderId` | `merchant_reference1` | One unique value per billing period |
| `externalId` | `merchant_reference2` | |
| `type: UNSCHEDULED` | *(no difference)* | Every token charge is on demand |
| `Idempotency-Key` header | *(none)* | The token charge takes no idempotency key. See step 6 before you retry |

### 5. Manage the charge in Order Management

A Vipps charge lives under its agreement. A Kustom token charge is an ordinary order, so you
manage it with Order Management by the `order_id` the charge returned — see the
[endpoint mapping](#endpoint-mapping) below and [Capture and
ship](/order-management/capture/).

Capturing a charge made with `auto_capture: false`:

```http
POST /ordermanagement/v1/orders/a89ec121-1276-419d-882a-c343d58fd1bc/captures
Authorization: Basic <token>
Content-Type: application/json
Kustom-Idempotency-Key: sub-1001-3-capture
```

```json
{
  "captured_amount": 29900,
  "description": "Coffee subscription, March"
}
```

The Order Management API reference lists this header as `Klarna-Idempotency-Key`. Both names
work; see [Idempotency and retries](/order-management/idempotency-and-retries/).

| Vipps charge status | Kustom |
|---|---|
| `PENDING`, `DUE`, `PROCESSING` | No equivalent. The charge doesn't exist until you call |
| `RESERVED` | Order status `AUTHORIZED` |
| `CHARGED` | Order status `CAPTURED` |
| `PARTIALLY_CAPTURED` | Order status `PART_CAPTURED` |
| `FAILED` | The charge call returned an error — see step 6 |
| `CANCELLED` | Order status `CANCELLED` |
| `REFUNDED` or `PARTIALLY_REFUNDED` | The status stays `CAPTURED` and `refunded_amount` grows |

The [order lifecycle](/order-management/) has every state.

### 6. Take over scheduling and retries

Vipps runs the charge on the due date and retries it for you. Kustom does neither, so your
billing job must decide which subscriptions are due today, charge each token once per billing
period and record that it did, then handle the result.

Only one failure carries an error code that describes the charge, and most come back with no
body at all, so branch on the HTTP status first:

| Status | Body | What to do |
|---|---|---|
| `403` | `PAYMENT_METHOD_FAILED` | The method didn't work this time. Retry with backoff, keep the `correlation_id`, and if it keeps failing ask the customer to update their payment method |
| `400` | `BAD_VALUE` | A field is invalid (named in `error_messages`), or the token belongs to another merchant ID. Fix the request; don't retry it unchanged |
| `400` | empty | The token is cancelled, or recurring isn't enabled on your merchant account. Retrying won't help |
| `404` | empty | No such token. Check the value and the environment |
| `500` | empty | The outcome is unknown. Check whether the order was created before you charge again |

With no idempotency key on the charge, that last row is the one that double-charges customers:
if a charge times out or returns `500`, find out whether the order exists before you send it
again. The [error catalogue](/api/errors/#errors-customer-token) has the coded errors in full.

**Updating the payment method** means a new token, not a repair of the old one. Send the
customer through a new recurring checkout order (step 2), store the new `recurring_token`, then
cancel the old one (step 7).

### 7. Read and stop a token

Vipps agreements read back as `PENDING`, `ACTIVE`, `STOPPED` or `EXPIRED`, and a `PATCH` stops
them or changes their price or interval. A Kustom token has no price or interval to change, so
the only things you do to it are [read it](/api/customer-tokens/read-token/) and
[cancel it](/api/customer-tokens/update-token-status/).

```http
GET /customer-token/v1/tokens/{recurring_token}
Authorization: Basic <token>
```

```json
{
  "payment_method_type": "CARD",
  "status": "ACTIVE",
  "card": {
    "brand": "visa",
    "expiry_date": "11/2031",
    "masked_number": "************4242"
  }
}
```

A token reads back `ACTIVE` while it can be charged and `CANCELLED` once it's been cancelled —
it doesn't expire from disuse. A card token carries a `card` object and a direct-debit token a
`direct_debit` one. An unknown token returns `404` with an empty body. Check the status before a
billing run rather than finding a dead token in a failed batch.

To cancel:

```http
PATCH /customer-token/v1/tokens/{recurring_token}/status
Authorization: Basic <token>
Content-Type: application/json
```

```json
{
  "status": "CANCELLED"
}
```

Kustom answers `202 Accepted` with an empty body. `CANCELLED` is final — a cancelled token can't
be made active again, and cancelling it a second time returns `400`. Orders already created with
the token can still be captured and refunded.

| Vipps agreement status | Kustom token |
|---|---|
| `PENDING` | No token yet. The checkout order is still `checkout_incomplete` |
| `ACTIVE` | `ACTIVE` |
| `STOPPED` | `CANCELLED` |
| `EXPIRED` | No equivalent. A token stays `ACTIVE` until you cancel it |

**Changing the price or interval** is a change to your own records: send the new amount on the
next charge and tell the customer as your terms require. **Pausing** is skipping charges in your
billing job — there's no pause status.

### 8. Replace agreement and charge webhooks

Vipps sends webhooks when agreements and charges change state. Kustom has none for tokens,
because nothing happens to a token without a call from you.

| Vipps event you listened for | What to do in Kustom |
|---|---|
| Agreement activated | Handle the push for the recurring checkout order (step 3) |
| Agreement stopped by the customer | Customers manage subscriptions with you. Cancel the token when they cancel on your "My subscriptions" page |
| Charge captured, failed or cancelled | Read the result of the charge call itself (steps 4 and 6). A token charge is never left pending |

### 9. Move your existing agreements

From **March 8, 2027** your existing active Vipps agreements can be charged through Kustom. You
don't create these tokens yourself: Kustom and Vipps transfer them and give you a mapping from
each Vipps `agreementId` to a Kustom `recurring_token`.

**How you receive the mapping is still being confirmed** with Vipps and Kustom's payment
partner. Kustom will contact each affected merchant with the delivery details and the format
before March 8, 2027, so make sure your Kustom account has an up-to-date technical contact.

Finish steps 1 to 8 before the mapping arrives. Then cut over:

1. **Import the mapping.** Store each `recurring_token` on its subscription and keep the Vipps `agreementId` beside it for reconciliation and support cases.
2. **Check the tokens.** Read each one and confirm `status: ACTIVE`. Spread the calls out to stay inside the [rate limits](/api/limits/).
3. **Stop scheduling new Vipps charges.** Don't create charges with a `due` date after your cut-over, and cancel any already queued for later.
4. **Switch each subscription at its next billing date**, charging it as in step 4. Charge each billing period through exactly one provider, and keep a per-subscription record of which one charged each period until April 30, 2027.
5. **Stop the Vipps agreement only after the first Kustom charge succeeds.** Stop it earlier and you have no fallback for that customer.
6. **Finish before April 30, 2027.** From that date all Vipps recurring charges must run through Kustom.

## Endpoint mapping

| Operation | Vipps Recurring API | Kustom |
|---|---|---|
| Authenticate | `POST /accesstoken/get` | HTTP Basic on every call |
| Create subscription | `POST /recurring/v3/agreements` | `POST /checkout/v3/orders` with `recurring: true` |
| Read subscription | `GET /recurring/v3/agreements/{agreementId}` | `GET /customer-token/v1/tokens/{recurring_token}` |
| Stop subscription | `PATCH /recurring/v3/agreements/{agreementId}` | `PATCH /customer-token/v1/tokens/{recurring_token}/status` |
| Charge | `POST /recurring/v3/agreements/{agreementId}/charges` | `POST /customer-token/v1/tokens/{recurring_token}/order` |
| Read charge | `GET /recurring/v3/agreements/{agreementId}/charges/{chargeId}` | `GET /ordermanagement/v1/orders/{order_id}` |
| Capture charge | `POST /recurring/v3/agreements/{agreementId}/charges/{chargeId}/capture` | `POST /ordermanagement/v1/orders/{order_id}/captures` |
| Cancel charge | `DELETE /recurring/v3/agreements/{agreementId}/charges/{chargeId}` | `POST /ordermanagement/v1/orders/{order_id}/cancel` |
| Refund charge | `POST /recurring/v3/agreements/{agreementId}/charges/{chargeId}/refund` | `POST /ordermanagement/v1/orders/{order_id}/refunds` |

## Check that it works

Against Playground, in this order:

1. Create an order with `recurring: true` and complete it with [test data](/basics/test-data/). The checkout offers no financing methods — that's the flag taking effect.
2. Your push handler reads a `recurring_token` off the order, stores it and acknowledges the order.
3. Charge the token and get back an `order_id` with `fraud_status: ACCEPTED`. The order appears in Order Management, captured if you sent `auto_capture: true`.
4. Charge a deliberately mangled token id and confirm your billing job stops on the empty `404` instead of retrying.
5. Cancel a token, read it back as `CANCELLED`, charge it and confirm the empty `400` stops your retries.

## Troubleshooting

**Customers were charged early.** A Vipps charge is queued for its `due` date; a Kustom charge
is taken when you call. A job that created Vipps charges days in advance has to move to the
billing date itself.

**Failed charges are never retried.** Kustom doesn't retry. Build retries on
`PAYMENT_METHOD_FAILED` into your billing job before you move any volume.

**The charge returns `400 BAD_VALUE`.** Most often the amounts don't add up: `order_amount` must
equal the sum of `order_lines[].total_amount`, and `order_tax_amount` the sum of
`total_tax_amount`. A single `amount` isn't enough. It's also what a token from another merchant
ID gets — if you run several MIDs, store which one each token belongs to.

**The charge is rejected for its currency.** Vipps stores the currency on the agreement. Kustom
needs `purchase_currency` on every charge.

**Every charge returns an empty `400`.** Recurring isn't enabled on that merchant ID. Ask
Kustom merchant support to enable it on both your Playground and production MIDs.

**A customer was charged twice during cut-over.** A billing period went through both providers,
or a charge that returned `500` was resent without checking. Record per subscription which
provider charged each period.

## Related

- [Charge a customer token](/checkout/recurring/charging-a-token/) — the charge call and its failures in full
- [Create a customer token](/checkout/recurring/minting-a-token/) — the sign-up order
- [Capture an order](/order-management/capture/) — collecting charges made without `auto_capture`
- [Charge a customer later](/checkout/recurring/) — how the token and the charge fit together
- [Customer Token API reference](/api/customer-tokens/) — every token call
- [Migrating from Vipps Checkout](/checkout/surfaces/migrating-from-vipps-checkout/) — the one-off purchase half of a Vipps integration
- [Vipps MobilePay Recurring API guide](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/) — the integration you're leaving
