Conformance with the Vipps MobilePay documentation
Checked against the ePayment API spec 1.8.5, the Webhooks API spec, the Access Token API spec and the guide pages listed below, read 2026-11-07. This add-on is a port of our OpenCart extension, which was checked against the same documents.
How it was proven, and what was not
Section titled “How it was proven, and what was not”- Unit tests (88 checks, PHP 8.1 and 8.3) of the pure logic: money and minor units, references, state mapping, the documented webhook signature algorithm, header reading, secrets, the queue back-off and the client.
- HTTP end-to-end tests and a Multi-Vendor end-to-end test against a local stand-in built from the specs below (
tests/mock-server/server.js) on a CS-Cart Multi-Vendor 4.17.2 development store: connection, webhook registration and signed delivery, checkout redirect, return, waiting, abort, amount mismatch, capture, refund, release, capture and release at order statuses, cron recovery, unanswered operations, vendor and owner mode. Seetests/test-plan.mdfor the counts and the stores that really ran. - It has not been run against the real Vipps MobilePay test environment. We have no test sales unit yet, and no real app approved a payment. Confirm the points under “To confirm with your own test keys” once. Tested on CS-Cart 4.17.2 (cs417: e2e 55/55, licence 43/43, lifecycle 12/12) and Multi-Vendor 4.17.2 (mv417c: e2e 40/40 after the audit fixes, vendor e2e 38/38), PHP 8.1 and 8.3 (unit 90), Smarty 5.4.3 offline compile of every template against the 4.21.2 engine; all against a stand-in built from the official specs. Not run: the Vipps MobilePay test environment, a live 4.21 store, CS-Cart Ultimate with several storefronts.
Sources
Section titled “Sources”All read 2026-11-07:
- ePayment API OpenAPI spec 1.8.5: https://developer.vippsmobilepay.com/redocusaurus/epayment-swagger-id.yaml (“ePayment spec”)
- Webhooks API OpenAPI spec v1: https://developer.vippsmobilepay.com/redocusaurus/webhooks-swagger-id.yaml (“Webhooks spec”)
- Access Token API OpenAPI spec: https://developer.vippsmobilepay.com/redocusaurus/access-token-swagger-id.yaml (“Token spec”)
- Guide pages under https://developer.vippsmobilepay.com/docs/:
APIs/epayment-api/api-guide/operations/create|capture|refund|cancel/,APIs/epayment-api/api-guide/webhooks/,APIs/webhooks-api/request-authentication/,APIs/webhooks-api/api-guide/,APIs/access-token-api/standard-authentication/,knowledge-base/http-headers/,knowledge-base/polling-guidelines/,knowledge-base/errors/,knowledge-base/orderid/,knowledge-base/test-environment/,knowledge-base/design-guidelines/ - Brand book (paymarks, checkout branding): see research notes
research/brand-assets/vipps-mobilepay/SOURCE.mdin the build workspace.
Every endpoint, header, field and state
Section titled “Every endpoint, header, field and state”| What | Source | Used for / fields |
|---|---|---|
| Hosts | ePayment spec servers: https://api.vipps.no/epayment (production), https://apitest.vipps.no/epayment (test); Token spec https://api.vipps.no, https://apitest.vipps.no; Test environment page |
Live and Test mode (Config::LIVE, Config::TEST) |
POST /accesstoken/get |
Token spec fetchAuthorizationTokenUsingPost; Standard authentication page |
Headers client_id, client_secret, Ocp-Apim-Subscription-Key, Merchant-Serial-Number, Vipps-System-*; answer access_token, expires_in (“valid for 1 hour in the test environment and 24 hours in production”). Cached sealed until shortly before expiry; a 401 on a later call drops it and fetches a new one |
| Common headers | ePayment spec parameters (Merchant-Serial-Number and Ocp-Apim-Subscription-Key required, Vipps-System-Name/Version/Plugin-Name/Plugin-Version max 30 characters); HTTP headers page (“required for plugins and partners”) |
Sent on every call: Authorization: Bearer, subscription key, MSN, Vipps-System-Name: cscart, Vipps-System-Version: <CS-Cart version>, Vipps-System-Plugin-Name: kaviolabs-cscart-vipps, Vipps-System-Plugin-Version: <add-on version> |
POST /epayment/v1/payments |
ePayment spec createPayment, schema CreatePaymentRequest; Create page |
amount {currency, value} (minor units, integer), paymentMethod.type = WALLET, reference, userFlow = WEB_REDIRECT, returnUrl (“must use the https:// scheme”), paymentDescription (3 to 100 characters). Idempotency-Key header required (max 50). Answer 201 {redirectUrl, reference}; 400 / 403 / 409 Problem. Not sent: customer (the Create page says users should never be asked for the phone number), receipt, profile, expiresAt (long-living payments need approval), merchantLegalLinks |
| Redirect | Create page, WEB_REDIRECT: “It is crucial that this URL is opened directly, without making any changes to it … Never open the URL in a web view, iframe” | The payment script redirects to the redirectUrl as returned (top-level navigation) |
| Return URL | Create page: “The returnUrl may be opened more than once, including simultaneously. Opening it must not trigger order updates or other side effects. Confirm the payment status and process the order on your backend, with protection against repeated and concurrent processing.” | The return only reads the payment back (three times, 2 seconds apart while it is open) and applies it under the payment lock, idempotently |
reference |
ePayment spec schema Reference: ^[a-zA-Z0-9-]{8,64}$, unique per sales unit; orderId page |
Money::reference(): <prefix>-<order id>-<8 hex> (the random part keeps a reinstalled shop from reusing one) |
| Amount | ePayment spec schema Amount: minor units; minimum NOK 100, DKK 1, EUR 1; maximum 65000000; schema Currency: NOK, DKK, EUR for all merchants; Create page: “The currency must match the currency for your sales unit” |
Sales unit currency is a setting per mode; other cart currencies are converted once with the shop’s rate and never again |
GET /epayment/v1/payments/{reference} |
ePayment spec getPayment, schema GetPaymentResponse |
Reads state, amount, aggregate.{authorizedAmount, capturedAmount, refundedAmount, cancelledAmount}, pspReference, redirectUrl, captureGuaranteedUntil. 404 = unknown reference |
| States | ePayment spec schema State: CREATED, ABORTED, EXPIRED, AUTHORIZED, TERMINATED; “A payment that has been refunded may have one or more refund events, but the state would be AUTHORIZED” |
Only ABORTED, EXPIRED and TERMINATED end a payment unpaid; anything else, also an unknown value, stays pending; after AUTHORIZED the local status comes from the aggregate |
POST .../capture |
ePayment spec capturePayment (Idempotency-Key required, CaptureModificationRequest {modificationAmount}); Capture page: “Verify that capturedAmount in the response matches the amount you intended to capture - the HTTP status code alone is not sufficient” |
Partial and full capture; the answer’s aggregate is checked before the operation counts as done |
POST .../refund |
ePayment spec refundPayment (Idempotency-Key required, RefundModificationRequest); Refund page |
Partial and full refunds of captured money |
POST .../cancel |
ePayment spec cancelPayment (optional body CancelModificationRequest, cancelTransactionOnly: “Only cancel transaction if it has not been authorized”); Cancel page: before authorization the state becomes TERMINATED, after authorization it stays AUTHORIZED and the uncaptured amount is released; “Cancelling after a partial capture will always release the entire not-yet-captured amount” |
Release of reservations (order tab, release status, mismatch and duplicate); cancelTransactionOnly: true when the order is cancelled or a new attempt replaces an open one before approval. The spec lists no Idempotency-Key for cancel; the Refund page’s cancel example sends one, so the add-on sends one too |
| Idempotency | HTTP headers page: “can safely be retried with the same Idempotency-Key without causing a duplicate”; Errors page (423 and 429: retry with the Idempotency-Key) | Every money call is a row with its key BEFORE the call. Create: no answer = looked up by reference, then sent again with the SAME key. Capture, refund and cancel without a clear answer: the payment is read back FIRST; if the aggregate shows the operation it is marked done and never resent; otherwise it is sent again with the same key for up to 24 hours, then closed (“check the business portal”) |
| Errors | ePayment spec schema Problem (RFC 7807: type, title, detail, status, traceId, extraDetails) |
Title, detail, extra details and the trace id are shown to the administrator (masked in the log) |
| Webhooks API | Webhooks spec: GET/POST /webhooks/v1/webhooks, DELETE /webhooks/v1/webhooks/{id}; RegisterRequest {url, events}, RegisterResponse {id, secret}; url “must use HTTPS … Can not be a URL that redirects … use common ports such as: 80, 443, 8080” |
The “Register webhook” button; the secret is returned only once, so a registration of the same address whose secret we do not hold is deleted and registered again |
| Webhook address | Webhooks spec RegisterRequest.url (no rule against a query string); Webhook authentication page: the signature covers “path and query” |
<shop>/index.php?dispatch=kv_vipps_mobilepay.webhook&c=<account>: a plain front controller dispatch, no SEO rewrite; the query is part of the signed path and is stable. See “Unverifiable or flagged” |
| Events | ePayment Webhooks page table: `epayments.payment.created | aborted |
| Webhook payload | ePayment spec schema WebhookEvent (msn, reference, pspReference, name, amount, timestamp, success, …) |
Only reference (checked against the documented reference format) is used; the state always comes from GET |
| Webhook authentication | Webhook authentication page: content hash = base64(SHA-256(body)) must equal x-ms-content-sha256; string to sign POST\n<pathAndQuery>\n<date>;<host>;<hash> (\n, not \r\n), HMAC-SHA256 with the registration secret, base64, equals the Signature of Authorization: HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=... |
Webhook::verify(), constant-time compares; host and path candidates are the request’s Host and URI plus the registered address (a proxy may change the Host the shop sees; every candidate still needs the secret). The secret of either mode of the account is tried |
| Authorization header | HTTP headers page, Special headers: X-Vipps-Authorization “Must not be used, and may change or disappear without notice” |
Read from HTTP_AUTHORIZATION, REDIRECT_HTTP_AUTHORIZATION, getallheaders(), apache_request_headers(). X-Vipps-Authorization is not used |
| Retries and delivery order | Webhooks API guide: retried on 4xx / 5xx or no answer within 10 s, up to 7 days; “your server needs to accept all preceding requests for a given payment, before any new notifications can be received for the same payment” | 200 when handled; 400 when the body is not an ePayment event; 401 for a bad signature; 503 when the read-back failed (so Vipps MobilePay retries) |
| Polling | Polling guidelines: “Start after 5 seconds, Check every 2 seconds”; ePayment Webhooks page: “you should not rely on webhooks alone” | Return: three reads 2 seconds apart; cron: every run reads open payments not read for a minute |
| Capture deadline | captureGuaranteedUntil (GetPaymentResponse, WebhookEvent); Capture page: “between 14 and 180 days” |
Shown on the order tab in shop time; highlighted within 3 days; banner; log warning |
| Test environment | Test environment page: https://apitest.vipps.no, test sales unit keys, MT test app; “Card payment (CARD) is not available in the test environment” |
Test mode; WALLET only |
| Brand | Brand guidelines page and brand book | Official “Pay Mark Small” paymark shipped unchanged (assets/vipps-paymark.svg, assets/mobilepay-paymark.svg); local brand by default; line “Pay safely and easily. No fees, no matter the amount.” |
To confirm with your own test keys
Section titled “To confirm with your own test keys”- One payment approved in the Vipps MobilePay test app, and the authorized webhook arriving with a signature that verifies (the documented sample signature itself does not verify with its sample secret; see below).
- One capture, one partial refund and one release, and their webhooks.
- The webhook registration with the query string in the address (
&c=<account>) is accepted and the delivered signature covers it. - Your firewall or bot filter lets Vipps MobilePay reach the webhook address.
Unverifiable or flagged
Section titled “Unverifiable or flagged”- The documented sample signature does not verify. The sample on the Webhook authentication page (secret
AAAA...==, dateThu, 30 Mar 2023 08:38:32 GMT, hostwebhook.site) gives the documented content hash, but the page’s signature is not what its own sample code computes from those values. We follow the sample code (the secret as a UTF-8 string). Confirm with the first real webhook. - A query string in the webhook address. The spec has no rule against it and the signature covers “path and query”, but we could not test a real registration with one. If Vipps MobilePay refuses it, the Register webhook button shows the error and nothing else is affected (the return and the cron still settle payments).
- No timestamp window. The docs define no maximum age for
x-ms-dateand retries run up to 7 days, so none is checked; replay is harmless because the state always comes fromGET. - Idempotency of cancel. Not in the spec’s parameter list, sent anyway (Refund page example).
- Idempotency-Key lifetime. The docs give none, so a late resend never relies on the key alone: the payment is read back first.
- Same reference with a different key, or the same key with a different body. The docs do not say what the API answers. The add-on never does either: one key and one reference per attempt; on 409 it looks the reference up.
- Real test merchant. None available to us; every flow was proven against the stand-in.
- Rename on 12 January 2027. “MobilePay becomes Vipps in Denmark and Finland on 12 January”; the API hosts are api.vipps.no already; nothing in the integration changes, the default name and paymark switch by themselves.
Not covered in 1.0.0
Section titled “Not covered in 1.0.0”Freestanding card payments (CARD), Express checkout and dynamic shipping, profile sharing (userinfo), long-living payments, QR and push-message flows, recurring payments.