With Checkout
Use this guide if you want ePay to host the payment window.
Checkout is the simplest custom integration. Your backend creates a payment session, and ePay returns a payment window URL. You redirect the customer to that URL so they can complete the payment.
What you will build
In this guide, you will:
Create a payment session from your backend
Redirect the customer to ePay Checkout
Complete a test payment
Receive the payment result
Confirm that your order can be updated correctly
How Checkout works
Checkout uses a hosted payment window.
Your system creates the payment. ePay handles the payment page.
The basic flow is:
Customer starts checkout on your website
Your backend creates a payment session with ePay
ePay returns a payment window URL
Your website redirects the customer to the payment window
Customer completes the payment
ePay redirects the customer back to your website
ePay sends the payment result to your backend
Example: Open the hosted Checkout page
This example creates a payment and shows a button that opens the returned Checkout URL.
Before you start
You need:
- An API key
- A Point of Sale ID
- A backend endpoint that can create payments
- A success URL
- A failure URL
- A notification URL
Your API key must only be used server-side. Never expose your API key in frontend code.
For the standard Checkout redirect flow, your backend authenticates the session creation request with your API key.
Step 1: Create an order in your system
Before creating the payment in ePay, create an order in your own system.
Set the order status to pending.
const order = await createOrder({ amount: 10000, currency: "DKK", status: "pending",});The order should not be marked as paid yet.
Step 2: Create a payment session
Create a payment session from your backend.
API referencePOSTInitialize Payment Session/public/api/v1/citInitializes a new payment session with the ePay Payments API.const body = JSON.stringify({ "pointOfSaleId": "<POINT_OF_SALE_ID>", "amount": 10000, "currency": "DKK", "reference": "ORDER-1001", "notificationUrl": "https://example.com/api/epay/notification", "successUrl": "https://example.com/payment/success", "failureUrl": "https://example.com/payment/failure", "scaMode": "NORMAL", "timeout": 60, "instantCapture": "OFF", "maxAttempts": 3})fetch("https://payments.epay.eu/public/api/v1/cit", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer <TEST_API_KEY>", "Idempotency-Key": "<UUID>" }, body})Use your actual ePay endpoint and the fields your checkout flow requires.
Step 3: Understand the request body
pointOfSaleId, amount, and currency are always required in the request body.
The full schema below also includes advanced fields such as customer, orderLines, subscription, allowedPaymentMethods, returnUrl, retryUrl, generateQrCode, reportExpired, dynamicAmount, and ageVerification.
The table below covers the request fields used when initializing a payment session.
Prop
Type
pointOfSaleIduuid
amountinteger
2.50 DKK must be sent as 250. If the amount is 0, a token is created without charging the customer.currencystring
DKK.reference?string | null
36.Type
- is removed if present, and if the value is longer than 12 characters, only the left part is used.transactionType?PAYMENT | MOTO | null
Type
PAYMENT is the normal checkout flow. MOTO is for Mail Order / Telephone Order and requires a special acquirer agreement.Default
PAYMENTscaMode?SKIP | NORMAL | FORCE | null
Type
SKIP does not attempt 3DS and leaves liability with the merchant. NORMAL attempts 3DS normally. FORCE requests a challenge flow, although third parties in the 3DS chain may still ignore it.timeout?integer | null
Type
1, maximum 120.instantCapture?OFF | VOID | NO_VOID | null
Type
OFF does not capture immediately. VOID captures immediately and voids the authorization if capture fails. NO_VOID captures immediately and keeps the authorization if capture fails.allowedPaymentMethods?string[] | null
Type
VIPPS_MOBILEPAY is allowed, the customer is redirected directly to that method. If they cancel, they return to returnUrl or failureUrl.exemptions?string[] | null
Type
TRA and LVT.maxAttempts?integer | null
Type
1, maximum 25.generateQrCode?boolean | null
Default
falsereportFailure?boolean | null
reportExpired?boolean
Default
falsedynamicAmount?boolean | null
Type
Default
falsenotificationUrl?uri | null
Type
1024. Supports URL templating.preAuthUrl?uri | null
Type
1024.returnUrl?uri | null
Type
1024. Supports URL templating.successUrl?uri | null
Type
1024.failureUrl?uri | null
Type
1024.retryUrl?uri | null
Type
failureUrl. Maximum length 1024.textOnStatement?string | null
Type
1, maximum length 39.attributes?object | null
Type
1kb.customerId?string | null
Type
customer?object | null
Type
orderLines?object[] | null
Type
1000 items. See the orderLines table below.subscription?object | null
Type
scaMode is always set to FORCE.ageVerification?object | null
Type
Prop
Type
firstName?string | null
lastName?string | null
birthdate?date | null
email?string | null
phoneNumber?string | null
Type
+45 12345678.shippingAddress?object | null
Type
countryCode, postalCode, city, line1, and optional line2 / line3. countryCode must be ISO 3166-1 alpha-2.billingAddress?object | null
Type
shippingAddress. If billing and shipping are the same, send both with identical values.Prop
Type
descriptionstring
imageUrl?url | null
Type
1024. A minimum resolution of 250x250 is recommended.quantityinteger
Type
totalAmountinteger
unitPriceinteger
typePHYSICAL | DISCOUNT | SHIPPING_FEE | SALES_TAX | DIGITAL | GIFT_CARD | STORE_CREDIT | SURCHARGE | OTHER
Prop
Type
id?uuid | null
amount?integer | null
Type
amount to 0 and set subscription.amount to the future recurring amount.typeSCHEDULED | UNSCHEDULED
Type
SCHEDULED is for fixed intervals such as monthly billing. UNSCHEDULED is for pay-as-you-go charging.reference?string | null
expiryDate?date | null
interval?object | null
Type
period (DAY, WEEK, MONTH, YEAR) and frequency (1-31). Omit for UNSCHEDULED.billingAgreement?object | null
Type
billingPlanId and optional reference / nextChargeAt. Mutually exclusive with subscription.id and subscription.interval.Prop
Type
minimumAgeinteger
Type
0, maximum 99.country?DK | null
Type
MitID.POS defaults and request overrides
scaMode, timeout, instantCapture, maxAttempts, notificationUrl, successUrl, failureUrl, and exemptions. If a value is sent in the API request, it overrides the Point of Sale default for that session.scaMode: "SKIP" outside tests. Many acquirers do not accept payments without 3D Secure data, and liability can shift to the merchant.Step 4: Inspect the response
The response contains the values needed to continue the flow.
For Checkout, the important field is paymentWindowUrl, because that is the URL you redirect the customer to. The javascript and key fields are mainly relevant for Blocks or other embedded flows. If you set generateQrCode to true, the response also includes qrCode.
{ "paymentWindowUrl": "https://payments.epay.eu/payment-window?sessionId=0192473a-e382-79a9-bfc2-65da88fe812f&sessionKey=4651656e-f29e-4dfa-a1cd-a65647862011", "javascript": "https://payments.epay.eu/sessions/0192473a-e382-79a9-bfc2-65da88fe812f/client.js", "key": "4651656e-f29e-4dfa-a1cd-a65647862011", "session": { "id": "0192473a-e382-79a9-bfc2-65da88fe812f", "subscriptionId": "01929a94-5fce-7ccc-a7e4-7e9249133b39", "amount": 1000, "attributes": { "key1": "value1", "key2": "value2" }, "exemptions": ["TRA"], "currency": "DKK", "instantCapture": "OFF", "maxAttempts": 3, "reportFailure": false, "dynamicAmount": false, "pointOfSaleId": "0192473a-e381-705c-b61c-fc2ac9624afc", "reference": "ORDER-1001", "state": "PENDING", "scaMode": "NORMAL", "timeout": 60, "notificationUrl": "https://example.com/api/epay/notification", "preAuthUrl": "https://example.com/pre-auth", "successUrl": "https://example.com/payment/success", "failureUrl": "https://example.com/payment/failure", "textOnStatement": "The text", "createdAt": "2024-10-01T10:38:14.658688472+02:00", "expiresAt": "2024-10-01T12:41:14.658688472+02:00" }}Prop
Type
qrCode?string | null
paymentWindowUrluri
javascripturi
keystring
sessionobject
Type
session
Prop
Type
iduuid
subscriptionId?uuid | null
amountinteger
attributes?object | null
exemptions?string[]
allowedPaymentMethods?string[] | null
currencystring
expiresAtdate-time
instantCapture?OFF | VOID | NO_VOID
maxAttempts?integer
attempts?integer
minimumAge?integer | null
ageVerified?boolean | null
reportFailure?boolean
reportExpired?boolean
dynamicAmount?boolean
notificationUrl?uri
preAuthUrl?uri | null
successUrl?uri
returnUrl?uri | null
failureUrl?uri
retryUrl?uri | null
customerId?string | null
pointOfSaleId?uuid
reference?string | null
state?PENDING | PROCESSING | COMPLETED | EXPIRED
textOnStatement?string
scaMode?SKIP | NORMAL | FORCE
timeout?integer
createdAt?date-time
Step 5: Redirect the customer
The response contains the information needed to open the payment window.
Redirect the customer to the payment window.
return redirect(payment.paymentWindowUrl);The customer now completes the payment on the ePay-hosted payment page.
Optional: Use URL templating
Redirect and notification URLs can include dynamic values from the session or transaction.
This is especially useful for notificationUrl, successUrl, failureUrl, retryUrl, and returnUrl.
Example:
{ "successUrl": "https://example.com/payment/success/${transaction.id}", "failureUrl": "https://example.com/payment/failure/${session.id}", "notificationUrl": "https://example.com/api/epay/notification/${transaction.reference}"}Supported template values include:
| Key | Description |
|---|---|
${transaction.id} | The ID of the transaction |
${transaction.reference} | The reference supplied when creating the session |
${session.id} | The ID of the payment session |
Unknown template keys are rejected during request validation.
Optional: Override the default payment window
In some cases, you may want to use a specific payment window configuration instead of the default configuration defined on your Point of Sale.
This lets you choose another visual or functional checkout setup for a single payment session without changing the POS defaults in the ePay backoffice.
When you append paymentWindowId to the returned paymentWindowUrl, the payment window configuration associated with that ID is used instead of the POS default, as long as the payment window belongs to the same account as the session.
Common use cases:
- Apply campaign-specific branding or themes
- Use a simplified flow for selected customer groups
- Test a new payment window configuration without updating the POS default
Example:
https://payments.epay.eu/payment-window?sessionId=0192473a-e382-79a9-bfc2-65da88fe812f&sessionKey=4651656e-f29e-4dfa-a1cd-a65647862011&paymentWindowId=01234567-89ab-cdef-0123-456789abcdefImportant notes:
- The provided
paymentWindowIdmust belong to the same account as the Point of Sale used for the payment session. - If
paymentWindowIdis invalid or does not exist, the default payment window configuration is used. - The override happens entirely by modifying the returned
paymentWindowUrl; you do not need to change the session initialization request.
Step 6: Complete a test payment
Use a test card to complete the payment.
A successful test payment should redirect the customer to your success URL.
A failed payment should redirect the customer to your failure URL or allow the customer to try again, depending on your setup.
Step 7: Handle the payment result
The customer may close the browser before returning to your website.
Use your notification URL or webhook to update the order in your backend.
Do not mark the order as paid only because the customer lands on your success page.
DocumentationHandle notification URLs and webhooks correctlyUse the full guide for webhook payloads, authorization validation, retries, duplicate handling, and safe order updates.Recommended order flow:
Create order as pending
Create ePay payment
Redirect customer to Checkout
Receive payment result server-side
Verify the result
Mark order as paid
Step 8: Show the final status to the customer
Your success page should show the customer that the payment was completed or is being confirmed.
If your backend has not processed the payment result yet, show a neutral message such as:
Your payment has been received and is being confirmed.
Avoid showing a final paid state before your backend has confirmed the payment result.
Common problems
I get unauthorized when creating the payment
Check that:
- You are using the correct API key
- The API key belongs to the correct environment
- The request is made from your backend
- The authorization header is formatted correctly
If the request still fails, use the error code and response body to identify whether the issue is authentication, validation, or Point of Sale configuration.
GuideError CodesReference the most common ePay API error codes, their meaning, and how they should be handled in client integrations.The payment window does not open
Check that:
- The payment session was created successfully
- You are redirecting to the correct payment window URL
- The Point of Sale ID is correct
- The amount and currency are valid
The customer returns, but my order is not paid
Check that:
- Your notification URL is publicly available
- Your backend receives the notification
- Your backend matches the payment to the correct order
- Your system updates the order based on the server-side result
What you built
You have now created your first ePay Checkout payment.
You created a payment session from your backend, redirected the customer to ePay and completed a test payment.