> **Description:** Technical documentation for PSPs integrating card passthrough with the Recurring API.

# Recurring PSP API guide

**Work in progress**

Recurring API card passthrough for PSPs is in development and targeted for Q2/Q3 2026.

PSPs using the Recurring API process card payments through their own infrastructure using card passthrough.

This page covers the PSP-specific card passthrough additions; for full Recurring API documentation, see the [Recurring API](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/README.md) section.

## Overview

Key PSP constraints for card passthrough:

- **Agreements**: PSPs must process the Customer-Initiated Transaction (CIT) themselves to verify the payment source and confirm the agreement -- Vipps MobilePay does not handle this on your behalf. See [Agreements](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#agreements) in the Recurring API guide.
- **Charges**: PSPs can create [recurring charges](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#recurring-charge) and [unscheduled charges](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#unscheduled-charge). An optional [initial charge](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#initial-charge) on the agreement controls the CIT amount during sign-up; if omitted, a zero-amount CIT is performed. See [Charges](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#charges) in the Recurring API guide.
- **Payment sources**: Cards are the only supported payment source for PSPs initially. When a user changes their card on an existing agreement, you receive a zero-amount CIT callback to verify the new card -- handled entirely through the [card callback](#card-callback).

### Flows

There are three main PSP card passthrough flows:

- **[Agreement sign-up](#agreement-sign-up)** -- user selects a card when signing an agreement. You process a CIT.
- **[Charge creation](#charge-creation)** -- you create a charge and obtain its card info (returned in the response for unscheduled charges, or fetched via [Get payment info](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) on the due date for recurring charges). You process the payment and update us asynchronously.
- **[Payment source update](#payment-source-update)** -- user changes their card on an existing agreement. You process a zero-amount CIT.

### Endpoints

The following table shows how Recurring API endpoints are used in a PSP card passthrough integration.

| Recurring API endpoints | PSP usage and notes |
| --- | --- |
| Draft agreement:[`POST:/recurring/v3/agreements`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) | Draft agreement with the merchant's MSN and `cardPassthrough` fields. Card token delivered via callback to `cardCallbackUrl`. |
| Update an agreement:[`PATCH:/recurring/v3/agreements/{agreementId}`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) | Stop an agreement or update its terms when the user cancels on your end or terms change. |
| Check the agreement status:[`GET:/recurring/v3/agreements/{agreementId}`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml)[`GET:/recurring/v3/agreements/{agreementId}/charges/{chargeId}`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) | Optional: compare current agreement and charge state with PSP records and decide if updates are needed. |
| Create charges:[`POST:/recurring/v4/agreements/charges`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) | Batch-create PSP recurring or unscheduled charges. Card info is returned directly in the response body for successful unscheduled charges. |
| Get payment info:[`GET:/recurring/v4/agreements/{agreementId}/charges/{chargeId}/payment-info`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) | Fetch card info for a scheduled `RECURRING` charge on its due date, before reporting its result. Unscheduled charges already receive card info from charge creation. |
| Report a charge result:[`POST:/recurring/v4/agreements/{agreementId}/charges/{chargeId}/result`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) | Report the PSP processing outcome with `SUCCESS` or `FAILED`. This keeps Vipps MobilePay and the user's app aligned with the downstream payment result. |
| Update the status of a reserved charge:[`POST:/recurring/v3/agreements/{agreementId}/charges/{chargeId}/capture`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml)[`POST:/recurring/v3/agreements/{agreementId}/charges/{chargeId}/refund`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml)[`DELETE:/recurring/v3/agreements/{agreementId}/charges/{chargeId}`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) | Communicate capture, refund, and cancel status to Vipps MobilePay. Payment processing happens in PSP/acquirer systems. |

PSPs use the standard Recurring API authentication headers. The PSP-specific parts of the integration are the `cardPassthrough` object on [draft agreement](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) and the PSP-only v4 charge endpoints ([create charges](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml), [get payment info](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml), and [report result](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml)). Other endpoints are the same as direct.

These endpoints are rate limited like the rest of the Recurring API. See [Rate limits](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#rate-limiting) for the current limits.

**IMPORTANT**

Include the `Merchant-Serial-Number` header in every request. This identifies the merchant/sales unit you are operating on behalf of.

### Authorization

All Recurring API requests require an access token. Obtain one from the
[Access Token API](https://developer.vippsmobilepay.com/docs/APIs/access-token-api/README.md) using
[`POST:/accesstoken/get`](https://developer.vippsmobilepay.com/redocusaurus/access-token-swagger-id.yaml):

```bash
curl -X POST 'https://apitest.vipps.no/accesstoken/get' \
-H "Content-Type: application/json" \
-H 'client_id: YOUR-CLIENT-ID' \
-H 'client_secret: YOUR-CLIENT-SECRET' \
-H 'Ocp-Apim-Subscription-Key: YOUR-SUBSCRIPTION-KEY' \
-H 'Merchant-Serial-Number: YOUR-MSN' \
--data ''
```

In production, include all `Vipps-System` headers to aid debugging.
See [HTTP headers](https://developer.vippsmobilepay.com/docs/knowledge-base/http-headers.md) for details.

Use the returned `access_token` value as the Bearer token in the `Authorization` header of every subsequent API request:

```text
Authorization: Bearer <access_token>
```

PSP credentials are not interchangeable with regular merchant or partner credentials. If you already have an ePayment integration as a direct merchant or partner, you need PSP keys. Contact your account manager or [customer service](https://help.vippsmobilepay.com/) to obtain PSP-specific API keys.

## Agreements

### Agreement sign-up

Agreement sign-up is a user-initiated flow. The user selects a card in the Vipps MobilePay app, and Vipps MobilePay calls your `cardCallbackUrl` synchronously with the card token. You process the CIT and respond with the result to confirm the agreement.

PSP merchant agreement sign-up flow

```mermaid
sequenceDiagram
    participant PSP
    participant VM as Vipps MobilePay
    participant User

    PSP->>VM: POST /recurring/v3/agreements (with cardPassthrough)
    VM-->>PSP: vippsConfirmationUrl, agreementId, chargeId
    PSP->>User: Present URL to user
    User->>VM: User selects card and confirms agreement
    activate VM
    VM->>PSP: POST cardCallbackUrl (card token)
    PSP->>PSP: Process payment
    PSP-->>VM: 200 OK (status: RESERVE/CAPTURE)
    VM-->>User: Agreement signed
    deactivate VM
    User-->PSP: Redirect to returnUrl
    PSP->>User: Starts charging user
```

  PSP creates an agreement with Vipps MobilePay (POST /recurring/v3/agreements with `cardPassthrough`).
  Vipps MobilePay returns the vippsConfirmationUrl, agreementId, and chargeId to PSP.
  PSP presents the URL to the user.
  User selects a card and confirms the agreement with Vipps MobilePay.
  Vipps MobilePay posts a card token to the PSP's cardCallbackUrl.
  PSP processes the payment.
  PSP returns 200 OK with status RESERVE or CAPTURE to Vipps MobilePay.
  Vipps MobilePay notifies the user that the agreement is signed.
  User is redirected to the PSP's returnUrl.
  PSP begins charging the user.

Steps:

1. [Send the agreement request](#send-the-agreement-request)
2. [Respond to the card callback](#card-callback)
3. [Start creating charges](#charge-creation)

#### Send the agreement request

Agreement sign-up with card passthrough is initiated by sending a
[draft agreement](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) request
with the following settings:

- Use the `Merchant-Serial-Number` header for the merchant/sales unit whose agreement is being created
- Include the `cardPassthrough` object, filled out
  - **`pspReference`** (required): Your unique reference for CITs on this agreement. Included in the card callback during agreement sign-up and payment source updates.
  - **`cardCallbackUrl`** (required): URL where we POST the user's card data (network token, or -- when `publicEncryptionKeyId` is set -- the encrypted PAN if no token is available).
  - **`allowedCardTypes`** (required): Card types the user can select. At least one of: `VISA_DEBIT`, `VISA_CREDIT`, `ELEC_DEBIT`, `MC_DEBIT`, `MC_CREDIT`, `DANKORT`.
  - **`preferVisaPartOfVisaDankort`**: For Danish co-branded Visa/Dankort cards, set to `true` to route payments through the Visa scheme rather than Dankort. Has no effect on cards that are not Visa/Dankort. Default: `false`.
  - **`publicEncryptionKeyId`**: UUID of your public encryption key registered with us. When set, the PAN is encrypted with this key in card callbacks. Otherwise, only network tokens are delivered.

#### `cardPassthrough`

The `cardPassthrough` object is required when a PSP merchant drafts an agreement. It is not supported for non-PSP merchants.

| Field | Required | Description |
|---|---|---|
| `pspReference` | Yes | Your unique reference for CITs on this agreement. Included in the card callback during agreement sign-up and payment source updates. |
| `cardCallbackUrl` | Yes | URL where we POST the user's card data. HTTPS only; deeplinks are not allowed. See [Card callback](#card-callback) for the expected request and response format. |
| `allowedCardTypes` | Yes | Card types the user can select. At least one of: `VISA_DEBIT`, `VISA_CREDIT`, `ELEC_DEBIT`, `MC_DEBIT`, `MC_CREDIT`, `DANKORT`. |
| `preferVisaPartOfVisaDankort` | No | For Danish co-branded Visa/Dankort cards, set to `true` to route payments through the Visa scheme rather than Dankort. Has no effect on cards that are not Visa/Dankort. Default: `false`. |
| `publicEncryptionKeyId` | No | Identifier of your public encryption key registered with us. When provided, the PAN is encrypted with this key in card callbacks. Otherwise, only tokens are returned. |

**NOTE**

If you include `DANKORT` in `allowedCardTypes`, consider providing a `publicEncryptionKeyId`.
Standalone Dankort cards are not tokenizable -- without a key, payments from those cards will fail.
See [Token vs encrypted PAN](#token-vs-encrypted-pan) in the Card callback section for details.

**NOTE**

The `cardPassthrough` object can be updated on an existing agreement via [`PATCH:/recurring/v3/agreements/{agreementId}`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml). All fields are optional in the patch body -- included fields replace existing values, omitted fields are left unchanged.

**Example request body:**

```json
{
  "pricing": {
    "type": "LEGACY",
    "currency": "NOK",
    "amount": 10000
  },
  "interval": {
    "unit": "MONTH",
    "count": "1"
  },
  "initialCharge": {
    "amount": 10000,
    "description": "First payment",
    "transactionType": "RESERVE_CAPTURE"
  },
  "merchantRedirectUrl": "https://example.com/redirect",
  "merchantAgreementUrl": "https://example.com/agreement",
  "productName": "Streaming subscription",
  "cardPassthrough": {
    "pspReference": "subscription-product-123",
    "cardCallbackUrl": "https://example.com/psp-callback",
    "allowedCardTypes": ["VISA_DEBIT", "VISA_CREDIT", "ELEC_DEBIT", "MC_CREDIT", "MC_DEBIT", "DANKORT"],
    "preferVisaPartOfVisaDankort": true
  }
}
```

Full example

**Example request** (PSP-specific fields are highlighted):

```bash
curl -X POST https://apitest.vipps.no/recurring/v3/agreements/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR-ACCESS-TOKEN" \
-H "Ocp-Apim-Subscription-Key: YOUR-SUBSCRIPTION-KEY" \
-H "Merchant-Serial-Number: MERCHANT-MSN" \
-H 'Idempotency-Key: YOUR-IDEMPOTENCY-KEY' \
-H "Vipps-System-Name: acme" \
-H "Vipps-System-Version: 3.1.2" \
-H "Vipps-System-Plugin-Name: acme-webshop" \
-H "Vipps-System-Plugin-Version: 4.5.6" \
-d '{
  "cardPassthrough": {
    "pspReference": "subscription-product-123",
    "cardCallbackUrl": "https://example.com/psp-callback",
    "allowedCardTypes": ["VISA_DEBIT", "VISA_CREDIT", "ELEC_DEBIT", "MC_CREDIT", "MC_DEBIT", "DANKORT"],
    "preferVisaPartOfVisaDankort": true
  },
   "interval": {
      "unit" : "WEEK",
      "count": 2
   },
   "pricing": {
      "amount": 1000,
      "currency": "NOK"
   },
   "merchantRedirectUrl": "https://example.com/redirect-url",
   "merchantAgreementUrl": "https://example.com/agreement-url",
   "phoneNumber": "12345678",
   "productName": "Test product"
}'
```

The optional `initialCharge` field controls the CIT amount -- omit it and a zero-amount CIT is performed instead. We forward the card details to you, and it is your responsibility to process the CIT and respond with the result.

Once the agreement is successfully signed, you can start charging the user according to the agreement terms.

The user may also reject the agreement or abandon the flow -- subscribe to [agreement webhooks](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#agreement-webhooks) to be notified of these and other events.

For all available fields, see [Agreements](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#agreements) in the Recurring API guide.

**NOTE**

When the user confirms the agreement, Vipps MobilePay sends a card token to your `cardCallbackUrl`. See [Card callback](#card-callback) for the request format, HMAC authentication, and expected response.

### Update the status of an agreement

Use these Recurring API endpoints to keep us aligned with the agreement
state in your systems.

- [`PATCH:/recurring/v3/agreements/{agreementId}`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml)
  - Use this to stop an agreement when the user cancels on your end, or to update the agreement terms such as pricing or product description.

For reconciliation, you can optionally use:

- [`GET:/recurring/v3/agreements/{agreementId}`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml)

For the full range of options available, see [Agreements](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#agreements) in the Recurring API guide.

## Charges

### Charge creation

PSPs can create both [recurring charges](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#recurring-charge) and [unscheduled charges](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#unscheduled-charge).
You are responsible for creating charges according to the terms of each agreement.

Charges are submitted in batches of 1-50 against active agreements. Each charge is validated and created independently. The response separates successful, failed, and retryable charges, so a single bad item never blocks the rest of the batch.

A single batch can contain both unscheduled and recurring charges; the per-charge flow differs by `type`, shown separately below.

PSP unscheduled charge flow (v4 batch)

```mermaid
sequenceDiagram
    participant PSP
    participant VM as Vipps MobilePay

    PSP->>VM: POST /recurring/v4/agreements/charges (UNSCHEDULED items)
    VM-->>PSP: successfulCharges (incl. cardInfo) + failedCharges + retryableCharges
    loop For each successful charge
        PSP->>PSP: Process payment downstream
        PSP->>VM: POST .../{chargeId}/result (SUCCESS or FAILED)
        VM-->>PSP: 202 Accepted
    end
```

  PSP submits a batch of unscheduled charges with Vipps MobilePay (POST /recurring/v4/agreements/charges).
  Vipps MobilePay returns the response split into successfulCharges (each including the card data needed for downstream processing), failedCharges (with non-retryable error messages), and retryableCharges (with transient errors that can be retried with the same agreementId and chargeId).
  For each successful charge: PSP processes the payment downstream, then reports the outcome to Vipps MobilePay using the result endpoint, which returns 202 Accepted.

PSP recurring charge flow (v4 batch)

```mermaid
sequenceDiagram
    participant PSP
    participant VM as Vipps MobilePay

    PSP->>VM: POST /recurring/v4/agreements/charges (RECURRING items, with due date)
    VM-->>PSP: successfulCharges (DUE, no cardInfo) + failedCharges + retryableCharges
    loop For each successful charge, on its due date
        PSP->>VM: GET .../{chargeId}/payment-info
        VM-->>PSP: cardInfo
        PSP->>PSP: Process payment downstream
        PSP->>VM: POST .../{chargeId}/result (SUCCESS or FAILED)
        VM-->>PSP: 202 Accepted
    end
```

  PSP submits a batch of recurring charges with Vipps MobilePay (POST /recurring/v4/agreements/charges), each with a future due date.
  Vipps MobilePay returns the response split into successfulCharges (created DUE, without card data), failedCharges (with non-retryable error messages), and retryableCharges (with transient errors that can be retried with the same agreementId and chargeId).
  On the due date, for each successful charge the PSP fetches the current card data with GET .../payment-info.
  The PSP processes the payment downstream, then reports the outcome to Vipps MobilePay using the result endpoint, which returns 202 Accepted.

Steps:

1. [Send the charge request](#send-the-charge-request)
2. [Get payment info](#get-payment-info) (recurring charges)
3. [Process the charge](#process-the-charge)
4. [Report the charge result](#report-the-charge-result)

#### Send the charge request

Submit a batch of charges to [`POST:/recurring/v4/agreements/charges`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml). Each charge is validated and created independently. The `Merchant-Serial-Number` header identifies the merchant the PSP is creating charges for, so all agreements in the batch must belong to that merchant.

Charge creation uses item-level idempotency. For the `Merchant-Serial-Number` in the request header, the `agreementId` + `chargeId` pair identifies the charge. If an item is returned in `retryableCharges`, retry it with the same `agreementId` and `chargeId`.

Response item order is unspecified. Correlate response items by `agreementId` + `chargeId`, not by array position.

**NOTE**

**retryDays for recurring charges**

For `RECURRING` charges, `retryDays` controls how many days after the `due` date the charge stays payable before it is automatically failed. Throughout this window, [Get payment info](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) keeps returning the charge's payment info so you can attempt the payment again. It defaults to `14` and must be between 0 and 14. For `UNSCHEDULED` charges it does not apply -- omit it or send `0`.

A `RECURRING` charge item also requires a `due` date and accepts `retryDays`, for example:

```json
{
  "type": "RECURRING",
  "due": "2026-12-01",
  "retryDays": 5,
  "amount": 49900,
  "description": "December subscription",
  "chargeId": "acme-shop-123-charge-004",
  "agreementId": "agr_5kSeqz",
  "transactionType": "RESERVE_CAPTURE"
}
```

The following batch request uses unscheduled charges:

```bash
curl -X POST https://apitest.vipps.no/recurring/v4/agreements/charges \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR-ACCESS-TOKEN" \
-H "Ocp-Apim-Subscription-Key: YOUR-SUBSCRIPTION-KEY" \
-H "Merchant-Serial-Number: MERCHANT-MSN" \
-H "Vipps-System-Name: acme" \
-H "Vipps-System-Version: 3.1.2" \
-H "Vipps-System-Plugin-Name: acme-webshop" \
-H "Vipps-System-Plugin-Version: 4.5.6" \
-d '[
  {
    "type": "UNSCHEDULED",
    "amount": 49900,
    "description": "October subscription",
    "chargeId": "acme-shop-123-charge-001",
    "agreementId": "agr_5kSeqz",
    "transactionType": "RESERVE_CAPTURE"
  },
  {
    "type": "UNSCHEDULED",
    "amount": 49900,
    "description": "October subscription",
    "chargeId": "acme-shop-123-charge-002",
    "agreementId": "agr_unknown",
    "transactionType": "RESERVE_CAPTURE"
  },
  {
    "type": "UNSCHEDULED",
    "amount": 49900,
    "description": "October subscription",
    "chargeId": "acme-shop-123-charge-003",
    "agreementId": "agr_5kSeqz",
    "transactionType": "RESERVE_CAPTURE"
  }
]'
```

Unlike the agreement sign-up and payment source update flows, charges do not use a [card callback](#card-callback). For `UNSCHEDULED` charges the card data is returned directly in the charge creation response; for `RECURRING` charges it is fetched later from [Get payment info](#get-payment-info) on the due date.

**Example response:**

```json
{
  "successfulCharges": [
    {
      "chargeId": "acme-shop-123-charge-001",
      "agreementId": "agr_5kSeqz",
      "cardInfo": {
        "maskedCardNumber": "492500******0004",
        "cardType": "VISA_DEBIT",
        "cardIssuedInCountryCode": "NO",
        "cardDataType": "TOKEN",
        "networkToken": {
          "number": "4111111234567890",
          "cryptogram": "AgAAAAAAAIR8CQrXcIhbQAAAAAA=",
          "expiryMonth": "12",
          "expiryYear": "30",
          "eci": "05",
          "tokenType": "CLOUD",
          "paymentAccountReference": "V0010013020091420099000060000"
        }
      }
    }
  ],
  "failedCharges": [
    {
      "chargeId": "acme-shop-123-charge-002",
      "agreementId": "agr_unknown",
      "errors": [
        {
          "code": "agreement_not_found",
          "description": "Agreement not found."
        }
      ]
    }
  ],
  "retryableCharges": [
    {
      "chargeId": "acme-shop-123-charge-003",
      "agreementId": "agr_5kSeqz",
      "error": {
        "code": "technical_error",
        "description": "A transient technical error occurred. Retry with the same agreementId and chargeId."
      }
    }
  ]
}
```

Check all three response arrays:

- `successfulCharges`: Charges that were created. Process the payment downstream and then report the result of each charge.
- `failedCharges`: Charges that were rejected and should not be retried unchanged. Fix the request before trying again.
- `retryableCharges`: Transient item-level failures. Retry these items with the same `agreementId` and `chargeId`, even if the error code is new to your integration.

When a network token isn't available and `publicEncryptionKeyId` is set on the agreement, `cardInfo` returns `cardDataType: PAN` and an `encryptedPan` instead of a `networkToken`. When neither is available, the charge is returned in `failedCharges` with an explanatory error.

#### Charge creation error codes

Use the response array to decide what to do next:

- Retry `retryableCharges` with the same `agreementId` and `chargeId`.
- Do not retry `failedCharges` unchanged. Fix the request first.

**NOTE**

Use `code` for logic, not the human-readable `description`. The description can change, and new codes may be added. Retry charges returned in `retryableCharges`.

| Code | Returned in | Meaning |
| ---- | ----------- | ------- |
| `type_empty` | `failedCharges` | The `type` field is missing, null, or empty. |
| `type_invalid` | `failedCharges` | The `type` value is invalid. |
| `type_unscheduled_due_date` | `failedCharges` | `UNSCHEDULED` charges must not include `due`. |
| `due_date_empty` | `failedCharges` | `RECURRING` charges must include `due`. |
| `due_date_invalid_format` | `failedCharges` | The `due` value is not a valid date. |
| `due_date_too_soon` | `failedCharges` | The `due` date is too soon. |
| `due_date_too_far` | `failedCharges` | The `due` date is too far in the future. |
| `amount_empty` | `failedCharges` | The `amount` field is missing or null. |
| `amount_below_min` | `failedCharges` | The amount is below the minimum allowed. |
| `amount_too_high` | `failedCharges` | The amount exceeds the maximum allowed for the agreement. |
| `description_empty` | `failedCharges` | The `description` field is missing, null, or empty. |
| `description_too_long` | `failedCharges` | The `description` field is too long. |
| `charge_id_empty` | `failedCharges` | The `chargeId` field is missing or null. |
| `charge_id_invalid_format` | `failedCharges` | The `chargeId` value has an invalid format. |
| `agreement_id_empty` | `failedCharges` | The `agreementId` field is missing or null. |
| `agreement_id_invalid_format` | `failedCharges` | The `agreementId` value has an invalid format. |
| `agreement_not_found` | `failedCharges` | The agreement was not found. |
| `agreement_not_psp` | `failedCharges` | The agreement is not a PSP agreement. |
| `agreement_not_active` | `failedCharges` | The agreement is not active. |
| `transaction_type_empty` | `failedCharges` | The `transactionType` field is missing or null. |
| `transaction_type_invalid` | `failedCharges` | The `transactionType` value is invalid. |
| `retry_days_invalid` | `failedCharges` | `retryDays` must be between 0 and 14. |
| `retry_days_not_allowed` | `failedCharges` | `retryDays` cannot be set for `UNSCHEDULED` charges. |
| `charge_already_exists` | `failedCharges` | A charge with this `chargeId` already exists for the agreement. |
| `charge_in_invalid_state` | `failedCharges` | The charge already exists in a state that cannot be processed. Create a new charge with a different `chargeId`. |
| `charge_not_creatable` | `failedCharges` | The charge cannot be created. |
| `processing_timeout` | `retryableCharges` | The charge did not finish processing in time. The outcome is uncertain, and it can be retried with the same `agreementId` + `chargeId` pair. |
| `technical_error` | `retryableCharges` | A transient technical error occurred. The charge can be retried with the same `agreementId` + `chargeId` pair. |
| `internal_processing_error` | `failedCharges` | An internal error occurred while processing the charge. |

Always [report the charge result](#report-the-charge-result) once you've processed it. Without it, the charge remains unresolved in both our system and the user's app -- the user will see a pending charge they cannot act on, which typically leads to confusion and support requests.

#### Get payment info

**Recurring charges only.** A `RECURRING` charge is created `DUE` for a future date, and its `cardInfo` is *not* returned in the [charge creation](#send-the-charge-request) response. On the due date, fetch the current card data with [`GET:/recurring/v4/agreements/{agreementId}/charges/{chargeId}/payment-info`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml).

- It returns the same `cardInfo` (a `networkToken`, or an `encryptedPan` when `publicEncryptionKeyId` is set on the agreement) that unscheduled charges receive at creation.
- It is available from the `due` date onward -- calling it earlier returns `409 Conflict` -- and keeps returning payment info throughout the `retryDays` window so you can retry a failed payment.
- You must call it before [reporting the result of](#report-the-charge-result) a `RECURRING` charge. reporting the result first returns `409 Conflict`.

`UNSCHEDULED` charges skip this step -- their `cardInfo` is already returned in the charge creation response.

#### Process the charge

This step is done by you, the PSP.

For each successful charge, process the payment using the `networkToken` or `encryptedPan` returned in `cardInfo`.

We aren't involved in the actual card processing; we only provide card data.

Once you've processed the charge, [report its result](#report-the-charge-result) to keep us aligned.

#### Report the charge result

Report the outcome of each charge to [`POST:/recurring/v4/agreements/{agreementId}/charges/{chargeId}/result`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml). Call this once per charge created via the batch endpoint.

- **Success:** send `{ "status": "SUCCESS" }` when the payment was reserved in your systems.
- **Failure:** send `{ "status": "FAILED", "error": { "code": <code>, "retry": <true|false>, "message": "<reason>" } }`. `retry` is required; `message` is optional and stored for auditing only -- it is not shown to the payer. See the [endpoint reference](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) for the accepted error codes.

On a failure, `error.retry` decides whether the charge can still be paid:

- **`retry: true` -- you intend to try again.** The charge stays `DUE`, its failure reason is recorded, and the user is notified. Fetch fresh card data with [Get payment info](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) and report a new result before the charge's retry window ends.
- **`retry: false` -- you have given up.** The charge is settled to `FAILED` and no further attempts are possible.

The retry window runs from the `due` date through the end of that day plus the charge's `retryDays` (set at [creation](#charge-creation)).

- `RECURRING`: the due date plus `retryDays` (`0`-`14`).
- `UNSCHEDULED`: the due date only, since `retryDays` is always `0`.

Keep track of the window on your side. While it's open you can retry with `retry: true`. Once it closes, the charge can no longer be retried. At that point report a final result: `SUCCESS` if the payment went through, `FAILED` with `retry: false` if the payment failed. A `retry: true` sent after the window closes returns `400 Bad Request`.

When you report a failure, `error.code` is mapped to an equivalent `failureReason` on the charge. See [Charge failure reasons](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#charge-failure-reasons) for what each value means for the user.

| `error.code` | `failureReason` on the charge |
| --- | --- |
| `100`, `200`, `400` | `user_action_required` |
| `210`, `300`, `500` | `non_technical_error` |
| `600`, `700`, `900` | `technical_error` |

The failure reason is used to flag the agreement and guide the user in the app toward fixing the underlying issue -- for example, adding funds or changing the agreement's payment source -- which improves the odds that your next attempt succeeds. Reporting a final result (`SUCCESS`, or `FAILED` with `retry: false`) clears these flags.

**NOTE**

For `RECURRING` charges you must call [Get payment info](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) before reporting the result. This guarantees you process the payment against the latest card info for the agreement. Reporting the result of a `RECURRING` charge before fetching its payment info returns `409 Conflict`. This doesn't apply to unscheduled charges -- you already receive their card info in the [charge creation response](#charge-creation).

The `transactionType` set when [creating the charge](#charge-creation) determines the resulting state when you report a `SUCCESS` result:

- `RESERVE_CAPTURE`: the charge is **reserved**. Capture it later with the [Capture charge](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) endpoint, which also supports partial captures.
- `DIRECT_CAPTURE`: the charge is **captured for its full amount** automatically and goes straight to `CHARGED` -- no separate capture call is needed. Only full capture is supported, so use `RESERVE_CAPTURE` if you need partial captures.

For subsequent state changes after a successful charge -- captures, refunds, or cancellations -- use the existing Recurring API endpoints:

- [`POST:/recurring/v3/agreements/{agreementId}/charges/{chargeId}/capture`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml)
  - Register a capture. Only `RESERVE_CAPTURE` charges can be captured; a `DIRECT_CAPTURE` charge is already captured in full.
- [`POST:/recurring/v3/agreements/{agreementId}/charges/{chargeId}/refund`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml)
  - Register a refund. Available for both `RESERVE_CAPTURE` and `DIRECT_CAPTURE` charges once they are captured.
- [`DELETE:/recurring/v3/agreements/{agreementId}/charges/{chargeId}`](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml)
  - Cancel remaining authorized funds, or cancel a charge before it has been authorized.

**NOTE**

State changes on charges are delivered as webhook events. Subscribe to the relevant events in [Charge webhooks](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#charge-webhooks) to be notified.

#### Example: charge lifecycle with reserve, capture, and refund

This example shows the full lifecycle of a `RESERVE_CAPTURE` charge: reserved at creation, captured later when the merchant fulfils the order, and refunded later still after the customer returns it. Each state change is acknowledged synchronously by the API and confirmed asynchronously via a webhook event.

PSP merchant charge lifecycle (reserve -> capture -> refund)

```mermaid
sequenceDiagram
    participant PSP
    participant VM as Vipps MobilePay

    PSP->>VM: POST /recurring/v4/agreements/charges (transactionType: RESERVE_CAPTURE)
    VM-->>PSP: successfulCharges[] with cardInfo
    PSP->>PSP: Reserve funds downstream
    PSP->>VM: POST .../{chargeId}/result ({ "status": "SUCCESS" })
    VM-->>PSP: 202 Accepted
    VM-->>PSP: webhook: recurring.charge-reserved.v1

    Note over PSP,VM: Merchant fulfils the order

    PSP->>VM: POST .../{chargeId}/capture
    VM-->>PSP: 204 No Content
    VM-->>PSP: webhook: recurring.charge-captured.v1

    Note over PSP,VM: Customer returns the order

    PSP->>VM: POST .../{chargeId}/refund
    VM-->>PSP: 204 No Content
    VM-->>PSP: webhook: recurring.charge-refunded.v1
```

For the full range of options available, see [Recurring API guide: Charges](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#charges).

## Payment source

### Payment source update

Each agreement has an attached payment source that can be changed by the user in the app at any time. The new payment source must be verified with a zero-amount CIT -- it is your responsibility to process this transaction. The flow is user-initiated and otherwise identical to agreement sign-up.

PSP merchant payment source update flow

```mermaid
sequenceDiagram
    participant PSP
    participant VM as Vipps MobilePay
    participant User

    User->>VM: Initiates payment source change on agreement
    activate VM
    VM->>PSP: POST cardCallbackUrl (new card token)
    PSP->>PSP: Process verification payment
    PSP-->>VM: 200 OK
    VM-->>User: Update Ok
    deactivate VM
```

  User initiates a payment source change on their agreement with Vipps MobilePay.
  Vipps MobilePay posts the new card token to the PSP's cardCallbackUrl.
  PSP processes a verification payment using the new token.
  PSP returns 200 OK to Vipps MobilePay.
  Vipps MobilePay notifies the user that the update was successful.

**NOTE**

See [Card callback](#card-callback) for the request format, HMAC authentication, and expected response.

## Card callback

The card callback applies to:

- ePayment PSP API: creating a payment
- Recurring PSP API: agreement sign-up and payment source updates

The flow works like this:

1. The PSP sends Vipps MobilePay an API request to initiate an ePayment or Recurring request.
2. Vipps MobilePay presents the request to the user in the app.
3. If the user approves and selects their card, Vipps MobilePay sends a `POST` request to the `cardCallbackUrl` specified in the initial request.
4. The PSP must decrypt the message and respond within 20 seconds, or the request will fail.

**How the callback works:**

- The callback is synchronous -- your endpoint must respond with the payment authorization result.
- If you respond with a retryable error, the user can retry with the same or a different card.
- HTTP 500 errors and timeouts are treated as non-retryable.

**TIP**

If you need to whitelist our servers, find details on the [servers page](https://developer.vippsmobilepay.com/docs/knowledge-base/servers.md).

### Callback request

#### Request body

The callback we send to your server contains the following properties:

- **`pspReference`**: Your unique reference for this payment, as provided in the create payment request.
- **`authorizationAttemptId`**: Unique identifier for this authorization attempt.
- **`merchantSerialNumber`**: The merchant serial number for the payment.
- **`amount`**: Object containing the payment amount:
  - **`value`**: The amount in minor units.
  - **`currency`**: The three-letter ISO 4217 currency code.
- **`softDeclineCompletedRedirectUrl`**: URL to redirect to after a soft decline is resolved.
- **`cardInfo`**: Object containing the card details:
  - **`maskedCardNumber`**: The masked card number (for example, `47969485XXXX1234`).
  - **`cardType`**: The card type (for example, `VISA-DEBIT`).
  - **`cardIssuedInCountryCode`**: The ISO 3166-1 alpha-2 country code where the card was issued.
  - **`cardDataType`**: The type of card data included in the callback. Possible values are `TOKEN` or `PAN`. See [Token vs encrypted PAN](#token-vs-encrypted-pan).
  - **`networkToken`**: Object containing the network token details when `cardDataType` is `TOKEN`:
    - **`number`**: The token number.
    - **`cryptogram`**: The cryptogram for the transaction.
    - **`expiryMonth`**: Token expiry month.
    - **`expiryYear`**: Token expiry year.
    - **`tokenType`**: Token network type (for example `VISA`).
    - **`eci`**: Electronic Commerce Indicator.
    - **`paymentAccountReference`**: Stable reference across token renewals.
  - **`encryptedPan`**: Encrypted PAN when `cardDataType` is `PAN`.

The callback body is plain JSON -- no decryption is needed to read it. Use [HMAC authentication](#hmac-authentication) to verify that the callback is genuine before processing it.

An example request body containing a token:

```json
{
  "pspReference": "7686f7788898767977",
  "authorizationAttemptId": "d8f9a1d7-b9d3-4c2f-b5c2-7d8b93df12ab",
  "merchantSerialNumber": "123456",
  "amount": {
    "value": 1000,
    "currency": "DKK"
  },
  "softDeclineCompletedRedirectUrl": "https://vipps.no/mobileintercept?transactionId=123456789&responsecode=OK",
  "cardInfo": {
    "maskedCardNumber": "47969485XXXX1234",
    "cardType": "VISA-DEBIT",
    "cardIssuedInCountryCode": "DK",
    "cardDataType": "TOKEN",
    "networkToken": {
      "number": "5000000000000000001",
      "cryptogram": "AAAAAAAAAAAAAAA=",
      "expiryMonth": "03",
      "expiryYear": "2030",
      "tokenType": "VISA",
      "eci": "7",
      "paymentAccountReference": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    },
    "encryptedPan": null
  }
}
```

If you requested an encrypted PAN and one is available, `encryptedPan` will be populated instead of `networkToken`:

```json
{
  "pspReference": "7686f7788898767977",
  "authorizationAttemptId": "731C95C5-7E8B-40C3-A6EA-713B24694E6E",
  "merchantSerialNumber": "123456",
  "softDeclineCompletedRedirectUrl": "https://vipps.no/mobileintercept?transactionId=123456789&responsecode=OK",
  "cardInfo": {
    "maskedCardNumber": "47969485XXXX1234",
    "cardType": "VISA-DEBIT",
    "cardIssuedInCountryCode": "DK",
    "cardDataType": "PAN",
    "encryptedPan": "fsfnsdjkfbgdft34895u7345"
  },
  "amount": {
    "value": 1000,
    "currency": "DKK"
  }
}
```

You must decrypt `encryptedPan` using your RSA private key to obtain the card number. See [Token vs encrypted PAN](#token-vs-encrypted-pan) for details.

#### HMAC authentication

To verify that the callback originates from us and has not been tampered with, we sign each callback using HMAC with a shared secret.

The shared secret is your PSP client secret.

Use the shared secret together with the `Host`, `x-ms-date`, `x-ms-content-sha256`, and `Authorization` headers of the request.

How HMAC works

1. **Secret key**: A secret key is shared between the sender and the receiver.
2. **Message**: The message to be authenticated.
3. **Hash function**: A cryptographic hash function such as SHA-256 is used.
4. **HMAC generation**:
   - The message and the secret key are combined in a specific way.
   - The combined data is hashed using the cryptographic hash function.
   - The result is the HMAC value.
5. **Verification**:
   - The receiver uses the same secret key and hash function to generate an HMAC value for the received message.
   - The receiver compares the generated HMAC value with the HMAC value sent with the message.
   - If they match, the message is verified as authentic and unaltered.

You will receive an `HTTP POST` with this format:

```http
POST https://example.com/psp-makepayment

Host: example.com
x-ms-date: Thu, 30 Mar 2023 08:38:32 GMT
x-ms-content-sha256: WyZnKtAizV4gkGbiMMhm2NIrvlumpic9Zdjcqs6Q2hw=
Authorization: HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=RwcYy13oXAu1ZFU1zOi0MmSIHynnNnHe9lwNx+LgMqc=
X-Vipps-Authorization: HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=RwcYy13oXAu1ZFU1zOi0MmSIHynnNnHe9lwNx+LgMqc=
Content-Type: application/json

{ ... }
```

`X-Vipps-Authorization` contains the same value as `Authorization`. You can verify either header, but they should match when both are present.

1. **Check that the content has not been modified**

   Hash the exact request body as UTF-8 using SHA-256, then base64 encode it.
   This hash must match the `x-ms-content-sha256` header.
   Use the raw body exactly as received. Re-serializing the JSON may change whitespace and produce a different hash.

2. **Verify the authentication header**

   Concatenate the request method, path and query, date, host, and content hash in this format:

   `POST
<pathAndQuery>
<date>;<host>;<hash>`

   The `host` value must match the `Host` header, including the port if one is present.

   **Please note the use of `
` *not* `
`.**

   Sign the string with HMAC-SHA256 using your shared secret.
   This must match the `Signature` part of the `Authorization` header.

Sample code

```js
'use strict';

const assert = require('node:assert');
const { describe, it } = require('node:test');
const crypto = require('crypto');

describe('Sample code', () => {
    it('Verifying card callback HMAC headers', () => {
        const secret = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==';

        const request = {
            method: 'POST',
            url: 'https://example.com/psp-makepayment',
            pathAndQuery: '/psp-makepayment',
            headers: {
                'Host': 'example.com',
                'x-ms-date': 'Thu, 30 Mar 2023 08:38:32 GMT',
                'x-ms-content-sha256': 'WyZnKtAizV4gkGbiMMhm2NIrvlumpic9Zdjcqs6Q2hw=',
                'Authorization': 'HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=RwcYy13oXAu1ZFU1zOi0MmSIHynnNnHe9lwNx+LgMqc=',
                'X-Vipps-Authorization': 'HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=RwcYy13oXAu1ZFU1zOi0MmSIHynnNnHe9lwNx+LgMqc='
            },
            content: '{"pspReference":"7686f7788898767977","authorizationAttemptId":"3030303thisisaguid","merchantSerialNumber":"123456"}'
        };

        const expectedContentHash = crypto
            .createHash('sha256')
            .update(request.content, 'utf8')
            .digest('base64');

        assert.equal(
            request.headers['x-ms-content-sha256'],
            expectedContentHash,
            'Content hash was not valid');

        const expectedSignedString =
            `${request.method}
` +
            `${request.pathAndQuery}
` +
            `${request.headers['x-ms-date']};${request.headers['Host']};${request.headers['x-ms-content-sha256']}`;

        const expectedSignature = crypto
            .createHmac('sha256', secret)
            .update(expectedSignedString, 'utf8')
            .digest('base64');

        const expectedAuthorization =
            `HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=${expectedSignature}`;

        assert.equal(expectedAuthorization, request.headers.Authorization, 'Authorization was not valid');
        assert.equal(request.headers['X-Vipps-Authorization'], request.headers.Authorization, 'Headers did not match');
    });
});
```

```csharp
using System.Security.Cryptography;
using System.Text;

public class SampleCode
{
    public void Verifying_Card_Callback_Hmac_Headers()
    {
        var secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";

        var request = new
        {
            Method = "POST",
            Url = "https://example.com/psp-makepayment",
            PathAndQuery = "/psp-makepayment",
            Headers = new Dictionary<string, string>
            {
                { "Host", "example.com" },
                { "x-ms-date", "Thu, 30 Mar 2023 08:38:32 GMT" },
                { "x-ms-content-sha256", "WyZnKtAizV4gkGbiMMhm2NIrvlumpic9Zdjcqs6Q2hw=" },
                {
                    "Authorization",
                    "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=RwcYy13oXAu1ZFU1zOi0MmSIHynnNnHe9lwNx+LgMqc="
                },
                {
                    "X-Vipps-Authorization",
                    "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=RwcYy13oXAu1ZFU1zOi0MmSIHynnNnHe9lwNx+LgMqc="
                }
            },
            Content = """{"pspReference":"7686f7788898767977","authorizationAttemptId":"3030303thisisaguid","merchantSerialNumber":"123456"}"""
        };

        var contentHashInBytes = SHA256.HashData(Encoding.UTF8.GetBytes(request.Content));
        var expectedContentHash = Convert.ToBase64String(contentHashInBytes);
        Assert.Equal(expected: expectedContentHash, actual: request.Headers["x-ms-content-sha256"]);

        var expectedSignedString =
            $"{request.Method}
" +
            $"{request.PathAndQuery}
" +
            $"{request.Headers["x-ms-date"]};{request.Headers["Host"]};{request.Headers["x-ms-content-sha256"]}";

        using var hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var hmacSha256Bytes = Encoding.UTF8.GetBytes(expectedSignedString);
        var hmacSha256Hash = hmacSha256.ComputeHash(hmacSha256Bytes);
        var expectedSignature = Convert.ToBase64String(hmacSha256Hash);
        var expectedAuthorization = $"HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={expectedSignature}";

        Assert.Equal(expected: expectedAuthorization, actual: request.Headers["Authorization"]);
        Assert.Equal(expected: request.Headers["Authorization"], actual: request.Headers["X-Vipps-Authorization"]);
    }
}
```

#### Token vs encrypted PAN

By default, the card callback includes a **card token and cryptogram**, covering the vast majority of Visa and Mastercard cards.

A small number of cards (~0.1%) are not tokenizable -- primarily the Danish **Dankort** rail, but occasionally individual Visa or Mastercard issuers. Provide a `publicEncryptionKeyId` to receive an **encrypted PAN** for these cards; without it, those payments will fail.

Encrypted PAN setup and handling

To enable encrypted PAN, you must:

1. Onboard a public encryption key with us.

    The RSA public key should be provided as `X.509 SubjectPublicKeyInfo`
    (using ASN.1 DER Encoding) represented in PEM encoding (use PEM file extension).
    The public key must have a length of 4096 bits.
    You must clearly state your PSP name in the file name.
    Naming template for public key: `{integratorname}-{environment}-public`

    - Example: `company-prod-public`

    Please send the public keys in a ZIP-file.
    We will register the keys and supply you with a `PublicKeyId` to be used when initiating payments.

    Please note that if a public key is unused for 6 months, we will delete it.
    If this happens, you must supply a new public key.

2. Pass the resulting `publicEncryptionKeyId` in the `cardPassthrough` object when creating the payment or agreement.

The `encryptedPan` is encrypted using `RSA/NONE/OAEPWithSHA256AndMGF1Padding` (SHA-256 is also used for padding). Once decrypted, the structure looks like this:

```json
{"timestampticks":123456789123456789,"encryptedCardData": { "cardNumber": 1234567812345678, "expiryMonth": 12, "expiryYear": 28 }}
```

**Always use the `cardType` field**

Always process the payment using the card type specified in the `cardType` field. This is the only way we can offer card type picking for co-branded cards in accordance with PSD2 requirements. If you process a PAN where `cardType` is `DANKORT` as a Visa transaction, the SCA will be missing and an unwanted 3DS step-up is likely to occur. PAN-based transactions are not regarded as authenticated -- handle them according to your acquirer's scheme rules for SCA compliance.

**Dankort co-branded cards**

Most Dankort cards are co-branded as Visa/Dankort, supporting both a tokenizable Visa flow and a PAN-based Dankort flow. If you include `DANKORT` in `allowedCardTypes` and provide a `publicEncryptionKeyId`, the Dankort rail will be available. Use the `preferVisaPartOfVisaDankort` flag to route co-branded cards through the Visa rail instead.

To process tokens only, omit `publicEncryptionKeyId` and remove `DANKORT` from `allowedCardTypes` -- standalone Dankort cards are not tokenizable.

### Callback response

You must return a valid callback response within 20 seconds or the operation will fail.

Respond with `HTTP 200 OK` and a JSON body with the following properties:

- **`status`** (required): The payment authorization result. One of:
  - `RESERVE` - The authorization succeeded, and the amount was reserved.
  - `SOFT_DECLINE` - Additional cardholder action is required to complete the authorization. Include `softDeclineUrl`.
  - `FAIL` - The authorization failed. Include `errorCode` and `errorMessage`.
- **`networkTransactionReference`**: Your reference for the network transaction. Include this when the authorization succeeded and you have such a reference.
- **`softDeclineUrl`**: URL the user should be redirected to in order to complete a soft decline flow. Required when `status` is `SOFT_DECLINE`.
- **`errorCode`**: Numeric error code describing why the authorization failed. Required when `status` is `FAIL`.
- **`errorMessage`**: Human-readable description of the failure. Required when `status` is `FAIL`.

The following examples show what the response body should look like for each `status` value.

Use `RESERVE` when the authorization succeeded and the payment should remain reserved for a later capture.

```json
{
  "networkTransactionReference": "123456789",
  "status": "RESERVE"
}
```

Use `SOFT_DECLINE` when the cardholder must complete an additional issuer or authentication step before the payment can proceed.

```json
{
  "status": "SOFT_DECLINE",
  "softDeclineUrl": "https://example.com"
}
```

Use `FAIL` when the authorization did not succeed and no soft decline flow is available.

```json
{
  "status": "FAIL",
  "errorCode": 300,
  "errorMessage": "Refused by Issuer"
}
```

When you respond with `status: FAIL`, use one of the following `errorCode` values:

| `errorCode` | Name | Retryable | Description |
|---|---|---|---|
| `100` | Card Error | Yes | Card-related failure where the card should not be retried unchanged for this payment. |
| `200` | Insufficient Funds | Yes | The card or account does not have enough available funds to complete the payment. |
| `210` | Limit Exceeded | Yes | A card, account, or issuer limit has been reached, so the payment cannot be authorized. |
| `300` | Issuer Declined | Yes | The issuer declined the payment without providing a more specific reason. |
| `400` | Permanent Decline | No | A hard decline where the payment must not be retried. |
| `500` | Risk Declined | Yes | The payment was blocked by fraud or risk controls. |
| `600` | Temporary Technical Error | Yes | A transient PSP, issuer, or network problem prevented the authorization attempt from completing. |
| `700` | Merchant Configuration Error | No | The payment failed because of merchant, PSP, or transaction configuration issues. |
| `800` | Duplicate or In Progress | No | The payment is already being processed, or a duplicate attempt was detected, so retrying immediately is not allowed. |
| `900` | Unknown Error | Yes | The authorization failed, but the reason could not be mapped confidently to a more specific error. |

If we receive a `FAIL` response, we will allow the user to retry with the same or a new payment source unless the `errorCode` maps to a non-retryable failure.

If there is a timeout or `HTTP 500`, the payment cannot be tried again by the user. You will need to initiate a new payment request.

## Related Recurring API features

The [Recurring API section](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/README.md) covers the full breadth of what's possible with the underlying recurring payment platform -- including detailed flow diagrams, user journey walkthroughs, and descriptions of advanced features that apply to PSP integrations as well.

It's worth exploring to understand what you can offer your customers:

- [How it works](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/how-it-works/README.md) -- visual flows for payment agreements, charges, and other recurring scenarios
- [Agreement guide](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#agreements) -- creating and managing payment agreements
- [Charges guide](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide.md#charges) -- creating, capturing, and refunding charges
- [Recurring API spec](https://developer.vippsmobilepay.com/redocusaurus/recurring-swagger-id.yaml) -- the full technical specification for all endpoints, including required fields, data types, and valid formats

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