# Development tunnels (/build-and-go-live/development-tunnels) When developing locally, your webhook handler might be available only at an address such as `http://localhost:8080`. ePay cannot send webhooks to `localhost` that address refers to the machine making the request, not your computer. A development tunnel creates a public HTTPS URL and securely forwards its requests to your local application. ## Choose a tunnel [#choose-a-tunnel] | | ngrok | Cloudflare Tunnel | | ----- | ------------------- | ----------------------------------------- | | Cost | Free tier available | Free | | Setup | Very quick | More involved | | URL | Random public URL | Static hostname on your Cloudflare domain | ## ngrok [#ngrok] ngrok is the easiest option for receiving a webhook on your local machine. It runs a small command-line agent and gives you a public HTTPS URL. 1. Create a free [ngrok account](https://dashboard.ngrok.com/signup) and [install the ngrok agent](https://ngrok.com/download). 2. Copy your authtoken from the ngrok dashboard and register it with the agent: ```bash ngrok config add-authtoken ``` 3. Start your application locally. In this example, the webhook route is served at `http://localhost:8080/webhooks/epay`. 4. In a second terminal, expose the application's port: ```bash ngrok http 8080 ``` ngrok prints a public forwarding URL, similar to this: ```txt https://example-1234.ngrok-free.app ``` Use that URL plus your webhook path as the `notificationUrl` when you create a test payment: ```json { "notificationUrl": "https://example-1234.ngrok-free.app/webhooks/epay" } ``` Keep ngrok running until ePay has delivered the webhook. The generated hostname can change when you restart the tunnel, so update the notification URL for each new test session. For installation details and options, see the [ngrok agent documentation](https://ngrok.com/docs/agent/). ## Cloudflare Tunnel [#cloudflare-tunnel] Use a named Cloudflare Tunnel when you want a stable development URL, for example `https://webhooks-dev.example.com`. Your domain must be added to Cloudflare and use Cloudflare DNS. 1. [Install `cloudflared`](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/). 2. In the Cloudflare dashboard, go to **Networking** → **Tunnels** and create a tunnel. 3. Select your operating system, then run the command Cloudflare provides to connect your local machine. It has this form: ```bash cloudflared tunnel run --token ``` 4. Open the tunnel's **Routes** tab and add a **Published application** route: | Setting | Value | | ----------- | -------------------------- | | Hostname | `webhooks-dev.example.com` | | Service URL | `http://localhost:8080` | 5. Use the stable hostname and webhook path in your test payment: ```json { "notificationUrl": "https://webhooks-dev.example.com/webhooks/epay" } ``` Keep `cloudflared` running while testing. The tunnel dashboard should report the tunnel as healthy before you create the payment. For the complete setup flow, see Cloudflare's guide to [create a remotely-managed tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/). ## Test the webhook handler [#test-the-webhook-handler] Before testing a payment, verify that your local route accepts a request: ```bash curl --include --request POST http://localhost:8080/webhooks/epay \ --header 'Content-Type: application/json' \ --data '{}' ``` Then create a test payment with the tunnel URL as its notification URL. Treat the webhook exactly as you would in production: verify the payment result, make processing idempotent, and return a successful response after handling it. See [Handle payment results](./handle-payment-results) for the webhook requirements. # Go live (/build-and-go-live/go-live) Before you can start accepting payments with ePay, let's make sure everything is set up correctly. This checklist helps you tick off all the key requirements so you can go live without a hitch! ## General Requirements [#general-requirements] ## Website Requirements [#website-requirements] ## Extra Requirements for Subscription Payments [#extra-requirements-for-subscription-payments] If you're offering subscription-based payments, here are some additional things to check off: By following this checklist, you'll be well on your way to accepting payments smoothly with ePay. Happy selling! # Handle payment results (/build-and-go-live/handle-payment-results) After a customer completes a payment, your system needs to know what happened. A payment result can affect the customer experience, the order status, fulfillment, emails, invoices and accounting. Because of that, your backend should handle payment results reliably and safely. ## The important rule [#the-important-rule] Do not rely only on the customer returning to your website. A customer may close the browser before returning. A redirect may fail. A mobile browser may lose the session. Use a server-side payment result to update the order status in your system. That also applies to client-side ePay.js callbacks such as `transactionAccepted`: use them for UI feedback, not as the final payment confirmation. ## Customer redirects vs server-side results [#customer-redirects-vs-server-side-results] Redirects and server-side results solve different problems. | Method | Used for | Should update order status? | | ---------------- | -------------------------------------- | --------------------------- | | Success URL | Showing a success page to the customer | No, not alone | | Failure URL | Showing a failure page to the customer | No, not alone | | Notification URL | Updating your backend | Yes | | Webhook | Updating your backend | Yes | Use redirects for the customer experience. Use a notification URL or webhook for your backend order status. ## Recommended payment flow [#recommended-payment-flow] A safe payment flow usually looks like this: Create an order in your system Set the order status to pending Create a payment with ePay Send the customer to payment Receive the payment result server-side Verify the payment result Update the order status Show the final status to the customer ## Prepare your order [#prepare-your-order] Use clear order states in your own system. | State | Meaning | | -------------------- | -------------------------------------------------------- | | `pending` | The order has been created, but payment is not completed | | `paid` | The payment has been completed successfully | | `failed` | The payment failed | | `cancelled` | The payment was cancelled or abandoned | | `refunded` | The payment has been refunded | | `partially_refunded` | Part of the payment has been refunded | Your exact states may differ, but the important part is that an order is not marked as paid until your backend has confirmed the payment. Create the order before sending the customer to payment and keep it in a pending state until your backend has processed the result. ```ts const order = await createOrder({ reference: "ORDER-1001", amount: 10000, currency: "DKK", status: "pending", }); ``` When creating the payment, include a reference that connects the ePay payment to your internal order. ```json { "reference": "ORDER-1001" } ``` When you receive the payment result, use the reference or payment ID to find the correct order. ## Notification URLs and webhooks [#notification-urls-and-webhooks] A notification URL is an endpoint in your backend where ePay can send the payment result. Example: ```txt https://example.com/api/epay/notification ``` Your notification endpoint should: ### What you receive [#what-you-receive] When the payment is completed, ePay sends a webhook HTTP request with the payment data. The top-level objects are: | Name | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------- | | `sca` | Information about strong customer authentication, for example whether 3DS or delegated authentication was used. | | `session` | The ePay session object, including the original session configuration and state. | | `transaction` | The ePay transaction object with the payment result, reference, amount, currency and payment method details. | | `subscription` | The subscription object when the payment is part of a subscription flow. | | `acquirerAgreement` | Acquirer-specific context such as acquirer name or MCC. | In practice, `transaction.id`, `transaction.state`, `transaction.reference`, `session.id`, and your own reference or attributes are the most important values for matching the payment and deciding what to do next. Example webhook payload: ```json { "sca": { "rejected": false, "type": "3DS", "verification": "NONE" }, "session": { "id": "0192473a-e382-79a9-bfc2-65da88fe812f", "subscriptionId": "01929a94-5fce-7ccc-a7e4-7e9249133b39", "amount": 1000, "attributes": { "key1": "value1", "key2": "value2" }, "exemptions": ["TRA"], "createdAt": "2024-10-01T10:38:14.658688472+02:00", "currency": "DKK", "expiresAt": "2024-10-01T12:41:14.658688472+02:00", "instantCapture": "OFF", "maxAttempts": 10, "reportFailure": false, "dynamicAmount": false, "notificationUrl": "https://example.com/notification", "preAuthUrl": "https://example.com/pre-auth", "successUrl": "https://example.com/success", "failureUrl": "https://example.com/failure", "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "reference": "reference-1", "state": "COMPLETED", "textOnStatement": "The text", "scaMode": "SKIP", "timeout": 60 }, "transaction": { "id": "01924756-d1f6-7bc6-bb51-2b5f87b43925", "subscriptionId": "01929a94-5fce-7ccc-a7e4-7e9249133b39", "state": "SUCCESS", "errorCode": null, "createdAt": "2024-10-01T09:08:45.174774Z", "sessionId": "01924756-badd-71d4-be55-da367f434da4", "paymentMethodId": "01924756-d1f6-738d-8040-90d76cedf01f", "paymentMethodType": "CARD", "paymentMethodSubType": "Visa", "paymentMethodExpiry": "2050-01-01", "paymentMethodDisplayText": "40000000XXXX0003", "scaMode": "SKIP", "amount": 1000, "currency": "DKK", "customerId": "User159", "instantCapture": "OFF", "notificationUrl": "https://example.com/notification", "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "reference": "reference-1", "textOnStatement": "The text", "exemptions": ["TRA"], "attributes": { "key1": "value1", "key2": "value2" }, "clientIp": "1.2.3.4", "type": "PAYMENT" }, "subscription": { "id": "01929a94-5fce-7ccc-a7e4-7e9249133b39", "paymentMethodId": "01924756-d1f6-738d-8040-90d76cedf01f", "currency": "DKK", "customerId": "User159", "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "reference": "subscription-1", "state": "ACTIVE", "type": "SCHEDULED", "expiryDate": null, "interval": { "period": "MONTH", "frequency": 1 }, "createdAt": "2024-10-01T10:38:14.658688472+02:00" }, "acquirerAgreement": { "acquirer": "shift4", "mcc": "4514" } } ``` ### Webhook authentication [#webhook-authentication] Every notification or webhook request includes an `Authorization` header. Validate that header before trusting the request body. **Partners:** For transactions initiated using merchant access tokens generated through the Partner API, validate the full `Authorization` header against your **partner notification secret** instead of the Point of Sale secret. The same partner secret applies to payment notifications and pre-authorization callbacks, across all your merchants and both test and live environments. Copy it from **Notification secret** in the [Partner Portal](https://partner.epay.eu/callback-key). Transactions initiated with a merchant API key directly or from the backoffice still use the merchant-specific Point of Sale secret. See [partner callback authentication](/partners/partner-api#notification-authentication). For merchant-specific callbacks, ePay generates a random Bearer token when the account is created. The token and the authorization scheme can be changed in ePay Backoffice. Example: ```ts const authorization = request.headers.get("authorization"); if (authorization !== process.env.EPAY_WEBHOOK_AUTHORIZATION) { return new Response("Unauthorized", { status: 401 }); } ``` For transactions using the merchant-specific secret, configure the authorization value on the Point of Sale in ePay Backoffice. Using a Point of Sale notification URL does not override the partner-secret rules above. ### Acknowledge quickly [#acknowledge-quickly] ePay expects a standard HTTP `200 OK` response when the notification has been received successfully. The webhook request has a strict timeout of 5 seconds. That means your endpoint should do only the minimum necessary work synchronously: Long-running business logic such as fulfillment, invoicing, email sending, ERP sync, or other external integrations should happen after the webhook has been acknowledged. Merchants risk being moved to the low-priority queue if the webhook endpoint does not respond quickly enough. ### Retry behavior [#retry-behavior] If ePay does not receive `200 OK`, the webhook is retried automatically with exponential backoff. | Attempt | Delay | | ------- | ------------------- | | 1 | 0 seconds | | 2 | 0 seconds | | 3 | 4 seconds | | 4 | 8 seconds | | 5 | 16 seconds | | ... | Exponential backoff | | 14+ | Capped at 3 hours | | 25 | Final attempt | Retries stop after 25 attempts. ### Example handler [#example-handler] This example shows the basic idea. Your real implementation should match your backend, order system and the actual ePay payload. ```ts export async function handlePaymentNotification(payload) { const payment = await verifyPaymentResult(payload); const order = await findOrderByReference(payment.reference); if (!order) { throw new Error("Order not found"); } if (order.status === "paid") { return { ok: true }; } if (payment.status === "accepted") { await markOrderAsPaid(order.id); } if (payment.status === "declined") { await markOrderAsFailed(order.id); } return { ok: true }; } ``` ## Handle duplicate notifications [#handle-duplicate-notifications] Your notification handler should be idempotent. That means it should be safe to process the same payment result more than once. This is important because notifications may be retried. Recommended approach: Store the payment ID or transaction ID Check whether it has already been processed If it has already been processed, return success If it has not been processed, update the order Store that the payment result has been processed Example: ```ts if (await hasProcessedPayment(payment.id)) { return { ok: true }; } await processPayment(payment); await markPaymentAsProcessed(payment.id); return { ok: true }; ``` ## Verify and handle API errors [#verify-and-handle-api-errors] If your backend calls the API while handling a payment result, for example to verify a session or fetch the latest transaction state, you should log and handle API errors explicitly. There are two common response formats: **Validation errors (`422`)** ```json { "errorCode": "VALIDATION_ERROR", "message": "Input validation errors", "errors": { "amount": [ "[required]: Is a required non-nullable field", "[int]: Must be an integer", "[min:0]: Must be greater than 0", "[max:999999999]: Must be less than 999999999" ] } } ``` **Other API errors (`4XX` and `5XX`)** ```json { "errorCode": "INVALID_OR_EXPIRED_SESSION", "message": "Invalid session authentication or the session has expired." } ``` In this flow, use `errorCode` for programmatic handling, and use `message` plus the request context for logs and debugging. ## Handle different payment outcomes [#handle-different-payment-outcomes] **Successful payments** When a payment is successful: Verify the payment result Find the order Confirm that the amount and currency match the order Mark the order as paid Continue with fulfillment, email confirmation or invoice creation Always check that the amount and currency are what you expect. **Failed payments** A failed payment should not trigger fulfillment. Find the order Keep the order unpaid Mark the payment attempt as failed, if relevant Let the customer try again, if your flow supports it **Abandoned payments** A customer may abandon a payment before completion. Your system should usually keep the order as pending until it expires or is cancelled. ## Customer pages [#customer-pages] Your success and failure pages are part of the customer experience, not the source of truth for your backend. For success pages: Example: ```txt Your payment has been received and is being confirmed. ``` For failure pages: Example: ```txt The payment could not be completed. Please try again or use another payment method. ``` ## Logging and common problems [#logging-and-common-problems] Add useful logs while building and testing. Log: Do not log sensitive payment data. Common problems to check first: ## What you built [#what-you-built] You now have a safer payment result flow. Your system can receive payment results, update order statuses and avoid common issues like duplicate processing or relying only on redirects. ## Next steps [#next-steps] * [Test your integration](./test-your-integration) * [Go live](./go-live) * [Troubleshooting](./troubleshooting) # Migration (/build-and-go-live/migration) When migrating from one PSP (Payment Service Provider) to another, it is essential to plan the migration process carefully to avoid service interruptions. If you have active subscriptions or stored cards that need to remain valid after switching providers, a formal migration is required. ## What to do as a Merchant [#what-to-do-as-a-merchant] To initiate a migration, you as the merchant must contact your **current provider (old PSP)** and request that they export your stored cards and subscription details in a format suitable for transfer to **ePay**. We have often experienced delays when PSPs handle export requests. It is therefore **strongly recommended** that you contact your current provider well in advance and schedule the export as early as possible to avoid impacting your migration timeline. ### Step-by-step guide [#step-by-step-guide] 1. **Plan the migration date** * Coordinate with **ePay** to agree on a preferred date for the export/import process. * Ensure your ePay account is fully configured with any necessary acquirer agreements. * Ensure you have tested your setup at ePay before migrating to avoid downtime. 2. **Contact your current provider** * Submit an export request including the following information: * Your new payment provider: **ePay Payment Solutions** * A link to our migration guide and public encryption key: **[https://docs.epay.eu/build-and-go-live/migration](https://docs.epay.eu/build-and-go-live/migration)** * Contact details for ePay: * **Primary Contact:** [thomas@epay.dk](mailto\:thomas@epay.dk) (CTO) * **CC:** [niki@epay.dk](mailto\:niki@epay.dk) (Tech Lead) 3. Once your request has been sent, your current provider will contact ePay to plan the data handover. 4. ePay will contact you once the technical plan has been agreed upon between the two PSPs to keep you updated on progress. 5. After the import is completed, your data will be available through the ePay [APIs](/api), where you can verify or fetch your stored card and subscription data. *** ### Email request template [#email-request-template] Below is a ready‑to‑use email template that you can copy, fill out, and send to your current provider. ```text Subject: Export Request – Migration to ePay Payment Solutions Dear [Provider name or contact], We are planning to migrate our stored card and subscription data to our new PSP, ePay Payment Solutions. Please initiate the export and coordinate the secure data transfer directly with ePay using the contact details below: New Provider: ePay Payment Solutions Migration Guide and Encryption Information: https://docs.epay.eu/build-and-go-live/migration#what-to-do-as-a-psp Primary Contact: thomas@epay.dk (CTO) CC: niki@epay.dk (Tech Lead) We would appreciate it if you could confirm receipt of this request and provide an estimated timeline for the export to ensure a smooth transition. Best regards, [Your name] ``` ### How to link subscriptions from your old provider to ePay [#how-to-link-subscriptions-from-your-old-provider-to-epay] Once your subscriptions have been imported into ePay, they become immediately available in both the back office interface and through our [API](/api). When ePay receives your subscription data, each record includes the old provider’s subscription reference. This value will populate the ePay field `reference` in your imported subscriptions. This allows you to correlate subscriptions in your own system by linking: * Old PSP subscription identifier → ePay field `reference` * ePay subscription identifier (new) → ePay field `id` For implementation details, see our [API documentation](/api/subscriptions/list-subscriptions). *** ## What to do as a PSP [#what-to-do-as-a-psp] If you have received an export request from a merchant migrating to ePay, please reach out to our team to coordinate the transfer. ### ePay Contacts [#epay-contacts] | Type | Email | Role | | --------------- | ---------------------------------------- | --------- | | Primary Contact | [thomas@epay.dk](mailto\:thomas@epay.dk) | CTO | | CC | [niki@epay.dk](mailto\:niki@epay.dk) | Tech Lead | Before transmitting any card data, please ensure the data is encrypted with ePay’s public PGP key. You can verify ePay’s PCI DSS compliance below. ### What ePay requires [#what-epay-requires] To complete a card or subscription migration, ePay requires the following data fields for each record: | Field | Description | | --------------- | ----------------------------------------------------------------------------------------------------------- | | **PAN** | The full primary account number (card number). | | **Expiry** | Expiration year and month of the card. | | **Trace Id** | The original authorization or trace ID, required to link MIT transactions to the initial CIT transaction. | | **TLID** | The Transaction Link Identifier for Mastercard transactions (when available). | | **Reference** | The subscription or card reference from the old PSP. This enables the merchant to link data post-migration. | | **Customer Id** | Merchant-defined customer identifier (if available). | | **Type** | The stored payment method type. This should be one of: card, token, Google Pay, or Apple Pay. | | **Created At** | The date and time at which the card or payment method was originally stored with the old PSP. | *** ### Attestation of Compliance (AOC) [#attestation-of-compliance-aoc] [Download ePay PCI-DSS Attestation of Compliance (AOC)](/documents/ePay_PCI-DSS.pdf) ### Public PGP Key [#public-pgp-key] When sending card data to ePay during migration, you must encrypt the data using ePay’s public PGP key to ensure data confidentiality and compliance with PCI DSS requirements. ``` -----BEGIN PGP PUBLIC KEY BLOCK----- mQGNBGfUQgEBDACtdeWAQmpw8p+LUD6g0iXU4ikkHdDEFDtuAqJQkXjBWz61Vozy JCmZ+1ywoRKYFk4UeoxtjZ4ssCWA5jW13u5/Q+SvPZkKRgd5k+3tPtmdKXhsOefz MBN6tCJwtZYR2trEDTypO0knxHbLhCk3fBt83M1kepljZ2PRVRmMzU+j5HHKl+Zy hFt9nocd4bQuA41cIyWRgmFkAusfXPacgr9ezYmDkMbQwawcQdBwLYyqb/Scwa71 X3VgtV5Psh+RWnB0Xq7K3drCIhxDh0Y7ljadWaOMNoUsKeHIFoNEXXrprCTKb923 plusaWoeUO5gcsVvCjEiJK3/E9ZrlVAuslsb8udSW7csEQjC68LR1N3kja8UjTCx EfzE4c7Jv4LCaRrJcOKEGbrlQne0amrJH1Ti9afWxPoBcwrnhRfywIk+WCgq+1K4 dFLr1hi2IrSlLaVjnjHbzs+0bv30897BqNVymLCG2tInIto1eDHQTu3wQresxWZf 8U7/ZuyjQ2XkaKcAEQEAAbQfTmlraSBaYWthcmlhc3NlbiA8bmlraUBlcGF5LmRr PokB0QQTAQoAOwULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgBYhBDnW9r8WV9Kr Gb3qp5zOlGr3JKlQBQJn1VbQAhsPAAoJEJzOlGr3JKlQC18L/jrBTguqALirN6S4 BJt0Z2KsCdsUNzmLpovyXD70uKgeJnLDW0XGJWDnPR1kpdxu/CMyuCljamYPh42K a+9vgxuu6YJJU735ePpJsOo7td/4SY6/ruxGS69WXFfXkQTNLyjvqKZ8bj/XckBD mEkuPek7yiUldWkSUbyLUq3Abt1ETjQSc+2jch1/JvG4nO73ojWkQnuJJowgU1dp FdACEVUJSf+GL/eUsHRJd1Blbyc36UltAPgqC1wnycbspUDQ1L0J2CYC7rudVofY I991hZ9J1orHrwIIfJsPHQg83kyjMZ+U8l+bgoqqgf7Ilyf5V4n2+U0FNFfyd+Vx VvnpfBprUWGSq8psEhAFTOqOBqcs1CbNxBvVNEPHvZquxPgULKGAU6Ry2nJO6Sce MOYaaP4u3W4l1ix2J6jhwfpIxHRg8onpLdt6HzjerMSG0aYenjgTMbzlnpHUvs4v zTe+jWBDpoPJXY8TwwfihZjd37mFE1U5vjkmo0AfkV7OLdKoGbkBjQRn1EIBAQwA 1BcA7Fx4EF1gPCgTd/kaua6gPPM/PMWfQ6JFpglCVGLIoJL4W+j3IpJEGxMesK7G LEm5gSOpZ+QdCePxUDb8eFuOR1zYIxyQijbOyEAAcmqz0JUpqND/x3cQg73GjlAG PmfEDGV3AofSCBztyVLvjpYD8n29KL6T6xUT7UefwnHB+zXSTW858ABXmOGi9xYV W54pabzg5+UqZ3TonRIqtIs+V4S3Pna1G0iiPptRjQ9vD0whxiozKBPM1pa8O6VI JooEsk/Bpcae6TVvj2OYFmmsEzTGFm7Un78nwsc9Qwel9et2grc99zOk8EQ68VVw O6DrUUaMlTLJ5c/Xp7SJDf8ks972E5y6bpO4CIDrx3NtlXKQAhZ0SGi2a0g7ySYs zM7k4qRcBdX70M4eJr0Kmgv7LFYtfxisepp++U+6CeJWHtxEhtkCnJsGjV0QuCwL pQF6DwhIR5838NXOiZxNhyaaO+53uxrWdnAoSHJwzZPZD6N2973CI5uKIlWVCdML ABEBAAGJAbYEGAEKACAWIQQ51va/FlfSqxm96qeczpRq9ySpUAUCZ9RCAQIbDAAK CRCczpRq9ySpUPBkC/0THDhQR/pagk8Z5w69DNCH/TrNLJJ02KicFHLZzkxHinZY /h13ojCYcZYoc78U0ZLAhUHUlLru+VYSLmv1D8oIzNmJH9pW01dz2+ptV9S4FPib rv+IoKM8wO0lVAhe6W4XthTj2vpkPoJFSBXM7Xk+1+uosxuqK8jFL9y5JypOjroN YHIxPRHPpOJO6pRVUbChztTWUl59h7qFq48bbS06W1o1K/lVA9mh3QL1f5rOvt56 ZsepvfZVDKMsp0D3Kiy0uEVylx6t0wCwPGnPNaAEGTAvGNj8FWpywPFRNSe+p0Xg gPdALJ+CyCpd7utbTaw68zgxDp82o8yUbe+RBs3zK0KSe02nfuAZbUjm6IwAoxDH GRS6MQMv+4QGLjJxMCnKD3fPz8aQuFTJf2jH9MZIqIhh9APv5AHyPmVFDFQPk5XC lmth/WbH7146xbZRP3lAOdV1pb3zhBteGPX5fPkb+EiLxGSUP0S09mdk6dpBKHKV Y6T6rsjVC12mZvm99uY= =a3+T -----END PGP PUBLIC KEY BLOCK----- ``` # Pre-authorization webhooks (/build-and-go-live/pre-authorization-webhooks) Use pre-authorization webhooks when your backend must review or adjust a transaction after the shopper has entered payment data, but before authorization or SCA begins. This is an advanced flow. Most merchants should start without `preAuthUrl` and only enable it when they have a concrete need such as risk rules, custom anti-fraud checks, or server-side validation of dynamic amounts. ## What pre-authorization webhooks do [#what-pre-authorization-webhooks-do] When `preAuthUrl` is set on a payment session, ePay sends a server-to-server callback after payment data collection is complete and before authorization starts. This gives your backend one last decision point where it can: * Reject the transaction * Force a different `scaMode` * Update exemptions * Add or merge transaction attributes * Change `instantCapture` Typical use cases: ## Before you use it [#before-you-use-it] You need: Pre-authorization webhooks must be enabled by ePay before use. Contact ePay if necessary. ## How it fits into the payment flow [#how-it-fits-into-the-payment-flow] The flow usually looks like this: Create a payment session and include preAuthUrl The shopper selects a payment method and enters payment details ePay sends the pre-authorization webhook to your backend Your backend accepts, rejects, or updates the transaction settings ePay continues with authorization and SCA based on the response ## Enable it on the payment session [#enable-it-on-the-payment-session] Include `preAuthUrl` when creating the payment session. ```json { "pointOfSaleId": "", "amount": 10000, "currency": "DKK", "reference": "ORDER-1001", "notificationUrl": "https://example.com/api/epay/notification", "preAuthUrl": "https://example.com/api/epay/pre-auth", "successUrl": "https://example.com/payment/success", "failureUrl": "https://example.com/payment/failure" } ``` This can be used with Checkout, Blocks, payment links, subscription initialization, and other payment-session-based flows where `preAuthUrl` is supported. ## What ePay sends [#what-epay-sends] The pre-authorization webhook request contains the current session and transaction context. For card payments, it also includes card metadata that can be used in risk analysis. Example request payload: ```json { "session": { "id": "0192473a-e382-79a9-bfc2-65da88fe812f", "subscriptionId": "01929a94-5fce-7ccc-a7e4-7e9249133b39", "amount": 1000, "attributes": { "key1": "value1", "key2": "value2" }, "exemptions": ["TRA"], "createdAt": "2024-10-01T10:38:14.658688472+02:00", "currency": "DKK", "expiresAt": "2024-10-01T12:41:14.658688472+02:00", "instantCapture": "OFF", "maxAttempts": 10, "attempts": 1, "reportFailure": false, "dynamicAmount": false, "notificationUrl": "https://example.com/notification", "preAuthUrl": "https://example.com/pre-auth", "successUrl": "https://example.com/success", "failureUrl": "https://example.com/failure", "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "reference": "reference-1", "state": "PROCESSING", "textOnStatement": "The text", "scaMode": "SKIP", "timeout": 60 }, "transaction": { "id": "01924756-d1f6-7bc6-bb51-2b5f87b43925", "subscriptionId": "01929a94-5fce-7ccc-a7e4-7e9249133b39", "state": "PROCESSING", "errorCode": null, "createdAt": "2024-10-01T09:08:45.174774Z", "sessionId": "01924756-badd-71d4-be55-da367f434da4", "paymentMethodId": "01924756-d1f6-738d-8040-90d76cedf01f", "paymentMethodType": "CARD", "paymentMethodSubType": "Visa", "paymentMethodExpiry": "2050-01-01", "paymentMethodDisplayText": "40000000XXXX0003", "customerId": "User159", "scaMode": "SKIP", "amount": 1000, "currency": "DKK", "instantCapture": "OFF", "notificationUrl": "https://example.com/notification", "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "reference": "reference-1", "textOnStatement": "The text", "exemptions": ["TRA"], "attributes": { "key1": "value1", "key2": "value2" }, "clientIp": "1.2.3.4", "type": "PAYMENT" }, "card": { "pan": "40000000XXXX0003", "expireMonth": "01", "expireYear": "30", "issuer": "Danske Bank", "scheme": "Visa", "country": "DK", "funding": "debit", "segment": "consumer" } } ``` ## How your backend responds [#how-your-backend-responds] Your endpoint can reject the transaction or return updates that ePay should apply before continuing. Example response: ```json { "reject": false, "update": { "scaMode": "FORCE", "exemptions": ["TRA"], "attributes": { "key": "value" }, "instantCapture": "VOID" } } ``` Response fields: | Field | Description | Required | | ----------------------- | ------------------------------------------------------------------------ | -------- | | `reject` | If `true`, the transaction is rejected and no authorization is attempted | No | | `update.scaMode` | Updates the SCA mode. Must be a valid `scaMode` value | No | | `update.exemptions` | Replaces the exemptions used when applicable | No | | `update.attributes` | Recursively merges attributes into the existing transaction attributes | No | | `update.instantCapture` | Updates the `instantCapture` mode | No | ## Security and operational notes [#security-and-operational-notes] Use the same defensive webhook handling here as in your normal payment result flow. **Partners:** For transactions initiated using merchant access tokens generated through the Partner API, validate the full `Authorization` header against your **partner notification secret** instead of the Point of Sale secret. The same partner secret applies to payment notifications and pre-authorization callbacks, across all your merchants and both test and live environments. Copy it from **Notification secret** in the [Partner Portal](https://partner.epay.eu/callback-key). Transactions initiated with a merchant API key directly or from the backoffice still use the merchant-specific Point of Sale secret. See [partner callback authentication](/partners/partner-api#notification-authentication). ## Dynamic amount and pre-auth [#dynamic-amount-and-pre-auth] If you enable `dynamicAmount`, the shopper can influence the amount client-side. That makes pre-authorization webhooks especially important, because your backend can validate whether the chosen amount is allowed before authorization begins. Most merchants are advised against using dynamic amounts because it increases integration complexity. If you do enable it, validate the selected amount server-side in your pre-authorization webhook. ## Liability notes [#liability-notes] Pre-authorization responses can change who carries risk in some flows. ## When to avoid pre-auth [#when-to-avoid-pre-auth] Do not add pre-authorization webhooks just because they exist. Avoid them if you only need a normal payment flow with server-side notification handling after the payment attempt is complete. In that case, keep the session simple and rely on your normal notification URL or webhook instead. # Servers (/build-and-go-live/servers) Our API domain is `https://payments.epay.eu` for both `TEST` and `LIVE` transactions. ## Outbound Servers [#outbound-servers] ePay may send callbacks or API requests from these outbound servers. for both `TEST` and `LIVE` transactions. ``` outbound.epay.eu ``` For services where ePay initiates requests to your servers, ensure that these outbound servers are permitted through any relevant firewalls or access controls. The IP addresses of our outbound servers may change at any time without prior notice. To maintain proper access, always use DNS hostnames for whitelisting and ensure your firewall rules are updated automatically based on DNS lookups. We only make IP information available via DNS and do not distribute it through other channels. ## DNS lookup [#dns-lookup] Here are some quick commands you can use to retrieve the current IP addresses for all outbound servers via DNS: # Test cards (/build-and-go-live/test-cards) Use these test cards when validating payment flows in the ePay test environment. Click a card number to copy it. Use expiry `01/35` and CVC `123` for all cards below. ## Available test cards [#available-test-cards] ## How to use them [#how-to-use-them] Use the cards above to verify: ## Next steps [#next-steps] * [Handle payment results](./handle-payment-results) * [Go live](./go-live) # Test your integration (/build-and-go-live/test-your-integration) Before you accept live payments, test the full payment flow in the ePay test environment. Testing should confirm more than whether the payment window opens. It should confirm that your system creates the payment correctly, handles the payment result, updates the order status and behaves safely if something fails or is retried. ## What you will test [#what-you-will-test] By the end of this guide, you should have tested: ## Before you start [#before-you-start] You need: If you use a plugin, you also need access to your webshop administration panel. If you build a custom integration, you need access to your backend logs. ## Minimum test checklist [#minimum-test-checklist] Before going live, test that: ## Test a successful payment [#test-a-successful-payment] Start with the happy path. Create an order in your system Create a payment with ePay Complete the payment with a test card Return to your website Confirm that your backend receives the payment result Confirm that the order is marked as paid Expected result: ## Test a failed payment [#test-a-failed-payment] A failed payment should not mark the order as paid. Create a new test payment Use a test scenario that declines or fails the payment Return to your website Check the order status in your system Expected result: ## Test an abandoned payment [#test-an-abandoned-payment] A customer may close the browser before completing the payment. Your integration should handle this safely. Create a payment Open the payment window Close the browser or tab before completing the payment Check the order status Expected result: ## Test redirects [#test-redirects] Redirects are part of the customer experience, but they should not be your only source of truth. Test both success and failure redirects. Check that: ## Test payment result handling [#test-payment-result-handling] Your backend should update the order from a server-side payment result. This can be a notification URL, webhook or another server-side confirmation flow. Check that: ## Test duplicate notifications [#test-duplicate-notifications] Payment result notifications may be retried. Your system should be safe to run the same update more than once. For example, if the same accepted payment result is received twice, your system should not: Recommended approach: Store the ePay payment ID or transaction ID Check whether the payment has already been processed Ignore or safely acknowledge duplicate events Return a successful response ## Test environment values [#test-environment-values] Make sure your integration uses test values while testing. | Value | Test environment | | ---------------- | ---------------------- | | API key | Test API key | | Point of Sale ID | Test Point of Sale ID | | Payments | Simulated payments | | Money movement | No real money is moved | Do not switch to live values until your account is ready for live payments. ## Test plugin integrations [#test-plugin-integrations] If you use a webshop plugin, test that: ## Test Checkout integrations [#test-checkout-integrations] If you use Checkout, test that: ## Test Blocks integrations [#test-blocks-integrations] If you use Blocks, test that: ## Common problems [#common-problems] ### The payment works, but the order is not updated [#the-payment-works-but-the-order-is-not-updated] Check that: ### The customer reaches the success page, but the payment is not confirmed [#the-customer-reaches-the-success-page-but-the-payment-is-not-confirmed] Do not use the success page alone to mark the order as paid. Use a server-side payment result from ePay before updating the order. ### The payment method does not appear [#the-payment-method-does-not-appear] Check that: ## What you tested [#what-you-tested] You have now tested the most important parts of your ePay integration: ## Next steps [#next-steps] * [Test cards](./test-cards) * [Handle payment results](./handle-payment-results) * [Go live](./go-live) * [Troubleshooting](./troubleshooting) # Troubleshooting (/build-and-go-live/troubleshooting) # Troubleshooting [#troubleshooting] Use this guide when your ePay integration does not behave as expected. Start with the symptom that matches your issue. Most problems are caused by environment mismatch, incorrect credentials, unreachable notification URLs or order status handling. ## Before you troubleshoot [#before-you-troubleshoot] Check these first: ## API request fails [#api-request-fails] ### 400 Bad Request [#400-bad-request] The request is invalid. Check that: ### 401 Unauthorized [#401-unauthorized] The API key is missing or invalid. Check that: Example: ```bash Authorization: Bearer ``` ### 422 Validation Error [#422-validation-error] The request is understood, but one or more values are not valid. Check that: ### 429 Rate Limited [#429-rate-limited] Too many requests were sent in a short period. Check that: ## Payment window does not open [#payment-window-does-not-open] Check that: If the payment session is created from your backend, inspect the response and backend logs. ## Payment fields do not appear in Blocks [#payment-fields-do-not-appear-in-blocks] Check that: Example: ```ts epay.mountFields({ cardNumber: "#card-number", expiryDate: "#expiry-date", cvc: "#cvc", }); ``` Make sure the elements exist in the page before mounting. ## Payment method does not appear [#payment-method-does-not-appear] Check that: For wallet payment methods, additional setup may be required. ## Customer reaches success page, but order is not paid [#customer-reaches-success-page-but-order-is-not-paid] The success page should not be the only source of truth. Check that: Use server-side payment results to mark orders as paid. ## Order is marked as paid twice [#order-is-marked-as-paid-twice] Your payment result handler may not be idempotent. Check that: Recommended pattern: ```ts if (await hasProcessedPayment(payment.id)) { return { ok: true }; } await processPayment(payment); await markPaymentAsProcessed(payment.id); ``` ## Notification URL is not called [#notification-url-is-not-called] Check that: A localhost URL cannot be reached by ePay. This will not work: ```txt http://localhost:3000/api/epay/notification ``` Use a public HTTPS URL while testing notifications. ## Notification URL is called, but fails [#notification-url-is-called-but-fails] Check that: Do not return an error response after processing a notification successfully. ## Plugin payment does not work [#plugin-payment-does-not-work] Check that: If the plugin has a log section, check the plugin logs first. ## Test works, but live does not [#test-works-but-live-does-not] Check that: ## Live works, but test does not [#live-works-but-test-does-not] Check that: ## Customer cannot complete 3D Secure [#customer-cannot-complete-3d-secure] Check that: ### MitID 3DS on mobile devices [#mitid-3ds-on-mobile-devices] Some Danish cardholders are asked to approve a 3D Secure challenge with the MitID app. On mobile devices, approving the challenge in MitID does not always complete the payment immediately. The cardholder must return to the browser that started the payment flow before the browser can resume the 3D Secure process. If the cardholder stays in the MitID app after approving, the payment can remain incomplete and the cardholder may not receive a final payment result. #### MitID Login and MitID 3DS are different [#mitid-login-and-mitid-3ds-are-different] MitID Login and MitID 3DS are separate solutions, even when their screens look similar. Guidance for MitID Login does not necessarily apply to a 3D Secure challenge. For example, MitID 3DS does not use a QR code and does not provide MitID Login controls such as an "Open on device" button. The available steps and controls are determined by the specific authentication flow. #### Who controls the 3D Secure page [#who-controls-the-3d-secure-page] The MitID 3D Secure challenge page is supplied by the cardholder's bank or card issuer. ePay cannot change its user interface, navigation, or completion behavior. ## Amount or currency is wrong [#amount-or-currency-is-wrong] Check that: ## Useful logs to collect [#useful-logs-to-collect] When troubleshooting, collect: Do not log sensitive payment data. ## When to contact support [#when-to-contact-support] Contact support if you have checked the relevant sections and still cannot resolve the issue. Email [support@epay.dk](mailto\:support@epay.dk). Include: Do not send API keys or sensitive payment data. ## Next steps [#next-steps] * [Test your integration](./test-your-integration) * [Handle payment results](./handle-payment-results) * [Go live](./go-live) # Overview (/create-your-first-payment) Not sure which to choose? Check out [Choose your integration](/get-started/choose-your-integration) for more information. ## Next step [#next-step] After creating your first payment, continue to: [Handle payment results](../handle-payment-results) # With API (/create-your-first-payment/with-api) Use this guide if you want to build directly against the ePay API. The API gives you full backend control over your payment flow. You can create payments, handle results, update your own order system and build the checkout experience that fits your product. ## What you will do [#what-you-will-do] In this guide, you will:

Authenticate with your API key

Create an order in your own system

Create a payment session with ePay

Inspect the response

Test the payment

Handle the result server-side

## Before you start [#before-you-start] You need: You can use cURL, Postman, Insomnia or your own backend code. Your API key must only be used server-side. Never expose your API key in frontend code. ## Step 1: Understand authentication [#step-1-understand-authentication] Authenticate API requests with your API key. ```bash Authorization: Bearer ``` Use your test API key while building and testing. Switch to your live API key only when your account is ready for live payments. ## Step 2: Use idempotency [#step-2-use-idempotency] When creating payments, include an idempotency key. This helps prevent duplicate payments if your system retries a request. ```bash Idempotency-Key: ``` Use a unique value for each payment creation attempt. ## Step 3: Create an order in your own system [#step-3-create-an-order-in-your-own-system] Before creating the payment in ePay, create an order in your own system. Set the order status to pending. ```ts const order = await createOrder({ reference: "ORDER-1001", amount: 10000, currency: "DKK", status: "pending", }); ``` The order should not be marked as paid until your backend receives and verifies the payment result. ## Step 4: Create the payment [#step-4-create-the-payment] Send a request to create a payment session. Use your actual ePay endpoint and the required fields for your payment type. ## Step 5: Understand the request [#step-5-understand-the-request] | Field | Description | | ----------------- | ------------------------------------------------------------------------ | | `pointOfSaleId` | Identifies which webshop, sales channel or system the payment belongs to | | `amount` | The amount in minor units | | `currency` | The payment currency | | `reference` | Your own order reference | | `notificationUrl` | Where ePay sends the payment result | | `successUrl` | Where the customer is redirected after success | | `failureUrl` | Where the customer is redirected after failure | The `reference` should connect the ePay payment to your internal order. ## Step 6: Inspect the response [#step-6-inspect-the-response] The response contains the payment information your system needs. Depending on your integration, this may include: For embedded flows such as Blocks, the most important response fields are `session.id`, `key`, and `javascript`. Example: ```json { "paymentWindowUrl": "https://payments.epay.eu/payment-window?sessionId=0192473a-e382-79a9-bfc2-65da88fe812f&sessionKey=4651656e-f29e-4dfa-a1cd-a65647862011", "session": { "id": "0192473a-e382-79a9-bfc2-65da88fe812f", "state": "PENDING" }, "key": "4651656e-f29e-4dfa-a1cd-a65647862011", "javascript": "https://payments.epay.eu/sessions/0192473a-e382-79a9-bfc2-65da88fe812f/client.js" } ``` Store the identifiers you need to match future payment results to your order. Do not store sensitive payment data. ## Step 7: Send the customer to payment [#step-7-send-the-customer-to-payment] What you do next depends on your frontend integration. If you use Checkout, redirect the customer to the payment window URL. ```ts return redirect(payment.paymentWindowUrl); ``` If you use Blocks, return the session values needed by ePay.js. ```ts return { sessionId: payment.session.id, sessionKey: payment.key, javascript: payment.javascript, }; ``` The `sessionKey` and `javascript` URL are scoped to this exact payment session. Only return values that are safe to expose to the frontend. Never return your API key. ## Step 8: Complete a test payment [#step-8-complete-a-test-payment] Complete the payment using a test card. Test at least: ## Step 9: Handle the payment result [#step-9-handle-the-payment-result] Use your notification URL or webhook to update your order. Recommended backend flow:

Receive payment result from ePay

Verify that the result is valid

Find the order using the payment ID or reference

Check whether the order has already been processed

Update the order status

Return a successful response

Your notification handler should be safe to run more than once. ## Example notification handler [#example-notification-handler] ```ts export async function handlePaymentNotification(payload) { const payment = await verifyPaymentResult(payload); const order = await findOrderByReference(payment.reference); if (!order) { throw new Error("Order not found"); } if (order.status === "paid") { return { ok: true }; } if (payment.status === "accepted") { await markOrderAsPaid(order.id); } if (payment.status === "declined") { await markOrderAsFailed(order.id); } return { ok: true }; } ``` This is only an example. Your implementation should match your own order system and the actual ePay payment result format. ## Common API errors [#common-api-errors] ### 400 Bad Request [#400-bad-request] The request is invalid. Check that: ### 401 Unauthorized [#401-unauthorized] The API key is missing or invalid. Check that: ### 422 Validation Error [#422-validation-error] The request is understood, but one or more values are not valid. Check that: ### 429 Rate Limited [#429-rate-limited] Too many requests were sent in a short period. Add retry handling with backoff. Do not retry payment creation without idempotency. ## What you built [#what-you-built] You have now created your first payment with the ePay API. You authenticated with your API key, created a payment session, inspected the response and prepared your backend to handle the payment result. ## Next steps [#next-steps] * [Test your integration](../test-your-integration) * [Handle payment results](../handle-payment-results) * [Go live](../go-live) # With Blocks (/create-your-first-payment/with-blocks) Use this guide if you want to embed secure payment fields directly in your own checkout. Blocks gives you more control over the checkout experience than Checkout. Your backend creates a payment session, and your frontend uses ePay.js to mount payment fields inside your own UI. ## What you will build [#what-you-will-build] In this guide, you will:

Create a payment session from your backend

Add ePay.js to your frontend

Mount secure payment fields

Submit the payment

Handle the payment result

Confirm that your backend can update the order

## How Blocks works [#how-blocks-works] Blocks separates the payment flow between your backend and frontend. Your backend creates the payment session securely. Your frontend uses the session information to render payment fields with ePay.js. The basic flow is:

Customer starts checkout on your website

Your backend creates a payment session

Your frontend initializes ePay.js

ePay.js mounts secure payment fields

Customer enters payment details

Your frontend submits the payment

ePay sends the result to your backend

Your system updates the order

### Example: Mount secure fields in your own checkout [#example-mount-secure-fields-in-your-own-checkout] This example creates a payment session, mounts the secure payment fields, and shows a payment button in your own UI. ## Before you start [#before-you-start] You need: Your API key must only be used server-side. Never expose your API key in frontend code. For Blocks, your backend authenticates the session creation request with your API key. The frontend only receives session-specific values from the session response. ## Step 1: Create an order in your system [#step-1-create-an-order-in-your-system] Create an order in your own system before creating the payment session. Set the order status to pending. ```ts const order = await createOrder({ amount: 10000, currency: "DKK", status: "pending", }); ``` ## Step 2: Create a payment session [#step-2-create-a-payment-session] Create the payment session from your backend. ## Step 3: Understand the request body [#step-3-understand-the-request-body] ## Step 4: Inspect the response and return frontend-safe values [#step-4-inspect-the-response-and-return-frontend-safe-values] For Blocks, the important response fields are: Example: ```json { "session": { "id": "0192473a-e382-79a9-bfc2-65da88fe812f", "amount": 10000, "currency": "DKK", "state": "PENDING" }, "key": "4651656e-f29e-4dfa-a1cd-a65647862011", "javascript": "https://payments.epay.eu/sessions/0192473a-e382-79a9-bfc2-65da88fe812f/client.js" } ``` Return only the frontend-safe values to your client: ```ts return { sessionId: payment.session.id, sessionKey: payment.key, javascript: payment.javascript, }; ``` The `sessionKey` and `javascript` URL are tied to this exact payment session. If the session expires or the values are lost, create a new payment session and use the new values. ## Step 5: Add ePay.js to your checkout page [#step-5-add-epayjs-to-your-checkout-page] Load ePay.js on the page where you want to show the payment form. ```html ``` Use the actual `javascript` URL returned by the payment session response. ## Step 6: Add Blocks [#step-6-add-blocks] Create the container where Blocks should be mounted. ```html
``` Then initialize the client and mount the hosted fields. ```html // Added ApplePay button ``` ## Advanced Setup [#advanced-setup] If you have any special needs, like tracking, regarding ApplePay transactions and the in-app experience you can modify the payment flow by manually creating the ApplePay payment session. ```js // Only call this function when the customer clicks on the pay button function beginApplePayPayment() { // Use the standard payment request by ePay that automatically adapts // to your payment session data and merchant capabilities. let sessionData = epay.createApplePayRequest(); // Or create the ApplePay data yourself. // Ensure amount and currency is the exact same as sent to ePay. let sessionData = { countryCode: "DK", currencyCode: "DKK", total: { label: "Køb hos XYZ", amount: 19900, }, supportedNetworks: ["visa", "masterCard", "maestro"], merchantCapabilities: ["supports3DS"], }; const session = new ApplePaySession(14, sessionData); // The customer has opened the app and requires merchant validation with Apple session.onvalidatemerchant = (event) => { epay.validateApplePayMerchant(session, event); // Required step by ePay }; // Customer authorized the payment in-app session.onpaymentauthorized = (event) => { epay.authorizeApplePayTransaction(session, event); // Required step by ePay }; // Customer dismissed the payment UI session.oncancel = (event) => { console.log("customer dismissed payment UI in-app"); }; session.begin(); } ``` *** ```html ``` ## Register merchant [#register-merchant] ### Prerequisites [#prerequisites] Before you begin, ensure you have the following: An Apple Developer account. Access to the Apple Developer Portal. The ability to upload certificates to ePay. ### 1. Create a Merchant ID [#1-create-a-merchant-id] Sign in to the Apple Developer Portal. Navigate to Certificates, Identifiers & Profiles. Under Identifiers, select Merchant IDs. <> Click the + button to create a new Merchant ID. <> Enter a Description and a Merchant Identifier, for example{" "} merchant.dk.yourcompany. <> Click Continue and then Register to save your Merchant ID. Register Merchant ### 2. Edit your Merchant ID [#2-edit-your-merchant-id] In the Apple Developer Portal, go to Certificates, Identifiers & Profiles. Under Identifiers, select Merchant IDs. Select your Merchant ID in the Apple Developer Portal. Edit Merchant ### 3. Create certificates [#3-create-certificates] While in the "Edit or Configure Merchant ID" panel you must now create your ApplePay certificates. #### Create Processing key and CSR [#create-processing-key-and-csr] ```bash openssl ecparam -out processing.key -name prime256v1 -genkey openssl req -new -sha256 -key processing.key -nodes -out processing.csr ``` When creating the CSR, you will be asked for various company details. The specific values are not important—just enter reasonable values. ``` Country Name (2 letter code) [AU]: DK State or Province Name (full name) [Some-State]: Jylland Locality Name (eg, city) []: Svenstrup Organization Name (eg, company) [Internet Widgits Pty Ltd]: ePay Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []: epay.eu Email Address []: A challenge password []: An optional company name []: ``` #### Upload the processing CSR to Apple [#upload-the-processing-csr-to-apple] After creating `processing.key` and `processing.csr` you must now upload `processing.csr` to Apple to receive your processing certificate. Upload processing certificate step 1 Upload processing certificate step 2 Upload processing certificate step 3 You should now have an active processing certificate registered for your ApplePay merchant. #### Create Merchant key and CSR [#create-merchant-key-and-csr] ```bash openssl req -sha256 -nodes -newkey rsa:2048 -keyout merchant.key -out merchant.csr ``` When creating the CSR, you will be asked for various company details. The specific values are not important—just enter reasonable values. ``` Country Name (2 letter code) [AU]: DK State or Province Name (full name) [Some-State]: Jylland Locality Name (eg, city) []: Svenstrup Organization Name (eg, company) [Internet Widgits Pty Ltd]: ePay Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []: epay.eu Email Address []: A challenge password []: An optional company name []: ``` #### Upload the merchant CSR to Apple [#upload-the-merchant-csr-to-apple] After creating `merchant.key` and `merchant.csr` you must now upload `merchant.csr` to Apple to receive your merchant certificate. Upload merchant certificate step 1 Upload merchant certificate step 2 Make sure to download the merchant certificate - This file is often named `merchant_id.cer` Upload merchant certificate step 3 Upload merchant certificate step 4 You should now have an active merchant certificate registered for your ApplePay merchant. #### Convert merchant certificate format [#convert-merchant-certificate-format] You must change the format of the `merchant_id.cer` merchant certificate to a `.pem` format. ```bash openssl x509 -inform der -in merchant_id.cer -out merchant.pem ``` ### 4. Upload the Certificate to ePay [#4-upload-the-certificate-to-epay] Sign in to your ePay Merchant Portal. Navigate to your point of sale Apple Pay Settings. <> Enter your Merchant Identifier: merchant.dk.yourcompany <> Upload the Merchant Identity Certificate: merchant.pem <> Upload the Merchant Private Key: merchant.key <> Upload the Payment Processing Private Key: processing.key Save your settings. Register Merchant ### 5. Verify your domain [#5-verify-your-domain] Verify Domain Verify Domain Step 2 Download the identity file and upload it to your server at the Apple requested location: Once completed click `Verify`. Verify Domain Step 3 Verify Domain Step 4 You should now have a verified domain. You can add more domains to the same merchant if you need to receive payments from multiple domains or subdomains. ### 6. Verify Integration [#6-verify-integration] Ensure your Apple Pay setup is active by testing payments in Test Mode. Use Apple's Apple Pay JS API to verify integration. # Google Pay (/payment-methods/google-pay) This documentation is intended for merchants integrating **ePay Blocks** directly. If you are using **ePay Checkout**, no additional setup is required for Google Pay. We recommend that all merchants use the [Quick Setup](/get-started/set-up-epay/payment-methods/google-pay/#quick-setup) guide, as it reduces the maintenance burden on your integration. By using [Quick Setup](/get-started/set-up-epay/payment-methods/google-pay/#quick-setup), ePay can automatically apply updates to your integration for any future changes to Google’s protocols. ## Quick Setup [#quick-setup] To integrate Google Pay, make the following changes to your implementation, where *googlePayContainer* is the id referring to the div element of the Google Pay button ```javascript ... let clientReady = epay .setSessionId("<-- sessionId here -->") .setSessionKey("<-- sessionKey here -->") .init(); epay.mountGooglePayButton('googlePayContainer', { buttonColor: 'black', buttonType: 'buy', buttonRadius: 48, buttonSizeMode: 'fill', }, ); ``` ### Google Pay button style [#google-pay-button-style] Refer to [Customize your button](https://developers.google.com/pay/api/web/guides/resources/customize) to preview different styles of the Google Pay button. ## Advanced Setup [#advanced-setup] If you have any special needs, like tracking, regarding Google Pay transactions and the in-app experience you can modify the payment flow by manually creating the Google Pay payment session. ### Card schemes [#card-schemes] We support the following card schemes: Visa, Mastercard, Amex, JCB and Discover for merchants in the Nordic region (Denmark including Greenland and Faroe Islands, Norway, Sweden, Finland and Iceland) ### Technical details [#technical-details] Other rather technical details regarding Google Pay integration follow below. #### TokenizationSpecification [#tokenizationspecification] Regarding [TokenizationSpecification](https://developers.google.com/pay/api/web/reference/request-objects#PaymentMethodTokenizationSpecification) object, which is part of request to [loadPaymentData](https://developers.google.com/pay/api/web/reference/client#loadPaymentData), set the parameters accordingly: * parameter: *gateway: epay* * parameter: *gatewayMerchantID* set this to Point Of Sale Id - available from ePay Backoffice ```javascript "tokenizationSpecification": { "type": "PAYMENT_GATEWAY", "parameters": { "gateway": "epay", "gatewayMerchantId": "263c20dc-20de-11f0-914a-00155d32ef77" } } ``` #### SCA / 3DS [#sca--3ds] Regarding strong customer authentication (3DS) for PAN\_ONLY authentication method - this is - like other transactions - controlled by the scaMode parameter when initializing the payment session. Default is NORMAL and will ensure 3DS flow is attempted - [further details](/api/initialize-payment-session) #### Billing Address [#billing-address] Google Pay allow filtering on cards returned by the wallet to e.g. only allow cards with full billing address specified. We don't require BillingAddressParameter to be specified. Refer to the [Google Pay reference](https://developers.google.com/pay/api/web/reference/request-objects#BillingAddressParameters) for details regarding object BillingAddressParameters ## External references [#external-references] * [Google Pay Web developer documentation](https://developers.google.com/pay/api/web/) * [Google Pay Web integration checklist](https://developers.google.com/pay/api/web/guides/test-and-deploy/integration-checklist) * [Google Pay Web Brand Guidelines](https://developers.google.com/pay/api/web/guides/brand-guidelines) * [Google Pay Customize your button](https://developers.google.com/pay/api/web/guides/resources/customize) * [Google Pay Using Android WebView](https://developers.google.com/pay/api/android/guides/recipes/using-android-webview) ## Register merchant [#register-merchant] To accept Google Pay payments, you need to register as a merchant with Google Pay and configure your merchant accordingly.\ This guide walks you through the necessary steps.\ Please note that Google Pay [Terms of Service](https://payments.developers.google.com/terms/sellertos) terms apply whenever the Google Pay service is offered. ### Create a Merchant ID [#create-a-merchant-id] Follow the steps on Google Pay [Publish your integration](https://developers.google.com/pay/api/web/guides/test-and-deploy/publish-your-integration) Notes regarding the described steps: * Via [Google Pay & Wallet Console](https://pay.google.com/business/console) you register your business and manage the apps, domain(s) and sub-domain(s) you want to use with Google Pay. * ePay hosts *a supported payment gateway* * Once the *MerchantId* is obtained, submit it to the [ePay backoffice](https://app.epay.eu)\ Navigate to Payment / Point of sale / Google Pay tab and enter your Google Pay Merchant id and update settings (save).\ This replaces the step about explicit set the merchantId property on the merchantInfo object, as this is done by the ePay Javascript class. * Likewise for the initialization of [PaymentsClient](https://developers.google.com/pay/api/web/reference/client#PaymentsClient) - this is wrapped by the ePay Javascript class # PayPal (/payment-methods/paypal) This documentation is intended for merchants integrating **ePay Blocks** directly. If you are using **ePay Checkout**, no additional PayPal integration changes are required here. We recommend that all merchants use the [Quick Setup](#quick-setup) guide, as it reduces the maintenance burden on your integration. By using [Quick Setup](#quick-setup), ePay can automatically apply updates to your integration for future changes to the PayPal flow. ## Quick Setup [#quick-setup] To integrate PayPal, add a mount target for the button and let ePay mount the PayPal button for the current payment session. ```javascript ...
... epay.mountPayPalButton("paypal-button", { style: { color: "gold", shape: "pill", label: "pay", }, }); ``` ## Bring your own button [#bring-your-own-button] If you do not want to use the native PayPal button, you instead can style the PayPal button yourself and `onclick` call the `epay.createPayPalTransaction()` to create the order and start the PayPal payment flow. ```javascript ... ``` ## Button customization [#button-customization] The second argument to `mountPayPalButton(target, buttonOptions, paymentOptions)` is passed directly to `paypal.Buttons(...)`. Use this for standard button customization such as `style` and other appearance-related PayPal button settings. The PayPal SDK might take a second to load the first time, so you can set a spinner while the button is loading, it being enabled by default. Avoid overriding flow callbacks such as `createOrder`, `onApprove`, `onCancel`, and `onError` unless you intentionally want to replace parts of the default ePay flow. ```javascript epay.mountPayPalButton("paypal-button", { style: { color: "gold", shape: "pill", label: "pay", }, }); ``` ## Advanced usage [#advanced-usage] The primary integration method is: ```javascript epay.mountPayPalButton(target, buttonOptions, paymentOptions); ``` ### Target [#target] `target` can be either: * A string containing the id of the target element * An `HTMLElement` ### Payment options [#payment-options] `paymentOptions` supports the same per-call overrides used by the ePay client. For PayPal, the main options are: * `amount`: used when the session supports dynamic amount ## Flow details [#flow-details] When the PayPal button is mounted, ePay handles the standard flow for you: 1. Load the PayPal SDK based on the session `payPalConfig` 2. Create a PayPal order through ePay when the payer clicks the PayPal button 3. Stores the approved PayPal `orderId` on the session 4. Start the ePay transaction using the `PAYPAL` payment method ## Notes and limitations [#notes-and-limitations] * PayPal requires the payment session to contain a valid `payPalConfig`. If PayPal is not configured for the session, the button cannot be mounted. * PayPal is not available for fixed zero-amount sessions. * PayPal is not currently available for subscription sessions. * `paymentCancel` is dispatched when the customer cancels the PayPal flow. * PayPal flow failures are dispatched through the generic `error` callback with PayPal-specific error messaging. ## External references [#external-references] * [PayPal JavaScript SDK overview](https://developer.paypal.com/sdk/js) # With plugin (/create-your-first-payment/with-plugin) Use this guide if your webshop runs on a supported platform such as WooCommerce, Magento, Shopify, Shopware, PrestaShop or OpenCart. A plugin is the fastest way to add ePay to an existing webshop. The plugin connects your webshop to ePay, creates payments and updates order statuses when payments are completed. ## What you will do [#what-you-will-do] In this guide, you will:

Install or open the ePay plugin

Add your API key

Add your Point of Sale ID

Configure your payment methods

Place a test order

Confirm that the order status updates correctly

## Before you start [#before-you-start] You need: You can find your API key and Point of Sale ID in ePay Backoffice. ## Step 1: Choose your platform [#step-1-choose-your-platform] Start by choosing the plugin guide for your webshop platform. Each platform has its own installation steps, but the basic setup is the same. ## Step 2: Install the plugin [#step-2-install-the-plugin] Install the ePay plugin in your webshop. Depending on your platform, you may install it from a marketplace, upload it manually or enable it from your webshop administration panel. After installation, open the ePay plugin settings. ## Step 3: Connect the plugin to ePay [#step-3-connect-the-plugin-to-epay] Add your ePay test credentials to the plugin. You usually need: | Value | Description | | ---------------- | ---------------------------------------- | | API key | Used to connect your webshop to ePay | | Point of Sale ID | Identifies the webshop or sales location | Make sure you use test credentials while setting up and testing the plugin. ## Step 4: Configure payment methods [#step-4-configure-payment-methods] Choose the payment methods you want to show in checkout. The available payment methods depend on your ePay account, your payment agreements and your plugin setup. Start with one payment method while testing. You can add more later. ## Step 5: Place a test order [#step-5-place-a-test-order] Go to your webshop and place an order like a normal customer. At checkout, choose ePay as the payment method. Complete the payment with a test card. ## Step 6: Check the order status [#step-6-check-the-order-status] After the payment, return to your webshop administration panel. Check that the order status has changed correctly. A successful test payment should usually move the order from pending payment to a paid or processing state, depending on your webshop platform and plugin settings. ## Step 7: Test a failed payment [#step-7-test-a-failed-payment] You should also test what happens when a payment fails. A failed payment should not mark the order as paid. Check that your webshop handles the failed payment in a way that makes sense for your customers. ## Common problems [#common-problems] ### The payment method does not appear in checkout [#the-payment-method-does-not-appear-in-checkout] Check that: ### The payment opens, but the order is not updated [#the-payment-opens-but-the-order-is-not-updated] Check that: ### The plugin says the API key is invalid [#the-plugin-says-the-api-key-is-invalid] Check that: ## What you built [#what-you-built] You have now connected your webshop to ePay and created your first test payment. Before going live, you should also test: ## Next steps [#next-steps] # Magento2 (/create-your-first-payment/with-plugin/magento2) Magento 2 header Use this guide if your webshop runs on Magento 2. The Magento 2 module is the fastest way to add ePay to an existing Magento shop without building a custom integration. You install the module with Composer, enable it in Magento, add your test credentials and place a test order. ## What you will do [#what-you-will-do] In this guide, you will:

Install the ePay module with Composer

Open the Magento payment settings

Add your test API key and test Point of Sale ID

Place a test order in your storefront

Confirm that Magento updates the order correctly

## Before you start [#before-you-start] You need: If you still need your credentials, see [Set up your account](../../get-started/set-up-your-account). Before installing a new module on a live shop, take a backup first. ## Step 1: Install the module with Composer [#step-1-install-the-module-with-composer] SSH into your Magento server and go to your Magento root directory. Install the module: ```bash composer require epay/magento2-epic-payment-module ``` Then run: ```bash php bin/magento setup:upgrade php bin/magento cache:flush ``` If your shop runs in production mode, also run: ```bash php bin/magento setup:di:compile php bin/magento setup:static-content:deploy -f ``` This makes sure Magento picks up the new module and refreshes generated code and static assets. ## Step 2: Open the Magento payment settings [#step-2-open-the-magento-payment-settings] After installation, log in to Magento Admin and open: **Stores** -> **Configuration** -> **Sales** -> **Payment Methods** Find ePay in the list and expand the configuration section. This is where you connect Magento to ePay. ## Step 3: Add your ePay test credentials [#step-3-add-your-epay-test-credentials] In the module settings, add: | Value | What to use while testing | | ---------------- | -------------------------- | | API key | Your test API key | | Point of Sale ID | Your test Point of Sale ID | Use test values while setting up the module. Do not mix test and live values. A live API key with a test Point of Sale ID, or the other way around, will usually cause checkout problems. ## Step 4: Review the key module settings [#step-4-review-the-key-module-settings] Start with the minimum setup that gets checkout working. The most important settings are: | Setting | What it does | | ---------------- | ---------------------------------------------------------------- | | Enabled | Turns the ePay payment method on in checkout | | API key | Connects Magento to your ePay account | | Point of Sale ID | Tells ePay which webshop or sales channel the payment belongs to | | Instant capture | Captures the payment immediately after authorization | If you are testing order flow first, keep the setup simple and only change the settings you need. ## Step 5: Save the config and refresh Magento [#step-5-save-the-config-and-refresh-magento] Save the configuration in Magento Admin. If Magento asks you to refresh caches, do that before testing checkout. A safe follow-up command is: ```bash php bin/magento cache:flush ``` ## Step 6: Place a test order [#step-6-place-a-test-order] Go through your storefront like a normal customer:

Add a product to the cart

Go to checkout

Choose ePay as the payment method

Place the order

Complete the payment in the ePay payment window

After the payment finishes, the customer should return to your Magento storefront. ## Step 7: Check the Magento order [#step-7-check-the-magento-order] Open the order in Magento Admin and confirm that: Also check ePay Backoffice to confirm that the transaction was received. ## Common problems [#common-problems] ### The payment method does not appear in checkout [#the-payment-method-does-not-appear-in-checkout] Check that: ### The module was installed, but Magento does not pick it up [#the-module-was-installed-but-magento-does-not-pick-it-up] Check that: ### The payment window does not open [#the-payment-window-does-not-open] Check that: ### The payment succeeds, but the order is not updated [#the-payment-succeeds-but-the-order-is-not-updated] Check that: ## What you built [#what-you-built] You have now connected Magento 2 to ePay and completed your first test payment through the module. Before going live, also test: ## Next steps [#next-steps] * [Test your integration](../../build-and-go-live/test-your-integration) * [Handle payment results](../../build-and-go-live/handle-payment-results) * [Go live](../../build-and-go-live/go-live) # Opencart (/create-your-first-payment/with-plugin/opencart) Use this guide if your webshop runs on OpenCart 4. The OpenCart module is the fastest way to connect your shop to ePay without building a custom integration. You install the module, activate the payment extension, add your ePay credentials and place a test order. ## What you will do [#what-you-will-do] In this guide, you will: Download and install the ePay module Activate the module in OpenCart Add your test API key, test Point of Sale ID and webhook authorization Enable the payment method Place a test order and confirm that the payment works ## Before you start [#before-you-start] You need: If you still need your credentials, see [Set up your account](../../get-started/set-up-your-account). Before installing a new module on a live shop, take a backup first. ## Step 1: Download the module [#step-1-download-the-module] Download the latest OpenCart module release from GitHub: [OpenCart4 releases](https://github.com/ePay/OpenCart4/releases) Use the latest released version unless you have a specific reason to stay on an older one. ## Step 2: Install the module in OpenCart [#step-2-install-the-module-in-opencart] In OpenCart administration: Go to Extensions -> Installer Click Upload Select the downloaded .zip file Wait for the upload to complete Uploading ePay OpenCart extension via the OpenCart installer After this step, the module files are installed but the payment extension is not yet active. ## Step 3: Activate the module [#step-3-activate-the-module] OpenCart activation happens in two places. First: Go to Extensions ->{" "} Installed Extensions Find the ePay module Click the green install button Installing the ePay OpenCart module from the Installed Extensions list Then: Go to Extensions Choose Payments in the extension type dropdown Find ePay Payment Solutions Click the green install button Activating ePay Payment Solutions under OpenCart payment extensions After this step, the payment method is available for configuration. ## Step 4: Add your ePay credentials [#step-4-add-your-epay-credentials] Go to **Extensions** -> **Payments**, find the ePay payment method and click the blue edit button. Editing the ePay payment method settings in OpenCart Then add: | Value | What to use while testing | | --------------------- | -------------------------------------------------------------- | | API key | Your test API key | | Point of Sale ID | Your test Point of Sale ID | | Webhook authorization | The webhook authorization value for the selected Point of Sale | **Partner integrations:** The Point of Sale secret described here applies when the plugin initiates payments using a merchant API key. If your integration instead initiates transactions using partner-generated merchant access tokens, use your **partner notification secret** for the incoming callbacks. Retrieve it from **Notification secret** in the [Partner Portal](https://partner.epay.eu/callback-key); it is shared across merchants and test/live environments. See [partner callback authentication](/partners/partner-api#notification-authentication). Save the configuration after entering the values. Entering ePay API key, Point of Sale ID and webhook credentials in OpenCart Use test values while setting up the module. Do not mix test and live values. A live API key with a test Point of Sale ID, or the other way around, will usually cause checkout problems. ## Step 5: Review the key module settings [#step-5-review-the-key-module-settings] Start with the minimum setup that gets checkout working. The most important settings are: | Setting | What it does | | ------------------------- | ---------------------------------------------------------------- | | API key | Connects OpenCart to your ePay account | | Point of Sale ID | Tells ePay which webshop or sales channel the payment belongs to | | Webhook authorization | Lets OpenCart verify incoming payment updates | | Status or enabled setting | Makes the payment method available in checkout | Keep the initial setup simple. You can fine-tune other options after the first successful payment. ## Step 6: Place a test order [#step-6-place-a-test-order] Go through your webshop like a normal customer: Add a product to the cart Go to checkout Choose ePay as the payment method Place the order Complete the payment in the ePay payment window OpenCart checkout before selecting payment OpenCart checkout with ePay selected After the payment finishes, the customer should return to your OpenCart storefront. OpenCart checkout after completed payment ## Step 7: Check the result [#step-7-check-the-result] Confirm that: ## Common problems [#common-problems] ### The payment method does not appear in checkout [#the-payment-method-does-not-appear-in-checkout] Check that: ### The payment starts, but the order is not updated correctly [#the-payment-starts-but-the-order-is-not-updated-correctly] Check that: ### The credentials seem valid, but checkout still fails [#the-credentials-seem-valid-but-checkout-still-fails] Check that: ## What you built [#what-you-built] You have now connected OpenCart 4 to ePay and completed your first test payment through the module. Before going live, also test: ## Next steps [#next-steps] * [Test your integration](../../build-and-go-live/test-your-integration) * [Handle payment results](../../build-and-go-live/handle-payment-results) * [Go live](../../build-and-go-live/go-live) # Prestashop (/create-your-first-payment/with-plugin/prestashop) Use this guide if your webshop runs on PrestaShop. The PrestaShop module is the fastest way to connect your shop to ePay without building a custom integration. You install the module, add your ePay credentials, enable the payment method and place a test order. ## What you will do [#what-you-will-do] In this guide, you will: Download and install the ePay module Enable the module and add your credentials Review the key module settings Place a test order and confirm that the payment works ## Before you start [#before-you-start] You need: If you still need your credentials, see [Set up your account](../../get-started/set-up-your-account). Before installing a new module on a live shop, take a backup first. ## Step 1: Download the module [#step-1-download-the-module] Download the latest PrestaShop module release from GitHub: [prestashop-epic-payment-module releases](https://github.com/ePay/prestashop-epic-payment-module/releases) Use the latest released version unless you have a specific reason to stay on an older one. ## Step 2: Install the module in PrestaShop [#step-2-install-the-module-in-prestashop] In PrestaShop administration: Go to Modules Click Upload a module Select the downloaded .zip file Wait for PrestaShop to finish the upload Find the ePay module in the module list Click Install or Configure, depending on your PrestaShop version Click on 'Add new module' in the menu Modules Choose the zip file you just downloaded Find the ePay module, and press 'Install' After this step, the module is installed but not yet fully configured. ## Step 3: Enable the module and add your credentials [#step-3-enable-the-module-and-add-your-credentials] Open the ePay module settings in PrestaShop and enable the module. Then add: | Value | What to use while testing | | ---------------- | -------------------------- | | API key | Your test API key | | Point of Sale ID | Your test Point of Sale ID | Save the configuration after entering the values. Use test values while setting up the module. Do not mix test and live values. A live API key with a test Point of Sale ID, or the other way around, will usually cause checkout problems. Press 'Configure' to enter the module settings ## Step 4: Review the key module settings [#step-4-review-the-key-module-settings] Start with the minimum setup that gets checkout working. The most important settings are: | Setting | What it does | | --------------------- | ------------------------------------------------------------------- | | API key | Connects PrestaShop to your ePay account | | Point of Sale ID | Tells ePay which webshop or sales channel the payment belongs to | | Remote API | Lets you capture, refund or delete payments from PrestaShop | | Instant capture | Captures the payment immediately after authorization | | Own receipt | Lets you define your own order confirmation page | | Age verification mode | Enables age checks for all orders or only Danish delivery addresses | | Minimum user age | Sets the minimum age when age verification is enabled | Keep the first setup simple. You can fine-tune advanced settings after the first successful payment. ## Step 5: Place a test order [#step-5-place-a-test-order] Go through your webshop like a normal customer: Add a product to the cart Go to checkout Choose ePay as the payment method Place the order Complete the payment in the ePay payment window The overlay version of ePay's payment window After the payment finishes, the customer should return to the PrestaShop order confirmation page. The order confirmation page in PrestaShop ## Step 6: Check the PrestaShop order [#step-6-check-the-prestashop-order] Open the order in PrestaShop administration and confirm that: You should also be able to see the transaction in ePay Backoffice. ## Payment information in PrestaShop [#payment-information-in-prestashop] The order view can show payment details such as: The order view can also include a link to the matching payment in ePay Backoffice. You can find information on the payment in PrestaShop ## Process payments from PrestaShop [#process-payments-from-prestashop] If **Remote API** is enabled, you can manage payments directly from PrestaShop. Depending on your setup, this can include: To use this safely, the PrestaShop server IP may need to be registered in ePay Backoffice under API or web service access settings. Enter the amount you want to capture, and press the 'Capture' button ## Payment requests [#payment-requests] Some versions of the PrestaShop module support payment requests directly from the order view. A payment request is an email with a payment link that opens the ePay payment window. This can be useful if an order amount changes after the original authorization. To use payment requests, you typically need: If you use this feature, test it separately before relying on it in production. Information on the recipient and the requester. Press 'Send payment requst' to send it You can see the transaction number of the payment request under Messages ## Common problems [#common-problems] ### The payment method does not appear in checkout [#the-payment-method-does-not-appear-in-checkout] Check that: ### The payment window opens, but the order is not updated [#the-payment-window-opens-but-the-order-is-not-updated] Check that: ### Remote payment actions do not work [#remote-payment-actions-do-not-work] Check that: ### MD5 validation fails [#md5-validation-fails] If your setup uses MD5 validation, check that: If you are debugging a legacy PrestaShop setup with an older module version, validate the module version first before applying any file-level workaround. ### Order IDs do not match [#order-ids-do-not-match] PrestaShop may use a cart ID during checkout and create the final order ID only after the payment completes. That means the ID shown in ePay can differ from the final PrestaShop order ID, even when the payment is linked correctly. ## What you built [#what-you-built] You have now connected PrestaShop to ePay and completed your first test payment through the module. Before going live, also test: ## Next steps [#next-steps] * [Test your integration](../../build-and-go-live/test-your-integration) * [Handle payment results](../../build-and-go-live/handle-payment-results) * [Go live](../../build-and-go-live/go-live) # Shopify (/create-your-first-payment/with-plugin/shopify) Use this guide if your webshop runs on Shopify. The Shopify app is the fastest way to add ePay to your store. You install the app from the Shopify App Store, connect it to your ePay account, choose your payment methods and place a test order. If you only want MobilePay as a standalone payment method, you can install the dedicated MobilePay app instead of the full ePay app. ## What you will do [#what-you-will-do] In this guide, you will: Install the ePay app in Shopify Add your test and live credentials Choose the payment methods to show in checkout Configure the Shopify order webhook Place a test order and confirm that the payment works ## Before you start [#before-you-start] You need: If you still need your credentials, see [Set up your account](../../get-started/set-up-your-account). ## Step 1: Choose the right Shopify app [#step-1-choose-the-right-shopify-app] Shopify app installation You have two installation paths: * Use the full ePay app if you want card payments and other supported payment methods: [ePay Payment Solutions](https://apps.shopify.com/epay-payment-solutions) * Use the dedicated MobilePay app if you only want MobilePay as a standalone payment method: [MobilePay ePay](https://apps.shopify.com/mobilepay-epay) For most stores, the full ePay app is the best starting point. ## Step 2: Install the app from the Shopify App Store [#step-2-install-the-app-from-the-shopify-app-store] Shopify app setup Open the app page in the Shopify App Store and click **Install**. Then: Select the Shopify store you want to connect Log in to Shopify if needed Approve the installation After installation, Shopify opens the app inside your store admin. ## Step 3: Add your ePay account details [#step-3-add-your-epay-account-details] Shopify credentials In the app settings, add your ePay credentials. The required values are: | Value | What it is used for | | --------------------------- | ------------------- | | Production API key | Live payments | | Production Point of Sale ID | Live payments | | Test API key | Test payments | | Test Point of Sale ID | Test payments | You can find these values in ePay Backoffice. Make sure the app is enabled after entering the required fields. ## Step 4: Review the key app settings [#step-4-review-the-key-app-settings] Shopify app settings Start with the minimum setup that gets checkout working. The most important settings are: | Setting | What it does | | ------------------------------------------ | ---------------------------------------------------- | | Age verification | Sets a minimum age check, if required | | Only activate age verification for Denmark | Limits the age check to Danish delivery addresses | | Display name | Controls the payment method name shown in checkout | | Display priority | Controls the order of the payment method in checkout | | Hide payment method above amount | Hides the method for carts above a limit | | Hide payment method below amount | Hides the method for carts below a limit | | Shopify Webhooks secret | Used together with the Shopify webhook setup | | Enabled | Makes the payment method available in checkout | Keep the initial setup simple. You can tune display rules and naming after the first successful payment. ## Step 5: Choose payment methods [#step-5-choose-payment-methods] Shopify payment methods If you use the full ePay app, choose the payment methods you want to show in checkout. Only enable payment methods you already have agreements for. If you use the dedicated MobilePay app, select MobilePay as the payment method. ## Step 6: Configure the Shopify order webhook [#step-6-configure-the-shopify-order-webhook] Shopify webhook setup Set up a Shopify webhook so the order number can be passed correctly to ePay. In Shopify Admin: Go to Settings -> Notifications Create a new webhook Choose the Order creation event Set the format to JSON Use this URL: ```text https://yfipohs.epay.eu/app/orderwebhook ``` Choose the latest Shopify API version Save the webhook If the app uses a webhook secret, make sure the value matches your Shopify setup. ## Step 7: Place a test order [#step-7-place-a-test-order] Go through your storefront like a normal customer: Add a product to the cart Go to checkout Choose the ePay payment method, or MobilePay if you installed the dedicated app Complete the order Finish the payment in the payment window or payment app flow After the payment finishes, the customer should return to your Shopify storefront. ## Step 8: Check the result [#step-8-check-the-result] Confirm that: If you are using test values, make sure the test flow works before switching to live values. ## Edit the app later [#edit-the-app-later] To change your ePay settings later, open the installed app in Shopify and use **More actions** -> **Manage**, if available in your app view. ## Common problems [#common-problems] ### The payment method does not appear in checkout [#the-payment-method-does-not-appear-in-checkout] Check that: ### The wrong payment methods are shown [#the-wrong-payment-methods-are-shown] Check that: ### The payment works, but the order data is incomplete [#the-payment-works-but-the-order-data-is-incomplete] Check that: ### Test mode and live mode behave differently [#test-mode-and-live-mode-behave-differently] Check that: ## What you built [#what-you-built] You have now connected Shopify to ePay and completed the first setup of your payment app. Before going live, also test: ## Next steps [#next-steps] * [Test your integration](../../build-and-go-live/test-your-integration) * [Handle payment results](../../build-and-go-live/handle-payment-results) * [Go live](../../build-and-go-live/go-live) # Shopware (/create-your-first-payment/with-plugin/shopware) Use this guide if your webshop runs on Shopware 6. The Shopware plugin is the fastest way to connect your store to ePay without building a custom integration. You install the plugin, add your ePay credentials, connect a Point of Sale and place a test order. ## What you will do [#what-you-will-do] In this guide, you will: Install the ePay plugin in Shopware Open the plugin configuration Add your API key, webhook verification key and Point of Sale Connect the plugin to the correct sales channel Place a test order and confirm that the payment works ## Before you start [#before-you-start] You need: If you still need your credentials, see [Set up your account](../../get-started/set-up-your-account). Before installing a new plugin on a live shop, take a backup first. ## Step 1: Install the plugin [#step-1-install-the-plugin] You can install the plugin either by uploading a ZIP file in Shopware Admin or by using Composer on the server. ### Option A: Upload the ZIP file [#option-a-upload-the-zip-file] Download the plugin ZIP file:{" "} epay-main.zip Log in to Shopware Admin Go to Extensions -> My Extensions Click Upload extension Upload the downloaded ZIP file Find the ePay plugin Click Install Click Activate ### Option B: Install with Composer [#option-b-install-with-composer] SSH into the server and go to the Shopware root directory. Run: ```bash composer require tigermedia/epay bin/console plugin:refresh bin/console plugin:install --activate epay bin/console cache:clear ``` This installs the plugin, makes Shopware discover it and activates it. ## Step 2: Open the plugin settings [#step-2-open-the-plugin-settings] After installation, open: **Extensions** -> **My Extensions** Find the ePay plugin and click **Configure**. This is where you connect Shopware to ePay. ## Step 3: Add your ePay credentials [#step-3-add-your-epay-credentials] In the plugin configuration, add: | Value | What it is used for | | ------------------------ | ---------------------------------------------------------------- | | Sales Channel | Chooses which Shopware sales channel should use the plugin | | API key | Connects Shopware to your ePay account | | Webhook verification key | Lets Shopware verify incoming webhook calls | | Point of Sale | Tells ePay which webshop or sales channel the payment belongs to | Save the configuration after entering the values. If your setup supports separate test and live values, make sure you do not mix them. ## Step 4: Find the values in ePay Backoffice [#step-4-find-the-values-in-epay-backoffice] You can find the required values in ePay Backoffice: Log in to app.epay.eu Go to Developers to find or generate your API key Go to Point of Sale to find the Point of Sale you want to use Open the Point of Sale and find the webhook authorization or verification value **Partner integrations:** The Point of Sale secret described here applies when the plugin initiates payments using a merchant API key. If your integration instead initiates transactions using partner-generated merchant access tokens, use your **partner notification secret** for the incoming callbacks. Retrieve it from **Notification secret** in the [Partner Portal](https://partner.epay.eu/callback-key); it is shared across merchants and test/live environments. See [partner callback authentication](/partners/partner-api#notification-authentication). Copy each value into the Shopware plugin configuration and save again. ## Step 5: Connect the right sales channel [#step-5-connect-the-right-sales-channel] Choose whether the plugin should apply to one specific sales channel or all sales channels. If you are testing, start with one sales channel first. That makes it easier to validate checkout behavior before enabling the plugin more broadly. ## Step 6: Place a test order [#step-6-place-a-test-order] Go through your storefront like a normal customer: Add a product to the cart Go to checkout Choose ePay as the payment method Place the order Complete the payment in the ePay payment window After the payment finishes, the customer should return to your Shopware storefront. ## Step 7: Check the result [#step-7-check-the-result] Confirm that: ## Common problems [#common-problems] ### The payment method does not appear in checkout [#the-payment-method-does-not-appear-in-checkout] Check that: ### The plugin is installed, but Shopware does not pick it up [#the-plugin-is-installed-but-shopware-does-not-pick-it-up] Check that: ### Payments start, but the order is not updated correctly [#payments-start-but-the-order-is-not-updated-correctly] Check that: ### The wrong sales channel uses the plugin [#the-wrong-sales-channel-uses-the-plugin] Check that: ## What you built [#what-you-built] You have now connected Shopware 6 to ePay and completed your first test payment through the plugin. Before going live, also test: ## Next steps [#next-steps] * [Test your integration](../../build-and-go-live/test-your-integration) * [Handle payment results](../../build-and-go-live/handle-payment-results) * [Go live](../../build-and-go-live/go-live) # WooCommerce (/create-your-first-payment/with-plugin/woocommerce) WooCommerce header Use this guide if your webshop runs on WordPress with WooCommerce. The WooCommerce plugin is the fastest way to get from account setup to a working test payment. You install the plugin, add your ePay credentials, place a test order and confirm that WooCommerce updates the order correctly. ## What you will do [#what-you-will-do] In this guide, you will:

Install the ePay plugin in WordPress

Open the WooCommerce payment settings

Add your test API key and test Point of Sale ID

Place a test order in your webshop

Confirm that the WooCommerce order status updates correctly

## Before you start [#before-you-start] You need: If you still need your credentials, see [Set up your account](../../get-started/set-up-your-account). Before installing a new plugin on a live shop, take a backup first. ## Step 1: Install the plugin [#step-1-install-the-plugin] Install the ePay plugin from WordPress. Plugin page: [ePay Payment Solutions for WooCommerce](https://wordpress.org/plugins/epay-payment-solutions/) Recommended path:

Log in to `wp-admin`

Go to **Plugins** -> **Add New**

Search for `epay`

Find the ePay plugin and click **Install Now**

Click **Activate**

If you received the plugin as a `.zip` file, you can also install it with **Plugins** -> **Add New** -> **Upload Plugin**. ## Step 2: Open the WooCommerce payment settings [#step-2-open-the-woocommerce-payment-settings] After activation, open the plugin settings in WooCommerce:

Go to **WooCommerce** -> **Settings**

Open the **Payments** tab

Select **ePay Payment Solutions - EPIC**

Enable the payment method

This is where you connect WooCommerce to ePay. ## Step 3: Add your ePay test credentials [#step-3-add-your-epay-test-credentials] In the plugin settings, add: | Value | What to use while testing | | ---------------- | -------------------------- | | API key | Your test API key | | Point of Sale ID | Your test Point of Sale ID | Use test values while setting up the plugin. Do not mix test and live values. A live API key with a test Point of Sale ID, or the other way around, will usually cause checkout problems. ## Step 4: Review the key plugin settings [#step-4-review-the-key-plugin-settings] Start with the minimum setup that gets checkout working. The most important settings are: | Setting | What it does | | ----------------- | ---------------------------------------------------------------- | | Title | The payment method name shown to customers in checkout | | Description | Short text shown below the payment method | | API key | Connects WooCommerce to your ePay account | | Point of Sale ID | Tells ePay which webshop or sales channel the payment belongs to | | Credit card icons | Shows supported card brands in checkout | | Instant capture | Captures the payment immediately after authorization | Keep the setup simple during testing. You can fine-tune titles, icons and capture behavior after the first successful payment. ## Step 5: Register your test domain [#step-5-register-your-test-domain] Before you test checkout, make sure the domain you are using is registered on the relevant Point of Sale in ePay Backoffice. If the domain is missing, the payment window may fail to open. ## Step 6: Place a test order [#step-6-place-a-test-order] Go through your webshop like a normal customer:

Add a product to the cart

Go to checkout

Choose ePay as the payment method

Place the order

Complete the payment in the ePay payment window

After the payment finishes, the customer should return to your WooCommerce confirmation page. ## Step 7: Check the WooCommerce order [#step-7-check-the-woocommerce-order] Open the order in WooCommerce and confirm that: A successful payment should normally move the order out of a pending state and into the correct paid or processing state, depending on your WooCommerce setup. ## Manage payments from WooCommerce [#manage-payments-from-woocommerce] You can manage payments directly from WooCommerce instead of switching to ePay Backoffice for every action. Depending on your setup, this can include: This is especially useful if your support or operations team works primarily in WooCommerce. ## Subscription payments [#subscription-payments] If you want recurring payments, install the official [WooCommerce Subscriptions plugin](https://woocommerce.com/products/woocommerce-subscriptions). The ePay WooCommerce plugin supports subscription flows, but you should test them separately before going live. ## Common problems [#common-problems] ### The payment method does not appear in checkout [#the-payment-method-does-not-appear-in-checkout] Check that: ### The payment window does not open [#the-payment-window-does-not-open] Check that: ### The payment succeeds, but the order is not updated [#the-payment-succeeds-but-the-order-is-not-updated] Check that: ### You get an invalid credentials error [#you-get-an-invalid-credentials-error] Check that: ## What you built [#what-you-built] You have now connected WooCommerce to ePay and completed your first test payment through the plugin. Before going live, also test: ## Next steps [#next-steps] * [Test your integration](../../build-and-go-live/test-your-integration) * [Handle payment results](../../build-and-go-live/handle-payment-results) * [Go live](../../build-and-go-live/go-live) # Acquirer specifics (/guides/acquirer-specifics) ## Nets [#nets] ### Reference uniqueness [#reference-uniqueness] * The order id value is taken from the [reference](https://docs.epay.eu/api/initialize-payment-session) field * maximum 20 positions long - left most part used * value must be unique within 24 hours
if already used, then a new one is generated using the 15 left most position, concatenated with `-` and a 4 digit zero prefix random number. As an example - given a reference with value `A7kP2mX9qL4vN8tR1yH6cZ3uB5dFsJ` the order id will be `A7kP2mX9qL4vN8tR1yH6`
If e.g. the cardholder mistypes the card number and the transaction thereby is rejected and shortly after retried, the succeeding attempt will use *another* order id, e.g. `A7kP2mX9qL4vN8t-0842` ## Worldline [#worldline] ### Reference is exactly 12 position [#reference-is-exactly-12-position] * retrieval reference number (RRN) value is taken from the [reference](https://docs.epay.eu/api/initialize-payment-session) field * Dash's (`-`) removed from value if any occurrences * use left most 12 positions * left padded with `0` until 12 positions long if too short ### Multi capture - same amount - within same day [#multi-capture---same-amount---within-same-day] Internal rejection due to duplicate transaction is set up by Worldline internally to avoid any transactions being processed, that should not be processed. This happens if the merchant is sending capture for the - same order - same amount - on the same day. Worldline will contact the merchant to check if the capture shall be processed or rejected. We suggest that, the merchant does not capture the same amount twice - on the same day, but instead do one capture of the whole amount or capture the remaining the next day in case the amounts are identical. ## Vipps / MobilePay [#vipps--mobilepay] For CIT (non-subscription) transactions, the reference value has a minimum length of 8 positions. * left padded with `0` until 8 positions long if too short # Automatic Cleanup (/guides/automatic-cleanup) ## Stored Payment Methods (Saved cards) [#stored-payment-methods-saved-cards] Payment methods are automatically cleaned up when the following is true: * The payment method is stored in the customer's payment methods list, which is usable in CIT transactions. * The last attempt from the customer to use this payment method was more than **3 years** ago. When this happens, the payment method is deleted, which means that it will no longer be shown to the customer as a saved payment method. ## Subscriptions [#subscriptions] Subscriptions are automatically cleaned up when the following is true: * The subscription is in the `ACTIVE` state. * The last attempt to make a transaction on the subscription was more than **2 years** ago. When this happens, the subscription is marked as disabled and any related billing agreements are stopped. Note: Even if the subscription interval is set to a value greater than 2 years, the subscription will still be cleaned up after inactivity for more than 2 years anyway. According to Visa and Mastercard, subscriptions must have a transaction at least once every year. ## Summary [#summary] * **Visible payment methods** are deleted after more than **3 years** without any payment attempt. * **Active subscriptions** are marked `DISABLED` after more than **2 years** without any transaction. * When cleanup disables a subscription, related billing agreements are also stopped. # Integrating with e-conomic (/guides/e-conomic-integration) **Important:**\ To use payment links, you **must** configure an **AppConnection**. To create journal drafts (for invoices or webshop transactions), you **must** create the corresponding **InvoiceConfig** or **TransactionConfig** entries. Without these, the integration will not be able to handle payment links or create journal drafts. ## 1. What the integration offers [#1-what-the-integration-offers] * **Adds secure payment links to your e-conomic invoices.**\ Every invoice you create can automatically include a payment link. Customers can pay directly through our payment platform, reducing manual work and streamlining the payment experience.\ *Payment links are only available when an AppConnection has been configured.* * **Creates journal drafts automatically.**\ When an invoice is paid or a webshop transaction is completed, the service generates the corresponding journal draft inside e-conomic. This keeps your bookkeeping accurate without manual entry.\ *Journal drafts are only created when matching InvoiceConfig or TransactionConfig entries exist and the AppConnection mode is set to "INSTANT"* *** ## 2. How to configure [#2-how-to-configure] There are two main setup steps: * [Backoffice setup](#3-backoffice-setup) * [e-conomic setup](#4-e-conomic-setup) *** ## 3. Backoffice setup [#3-backoffice-setup] The app has the following permissions: [Bookkeeping, Sales and Project employee](https://www.e-conomic.com/developer/permissions) ### 3.1 Install the integration [#31-install-the-integration] * Click the installation URL. * Copy the `agreement token` into the backoffice. The token uniquely identifies your e-conomic agreement. * Then fill out the `pointOfSaleId`, `apiKey`, `webhookToken` and `mode`. * **Payment links cannot work without an AppConnection**, because the static fields in the payment link come from this configuration. * You can create multiple AppConnections if needed, each AppConnection represents a separate point of sale, allowing you to manage different payment flows independently. ### 3.2 Configure invoice [#32-configure-invoice] * Define how paid invoices should be journaled. * If you invoice in multiple currencies, create one **InvoiceConfig** per currency. * **Journal drafts for invoices are only created if you configure InvoiceConfig entries.** ### 3.3 Configure webshop transaction [#33-configure-webshop-transaction] * Define how completed transactions from your webshop should be journaled. * Create separate TransactionConfig entries for each currency if needed. * **Journal drafts for transactions are only created if TransactionConfig entries exist.** ### 3.4 Automatic journaling is controlled by mode [#34-automatic-journaling-is-controlled-by-mode] * Configurations (InvoiceConfig / TransactionConfig) are active as soon as they are created. * Whether a journal draft is **automatically** created after payment depends on the `Mode` field on the associated **AppConnection**: * `INSTANT` - journal drafts are created automatically when payments complete. * `MANUAL` - journal drafts are **not** created automatically. * Use the AppConnection `Mode` to control automatic vs manual journal creation for all configs that reference that AppConnection. *** ## 4. e-conomic setup [#4-e-conomic-setup] ### 4.1 Insert the dynamic payment link in your template [#41-insert-the-dynamic-payment-link-in-your-template] Add this payment link to your invoice template.\ Use the example that matches your account language. **Danish example:**\ `https://economic-integration.epay.eu/public/invoice?invoiceNumber=[FakturaNr]&amount=[TotalBeloebKroner][TotalBeloebOere]¤cy=[Valuta]&debitorNumber=[KundeNr]&pointOfSaleId=your_point_of_sale_id&testMerchant=is_test_merchant&accountId=your_account_id` **English example:**\ `https://economic-integration.epay.eu/public/invoice?invoiceNumber=[InvoiceNo]&amount=[TotalAmountPounds][TotalAmountPence]¤cy=[Currency]&debitorNumber=[CustomerNo]&pointOfSaleId=your_point_of_sale_id&testMerchant=is_test_merchant&accountId=your_account_id` *A button to generate this link will soon be available in the backoffice.* If your account uses a different language, open the Template Designer, insert these fields using the dynamic field tool, and copy the exact field names as they appear in your language. > **Important:**\ > Dynamic field names must match the language of your e-conomic account exactly.\ > e-conomic does **not** accept field names in any language other than the one your account was originally set up with. *** ### 4.2 Dynamic fields (4 required) [#42-dynamic-fields-4-required] These four placeholders **must** be included in the link, and must match the language of your e-conomic account: * **invoiceNumber** = `[InvoiceNo]` / `[FakturaNr]` * **amount** = `[TotalAmountPounds][TotalAmountPence]` / `[TotalBeloebKroner][TotalBeloebOere]` * **currency** = `[Currency]` / `[Valuta]` * **debtorNumber** = `[CustomerNo]` / `[KundeNr]` Supported dynamic fields: * [Danish dynamic fields](https://www.e-conomic.dk/support/artikler/om-dynamiske-felter) *** ### 4.3 Static fields (3 required) [#43-static-fields-3-required] These values do **not** come from the invoice, they come from your **AppConnection**: * **pointOfSaleId = your\_point\_of\_sale\_id** * **testMerchant = is\_test\_merchant** * **accountId = your\_account\_id** > **Note:** > > * These fields are taken directly from the AppConnection. > * `accountId` and `testMerchant` are assigned automatically when the AppConnection is created (based on your login session). > * You do not enter these two values manually. > * If you need to look them up later, you can find them in the Backoffice under *AppConnections*. > * These names never change; they are the same regardless of your e-conomic account’s language. *** ## 5. How configuration matching works [#5-how-configuration-matching-works] Each configuration represents **one specific currency**, and you can have multiple configurations, both for invoices and for webshop transactions. Examples: * One invoice configuration for **DKK** * One invoice configuration for **EUR** * One invoice configuration for **USD** * And the same applies for transaction configurations. This means: * All **invoices issued in DKK** use the DKK invoice configuration. * All **invoices issued in EUR** use the EUR invoice configuration. * All **webshop transactions in DKK** use the DKK transaction configuration. * And so on. When a payment is completed, the system reads the currency from the paid invoice or from the webshop transaction and selects the configuration with the same currency. Only that configuration is used to create the journal draft. If there is **no configuration** for the currency of the payment, or no configuration created at all, **no journal draft is created**. This ensures that each currency is journaled correctly according to your defined accounts. ## Dynamic fields [#dynamic-fields] | Field Name | | -------------------------------------------------- | | CompanyAddress1 | | CompanyAddress2 | | CompanyBank | | CompanyBankAccountNo | | CompanyCoRegNo | | CompanyCountry | | CompanyEmail | | CompanyFax | | CompanyGiroNo | | CompanyIBANNo | | CompanyMobilePhone | | CompanyName | | CompanyPostcode | | CompanySortCode | | CompanySWIFTBICCode | | CompanyTelephone | | CompanyTownCity | | CompanyWWW | | Currency | | CustomerAddress | | CustomerAttention | | CustomerAttentionExternalID | | CustomerBalancePerInvoiceDate | | CustomerCoRegno | | CustomerCountry | | CustomerEmail | | CustomerName | | CustomerNo | | CustomerPostcode | | CustomerTelephone | | CustomerTownCity | | DateDay | | DateDay2 | | DateMonth | | DateMonth2 | | DateYear | | DateYear2 | | DeductionAmount | | DeliveryAddress | | DeliveryCountry | | DeliveryDateDay | | DeliveryDateDay2 | | DeliveryDateMonth | | DeliveryDateMonth2 | | DeliveryDateYear | | DeliveryDateYear2 | | DeliveryPostcode | | DeliveryTerms | | DeliveryTownCity | | DistributionText1 | | DistributionText2 | | DueDateDay | | DueDateDay2 | | DueDateMonth | | DueDateMonth2 | | DueDateYear | | DueDateYear2 | | EANLocationNo | | ExchangeRate | | ExternalId | | FISupplierNo | | Heading | | InternalPublicEntryNo | | InvoiceNo | | LAYOUT\_MERGEFIELD\_INVOICE\_MONEYFLOW\_NOTE\_TEXT | | OCRLine | | OrderNo | | OtherRef | | OurRef | | OurRef2 | | PageCount | | PageNo | | PaymentID | | PaymentID16 | | PaymentTerms | | Text1 | | Text2 | | TotalAmountControlDigit | | TotalAmountGross | | TotalAmountNet | | TotalAmountPence | | TotalAmountPounds | | TotalDiscount | | TotalEnvironmentalTax | | TotalGrossWeight | | TotalNetVATExempt | | TotalNetVATLiable | | TotalNetWeight | | TotalPackagingCharge | | TotalQuantity | | TotalVATAmountBaseCurrency | | TotalVolume | | VATAmount | | VATRate | | YourRef | | YourRefExternalID | # Error Codes (/guides/error-codes) ## Response format [#response-format] Most API errors follow this structure: ```json { "success": false, "errorCode": "SOME_ERROR_CODE", "message": "Human readable message" } ``` ## Common error codes [#common-error-codes] | error\_code | description | | --------------------------------- | ------------------------------------------------------ | | SCA\_EXPIRED | 3DS / SCA authentication session expired | | INSUFFICIENT\_FUNDS | Account lacks sufficient funds or credit | | REJECTED\_BY\_SCA | 3DS / SCA authentication failed or declined by issuer | | DO\_NOT\_HONOR | Generic decline from issuer—no specific reason | | EXPIRED | Operation expired before completion | | SCA\_CANCELLED\_BY\_USER | User canceled 3DS / SCA authentication process | | REJECTED\_BY\_MERCHANT | Merchant intentionally declined the payment | | CARD\_EXPIRED | Card expired—use updated card details | | CANCELLED\_BY\_USER | Customer canceled the payment | | CARD\_LOST\_OR\_STOLEN | Card reported lost or stolen—must not retry | | ACQUIRER\_DECLINED | Acquirer declined authorization | | CARD\_RESTRICTED | Card restricted—transaction type not allowed | | SUSPECTED\_FRAUD | Declined due to suspected fraudulent activity | | ACQUIRER\_DOWN | Acquirer network temporarily unavailable | | AMOUNT\_LIMIT\_EXCEEDED | Amount exceeds card or issuer transaction limit | | INVALID\_CARD\_NUMBER | Card number invalid or not recognized | | CARD\_BLOCKED | Card blocked—contact issuer | | CARD\_CLOSED | Card account closed by issuer | | DECLINED\_BY\_ISSUER\_OR\_SCHEME | Issuer or card network declined transaction | | SCA\_REQUIRED | 3DS / SCA authentication required before authorization | | MISSING\_ACQUIRER\_AGREEMENT | Merchant–acquirer contract not configured | | INVALID\_CVC | Security code (CVC/CVV) incorrect | | ACQUIRER\_TIMEOUT | Acquirer did not respond within time limit | | AGE\_VERIFICATION\_NOT\_COMPLETED | Required age verification was not completed | | ISSUER\_DOWN | Issuer system unavailable—try again later | | INVALID\_AGREEMENT | Invalid or missing merchant setup agreement | | SYSTEM\_ERROR | Message or processing format error | | REJECTED\_UNKNOWN | Transaction rejected for unspecified reason | | MANUAL\_ABORTED | Transaction manually stopped | | TIMEOUT\_ABORTED | Process aborted due to timeout | | PRE\_AUTH\_FAILED | Pre‑authorization attempt unsuccessful | | CARD\_PIN\_ISSUE | Wrong or unverifiable PIN | | DUPLICATE\_TRANSACTION | Same transaction already processed | | TIMEOUT | Operation timed out before issuer response | | ACQUIRER\_ERROR | Error or malformed response from acquirer | | OPERATION\_NOT\_SUPPORTED | Operation type not supported by system/card | | REJECTED\_BY\_ISSUER | Transaction declined directly by issuer | | UNSUPPORTED\_SCHEME | Card scheme not supported for this merchant | | SECURITY\_VIOLATION | Security or data integrity check failed | | INVALID\_3DS\_AGREEMENT | Invalid or missing 3DS / SCA configuration | | RULE\_VIOLATION | Blocked by scheme or issuer transaction rules | | TECHNICAL\_FAILURE | Technical failure during authorization process | | INCONSISTENT\_TRANSACTION\_DATA | Transaction data inconsistent or corrupted | | WITHDRAWAL\_LIMIT | Withdrawal count or value limit exceeded | | VALIDATION\_ERROR | Request parameters failed validation checks | | TERMINAL\_NOT\_FOUND | Terminal or POS device not recognized | | FAILED\_REQUEST | Transaction request could not be processed | | PIN\_TIMEOUT | PIN entry not completed within time allowed | | TRY\_AGAIN | Temporary issue—please retry shortly | | SERVER\_ERROR | Unexpected server or integration error | ## External status codes in webhooks [#external-status-codes-in-webhooks] In the notification webhook, the `transaction` object may also contain `externalStatusCodes`. This object exposes any known raw or near-raw status codes returned by the underlying payment systems, such as the acquirer, the card network, the terminal, or the SCA flow. This is useful if you want to handle specific rejection reasons differently than the normalized `errorCode` returned by ePay. ### How it relates to `errorCode` [#how-it-relates-to-errorcode] * `transaction.errorCode` is ePay's interpreted and normalized error code. * `transaction.externalStatusCodes` contains the more specific underlying status codes when they are known. * If you only need a stable cross-provider integration, handle `errorCode`. * If you need provider-specific behavior, inspect `externalStatusCodes` as an additional input. ### Example [#example] ```json { "transaction": { "state": "FAILED", "errorCode": "DO_NOT_HONOR", "externalStatusCodes": { "acquirer": "05", "network": "05", "sca": "N07" } } } ``` In this example, ePay has interpreted the failure as `DO_NOT_HONOR`, while the webhook still gives you access to the more specific underlying values through `externalStatusCodes`. ### Available fields [#available-fields] * `acquirer`: The status code returned by the processing acquirer. * `network`: The status code returned by the card scheme network. * `terminal`: The status code returned by the terminal provider for in-person terminal payments. * `sca`: A unified status code representing the outcome of the SCA flow, such as 3DS. `externalStatusCodes` is nullable, and individual keys are only present when the value is known to ePay. ## How to use this guide [#how-to-use-this-guide] * Check the HTTP status code first. * Use `errorCode` for programmatic handling. * Use `message` for logging and debugging, not control flow. # Guides (/guides) # Prevent Cloudflare from Blocking ePay Webhooks (/guides/prevent-cloudflare-from-blocking-epay-webhooks) If your website is using ePay webhooks (callback URLs), Cloudflare security features such as WAF rules or Bot Fight Mode may in some cases block or challenge the incoming requests. This guide explains how to create a **Custom Rule** in Cloudflare to allow your ePay callback URL to pass without being blocked. > ⚠️ Note: This configuration only applies to the specific callback URL. Your other security settings remain unchanged. *** ## Step 1 - Open Your Domain in Cloudflare [#step-1---open-your-domain-in-cloudflare] 1. Log in to your Cloudflare account. 2. Select the relevant domain from your dashboard. Navigate to: Security -> Security rules Overview *** ## Step 2 - Create a New Custom Rule [#step-2---create-a-new-custom-rule] 1. Click **Create rule** 2. Select **Custom rules** Create rule *** ## Step 3 - Configure the Rule [#step-3---configure-the-rule] Fill in the rule using your callback URL path. ### Rule Settings [#rule-settings] * **Field:** URI Path * **Operator:** starts with * **Value:** `/webhook`\ *(or the specific path used for your ePay callback URL)* Example: This ensures Cloudflare matches all incoming requests that begin with this path. New custom rule *** ## Step 4 - Select Action [#step-4---select-action] Under **Then take action**: * Choose **Skip** This tells Cloudflare to skip security checks for this specific URL. No additional fields need to be modified. Take action *** ## Step 5 - Deploy the Rule [#step-5---deploy-the-rule] 1. Click **Deploy** The rule is now active. deploy *** # Result [#result] Cloudflare will now: * Allow ePay webhook requests to pass * Prevent WAF or Bot Fight Mode from blocking the callback * Ensure proper communication between ePay and your website *** # When Is This Necessary? [#when-is-this-necessary] You may need this configuration if: * Payment status updates are not being received * Webhook requests are failing * Cloudflare logs show blocked or challenged requests on your callback URL If you are unsure about your callback path, you can verify it in your ePay configuration. # Rate Limits (/guides/rate-limits) ## How Rate Limits Work [#how-rate-limits-work] Each request consumes from a limit “bucket.” Some limits allow a **burst**, which lets you temporarily exceed the steady rate before being throttled. If you exceed a limit, the API responds with **HTTP 429** and an error code. *** ## Response Headers [#response-headers] Every rate‑limited endpoint includes these headers: * `X-RateLimit-Limit`: Maximum number of requests allowed in the current window. * `X-RateLimit-Remaining`: Requests remaining in the current window. * `X-RateLimit-Reset`: Unix timestamp (seconds) when the limit fully resets. * `Retry-After`: Seconds to wait before retrying (present when throttled). Example headers: ``` X-RateLimit-Limit: 60 X-RateLimit-Remaining: 12 X-RateLimit-Reset: 1735689600 Retry-After: 5 ``` When a limit is exceeded, you will receive: ``` HTTP/1.1 429 Too Many Requests { "success": false, "errorCode": "RATE_LIMIT_REACHED", "message": "Too many requests" } ``` *** ## Rate Limit Categories [#rate-limit-categories] Different endpoint categories have different limits. The following limits are currently enforced: ### 1) Single MIT transactions [#1-single-mit-transactions] * **Rate:** 5 requests / second * **Burst:** 300 * **Ingestion:** 300 transactions / minute ### 2) Batch MIT transactions [#2-batch-mit-transactions] * **Rate:** 1 batch / second * A batch contains maximum 500 transactions * **Ingestion:** 30.000 transactions / minute ### 3) Index endpoints [#3-index-endpoints] * **Rate:** 1 request / 5 seconds * **Burst:** 30 ### 4) Resource endpoints [#4-resource-endpoints] * **Rate:** 5 requests / minute / resource ### 5) Operation endpoints (capture, void, refund) [#5-operation-endpoints-capture-void-refund] * **Rate:** 5 requests / minute / transaction > **Note:** A burst allows short spikes above the steady rate. After a burst is consumed, requests will be throttled until the bucket refills. > **Note:** Resource / Operation rate limits are grouped per resource id. Meaning you can capture as many transactions as you need, but cannot capture the same transaction more than 5 times a minute. *** ## Best Practices [#best-practices] * **Use `Retry-After`** to back off before retrying. * **Avoid polling** - Use webhooks where available. * **Use batch endpoints** for high‑volume processing. * **Handle 429 responses gracefully** with exponential backoff. If you have a use case that requires higher limits, contact support to discuss options. # Saved Cards in Your App with Blocks (/guides/saved-cards-with-blocks) For a standard in-app checkout, the [Checkout integration](/create-your-first-payment/with-checkout) provides a simpler integration. Open `paymentWindowUrl` in a Webview and let ePay provide the full payment UI. This guide shows how to build a more native app experience with Blocks and profile saved cards. This guide shows an app-first saved-card flow: * A signed-in cardholder adds a card to their profile. * The app securely stores the card with ePay Blocks. * On a later purchase, the app charges the selected saved card without asking for card credentials again. In this guide, the authenticated user is signed in to your merchant app, and `customerId` is that user's stable ID in your merchant system. The same card-setup flow works for every card the cardholder chooses to save. Store each `paymentMethodId` on the user's profile and let the cardholder select one at checkout. Only use a stable `customerId` from the signed-in merchant user's profile. Do not use guest IDs. Your backend must load the selected saved `paymentMethodId` from the signed-in user's profile before it creates a payment session. ePay verifies that the payment method belongs to the session's `customerId` when the transaction is created. ## Flows [#flows] This guide covers the two main flows of adding a card to the user profile and later charging the saved card. ### Add a card to the profile [#add-a-card-to-the-profile]
The profile payment-method screen before a card is saved
Profile with no saved card
Hosted card fields with the option to save the card
Card details entered in hosted fields
The user's profile after a card has been saved
Card saved to the profile
```mermaid sequenceDiagram participant App participant Merchant Server participant ePay App->>Merchant Server: Request card-setup session activate Merchant Server Merchant Server->>ePay: Create 0-amount CIT session
(customerId, action: SAVE_CARD) activate ePay ePay-->>Merchant Server: Session ID, key, and ePay.js URL deactivate ePay Merchant Server-->>App: Session ID, key, and ePay.js URL deactivate Merchant Server Note over App: Inside Webview App->>App: Mount Hosted fields App->>ePay: Submit card (createCardTransaction({ store: true })) activate ePay ePay-->>App: Redirect to successUrl deactivate ePay ePay-)Merchant Server: Payment notification (action: SAVE_CARD, paymentOptions.store: true) activate Merchant Server Merchant Server->>Merchant Server: Save paymentMethodId on user profile Merchant Server-->>ePay: 200 OK deactivate Merchant Server ``` ### Charge a saved card [#charge-a-saved-card]
An in-app purchase using a saved card
Purchase with a saved card
An in-app purchase while the saved-card payment is being processed
Payment processing
An order completed with a saved Visa card
Completed order and confirmed payment
```mermaid sequenceDiagram participant App participant Merchant Server participant ePay App->>Merchant Server: Request purchase session activate Merchant Server Merchant Server->>ePay: Create CIT session
(customerId, action: CHARGE_CARD) activate ePay ePay-->>Merchant Server: Session ID, key, and ePay.js URL deactivate ePay Merchant Server-->>App: Session values and
saved paymentMethodId deactivate Merchant Server App->>ePay: epay.createTransaction({paymentMethodId}) activate ePay ePay-->>App: Redirect to successUrl deactivate ePay ePay-)Merchant Server: Payment notification
(action: CHARGE_CARD) activate Merchant Server Merchant Server->>Merchant Server: Mark order paid and fulfill it Merchant Server-->>ePay: 200 OK deactivate Merchant Server ``` ## Before you start [#before-you-start] You need an authenticated user, a stable `customerId`, a backend endpoint for payment sessions, a notification endpoint, and a user-profile record for one or more saved `paymentMethodId` values. Keep the API key on your backend. Track the payment session ID on your server, then return only the session ID, key, and ePay.js URL to the app. ### Use an embedded Webview [#use-an-embedded-webview] In a native app, use an embedded Webview to load ePay.js, mount hosted fields, and start card transactions. ### Use the session return URLs for Webview navigation [#use-the-session-return-urls-for-webview-navigation] Set all three return URLs on each payment session: * `successUrl`: ePay redirects the Webview here after a successful transaction attempt. * `failureUrl`: ePay redirects the Webview here after the final failed attempt, when no attempts remain. * `retryUrl`: ePay redirects the Webview here after a failed attempt when the session still allows another attempt, for example after invalid card input or a cancelled 3DS challenge. Use the retry page to remount hosted fields for the same session and let the cardholder try again when adding a card. For a saved-card charge, render the appropriate retry or card-selection UI instead. The return URLs are suitable for deciding what UI to show the cardholder, but the asynchronous notification to `notificationUrl` is the source of truth for saving a card or completing an order. ## Add a card to the profile [#add-a-card-to-the-profile-1] ### 1. Request a card-setup session [#1-request-a-card-setup-session] When a signed-in user taps **Add card**, the app asks your backend for a card-setup session. Derive the user and `customerId` from authentication; never accept either value from the app request. Your backend creates a zero-amount CIT session. This performs a full zero-amount authorization to verify and store the card without charging the cardholder. It can include a 3DS challenge when required. When adding a card, we recommend setting `scaMode=FORCE`. This requires Strong Customer Authentication (SCA), such as 3DS, before ePay stores the card and reduces the risk of fraud. ```json title="POST /public/api/v1/cit" { "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "amount": 0, "currency": "DKK", "scaMode": "FORCE", "customerId": "user_123", "reference": "profile-card-setup-987", "notificationUrl": "https://api.example.com/payments/notification", "successUrl": "https://app.example.com/profile/payment-methods/success?sessionId=${session.id}", "failureUrl": "https://app.example.com/profile/payment-methods/failure?sessionId=${session.id}", "retryUrl": "https://app.example.com/profile/payment-methods/retry?sessionId=${session.id}", "attributes": { "action": "SAVE_CARD" } } ``` `attributes` is pass-through metadata. It has no effect on the payment flow, but it returns in the notification and makes it easy to distinguish this card-setup attempt from an order payment. Save `session.id` on your backend for tracking. Return only the frontend-safe values to the app: ```ts return { sessionId: payment.session.id, sessionKey: payment.key, javascript: payment.javascript, }; ``` ### 2. Open the card-setup Webview [#2-open-the-card-setup-webview] The app opens its embedded Webview and loads ePay.js from the session-specific `javascript` URL. ```html
``` Initialize the client, mount the hosted fields, and use `inputValidity` to enable the button only when the card details are valid. ```js epay .setSessionId(sessionId) .setSessionKey(sessionKey) .setCallbacks({ inputValidity: inputValidityHandler, }) .init(); epay.mountFields("field-window", { variables: {} }); // variables can be used for styling the input fields function inputValidityHandler(event) { document.querySelector("#add-card").disabled = !event.state.valid; } document.querySelector("#add-card").addEventListener("click", () => { if (document.querySelector("#save-card-consent").checked) { epay.createCardTransaction({ store: true, }); } }); ``` Hosted fields are a live payment application. Keep the mounted fields and their iframe in the Webview until the payment completes or ePay starts a redirect flow. Do not remove or replace the Webview content while payment processing is active. Make it clear that the cardholder is saving their card for future purchases. An explicit required consent checkbox is particularly important in regions such as Denmark. `store: true` requests that ePay saves the card for future customer-initiated payments. After a successful payment, ePay redirects the Webview to the session's `successUrl`. ### 3. Save the payment method from the asynchronous notification [#3-save-the-payment-method-from-the-asynchronous-notification] The Webview redirect means the card setup succeeded for the cardholder, but it is not your server-side confirmation. ePay sends the notification asynchronously after the redirect. Verify the notification authorization and transaction state before updating the profile. For card setup, the notification attributes include the payment options used by the Webview: ```json title="Notification transaction.attributes" { "attributes": { "action": "SAVE_CARD", "pspData": { "paymentOptions": { "paymentMethodId": "019aef8c-2771-73f1-a43f-1446f7b0752a", "store": true, "usesPaymentWindow": false } } } } ``` `pspData.paymentOptions.store` confirms that the Webview submitted the transaction with `store: true`. Check it together with `action` and the successful transaction state before saving the returned `transaction.paymentMethodId` to the authenticated user's profile. See [Handle payment results](/build-and-go-live/handle-payment-results) for notification authorization and handling requirements. ```ts const attributes = data.transaction.attributes; if (attributes.action === "SAVE_CARD" && attributes.pspData.paymentOptions.store === true && data.transaction.state === "SUCCESS") { await savePaymentMethodForUser({ userId: data.session.customerId, paymentMethodId: data.transaction.paymentMethodId, }); } ``` Store the card display data from the notification alongside the payment method: `data.card.pan`, `data.card.expireMonth`, and `data.card.expireYear`. For the app UI, show only the last four card digits from the PAN and the expiry date. This limits unnecessary card-number exposure on the client. For a more native app experience, the Webview pages at `successUrl`, `failureUrl`, and `retryUrl` can send a message through your Webview bridge. The app can then take over the success, failure, and retry UI; only hosted-field entry and 3DS need to remain visible inside the Webview. ## Charge a saved card [#charge-a-saved-card-1] ### 1. Request a purchase session [#1-request-a-purchase-session] Keep the checkout and saved-card summary in the app's native UI. When the cardholder taps **Pay**, the app asks your backend to create a session for the order. The backend loads the selected `paymentMethodId` from the authenticated user's profile. ePay validates that the payment method belongs to the session's `customerId` when the transaction is created. Create a new CIT session with the same `customerId`, the actual order amount, and a distinct action for the order flow. This example charges 199.00 DKK. ```json title="POST /public/api/v1/cit" { "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "amount": 19900, "currency": "DKK", "customerId": "user_123", "reference": "order-987", "notificationUrl": "https://api.example.com/payments/notification", "successUrl": "https://app.example.com/orders/987/success?sessionId=${session.id}", "failureUrl": "https://app.example.com/orders/987/failure?sessionId=${session.id}", "retryUrl": "https://app.example.com/orders/987/retry?sessionId=${session.id}", "attributes": { "action": "CHARGE_CARD" } } ``` Return the frontend-safe session values and the profile's saved `paymentMethodId` to the Webview. ### 2. Charge the saved card in the Webview [#2-charge-the-saved-card-in-the-webview] Only after the cardholder taps **Pay** does the app need to open the embedded Webview, load ePay.js, and initialize the new session. It does not need to mount hosted fields for this payment. ```js epay .setSessionId(sessionId) .setSessionKey(sessionKey) .init(); epay.createTransaction({ paymentMethodId: savedPaymentMethodId, }); ``` Show a processing state while the payment is handled. ePay redirects the Webview to `successUrl`, `failureUrl`, or `retryUrl` according to the transaction result. Use these destinations for the cardholder's UI, not to finalize the order in your backend. ### 3. Fulfill the order from the asynchronous notification [#3-fulfill-the-order-from-the-asynchronous-notification] When the notification arrives, verify its authorization, match `session.id` to the order, and confirm that it is a successful `CHARGE_CARD`. Then mark the order as paid and start fulfillment. ```ts if (data.transaction.attributes.action === "CHARGE_CARD" && data.transaction.state === "SUCCESS") { await markOrderAsPaid(data.session.id); } ``` Return `200 OK` after recording the notification. Your handler must be idempotent because ePay retries notifications that do not receive a successful response. For the full notification requirements, see [Handle payment results](/build-and-go-live/handle-payment-results). ## Remove a saved card [#remove-a-saved-card] When the cardholder chooses to remove a card, have your backend disable its stored payment method. Use the `paymentMethodId` from the signed-in user's profile and authenticate the request with your API key; never expose the API key to the app. The request permanently removes the payment method from the payment window, so it can no longer be used for quick checkout. It is irreversible and idempotent: ePay returns `200 OK` with no response body even if the payment method has already been removed. After a successful response, remove the corresponding `paymentMethodId` from the user's profile. ## What you built [#what-you-built] Your authenticated users can add one or more cards to their profile with `store: true`, and later pay with a selected saved `paymentMethodId` without re-entering card credentials. Your backend uses the transaction notification attributes, especially `pspData.paymentOptions.store`, to identify card setup and handles payments independently of Webview redirects to `successUrl`. ## Helpful notes [#helpful-notes] ### Letting the user use another card [#letting-the-user-use-another-card] The primary flow is saved-card payment. If a card is missing, expired, or declined, you can offer **Add another card** and return to the profile card-setup flow above. Keep this fallback secondary so users with a saved card get the fast purchase experience. ### Retrying a saved-card charge [#retrying-a-saved-card-charge] For saved-card payments, a single attempt is often the simplest choice. If your session allows another attempt, use the `retryUrl` to show UI that lets the cardholder try the selected card again, choose another saved card, or add a new card. Do not treat the redirect itself as server-side confirmation; wait for the payment notification before marking the order as paid. ### CIT versus MIT charges [#cit-versus-mit-charges] This guide covers a cardholder actively tapping **Pay** in the app. It is a customer-initiated transaction (CIT). Do not use this flow for recurring or background charges without the customer present; use the subscription/MIT flow instead. # SoftPay (/guides/softpay) ## How the Flow Works [#how-the-flow-works] A Softpay payment follows a simple sequence: 1. Your backend creates a transaction using the `/sale` endpoint 2. The transaction becomes available in the Softpay app 3. Your frontend switches to the Softpay app 4. The customer completes the payment 5. Softpay returns to your app 6. The final result is sent to your backend via the notification URL Softpay processes transactions based on what is already created and available, so the key is to ensure each step completes before moving to the next. *** ## Creating the Payment [#creating-the-payment] The flow starts with a backend request: POST [https://payments.epay.eu/public/api/v1/sale](https://payments.epay.eu/public/api/v1/sale) This creates the transaction and sends it to the Softpay terminal. A successful response means the transaction is ready for the Softpay app. For same-device SoftPOS setups, you may want to include: * `suppressAppNotifications: true` to avoid extra popups * `switchBackTimeout: 0` to return to your app as quickly as allowed Example request: ```json { "terminal": { "id": "...", "suppressAppNotifications": true, "switchBackTimeout": 0 }, "transaction": { "pointOfSaleId": "...", "amount": 1000, "currency": "DKK", "notificationUrl": "https://your-backend.com/callback" } } ``` *** ## Waiting for the Response [#waiting-for-the-response] After calling `/sale`, your frontend should wait for the response before continuing. * If the transaction state is `FAILED`, stop the flow * Otherwise, the transaction is ready in Softpay Only once the transaction has been successfully created should you proceed to the next step. This ensures the payment is immediately available when the Softpay app opens. *** ## Switching to the Softpay App [#switching-to-the-softpay-app] After receiving a successful response, trigger the app switch. For PWAs, this can be done using the Softpay JavaScript client or an intent URL. The switch must be initiated by user interaction due to Android platform requirements. #### Example intents: [#example-intents] Initiate pending payment with redirect back to previous app: ``` intent://softpay.io/pending#Intent;scheme=softpay;action=io.softpay.action.PENDING;launchFlags=0x10000000;end ``` Initiate pending payment with redirect to specific app or web page: ``` intent://softpay.io/pending?callback=yourapp://payment-result#Intent;scheme=softpay;action=io.softpay.action.PENDING;launchFlags=0x10000000;end ``` Used like this: ```js window.location.assign(`{intent_url}`); ``` Softpay will open and begin processing the pending transaction. ### Payment in Softpay [#payment-in-softpay] In the Softpay app, the customer taps their card and completes the payment. A confirmation screen is shown after processing. This is required by card schemes, though its duration can be minimized through configuration. ### Returning to Your App [#returning-to-your-app] Once the confirmation screen is complete, Softpay automatically switches back to your app. Successful payments return quickly, while failed transactions may take slightly longer. Setting `switchBackTimeout` to zero ensures the fastest allowed return. ### Receiving the Final Result [#receiving-the-final-result] The final payment result is sent to your backend via the `notificationUrl`. While the app switch provides immediate user feedback, your backend notification should always be treated as the source of truth for the transaction outcome. *** ## Best Practices [#best-practices] * Always create the transaction before switching to Softpay * Wait for a successful `/sale` response before triggering the app switch * Treat the frontend as a user interface layer only * Use backend notifications to determine the final payment result * Avoid having both the SoftPay live and test app installed on the same device Following this sequence ensures that the transaction is ready and visible when the Softpay app opens, resulting in a smooth and predictable user experience. ## Closing Notes [#closing-notes] For PWA integrations, use either the Softpay JavaScript client or intent-based switching. Native apps can use intents directly or the AppSwitch SDK for deeper integration. When the flow is implemented in the correct order, the Softpay experience is consistent and easy to work with. # Switch from ePay Classic to our new platform (/guides/switch-from-epay-classic-to-our-new-platform) This guide explains how to move from ePay Classic to our new platform. You create an account, configure and test the new plugin, ask us to activate live payments, and then retire Classic when you are ready. Most of the move is self-service, so you can complete it without waiting for us. We only need to step in once: when your live account is activated and your acquiring agreements are added to it. Your Classic account and acquiring agreements remain available during the move. You do not need to change acquiring agreement, and there is no additional cost for switching. Allow around an hour for the complete migration. **Keep your current Classic setup running.** Do not change your ePay Classic setup yet: customers must be able to pay normally while you configure our new platform. Do not disable Classic or remove the Classic plugin. The new plugin can run alongside it, so you can install and test our new platform without affecting daily operations. *** ## Before you start you'll need [#before-you-start-youll-need] *** Go to {"app.epay.eu/register"}. Registration is free and does not require a payment card while you test. 1. Enter your email address, company name, and webshop domain. 2. Confirm your email address using the link we send you. 3. Enter your full name and choose a password. You can then sign in at {"app.epay.eu/login"}. Our new platform registration form with the email address, company name, and domain fields highlighted You only need to complete three fields. The Backoffice for our new platform is separate from Classic Backoffice, so you can keep both open in different tabs while you work. Your account starts in the test environment. Nothing needs to be activated yet. Backoffice for our new platform showing the selected test environment The environment selector in the top-right corner is where you will later switch from **Test** to **Live**. Test and live are separate configurations, and your account remains in Test until we activate it. You do not just receive two API keys. You also receive two Points of Sale: one for Test and one for Live. Each Point of Sale has its own ID and webhook authorization. These are the three credentials you need for the plugin. A **Point of Sale** is the store a payment belongs to. If you have one webshop, you normally have one Point of Sale in each environment. This is also where your domain and payment window are configured. | Value | Test | Live | | --------------------- | -------------------------------- | ------------------------------------------- | | Point of Sale | Available after account creation | Created when Support activates your account | | API key | Available after account creation | Created when Support activates your account | | Webhook authorization | Available after account creation | Created when Support activates your account | You do not need to create any of these values yourself. Your test credentials are ready when you create the account, and live credentials are created automatically when Support activates the live account. Your test setup remains available after you go live. You can switch back to Test at any time to try changes without affecting real payments. There are three credentials to find. Two are together in the **Your credentials** panel on the dashboard, and the third is on the Point of Sale. Start from the dashboard. Dashboard for our new platform with Point of Sale ID and API key highlighted For now, just locate them. In the next step you can copy them one at a time while the plugin settings are open in another tab. ### You can also find them in the menu [#you-can-also-find-them-in-the-menu] The API key and Point of Sale ID are also available from the left-hand menu: | Credential | Where to find it | | --------------------- | ------------------------------------------------------------- | | API key | **Developers** → **API keys** | | Point of Sale ID | **Payment** → **Points of Sale** | | Webhook authorization | Open the Point of Sale, then select **Webhook authorization** | API keys page with the API key highlighted Find the API key under **Developers** → **API keys**. Select the copy icon next to the key to copy it. Points of Sale page with the Point of Sale ID highlighted The Point of Sale ID is shown in the rightmost **ID** column. ### Check your domain while you are here [#check-your-domain-while-you-are-here] Under **Payment** → **Points of Sale**, confirm that your webshop domain is listed in the **Domain** column. The domain is required for live payments; if it is missing, the payment window will not open in the live environment. This is the most common cause when the live payment window does not open. If the domain is incorrect or was entered incorrectly during registration, contact Support and we will correct it for you. ### Find the webhook authorization [#find-the-webhook-authorization] After a payment is completed, ePay sends a notification to your webshop. This is what updates the order status, so you do not have to update orders manually. The notification includes an agreed secret, which lets your webshop verify that the notification came from ePay. The secret is called **Webhook authorization**. It is on the Point of Sale, not on the dashboard. Open the relevant Point of Sale under **Payment** → **Points of Sale**, select **Webhook authorization**, then reveal and copy the full value. **Partner integrations:** The Point of Sale secret described here applies when the plugin initiates payments using a merchant API key. If your integration instead initiates transactions using partner-generated merchant access tokens, use your **partner notification secret** for the incoming callbacks. Retrieve it from **Notification secret** in the [Partner Portal](https://partner.epay.eu/callback-key); it is shared across merchants and test/live environments. See [partner callback authentication](/partners/partner-api#notification-authentication). Webhook authorization on a Point of Sale Keep your API key and webhook authorization secret. Do not expose them in browser code or share them publicly. Choose your webshop platform below. Its dedicated guide covers installation in detail; when you return here, use the test API key, Point of Sale ID, and webhook authorization from the previous section. ### WooCommerce example [#woocommerce-example] In WordPress, search for `epay` under **Plugins** and install **ePay Payment Solutions**. WordPress plugin search with ePay Payment Solutions highlighted The screenshots below are from WooCommerce. Other platforms look different, but the credential fields use the same names. ### Add your credentials [#add-your-credentials] Keep Backoffice open in one tab and the plugin settings in another. Then complete the fields one at a time: 1. Enable **Activate module**. 2. Copy the API key from Backoffice and enter it in **API Key**. 3. Copy the Point of Sale ID and enter it in **PointOfSale ID**. 4. Copy the webhook authorization from the Point of Sale and enter it in **Webhook Authorization**. 5. Save the settings. WooCommerce settings with the credential fields for our new platform highlighted The payment title and description are displayed to customers at checkout and can be adjusted later. The webhook authorization is important: if it is empty, the webshop accepts notifications without verifying their sender. If it is incorrect, the webshop rejects the notification and the order may remain unpaid even though the customer has been charged. ### Magento 2 example [#magento-2-example] In Magento, open **Stores** → **Configuration** → **Sales** → **Payment Methods**, then expand **ePay Payment**. **Webhook Authorization** is directly below **PointOfSale ID**, and the credentials are entered in the same order. Magento payment settings with the credential fields for our new platform highlighted It is expected that both the Classic payment method and the payment method for our new platform are visible in your shop at this stage. Leave both enabled until our new platform has been tested. Complete checkout as a customer would and select the payment method for our new platform. Use one of these test cards; no money is charged in the test environment. Use any future expiry date and any CVC for each card. For declined payments and 3D Secure flows, see Test cards. After the payment, confirm that: If the payment succeeds but the order does not update, compare the plugin's webhook authorization with the value on the Point of Sale. Your webshop must also be publicly reachable so it can receive the payment result. This is where we need to step in. Your account must be active before you can accept live payments. Once the test order works, contact {"support@epay.dk"} or call +45 9813 9040. We will: * activate your live account; * add your acquiring agreements to the new account. We will let you know when you can continue. Live environment option shown as requiring activation When we activate the account, your live Point of Sale, live API key, and live webhook authorization are created automatically. You do not need to create them, but you must retrieve the new credentials and update the plugin in the next step. After activation, select **Live** in the Backoffice for our new platform. The test banner disappears, and you now have a new set of credentials: a live Point of Sale, live API key, and live webhook authorization. Backoffice for our new platform showing live credentials Replace all three values. A live API key with a test Point of Sale, or any other mixed combination, prevents checkout from working. It is easy to replace the key and forget the Point of Sale ID. Retrieve the live API key from **Developers** → **API keys**; the live Point of Sale ID from **Payment** → **Points of Sale**; and the live webhook authorization from the Point of Sale itself. Then replace every value in the plugin. Check that the live Point of Sale has the correct domain. Test and live are separate setups, so the domain does not automatically carry over. Also compare the live webhook authorization with the plugin value: if they differ, orders can remain unpaid even after the customer has been charged. Make a real payment for a small amount with your own card and refund it afterwards. This verifies that the complete live setup works. Only after the live payment succeeds, disable the **Classic payment method** in your webshop. This sends new payments through our new platform. Do not deactivate or remove the Classic plugin yet. Existing Classic orders still rely on it for captures and refunds. Disable the Classic **payment method**, not the Classic **plugin**. Disabling the payment method stops new customers from selecting it at checkout, while keeping the plugin lets you capture and refund existing Classic orders. Orders paid through Classic remain on Classic. Capture or refund them as you normally would, and keep using Classic Backoffice until they are finished. When every Classic order has been captured or refunded, remove the Classic plugin. If you are unsure whether you have reached that point, leave it in place: an inactive plugin does no harm. *** ## Troubleshooting [#troubleshooting] ### The payment method does not appear in checkout [#the-payment-method-does-not-appear-in-checkout] * Confirm that the plugin is activated. * Confirm that the payment method for our new platform is enabled in the webshop payment settings. * Check that the API key and Point of Sale ID are complete and correct. * Make sure every credential is from the same environment. ### The payment window does not open [#the-payment-window-does-not-open] * Confirm that the webshop domain is registered on the selected Point of Sale. * Confirm that the listed domain is spelled correctly and current. Contact Support if it needs to be corrected. * Confirm that the API key and Point of Sale belong to the same environment. * Check for theme or plugin conflicts on the checkout page. ### The payment succeeds, but the order is not updated [#the-payment-succeeds-but-the-order-is-not-updated] * Confirm that the webshop can receive notifications from ePay. * Compare the plugin's webhook authorization with the value on the Point of Sale. * Ensure that the site is publicly available while testing; a private test server cannot receive notifications from ePay. * Confirm that the plugin settings were saved. ### Invalid credentials error [#invalid-credentials-error] * Confirm that the full API key was copied, without a leading or trailing space. * Confirm that each value is in the correct field: API key, Point of Sale ID, and webhook authorization. * Do not mix test and live values. The Point of Sale ID is the value most often missed after going live. ### Test works, but live does not [#test-works-but-live-does-not] Select **Live** in Backoffice, then compare all three live credentials with the plugin. Also confirm that the correct domain is registered on the live Point of Sale. ### Live cannot be selected [#live-cannot-be-selected] Your account must first be activated by ePay Support. See step 6 above. For more help, see Troubleshooting or contact Support. *** ## Need help? [#need-help] Contact {"support@epay.dk"} or call +45 9813 9040. We can help you complete the migration. # Updating Payment Method for a Subscription (/guides/updating-payment-method-for-a-subscription) ## Steps to Update a Subscription Payment Method [#steps-to-update-a-subscription-payment-method] 1. **Start a New Payment Session** * Send a `POST` request to initialize a CIT payment session. * Include the `subscription.id` field with the ID of the existing subscription. * If no immediate payment is needed, set `amount` to `0`. * For payment methods requiring a known recurring amount (e.g., Vipps MobilePay), set `subscription.amount` to the expected recurring subscription amount. 2. **Handle the Payment Session** * Redirect the customer to ePay Checkout or use Blocks * The customer completes authentication and payment method update. 3. **Receive Confirmation via Webhook** * Once the transaction completes, ePay sends a [webhook](/build-and-go-live/handle-payment-results#notification-urls-and-webhooks) confirming the updated payment method. ## Example Request [#example-request] **Updating a subscription without immediate payment:** ```json title="POST /public/api/v1/cit" { "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "amount": 0, "currency": "DKK", "subscription": { "id": "01929a94-5fce-7ccc-a7e4-7e9249133b39", "amount": 20000 } } ``` See all [fields](/create-your-first-payment/with-blocks#step-3-understand-the-request-body). ## Expected Behavior [#expected-behavior] * The new payment method is linked to the existing subscription. * No charge is made if `amount` is `0`. * Merchants will receive a webhook confirmation once the update is successful. This process ensures that customers can seamlessly update their payment details while maintaining their active subscriptions. # Using the Idempotency-Key Header (/guides/using-the-idempotency-key-header) The `Idempotency-Key` header ensures that a request can be safely retried without creating duplicate operations. It is especially useful for requests that perform actions such as creating payments, issuing refunds, or voiding transactions. Without an idempotency mechanism, network interruptions, client restarts, or repeated retry logic might cause the same request to be processed multiple times, potentially creating duplicate payments or operations. By including a unique key, you can safely retry a request and be confident it will only execute once. *** ## How Idempotency Works [#how-idempotency-works] When you include the `Idempotency-Key` header in a request: * ePay stores the response for the given combination of Key, Endpoint, and HTTP Verb. * If the exact same request (same endpoint, method, and key) is received again within **24 hours**, we do not perform the operation again. * Instead, ePay returns the **original response**, along with the header: ``` Idempotent-Replayed: true ``` * After 24 hours, the cached response expires, and the idempotency key will no longer apply. To see which endpoints support idempotency key, take a look at our [API](/api). > **Scope:** > The idempotency key is scoped per combination of **key + endpoint + HTTP verb**, meaning the same key on a different endpoint or method will not replay the original response. *** ## When to Use Idempotency Keys [#when-to-use-idempotency-keys] Include `Idempotency-Key` for any request where a duplicate operation could have negative effects, such as: * Creating payments or subscriptions * Issuing refunds * Voiding transactions * Any write operation that changes data or triggers money movement GET requests are naturally safe to retry, so idempotency keys are typically most useful for `POST`, `PUT`, or `DELETE` requests. *** ## How to Use It [#how-to-use-it] 1. **Generate a unique key** (for example, a UUID) for each operation: ``` Idempotency-Key: c4f5e8d2-1234-5678-90ab-cdef12345678 ``` 2. **Include the key in your request header**: ```http POST /public/api/v1/cit Idempotency-Key: c4f5e8d2-1234-5678-90ab-cdef12345678 Content-Type: application/json ``` 3. **Handle network retries**: * If the client times out or loses the connection before receiving a response, resend the **exact same request** with the same `Idempotency-Key`. * The server will return the original response once more. *** ## Example Use Case [#example-use-case] ### Retry a Payment Creation Request [#retry-a-payment-creation-request] If network issues occur while submitting a payment, you can safely retry: ```json title="POST /public/api/v1/cit" { "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "amount": 20000, "currency": "DKK" } ``` **Headers** ``` Idempotency-Key: 4a1f2eb3-911b-40cd-9bcb-be321aa7a123 Content-Type: application/json ``` If the client resends this request using the same key within 24 hours, ePay does not create a second payment but responds with: ``` HTTP/1.1 200 OK Idempotent-Replayed: true ``` *** ## Best Practices [#best-practices] * Always generate a **new key per unique operation** (for example, one key per payment or refund). * **Persist keys** on your side until you confirm the final state of the operation. * If an operation must truly be re-executed (for example, a second payment), use a **new idempotency key**. * Keep in mind that idempotency keys are **case-sensitive strings** and must remain **unique** for each intended operation. *** By adopting idempotency keys, you can make your integration more resilient to timeouts, connection issues, and client retries—ensuring a consistent experience for both your application and your customers. # Vipps MobilePay Campaign Functionality (/guides/vipps-mobilepay-campaign-functionality) ## How to begin [#how-to-begin] To use the Vipps MobilePay campaign functionality during subscription / agreement creation you must send the custom attribute `vippsMobilePayCampaign` using the field `attributes` in the request to [`/public/api/v1/cit`](/api/initialize-payment-session#:~\:text=Example%3A%20order%20123-,attributes,-object). The attribute value is sent directly to Vipps MobilePay and must conform to the field `campaign` defined in their [API specification](https://developer.vippsmobilepay.com/api/recurring/#tag/Agreement-v3-endpoints/operation/DraftAgreementsV3:~\:text=\(Price%20campaign%20\(object%20or%20null\)\)%20or%20\(Period%20campaign%20\(object%20or%20null\)\)%20or%20\(Event%20campaign%20\(object%20or%20null\)\)%20\(campaignV3\)). ## Examples [#examples] You can read more about campaigns at [Vipps MobilePay](https://developer.vippsmobilepay.com/docs/APIs/recurring-api/recurring-api-guide/#campaigns). ### 1. Period Campaign [#1-period-campaign] This will create a intro price campaign, where the customer will be paying 99.00 DKK this month, and then 299.00 DKK each following month. ```json title="POST /public/api/v1/cit" { "pointOfSaleId": "01924737-9c18-71c0-ab1a-88698eaceabf", "amount": 9900, "currency": "DKK", "subscription": { "type": "SCHEDULED", "amount": 29900, "interval": { "period": "MONTH", "frequency": 1 } }, "attributes": { "vippsMobilePayCampaign": { "price": 9900, "type": " PERIOD_CAMPAIGN", "period": { "count": 1, "unit": "MONTH" } } } } ``` ### 2. Price Campaign [#2-price-campaign] This will create a price campaign, where the customer will pay 99.00 DKK each month until 25th December 2025, after which the customer will pay 299.00 DKK per month. ```json title="POST /public/api/v1/cit" { "pointOfSaleId": "01924737-9c18-71c0-ab1a-88698eaceabf", "amount": 9900, "currency": "DKK", "subscription": { "type": "SCHEDULED", "amount": 29900, "interval": { "period": "MONTH", "frequency": 1 } }, "attributes": { "vippsMobilePayCampaign": { "type": "PRICE_CAMPAIGN", "price": 9900, "end": "2025-12-25T00:00:00Z" } } } ``` ### 3. Event campaign [#3-event-campaign] This will create an event campaign, where the customer will pay 99.00 DKK per month until Christmas 2025, after which the subscription continues at 299.00 DKK per month. ```json title="POST /public/api/v1/cit" { "pointOfSaleId": "01924737-9c18-71c0-ab1a-88698eaceabf", "amount": 9900, "currency": "DKK", "subscription": { "type": "SCHEDULED", "amount": 29900, "interval": { "period": "MONTH", "frequency": 1 } }, "attributes": { "vippsMobilePayCampaign": { "type": "EVENT_CAMPAIGN", "price": 9900, "eventDate": "2025-12-25T00:00:00Z", "eventText": "until Christmas" } } } ``` # Vipps MobilePay Subscription Cancelled Webhook (/guides/vipps-mobilepay-subscription-cancelled-webhook) ## Create the webhook [#create-the-webhook] Create a webhook using [`POST /public/api/v1/webhooks`](/api/create-webhook) and subscribe it to `subscription.disabled.v1`. ```json title="POST /public/api/v1/webhooks" { "url": "https://merchant.example.com/webhooks/subscription-disabled", "events": ["subscription.disabled.v1"], "secret": "Bearer webhook-secret-value" } ``` Example response: ```json title="200 OK" { "webhook": { "id": "019713fd-c838-79c8-8dbf-ef5df2817d65", "url": "https://merchant.example.com/webhooks/subscription-disabled", "events": ["subscription.disabled.v1"], "pausedAt": null, "pauseReason": null, "createdAt": "2026-05-27T10:15:00Z" } } ``` ## Example webhook [#example-webhook] When the subscription is cancelled by the customer, ePay will send a webhook like this. The subscription's `statusReason` explains why it reached the `DISABLED` state. For subscriptions closed by a cardholder in a wallet app such as Vipps MobilePay, its value is `CLOSED_BY_CARDHOLDER`. ```http title="POST https://merchant.example.com/webhooks/subscription-disabled" Authorization: Bearer webhook-secret-value Content-Type: application/json ``` *** ```json title="Webhook body" { "event": "subscription.disabled.v1", "data": { "subscription": { "id": "01997047-ead3-7cdb-9501-d226789ed32b", "createdAt": "2025-09-22T07:16:25.427852Z", "paymentMethodId": "01997048-1038-772e-99de-3a467ce1babd", "pointOfSaleId": "01932061-c660-714c-bfea-f6a9c1f4f35b", "currency": "DKK", "customerId": "customer-123", "reference": "subscription-1", "description": "MobilePay subscription", "expiryDate": null, "state": "DISABLED", "statusReason": "CLOSED_BY_CARDHOLDER", "type": "SCHEDULED", "interval": { "frequency": 1, "period": "MONTH" } } } } ``` ## Notes [#notes] * `statusReason` is nullable and is currently only provided for subscriptions that reach the `DISABLED` state. Possible values are `CLOSED_BY_MERCHANT_API`, `CLOSED_BY_MERCHANT_BACKOFFICE`, `CLOSED_BY_CARDHOLDER`, and `CLOSED_BY_EPAY`. * ePay expects a `200 OK` response. Otherwise the webhook will be retried. * The `Authorization` header contains the secret configured on the webhook and should be validated by your system.