> **Description:** UCP payment handler for creating payments via the ePayment API (WALLET and CARD).

# Vipps payment handler

**Under development**

This documentation is under development. Content and integration details may change as we continue internal discussions. Feedback is welcome.

- **Handler Name:** `no.vipps.ucp.payment_handler`
- **Version:** `2026-01-23`

## Introduction

This payment handler supports payment creation through the
[ePayment API](https://developer.vippsmobilepay.com/redocusaurus/epayment-swagger-id.yaml).
Two payment methods are supported:

- **WALLET** - For customers with a wallet. Uses push notification flow
  (`userFlow: PUSH_MESSAGE`).
- **CARD** - For customers without a wallet. Uses redirect flow (`userFlow: WEB_REDIRECT`) where
  users enter card details manually.

### Key benefits

- Efficient integration with our payment services.
- Seamless checkout experience for customers with or without a wallet.
- Secure handling of payment credentials and transactions.

### Integration guide

| Participant  | Integration Section                           |
| :----------- | :-------------------------------------------- |
| **Business** | [Business Integration](#business-integration) |
| **Platform** | [Platform Integration](#platform-integration) |

---

## Participants

This payment handler covers two participants: the **business** for which a _customer_ is shopping
from and the **platform** that facilitates the checkout process (such as an agent).

- The **business** is responsible for processing payments using this handler,
- The **platform** offers the supported _payment instrument_ to the customer in the checkout flow.
- The **business** registers this handler and configures/implements it, and the **platform**
  discovers and utilizes the handler during checkout.
- The **business** needs to have an active merchant agreement and an ecommerce sales unit registered
- The **platform** needs to validate that a user has a valid wallet identity before
  offering this payment option. Suggestion is to utilize the
  [Login Product](https://developer.vippsmobilepay.com/docs/APIs/login-api/README.md) to do so

> **Note on terminology:** While this specification refers to the participant as the **"business,"**
> technical schema fields may retain the standard industry nomenclature **`merchant_*`**(e.g.,
> `merchant_id`). Mappings are documented below.

| Participant  | Role                                                                                              | Prerequisites                                                  |
| :----------- | :------------------------------------------------------------------------------------------------ | :------------------------------------------------------------- |
|**Business**| Creates a payment for the defined customer                                                        | Needs to have an active merchant agreement and ecom sales unit |
|**Platform**| Collects the customer information, along with validating that the customer has a wallet identity. | Needs to validate that the user has a wallet identity          |

### Payment flow diagram

The following diagram shows the async payment flow for this handler. The credential can be either
MSISDN (phone number) or TOKEN (customer token):

```mermaid
sequenceDiagram
    participant Platform
    participant Business
    participant ePayment
    participant User

    Platform->>Business: POST /complete (msisdn or customerToken)
    Business->>ePayment: CreatePayment (msisdn or customerToken)
    ePayment-->>Business: state: CREATED
    ePayment->>User: Push notification
    Business-->>Platform: status: complete_in_progress
    Note over Platform: Tells user to check Vipps
    User->>ePayment: Approves payment
    ePayment->>Business: Callback: AUTHORIZED
    Note over Business: Updates session
    Platform->>Business: GET /checkout-sessions/{id}
    Business-->>Platform: status: completed + order
```

    Platform sends POST /complete to Business with an msisdn or customer token.

    Business calls ePayment CreatePayment with the msisdn or customer token.

  ePayment returns state: CREATED to Business.
  ePayment sends a push notification to the User.

    Business returns status: complete_in_progress to Platform. Platform tells
    the user to check Vipps.

  User approves the payment in ePayment.

    ePayment sends a callback to Business with status: AUTHORIZED. Business
    updates the session.

  Platform sends GET /checkout-sessions/&#123;id&#125; to Business.
  Business returns status: completed with the order to Platform.

---

>**Note:**To create a payment, the business**MUST**create an
> [Access Token](https://developer.vippsmobilepay.com/docs/APIs/access-token-api/README.md)
> using their credentials and then call the
> [Epayment API CreatePayments endpoint](https://developer.vippsmobilepay.com/docs/APIs/epayment-api/README.md)
> to create a payment. The suggested payment `userFlow` is `PUSH_MESSAGE`, which triggers a push
> notification to the user's app for approval. It is also possible to have the default user flow (`REDIRECT_URL`), the business must then echo back the `redirectUrl` to the platform using the UCP `status = requires_escalation` with the `continue_url` set to the `redirectUrl`.

## Business integration

### Business prerequisites

Before advertising this handler, businesses**MUST** complete:

1. Create a merchant agreement and register an Ecommerce sales unit.
2. Implement server-side logic to generate access tokens and create payments using the [Access Token](https://developer.vippsmobilepay.com/docs/APIs/access-token-api/README.md) and
   [ePayment](https://developer.vippsmobilepay.com/docs/APIs/epayment-api/README.md) APIs.

**Prerequisites Output:**

| Field                           | Description                                 |
| :------------------------------ | :------------------------------------------ |
| `identity.clientId`             | The client id to call the APIs with         |
| `identity.clientSecret`         | The secret to call the APIs with            |
| `identity.subscriptionKey`      | The subscription key to call the APIs with. |
| `merchant.merchantSerialNumber` | The merchant identifier in the systems      |

### Handler configuration

Businesses advertise support for this handler in their `/.well-known/ucp` profile under the
`payment.handlers` array.

#### Configuration schema

**Schema URL:** `https://ucp.vipps.no/ucp/2026-01-23/schemas/wallet_payment_handler.json`

| Field                         | Type   | Required | Description                                                                                                                                                   |
| :---------------------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `merchant_serial_number`      | string | **Yes**  | The merchant identifier (Merchant Serial Number). This is obtained when registering an ecommerce sales unit. Format varies by country (5-6 digits typically). |
| `environment`                 | enum   | No       | The environment. Either `TEST` or `PRODUCTION`. Defaults to `PRODUCTION` if not specified. Use `TEST` for sandbox testing with test credentials.              |
| `allowed_payment_instruments` | array  | No       | The payment instruments allowed for this handler. If not specified, all instrument types are allowed. Supported types: `WALLET` and `CARD`.                   |

#### Example handler declaration

```json
{
  "payment": {
    "handlers": [
      {
        "id": "vipps",
        "name": "no.vipps.ucp.payment_handler",
        "version": "2026-01-23",
        "spec": "https://ucp.vipps.no/ucp/2026-01-23/vipps_payment_handler",
        "config_schema": "https://ucp.vipps.no/ucp/2026-01-23/schemas/wallet_payment_handler.json",
        "instrument_schemas": [
          "https://ucp.vipps.no/ucp/2026-01-23/schemas/payment_instrument.json"
        ],
        "config": {
          "merchant_serial_number": "123456",
          "environment": "PRODUCTION"
        }
      }
    ]
  }
}
```

### Instrument schema

**Schema URL:** `https://ucp.vipps.no/ucp/2026-01-23/schemas/payment_instrument.json`

This payment instrument defines the structure for payments submitted by the platforms supporting this handler.

| Field        | Type   | Required              | Description                                                                                                                                            |
| :----------- | :----- | :-------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`       | enum   | **Yes**               | The payment instrument type. One of `WALLET` or `CARD`.                                                                                                |
| `credential` | object | **Yes** (WALLET only) | The credential information required to process WALLET payments. See [Credential Schema](#credential-schema).                                           |
| `return_url` | string | **Depends**           | The URL to redirect the user back to after a payment. This is **required** for non-push wallet flows and card. Can be a universal link or website URL. |

#### Instrument types

| Type     | Vipps `paymentMethod.type` | Vipps `userFlow`               | Description                                                                                                                                                                                                                                       |
| :------- | :------------------------- | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `WALLET` | `WALLET`                   | `PUSH_MESSAGE`, `WEB_REDIRECT` | When `userFlow` is `PUSH_MESSAGE`, this triggers a push notification to user's app. Requires customer credential (MSISDN or TOKEN). If `userFlow` is `WEB_REDIRECT`, the user must be redirected to the URL returned in the payment created call. |
| `CARD`   | `CARD`                     | `WEB_REDIRECT`                 | Redirect user to Vipps payment page to enter card details. No wallet required.                                                                                                                                                                    |

#### Credential schema

**Schema URL:**
`https://ucp.vipps.no/ucp/2026-01-23/schemas/wallet_payment_credential.json`

The credential object contains the buyer's wallet identifier. Two credential types
are supported:

| Field   | Type   | Required | Description                                                              |
| :------ | :----- | :------- | :----------------------------------------------------------------------- |
| `type`  | enum   | **Yes**  | The credential type. One of `MSISDN` or `TOKEN`.                         |
| `value` | string | **Yes**  | The credential value. Format depends on the credential type (see below). |

##### MSISDN credential (public)

The MSISDN credential uses the customer's phone number. This maps to the `customer.phoneNumber`
field in the
[Vipps ePayment API](https://developer.vippsmobilepay.com/redocusaurus/epayment-swagger-id.yaml).

| Field   | Description                                                                                                                                                                        |
| :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value` | The phone number in MSISDN format: digits only with country code and subscriber number, no leading zeros or plus sign. Example: `4712345678` for Norwegian number +47 12 34 56 78. |

**Example MSISDN Instrument:**

```json
{
  "type": "WALLET",
  "credential": {
    "type": "MSISDN",
    "value": "4712345678"
  }
}
```

##### TOKEN credential

The TOKEN credential uses an encoded customer token obtained from authenticated user sessions. This
maps to the `customer.customerToken` field in the
[ePayment API](https://developer.vippsmobilepay.com/redocusaurus/epayment-swagger-id.yaml).

| Field   | Description                                                            |
| :------ | :--------------------------------------------------------------------- |
| `value` | An encoded customer token string obtained from authenticated sessions. |

**Example TOKEN Instrument:**

```json
{
  "type": "WALLET",
  "credential": {
    "type": "TOKEN",
    "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }
}
```

#### CARD instrument (no wallet required)

The CARD instrument allows customers who do not have a wallet to pay using their
card. This uses the redirect flow where the user is sent to a payment page to
enter their card details.

| Field        | Type   | Required | Description                                                                                                                                                                   |
| :----------- | :----- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`       | enum   | **Yes**  | Must be `CARD`.                                                                                                                                                               |
| `return_url` | string | **Yes**  | The URL to redirect the user back to after payment. Can be a universal link (e.g., `myapp://payment-complete`) or website URL (e.g., `https://example.com/payment-complete`). |

**Example CARD Instrument:**

```json
{
  "type": "CARD",
  "return_url": "https://chat.vipps.no/payment-complete?session_id=cs-ABC123XYZ"
}
```

**Example CARD Instrument with Universal Link:**

```json
{
  "type": "CARD",
  "return_url": "vipps-chat://payment-complete?session_id=cs-ABC123XYZ"
}
```

---

### Processing payments

Upon receiving a call for completing a checkout with this handler, businesses **MUST**:

1. **Validate Handler:** Confirm `instrument.handler_id` matches an advertised handler.
2. **Ensure Idempotency:** If the request is a retry (matches a previous `checkout_id` or
   idempotency key), return the previous result immediately without re-processing funds.
3. **Determine Payment Method:** Based on the instrument type:

- `WALLET` -> Use `paymentMethod: { type: "WALLET" }` and `userFlow: "PUSH_MESSAGE" or "WEB_REDIRECT (requires return_url)"`
- `CARD` -> Use `paymentMethod: { type: "CARD" }` and `userFlow: "WEB_REDIRECT"`

1. **Build API Request:** See [WALLET Payment Flow](#wallet-payment-flow) or
   [Redirect Payment Flow](#redirect-payment-flow) below.
2. Call the
   [ePayment API CreatePayment](https://developer.vippsmobilepay.com/redocusaurus/epayment-swagger-id.yaml)
   endpoint.
3. **Return Response:** Based on instrument type:

- `WALLET` -> Return `status: complete_in_progress` with message to check the app
- `CARD` -> Return `status: complete_in_progress` with `continue_url` for platform to redirect
  user

1. **Handle callback and poll:** When the user approves/rejects, we send a callback.
   Update the checkout session accordingly. As callbacks are not guaranteed, implement polling as a
   backup to check payment status. It is also possible to base this on webhooks if the platform
   supports that.
2. **Return completed order:** When the platform polls and payment is authorized, return
   `status: completed` with the order.

#### `paymentMethod` mapping

The `paymentMethod.type` field in the ePayment API **MUST** match the instrument type:

| Instrument Type | `paymentMethod.type` | `userFlow`     | Description                               |
| :-------------- | :------------------- | :------------- | :---------------------------------------- |
| `WALLET`        | `WALLET`             | `PUSH_MESSAGE` | Push notification to user's app           |
| `CARD`          | `CARD`               | `WEB_REDIRECT` | Redirect to a payment page for card entry |

See the
[ePayment API documentation](https://developer.vippsmobilepay.com/redocusaurus/epayment-swagger-id.yaml)
for full details on the `paymentMethod` and `userFlow` fields.

---

### WALLET payment flow

The `PUSH_MESSAGE` flow is **asynchronous** - the user must approve the payment in
their app. This can take seconds to minutes. Businesses **MUST NOT** block the HTTP response
waiting for user approval.

#### Why async?

| Approach             | Problem                                        |
| -------------------- | ---------------------------------------------- |
| Block HTTP request   | Timeouts after 30s, user often needs more time |
| Short timeout (5s)   | User can't approve that fast                   |
| Keep connection open | Unreliable, poor scalability                   |

#### Recommended flow

1. **Business creates payment** -> We return `state: CREATED`
2. **Business returns immediately** with `status: complete_in_progress`
3. **We send push notification** to user's phone
4. **User approves** in app
5. **We sends callback** to business with `state: AUTHORIZED`
6. **Business updates checkout** to `status: completed`
7. **Platform polls** and receives completed order

#### Complete checkout response (pending approval)

When the payment is created but awaiting user approval:

```json
{
  "id": "cs-ABC123XYZ",
  "status": "complete_in_progress",
  "messages": [
    {
      "type": "info",
      "code": "payment_pending_user_approval",
      "content": "A payment request has been sent to your Vipps app. Please open Vipps and approve the payment to complete your order."
    }
  ],
  "payment": {
    "state": "pending_approval",
    "expires_at": "2026-01-26T12:35:00Z"
  },
  "ucp": {
    "version": "2026-01-11",
    "capability": "dev.ucp.shopping.checkout"
  }
}
```

#### Message codes

| Code                            | Type    | Instrument | Description                                                     |
| ------------------------------- | ------- | ---------- | --------------------------------------------------------------- |
| `payment_pending_user_approval` | `info`  | WALLET     | Payment request sent, awaiting user approval in the app         |
| `payment_redirect_required`     | `info`  | CARD       | User must be redirected to a payment page to enter card details |
| `payment_approved`              | `info`  | Both       | User approved/completed the payment                             |
| `payment_rejected`              | `error` | Both       | User rejected or cancelled the payment                          |
| `payment_expired`               | `error` | Both       | Payment request expired before user responded                   |

#### Timeout handling

Businesses **SHOULD** implement timeout handling:

- **Default timeout:** 5 minutes from payment creation
- **On timeout:** Cancel the payment and set checkout to error state
- **Expose expiry:** Include `payment.expires_at` in responses

---

### Callback handling

Businesses **MUST** implement a callback endpoint to receive payment status updates.

> **Important:** We don't guarantee callback delivery. Businesses **MUST** also implement
> polling as a backup. See
> [Polling Guidelines](https://developer.vippsmobilepay.com/docs/knowledge-base/polling-guidelines.md).

**Callback URL:** Configured in `MerchantInfo.callbackUrl`

**Callback Payload:**

```json
{
  "reference": "cs-ABC123XYZ",
  "state": "AUTHORIZED",
  "pspReference": "1234567890"
}
```

**Callback States:**

| State        | Action                                        |
| ------------ | --------------------------------------------- |
| `AUTHORIZED` | Update checkout to `completed`, create order  |
| `ABORTED`    | Update checkout with error: user rejected     |
| `EXPIRED`    | Update checkout with error: payment expired   |
| `TERMINATED` | Update checkout with error: payment cancelled |

---

### Business-side polling (backup)

Since callbacks aren't guaranteed, businesses **MUST** implement polling as a backup
mechanism to check payment status.

**Polling Guidelines:**

- **Start polling:** 5 seconds after payment creation
- **Poll interval:** Every 2 seconds
- **Endpoint:** `GET /epayment/v1/payments/{reference}`

**Example Polling Logic:**

```javascript
async function pollPaymentStatus(reference: string, maxAttempts = 150) {
  const POLL_INTERVAL_MS = 2000; // 2 seconds
  const INITIAL_DELAY_MS = 5000; // 5 seconds

  // Wait 5 seconds before starting
  await new Promise(resolve => setTimeout(resolve, INITIAL_DELAY_MS));

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const payment = await getPayment(reference);

    if (payment.state === "AUTHORIZED") {
      // Payment approved - complete the order
      return { success: true, state: "AUTHORIZED" };
    }

    if (["ABORTED", "EXPIRED", "TERMINATED"].includes(payment.state)) {
      // Payment failed - update checkout with error
      return { success: false, state: payment.state };
    }

    // Still CREATED - wait and poll again
    await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
  }

  // Timeout - treat as expired
  return { success: false, state: "TIMEOUT" };
}
```

> **Note:** The callback and polling mechanisms are complementary. Whichever receives the status
> update first should process it, and the other should recognize that processing has already
> occurred (idempotency).

---

### Redirect payment flow

This is applicable to `CARD` payments or `WALLET` payments with `WEB_REDIRECT`

In the payment flows that use `WEB_REDIRECT`, the user is redirected to a
payment page to complete their payment. This flow is for customers who do not have a wallet identity (card) or for cases where `PUSH_MESSAGE` is not possible.

#### Flow diagram

```mermaid
sequenceDiagram
    participant Platform
    participant Business
    participant ePayment
    participant User

    Platform->>Business: POST /complete (type: CARD, return_url)
    Business->>ePayment: CreatePayment (CARD, WEB_REDIRECT, returnUrl)
    ePayment-->>Business: state: CREATED, redirectUrl
    Business-->>Platform: complete_in_progress, continue_url
    Note over Platform: Redirect user to continue_url
    Platform->>ePayment: User opens payment page
    User->>ePayment: Enters card details
    ePayment->>Business: Callback: AUTHORIZED
    ePayment->>User: Redirect to returnUrl
    User->>Platform: Return via returnUrl
    Platform->>Business: GET /checkout-sessions/{id}
    Business-->>Platform: status: completed, order
```

    Platform sends POST /complete to Business with type: CARD and a return_url.

    Business calls ePayment CreatePayment with CARD, WEB_REDIRECT, and
    returnUrl.

  ePayment returns state: CREATED and a redirectUrl to Business.

    Business returns complete_in_progress and a continue_url to Platform.
    Platform redirects the user to the continue_url.

  User opens the payment page via Platform.
  User enters card details in ePayment.
  ePayment sends a callback to Business with status: AUTHORIZED.
  ePayment redirects the User to the returnUrl.
  User returns to Platform via the returnUrl.
  Platform sends GET /checkout-sessions/&#123;id&#125; to Business.
  Business returns status: completed with the order to Platform.

#### Redirect specific request fields

When creating a payment with `userFlow: { type: "WEB_REDIRECT" }`, the business **MUST** include:

| Field       | Description                                                                                   |
| :---------- | :-------------------------------------------------------------------------------------------- |
| `returnUrl` | The URL to redirect the user back to after payment. Use the `return_url` from the instrument. |

#### Business response

When the payment is created, we return a `redirectUrl`. The business **MUST** return this to the
platform as `continue_url` per the
[UCP Continue URL specification](https://ucp.dev/latest/specification/checkout/#continue-url).

The `continue_url` enables checkout handoff from platform to business UI, allowing the buyer to
complete the payment on the Vipps payment page. Per UCP, when `status` is
`complete_in_progress`, the business is processing the Complete Checkout request and may provide a
`continue_url` for user interaction.

```json
{
  "id": "cs-ABC123XYZ",
  "status": "complete_in_progress",
  "continue_url": "https://api.vipps.no/checkout/v3/session/...",
  "messages": [
    {
      "type": "info",
      "code": "payment_redirect_required",
      "content": "Please complete your payment on the payment page."
    }
  ],
  "payment": {
    "state": "pending_redirect",
    "expires_at": "2026-01-26T12:35:00Z"
  }
}
```

#### Platform redirect handling

When the platform receives a `complete_in_progress` response with a `continue_url` and message code
`payment_redirect_required`:

1. **Redirect the user:** Open the `continue_url` in a browser or webview to hand off to the
   payment page
2. **User completes payment:** User enters card details on the payment page
3. **Return redirect:** After payment, we redirect user to the `returnUrl` provided in the
   instrument
4. **Poll for completion:** After redirect, poll `GET /checkout-sessions/{id}` to get the final
   status (`completed` or error)

**Return URL Formats:**

| Type           | Example                                                 | Use Case                   |
| :------------- | :------------------------------------------------------ | :------------------------- |
| Universal Link | `vipps-chat://payment-complete?session_id=cs-ABC123XYZ` | Native mobile app          |
| Website URL    | `https://chat.vipps.no/payment-complete?session_id=...` | Web-based chat or fallback |

> **Note:** The `returnUrl` should include sufficient context (e.g., `session_id`) for the platform
> to resume the checkout flow after the user returns.

---

### Example business implementation

```javascript
const instrument: PaymentInstrument; // The payment instrument from the request
const checkout: Checkout; // The checkout with lineItems
const credentials: Credentials; // API Credentials
const accessToken: string; // The accessToken you received
const idempotencyKey: string; // Your idempotency key

const url = "https://apitest.vipps.no/epayment/v1/payments"; // Replace for production

/**
 * Build the API request body based on instrument type.
 * See: https://developer.vippsmobilepay.com/api/epayment/#tag/CreatePayments/operation/createPayment
 */
function buildPaymentRequest(instrument: PaymentInstrument, checkout: Checkout) {
  const baseRequest = {
    amount: {
      currency: checkout.currency,
      value: checkout.totals.amount
    },
    reference: checkout.id,
    paymentDescription: `Order ${checkout.id}`
  };

  if (instrument.type === "WALLET") {
    // WALLET: Use PUSH_MESSAGE flow with customer credential
    return {
      ...baseRequest,
      customer: buildCustomerObject(instrument.credential),
      paymentMethod: {
        type: "WALLET"  // REQUIRED for WALLET instruments
      },
      userFlow: "PUSH_MESSAGE" // Or web_Redirect, then returnUrl must be set
    };
  } else if (instrument.type === "CARD") {
    // CARD: Use WEB_REDIRECT flow, no customer credential needed
    return {
      ...baseRequest,
      paymentMethod: {
        type: "CARD"  // REQUIRED for CARD instruments
      },
      userFlow: "WEB_REDIRECT",
      returnUrl: instrument.return_url  // Platform provides this for redirect back
    };
  }

  throw new Error(`Unsupported instrument type: ${instrument.type}`);
}

/**
 * Build the customer object for WALLET payments based on credential type.
 */
function buildCustomerObject(credential: WalletCredential): { phoneNumber?: string; customerToken?: string } {
  switch (credential.type) {
    case "MSISDN":
      return { phoneNumber: credential.value };
    case "TOKEN":
      return { customerToken: credential.value };
    default:
      throw new Error(`Unsupported credential type: ${credential.type}`);
  }
}

const body = buildPaymentRequest(instrument, checkout);

const response = await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${accessToken}`,
    "Ocp-Apim-Subscription-Key": credentials.subscriptionKey,
    "Merchant-Serial-Number": credentials.merchantSerialNumber,
    "Idempotency-Key": idempotencyKey
  },
  body: JSON.stringify(body)
});

const payment = await response.json();

if (!response.ok) {
  return mapErrorToUCPResponse(response.status, payment);
}

// Build response based on instrument type
checkout.status = "complete_in_progress";
checkout.payment = {
  reference: payment.reference,
  expires_at: new Date(Date.now() + 5 * 60 * 1000).toISOString()
};

if (instrument.type === "WALLET") {
  // WALLET: Return message to check app
  checkout.payment.state = "pending_approval";
  checkout.messages = [{
    type: "info",
    code: "payment_pending_user_approval",
    content: "A payment request has been sent to your Vipps / Mobilepay app. Please open the app and approve the payment."
  }];
} else if (instrument.type === "CARD") {
  // CARD: Return continue_url for platform to redirect user (per UCP spec)
  checkout.payment.state = "pending_redirect";
  checkout.continue_url = payment.redirectUrl;  // From Vipps response, returned as continue_url
  checkout.messages = [{
    type: "info",
    code: "payment_redirect_required",
    content: "Please complete your payment on the payment page."
  }];
}

return checkout;
```

**TypeScript Types:**

```typescript
type InstrumentType = "WALLET" | "CARD";
type CredentialType = "MSISDN" | "TOKEN";

interface WalletCredential {
  type: CredentialType;
  value: string;
}

interface WalletInstrument {
  type: "WALLET";
  credential: WalletCredential;
  return_url?: string; // Only for non push-flow
}

interface CardInstrument {
  type: "CARD";
  return_url: string; // Universal link or website URL
}

type PaymentInstrument = WalletInstrument | CardInstrument;

// Example API payloads:

// WALLET with MSISDN -> paymentMethod.type: "WALLET", customer.phoneNumber
const walletMsisdnPayload = {
  paymentMethod: { type: "WALLET" },
  userFlow: "PUSH_MESSAGE",
  customer: { phoneNumber: "4712345678" },
  // ... other fields
};

// WALLET with TOKEN -> paymentMethod.type: "WALLET", customer.customerToken
const walletTokenPayload = {
  paymentMethod: { type: "WALLET" },
  userFlow: "PUSH_MESSAGE",
  customer: { customerToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." },
  // ... other fields
};

// WALLET with msisdn and redirect
const walletTokenPayload = {
  paymentMethod: { type: "WALLET" },
  userFlow: "WEB_REDIRECT",
  returnUrl: "vipps-chat://payment-complete?session_id=cs-ABC123XYZ",
  // ... other fields
};

// CARD -> paymentMethod.type: "CARD", no customer field needed
const cardPayload = {
  paymentMethod: { type: "CARD" },
  userFlow: "WEB_REDIRECT",
  returnUrl: "vipps-chat://payment-complete?session_id=cs-ABC123XYZ",
  // ... other fields
};
```

> NOTE: For `WEB_REDIRECT` flows, the `returnUrl` must be exactly the same as provided by the
> platform, to ensure the customer is successfully redirected back to the platform after payment
> approval.

### Error handling

Businesses **MUST** follow
[UCP Checkout error handling](https://ucp.dev/latest/specification/checkout/#error-handling): return
error messages in the checkout `messages` array only when the **platform or buyer can act** on them.
The `severity` field declares who resolves the error:

- **`recoverable`** -- The platform can fix via API (e.g. correct payment data and retry). Use for
  invalid instrument data the platform can correct.
- **`requires_buyer_input`** -- The business requires input that cannot be provided via API (e.g.
  buyer must install or link a wallet). Contributes to `status: requires_escalation`; platform
  should hand off via `continue_url`.

Do **not** return UCP error messages for **implementation or backend errors on the business side**
(e.g. merchant misconfiguration, auth failure, internal errors). The platform cannot resolve
these; the checkout should be discarded or set to a terminal state. The business may handle such
errors server-side (retries, alerts, logging) at their discretion.

Map
[ePayment API errors](https://developer.vippsmobilepay.com/docs/APIs/epayment-api/api-guide/errors.md#epayment-api-errors)
(RFC 7807, `extraDetails` error code when present) using the table below. **If the error does not
appear in the mapping**, treat it as a business-side/implementation error: do not surface it as a
UCP message to the platform; handle server-side and treat the checkout as non-recoverable.

> **Note:** This mapping covers only **payment creation** and UCP payment-handling flows.

| HTTP Status | Vipps Error Code | Context (Vipps title / description)     | UCP Error Code           | UCP Severity  | User-Facing Message                                                                         |
| :---------- | :--------------- | :-------------------------------------- | :----------------------- | :------------ | :------------------------------------------------------------------------------------------ |
| 400         | --                | Request body validation (no error code) | `invalid_payment_data`   | `recoverable` | The payment request contains invalid data. Please verify and try again.                     |
| 400         | 4050             | Invalid CustomerToken                   | `invalid_customer_token` | `recoverable` | The provided customer information is invalid. Please validate and try again.                |
| 400         | 4070             | Invalid phone number                    | `invalid_phone`          | `recoverable` | The provided phone number is invalid.                                                       |
| 400         | 7010             | Customer not found                      | `wallet_not_found`       | `recoverable` | No wallet account found for the provided identifier. Please try a different payment method. |

For **wallet_not_found** and **invalid_phone**, the business has no `continue_url` to redirect to
(the buyer cannot "fix" the wallet link in business UI). The platform should treat these as
**recoverable**--e.g. offer another payment method such as CARD, or prompt the user to try a
different identifier.

**Important:** Error messages returned to platforms should be opaque and user-friendly. Do not
expose internal API error details. Log detailed errors server-side for debugging.

**Example UCP Error Response** (e.g. for 7010 Customer not found):

```json
{
  "messages": [
    {
      "type": "error",
      "code": "wallet_not_found",
      "severity": "recoverable",
      "content": "No wallet account found for this phone number. Please try a different payment method.",
      "path": "$.payment.instruments[0]"
    }
  ]
}
```

---

## Platform integration

### Platform prerequisites

Before using this handler, Platforms **MUST** complete:

1. If applicable: Register with merchant to allow to call their `checkout_sessions` endpoint. This
   could involve getting an access token
2. Obtain a valid customer identifier based on the credential type:

- **MSISDN:** Capture the user's MSISDN registered with the wallet. A suggestion is to
  implement the [Login Product](https://developer.vippsmobilepay.com/docs/APIs/login-api/README.md)
  in your application.
- **TOKEN:** Obtain a customer token from an authenticated session.

**Prerequisites Output:**

| Field                     | Description                                                              |
| :------------------------ | :----------------------------------------------------------------------- |
| `merchant.access_token`   | Access token to call merchant (if applicable)                            |
| `vippsUser.msisdn`        | User MSISDN registered on their wallet (for MSISDN credential)           |
| `vippsUser.customerToken` | Encoded customer token from authenticated session (for TOKEN credential) |

### Payment protocol

Platforms **MUST** follow this flow to acquire a payment instrument:

#### Step 1: Discover handler

The Platform identifies `no.vipps.ucp.payment_handler` in the business's
`payment.handlers` array.

```json
{
  "id": "vipps",
  "name": "no.vipps.ucp.payment_handler",
  "version": "2026-01-23",
  "config": {
    "merchant_serial_number": "123456",
    "environment": "PRODUCTION"
  }
}
```

#### Step 2: Choose payment method and gather required data

The Platform **MUST** determine which payment method to use based on whether the customer has a
Vipps MobilePay wallet:

##### Option A: WALLET (User has wallet)

For users with a Vipps MobilePay wallet, use the WALLET instrument. The Platform **MUST** obtain a
customer identifier:

- **MSISDN:** Prompt the user for their MSISDN registered with Vipps MobilePay. Use the
  [Login Product](https://developer.vippsmobilepay.com/docs/APIs/login-api/README.md) to authenticate
  and retrieve the MSISDN.
- **TOKEN:** Use an encoded customer token obtained from an authenticated session.

##### Option B: CARD (User does not have wallet)

For users without a wallet identity, use the CARD instrument. The Platform **MUST** provide:

- **return_url:** The URL to redirect the user back to after payment. This can be:
- **Universal Link:** e.g., `vipps-chat://payment-complete?session_id=...` (for native mobile
  apps)
- **Website URL:** e.g., `https://chat.vipps.no/payment-complete?session_id=...` (for
  web-based platforms)

> **Tip:** Include the checkout session ID in the return URL so the platform can resume the checkout
> flow after redirect.

#### Step 3: Complete checkout

The Platform submits the checkout with the constructed payment instrument.

**Example with MSISDN credential:**

```json
POST /checkout-sessions/{checkout_id}/complete
Content-Type: application/json

{
  "payment_data": {
    "id": "instr_vipps_1",
    "handler_id": "vipps",
    "type": "WALLET",
    "credential": {
      "type": "MSISDN",
      "value": "4712345678"
    }
  },
  "risk_signals": {
    "device_id": "device_abc123",
    "session_id": "session_xyz789",
    "ip_address": "192.168.1.1",
    "user_agent": "Mozilla/5.0 ...",
    "accept_language": "nb-NO"
  }
}
```

**Example with TOKEN credential:**

```json
POST /checkout-sessions/{checkout_id}/complete
Content-Type: application/json

{
  "payment_data": {
    "id": "instr_vipps_1",
    "handler_id": "vipps",
    "type": "WALLET",
    "credential": {
      "type": "TOKEN",
      "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
  }
}
```

**Example with CARD instrument (No wallet required):**

```json
POST /checkout-sessions/{checkout_id}/complete
Content-Type: application/json

{
  "payment_data": {
    "id": "instr_card_1",
    "handler_id": "vipps",
    "type": "CARD",
    "return_url": "vipps-chat://payment-complete?session_id=cs-ABC123XYZ"
  }
}
```

#### Step 4: Handle async response

Both WALLET and CARD flows return `status: complete_in_progress`. The Platform **MUST** handle the
response based on the message code:

**WALLET Response (pending user approval in Vipps app):**

```json
{
  "id": "cs-ABC123XYZ",
  "status": "complete_in_progress",
  "messages": [
    {
      "type": "info",
      "code": "payment_pending_user_approval",
      "content": "A payment request has been sent to your Vipps Mobilepay app. Please open and approve the payment in the app to complete your order."
    }
  ],
  "payment": {
    "state": "pending_approval",
    "expires_at": "2026-01-26T12:35:00Z"
  }
}
```

**CARD Response (redirect required):**

```json
{
  "id": "cs-ABC123XYZ",
  "status": "complete_in_progress",
  "continue_url": "https://api.vipps.no/checkout/v3/session/...",
  "messages": [
    {
      "type": "info",
      "code": "payment_redirect_required",
      "content": "Please complete your payment on the payment page."
    }
  ],
  "payment": {
    "state": "pending_redirect",
    "expires_at": "2026-01-26T12:35:00Z"
  }
}
```

**Platform behavior for WALLET (`payment_pending_user_approval`):**

1. **Inform the user:** Display the message content (e.g., "Please check your app to approve
   the payment")
2. **Poll for completion:** Call `GET /checkout-sessions/{checkout_id}` every 2-5 seconds
3. **Check status:** Continue polling until `status` is `completed` or an error state
4. **Handle timeout:** If `payment.expires_at` passes, inform user the payment expired

**Platform behavior for CARD (`payment_redirect_required`):**

Per the
[UCP Continue URL specification](https://ucp.dev/latest/specification/checkout/#continue-url), the
`continue_url` enables checkout handoff from platform to business UI:

1. **Redirect the user:** Open the `continue_url` in a browser or webview to hand off to the
   payment page
2. **User completes payment:** User enters card details on the payment page
3. **Handle return:** After payment, we redirect user back to the `return_url` provided in the
   instrument
4. **Resume checkout:** Extract session ID from URL and poll `GET /checkout-sessions/{id}` for final
   status (`completed` or error)

#### Step 5: Poll for completion

```json
GET /checkout-sessions/{checkout_id}
```

**Response when payment is approved:**

```json
{
  "id": "cs-ABC123XYZ",
  "status": "completed",
  "order": {
    "id": "order-789",
    "reference": "ORD-2026-001234",
    "created_at": "2026-01-26T12:32:15Z"
  },
  "messages": [
    {
      "type": "info",
      "code": "payment_approved",
      "content": "Payment approved. Your order has been placed."
    }
  ]
}
```

**Response when user rejects:**

```json
{
  "id": "cs-ABC123XYZ",
  "status": "incomplete",
  "messages": [
    {
      "type": "error",
      "code": "payment_rejected",
      "severity": "requires_buyer_input",
      "content": "Payment was declined. Please try again or choose a different payment method."
    }
  ]
}
```

#### Polling best practices

| Recommendation          | Details                                         |
| ----------------------- | ----------------------------------------------- |
| **Poll interval**       | 2-5 seconds initially, increase if many retries |
| **Max duration**        | Stop after `payment.expires_at` + small buffer  |
| **Exponential backoff** | Consider increasing interval after 30 seconds   |
| **User feedback**       | Show progress indicator while polling           |

---

## Security considerations

| Requirement           | Description                                                                                                                                                                |
| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **TLS Required**      | All communication with our APIs **MUST** use TLS 1.2 or higher. Connections using older TLS versions will be rejected.                                                     |
| **MSISDN Validation** | For MSISDN credentials, the value **MUST** be validated to ensure it is a valid phone number format (digits only, 7-15 digits, no leading zero) before sending to the API. |
| **TOKEN Validation**  | For TOKEN credentials, the value **MUST** be validated to ensure it is a non-empty string. Token validity is verified by the API.                                          |
| **Data Residency**    | All payment data is processed within the EEA (European Economic Area). PII is handled according to GDPR requirements.                                                      |

---

## References

- **Handler Spec:** `https://ucp.vipps.no/ucp/2026-01-23/vipps_payment_handler`
- **Config Schema:**
  `https://ucp.vipps.no/ucp/2026-01-23/schemas/wallet_payment_handler.json`
- **Instrument Schema:**
  `https://ucp.vipps.no/ucp/2026-01-23/schemas/payment_instrument.json`
- **Credential Schema:**
  `https://ucp.vipps.no/ucp/2026-01-23/schemas/wallet_payment_credential.json`

> **Full site overview:** For every page in this documentation, read [https://developer.vippsmobilepay.com/llms.txt](https://developer.vippsmobilepay.com/llms.txt).
