Skip to main content

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 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 in the Recurring API guide.
  • Charges: PSPs can only create unscheduled charges — scheduled recurring charges with retries are not available. An optional initial charge on the agreement controls the CIT amount during sign-up; if omitted, a zero-amount CIT is performed. See 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.

Flows​

There are three main PSP card passthrough flows:

  • Agreement sign-up — user selects a card when signing an agreement; you process a CIT.
  • Charge creation — you create a charge and receive card info in the response; you process the payment and update us asynchronously.
  • Payment source update — user changes their card on an existing agreement; you process a zero-amount CIT.
FlowTriggered byCard deliveryPSP response
Agreement sign-upUser confirms agreementCard callbackCallback response
Charge creationPSP creates a chargeCharge creation responseCharge status update
Payment source updateUser changes card in appCard callbackCallback response
Agreement status updatePSP or user cancels agreement—Agreement status update
Charge status updatePSP processes charge—Charge status update

Endpoints​

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

Recurring API endpointsPSP usage and notes
Draft agreement:
POST:/recurring/v3/agreements
Draft agreement with Psp-Id and cardPassthrough fields. Card token delivered via callback to cardCallbackUrl.
Update an agreement:
PATCH:/recurring/v3/agreements/{agreementId}
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}
GET:/recurring/v3/agreements/{agreementId}/charges/{chargeId}
Optional: compare current agreement and charge state with PSP records and decide if updates are needed.
Create charge:
POST:/recurring/v3/agreements/{agreementId}/charges
POST:/recurring/v3/agreements/charges
Create single or batch charges with Psp-Id. Card info returned directly in the response body. PSPs can only create unscheduled charges — scheduled recurring charges with retries are not available.
Update the status of the charge:
POST:/recurring/v3/agreements/{agreementId}/charges/{chargeId}/capture
POST:/recurring/v3/agreements/{agreementId}/charges/{chargeId}/refund
DELETE:/recurring/v3/agreements/{agreementId}/charges/{chargeId}
Communicate capture, refund, and cancel status to Vipps MobilePay. Payment processing happens in PSP/acquirer systems.

PSPs can use all Recurring API endpoints with the Psp-Id header added to every request. The endpoints requiring additional PSP-specific configuration are the draft agreement (POST:/recurring/v3/agreements) and charge creation (POST:/recurring/v3/agreements/{agreementId}/charges) endpoints.

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

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

Steps:

  1. Send the agreement request
  2. Respond to the card callback
  3. Start creating charges

Send the agreement request​

Agreement sign-up with card passthrough is initiated by sending a draft agreement request with the following settings:

  • Add the Psp-Id header with your PSP identifier
  • Include the cardPassthrough object, filled out
    • pspReference (required): Your unique reference for the agreement.
    • cardCallbackUrl (required): URL where we will send the card token.
    • cardCallbackAuthHeader (required): Authentication header value for the callback.
    • allowedCardTypes: Card types the user can select. Values: VISA_DEBIT, VISA_CREDIT, VISA_DANKORT, DANKORT, MC_CREDIT, MC_DEBIT. If not specified, all types are allowed.
    • preferVisaPartOfVisaDankort: When true, prefer the Visa part of a Visa/Dankort co-branded card. Default: false.

Example request body:

{
"pricing": {
"type": "LEGACY",
"currency": "NOK",
"amount": 10000
},
"interval": {
"unit": "MONTH",
"count": "1"
},
"initialCharge": {
"amount": 10000,
"description": "First payment",
"transactionType": "DIRECT_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",
"cardCallbackAuthHeader": "Bearer your-secure-token",
"allowedCardTypes": ["VISA_DEBIT", "VISA_CREDIT", "VISA_DANKORT", "MC_CREDIT", "MC_DEBIT"],
"preferVisaPartOfVisaDankort": true
}
}
Full example

Example request (PSP-specific fields are highlighted):

curl -X POST https://apitest.vipps.no/recurring/v3/agreements/ \
-H "Content-Type: application/json" \
-H "Psp-Id: YOUR-PSP-ID" \
-H "Authorization: Bearer YOUR-ACCESS-TOKEN" \
-H "Ocp-Apim-Subscription-Key: YOUR-SUBSCRIPTION-KEY" \
-H "Merchant-Serial-Number: YOUR-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",
"cardCallbackAuthHeader": "Bearer your-secure-token",
"allowedCardTypes": ["VISA_DEBIT", "VISA_CREDIT", "VISA_DANKORT", "MC_CREDIT", "MC_DEBIT"],
"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 to be notified of these and other events.

For all available fields, see 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 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. Include your PSP identifier in the Psp-Id header.

For reconciliation, you can optionally use:

For the full range of options available, see Agreements in the Recurring API guide.

Charges​

Charge creation​

PSPs currently have access to unscheduled charges. You are responsible for creating charges according to the terms of each agreement.

You can send both single and batch charges. Here are the flows:

PSP merchant charge flow (single charges)

  1. PSP creates a charge with Vipps MobilePay (POST /recurring/v3/agreements/{agreementId}/charges).
  2. Vipps MobilePay returns the card info (token) to PSP.
  3. PSP processes the payment using the token.
  4. PSP updates the charge status with Vipps MobilePay.
  5. Vipps MobilePay returns OK.

PSP merchant charge flow (batch charges)

  1. PSP creates a batch of charges with Vipps MobilePay (POST /recurring/v3/agreements/charges).
  2. Vipps MobilePay returns batched card info (tokens) to PSP.
  3. For each charge in the batch: PSP processes the payment, PSP updates the charge status with Vipps MobilePay, and Vipps MobilePay returns OK.

Steps:

  1. Send the charge request
  2. Process the charge
  3. Update the status of the charge

Send the charge request​

Create charges using one of the Recurring API charge endpoints:

Include the Psp-Id header with your PSP identifier. For example:

curl -X POST https://apitest.vipps.no/recurring/v3/agreements/UNIQUE-AGREEMENT-ID/charges \
-H "Content-Type: application/json" \
-H "Psp-Id: YOUR-PSP-ID" \
-H "Authorization: Bearer YOUR-ACCESS-TOKEN" \
-H "Ocp-Apim-Subscription-Key: YOUR-SUBSCRIPTION-KEY" \
-H "Merchant-Serial-Number: YOUR-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 '{
"amount": 1000,
"description": "Monthly subscription.",
"due": "2026-08-08",
"retryDays": 0,
"transactionType": "DIRECT_CAPTURE",
"orderId": "UNIQUE-ORDERID"
}'

Unlike the agreement sign-up and payment source update flows, charges do not use a card callback. Card info is returned directly in the charge creation response.

Example response:

{
"chargeId": "chg_WCVbcAbRCmu2zk",
"pspReference": "subscription-product-123",
"authorizationAttemptId": "3030303thisisaguid",
"maskedCardNumber": "47969485XXXX1234",
"cardType": "VISA-DEBIT",
"cardIssuedInCountryCode": "NO",
"paymentInstrument": "TOKEN",
"networkToken": {
"number": "5000000000000000001",
"cryptogram": "aFgdgjdkfgjdFDF=",
"expiryMonth": "03",
"expiryYear": "2030",
"tokenType": "VISA",
"eci": "7",
"paymentAccountReference": "5001BO8B9NXVVIXCT0HAJU98I512Z"
},
"encryptedPan": "xyzxyz"
}

Always send the charge update. 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.

Process the charge​

This step is done by you, the PSP.

After receiving card data in the charge response, process the charge in your PSP/acquirer systems using the card token or PAN.

We aren't involved in the actual card processing. We only provide card data to the PSP.

Once you've processed the charge, update the charge status to keep us aligned.

Update the status of the charge​

Use these Recurring API endpoints to keep us aligned with the payment state in your PSP/acquirer systems.

For all calls below, include your PSP identifier in the Psp-Id header.

For the full range of options available, see Recurring API guide: 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

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

See Card callback for the request format, HMAC authentication, and expected response.

Card callback​

The card callback applies to both agreement sign-up and payment source updates. When the user confirms the payment in the Vipps MobilePay app and selects their card, we send a POST request to your cardCallbackUrl. You must respond within 20 seconds.

It will be encrypted and include the PAN if you included your publicEncryptionKeyId in the initial request.

Key behaviors:

  • The callback is synchronous — we expect a response with the payment authorization result.
  • If your response indicates a retryable error, the user can retry with the same or a different card.
  • HTTP 500 errors and timeouts are treated as non-retryable.
    • Agreements can be patched later to reflect whether the CIT was successfully processed on your end.
    • If an initial charge was included in the agreement, it can be updated later to reflect the payment outcome.
    • We will show the user an error message: "check the payment status at the merchant you came from".

Callback request​

We send a POST request to your cardCallbackUrl with 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.
    • 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.

For example:

POST /psp-makepayment HTTP/1.1
Host: example.com
Content-Type: application/json

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

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.

Example callback request​

You will receive an HTTP POST with this format:

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

{"pspReference":"7686f7788898767977","authorizationAttemptId":"3030303thisisaguid","merchantSerialNumber":"123456"}

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

How to verify the callback​

  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\n<pathAndQuery>\n<date>;<host>;<hash>

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

    Please note the use of \n not \r\n.

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

Sample code​

The following examples show how to validate the callback request:

'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 = 'A0+AeKBRG2KRGvnNwJpQlb6IJFk48CKXCIcrLoHncVJKDILsQSxS6NWCccwWm6r6FhGKhiHTBsG2wo/xU6FY/A==';

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}\n` +
`${request.pathAndQuery}\n` +
`${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');
});
});

Callback response​

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.

Example payloads​

RESERVE​

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

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

SOFT_DECLINE​

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

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

FAIL​

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

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

Error codes​

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

errorCodeNameRetryableDescription
100Card ErrorYesCard-related failure where the card should not be retried unchanged for this payment.
200Insufficient FundsYesThe card or account does not have enough available funds to complete the payment.
210Limit ExceededYesA card, account, or issuer limit has been reached, so the payment cannot be authorized.
300Issuer DeclinedYesThe issuer declined the payment without providing a more specific reason.
400Permanent DeclineNoA hard decline where the payment must not be retried.
500Risk DeclinedYesThe payment was blocked by fraud or risk controls.
600Temporary Technical ErrorYesA transient PSP, issuer, or network problem prevented the authorization attempt from completing.
700Merchant Configuration ErrorNoThe payment failed because of merchant, PSP, or transaction configuration issues.
800Duplicate or In ProgressNoThe payment is already being processed, or a duplicate attempt was detected, so retrying immediately is not allowed.
900Unknown ErrorYesThe 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.

The Recurring API section 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 — visual flows for payment agreements, charges, and other recurring scenarios
  • Agreement guide — creating and managing payment agreements
  • Charges guide — creating, capturing, and refunding charges
  • Recurring API spec — the full technical specification for all endpoints, including required fields, data types, and valid formats