Looking for ePay classic docs? Go to docs.epay.dk
ePay documentationDocsePay documentation

With Link

Use this guide if you want to create a payment link that can be sent to a customer or displayed as a QR code.

Link is a fast way to accept payments without building a full checkout flow. Your backend creates a payment link, and ePay returns a unique URL and a base64-encoded QR code that opens ePay Checkout.

You can also create a Link directly from the ePay backoffice for manual payment flows.

You can send the link by email, SMS, invoice, chat, or show the QR code on-screen.

What you will build

In this guide, you will:

Create an order or payment request in your system

Create a payment link from your backend

Send or display the payment link

Complete a test payment

Receive the payment result

Confirm that your order can be updated correctly

Link creates a hosted payment session that can be opened through a URL or QR code.

Your system creates the payment link. ePay handles the payment page.

The basic flow is:

Customer places an order, receives an invoice, or needs to pay

Your backend creates a payment link with ePay

ePay returns a payment link URL and QR code

You send the link to the customer or display the QR code

Customer opens the link and completes the payment in ePay Checkout

ePay redirects the customer back to your website if redirect URLs are provided

ePay sends the payment result to your backend

This is a simple example of how the returned payment link can be presented to the customer.

script.js
// -------------------------------// Payment Request Setup// -------------------------------// Create headers for the payment requestconst headers = new Headers();const demoApiKey = "test_93dc98ae-ff87-4671-a821-cda33fdfc26e"; // Demo API key: NEVER use this client-side in productionheaders.append("Content-Type", "application/json");headers.append("Accept", "application/json");headers.append("Authorization", `Bearer ${demoApiKey}`);// Define the payment payload dataconst paymentData = {  pointOfSaleId: "0195a362-5bf4-7bb9-a419-2367fda11e6f",  amount: 100, // amount in minor units (e.g. øre)  currency: "DKK",};// Setup the fetch optionsconst requestOptions = {  method: "POST",  headers: headers,  body: JSON.stringify(paymentData),  redirect: "follow",};// -------------------------------// Helper: Render Link + QR Code// -------------------------------/** * Renders a clickable link and a QR code inside a container. * * @param {string} url - The payment URL. * @param {string} qrBase64 - The QR code as a Base64 string. */function renderPaymentLinkAndQr(url, qrBase64) {  // Create a wrapper div  const container = document.createElement("div");  container.classList.add("payment-container");  // Create the link element  const linkEl = document.createElement("a");  linkEl.classList.add("payment-link");  linkEl.href = url;  linkEl.target = "_blank";  linkEl.textContent = url; // display the URL itself (you can customize to 'Pay Here' if you prefer)  container.appendChild(linkEl);  // Create the QR code image element  const imgEl = document.createElement("img");  imgEl.classList.add("payment-qr");  imgEl.src = `data:image/png;base64,${qrBase64}`;  imgEl.alt = "Payment QR Code";  container.appendChild(imgEl);  // Append the entire container to the body  document.body.appendChild(container);}// -------------------------------// Request to /payment-links// -------------------------------fetch("https://payments.epay.eu/public/api/v1/payment-links", requestOptions)  .then((response) => {    if (!response.ok) throw new Error(`HTTP ${response.status}`);    return response.json();  })  .then((result) => {    console.log("Payment-links response:", result);    // Expect result to have { url: "...", qrCode: "base64..." }    const { url, qrCode } = result;    // Render the payment link and QR code    renderPaymentLinkAndQr(url, qrCode);  })  .catch((error) => {    console.error("Error during payment-links request:", error);    // Optionally display an error message in the UI  });

Before you start

You need:

  • An API key
  • A Point of Sale ID
  • A backend endpoint that can create payment links
  • A notification URL
  • A success URL
  • A failure URL

Your API key must only be used server-side. Never expose your API key in frontend code.

Step 1: Create an order in your system

Before creating the payment link in ePay, create an order or payment request 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.

Create a payment link from your backend.

API referencePOSTCreate Payment Link/public/api/v1/payment-linksCreates a new payment link 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"})fetch("https://payments.epay.eu/public/api/v1/payment-links", {  method: "POST",  headers: {    "Content-Type": "application/json",    "Authorization": "Bearer <TEST_API_KEY>",    "Idempotency-Key": "<UUID>"  },  body})

Use your own order reference so you can match the ePay payment link to the order in your system.

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 creating a payment link.

Prop

Type

pointOfSaleIduuid
Id of the point of sale to associate the payment with.
amountinteger
The amount of the payment in minor units. Example: 2.50 DKK must be sent as 250. If the amount is 0, a token is created without charging the customer.
currencystring
The currency code of the payment using ISO 4217 alpha-3, for example DKK.
reference?string | null
Your own payment or order reference. It should be unique per payment. Only ASCII alphanumeric characters and dashes are allowed, with a max length of 36.

Type

Worldline-specific behavior: - is removed if present, and if the value is longer than 12 characters, only the left part is used.
transactionType?PAYMENT | MOTO | null
The type of transaction to process. Most merchants should use PAYMENT.

Type

PAYMENT is the normal checkout flow. MOTO is for Mail Order / Telephone Order and requires a special acquirer agreement.

Default

PAYMENT
scaMode?SKIP | NORMAL | FORCE | null
Controls how 3D Secure is handled.

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
How long the session stays valid in minutes.

Type

Minimum 1, maximum 120.
instantCapture?OFF | VOID | NO_VOID | null
Controls whether the payment is captured immediately.

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
Limits which payment methods are shown for this session.

Type

If only one redirect-based method such as VIPPS_MOBILEPAY is allowed, the customer is redirected directly to that method. If they cancel, they return to returnUrl or failureUrl.
exemptions?string[] | null
Exemptions to apply when possible. Using exemptions can shift fraud liability from the issuer to the merchant.

Type

Supported values are TRA and LVT.
maxAttempts?integer | null
Maximum allowed payment attempts for the session.

Type

Minimum 1, maximum 25.
generateQrCode?boolean | null
If true, the response includes a Base64 QR code version of the payment window URL.

Default

false
reportFailure?boolean | null
If true, failed transaction attempts are also reported to notificationUrl.
reportExpired?boolean
If true, sessions that reach the expired state are also reported to notificationUrl.

Default

false
dynamicAmount?boolean | null
Lets the cardholder define the transaction amount instead of using the initialized amount.

Type

This gives client-side control over the amount and is only meant for special cases. Must be enabled by ePay support.

Default

false
notificationUrl?uri | null
The URL where ePay sends payment status notifications and webhooks.

Type

Maximum length 1024. Supports URL templating.
preAuthUrl?uri | null
Enables pre-authorization webhooks before authorization is attempted.

Type

Maximum length 1024.
returnUrl?uri | null
Where the browser is returned when the customer clicks the back button in the payment window.

Type

Checkout only. Overrides the payment window default. Maximum length 1024. Supports URL templating.
successUrl?uri | null
Where the customer is redirected after a successful payment.

Type

Maximum length 1024.
failureUrl?uri | null
Where the customer is redirected when no more payment attempts are allowed.

Type

Maximum length 1024.
retryUrl?uri | null
Optional redirect for failed attempts when more attempts are still allowed.

Type

If omitted, the customer is redirected to failureUrl. Maximum length 1024.
textOnStatement?string | null
Text shown on the cardholder bank statement.

Type

Minimum length 1, maximum length 39.
attributes?object | null
Pass-through attributes returned back to the merchant on webhooks.

Type

Maximum total size is 1kb.
customerId?string | null
Merchant-defined customer identifier that uniquely represents the customer in your own system.

Type

Required for stored payment methods and enhanced age verification. Do not use guest IDs.
customer?object | null
Customer details used during SCA / 3DS and by payment methods such as Klarna.

Type

See the customer table below.
orderLines?object[] | null
Line items that make up the purchase. Used by some payment methods such as Klarna.

Type

Maximum 1000 items. See the orderLines table below.
subscription?object | null
Creates or updates a reusable subscription from the same CIT checkout flow.

Type

See the subscription table below. When a subscription is initialized, scaMode is always set to FORCE.
ageVerification?object | null
Requires the customer to verify that they meet a minimum age before the transaction can continue.

Type

See the ageVerification table below.
customer

Prop

Type

firstName?string | null
The first name of the paying customer.
lastName?string | null
The last name of the paying customer.
birthdate?date | null
The date of birth of the customer in YYYY-MM-DD format.
email?string | null
The email address of the paying customer.
phoneNumber?string | null
The phone number of the paying customer.

Type

Use E.164 with a single space between country code and local number, for example +45 12345678.
shippingAddress?object | null
Shipping address of the purchase. Leave empty when nothing is shipped.

Type

Uses countryCode, postalCode, city, line1, and optional line2 / line3. countryCode must be ISO 3166-1 alpha-2.
billingAddress?object | null
Billing address of the customer.

Type

Uses the same address shape as shippingAddress. If billing and shipping are the same, send both with identical values.
orderLines[]

Prop

Type

descriptionstring
Descriptive name of the order line item.
imageUrl?url | null
Optional image URL that can be used in merchant-provider communications such as Klarna flows.

Type

Maximum length 1024. A minimum resolution of 250x250 is recommended.
quantityinteger
Quantity of the order line item.

Type

Must be a non-negative number.
totalAmountinteger
Total order line amount in minor units, including tax and discounts.
unitPriceinteger
Single-item price in minor units, before discount and typically including taxes.
typePHYSICAL | DISCOUNT | SHIPPING_FEE | SALES_TAX | DIGITAL | GIFT_CARD | STORE_CREDIT | SURCHARGE | OTHER
Type of the order line item.
subscription

Prop

Type

id?uuid | null
Optional subscription ID to update an existing active subscription with new payment details.
amount?integer | null
Optional recurring amount in minor units when it differs from the current checkout amount.

Type

If the customer should not pay now, set top-level amount to 0 and set subscription.amount to the future recurring amount.
typeSCHEDULED | UNSCHEDULED
Subscription type.

Type

SCHEDULED is for fixed intervals such as monthly billing. UNSCHEDULED is for pay-as-you-go charging.
reference?string | null
Optional merchant-defined subscription reference. Falls back to the session reference if omitted.
expiryDate?date | null
Optional subscription expiry date used during 3DS. ePay does not reject charges after this date.
interval?object | null
Optional subscription schedule for SCHEDULED subscriptions.

Type

Contains period (DAY, WEEK, MONTH, YEAR) and frequency (1-31). Omit for UNSCHEDULED.
billingAgreement?object | null
Optional automatic billing agreement created from the subscription.

Type

Contains required billingPlanId and optional reference / nextChargeAt. Mutually exclusive with subscription.id and subscription.interval.
ageVerification

Prop

Type

minimumAgeinteger
Minimum required age for the purchase.

Type

Minimum 0, maximum 99.
country?DK | null
Country code deciding which electronic identity method is presented to the customer.

Type

For Danish customers this is typically verified with MitID.

POS defaults and request overrides

In Advanced Point of Sale Settings in the ePay backoffice, you can configure defaults for 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.
Avoid using scaMode: "SKIP" outside tests. Many acquirers do not accept payments without 3D Secure data, and liability can shift to the merchant.
DocumentationUse pre-authorization webhooks for risk checks and dynamic updatesReview the full pre-auth flow, payload shape, response fields, and the tradeoffs before enabling preAuthUrl on a payment session.

The reference should make it easy to match the payment link to the order in your own system.

Step 4: Inspect the response

The response contains the created payment link and a QR code.

{  "id": "KSNMWEWZPPA",  "sessionId": "01975f11-329f-7c14-a03a-3739d8b48ef3",  "url": "https://payments.epay.eu/link/KSNMWEWZPPA",  "qrCode": "/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDA..."}

Prop

Type

idstring
The ID of the created payment link.
sessionIduuid
The ID of the underlying payment session created in ePay.
urluri
The direct URL that opens ePay Checkout for this payment link.
qrCodestring
A Base64-encoded QR code version of the payment link URL for device-to-device sharing.

You can now send the url to the customer or display the qrCode.

How you use the payment link depends on your flow.

Common examples:

  • Send the link by email
  • Send the link by SMS
  • Add the link to an invoice
  • Show the link in your own admin system
  • Display the QR code on-screen
  • Print the QR code on a document or receipt

Example:

await sendPaymentEmail({  to: customer.email,  orderReference: order.reference,  paymentUrl: paymentLink.url,});

If you display the QR code, decode the base64 value and render it as an image in your frontend.

<img  src={`data:image/png;base64,${paymentLink.qrCode}`}  alt="Scan to pay"/>

Step 6: Complete a test payment

Open the payment link and use a test card to complete the payment.

A successful test payment should redirect the customer to your success URL if one was provided.

A failed payment should redirect the customer to your failure URL when no more payment attempts are allowed.

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 link

Send or display the payment link

Customer opens the link and pays

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.

Optional: Set a custom expiry

Payment links expire after 60 days by default.

You can define a custom timeout in minutes.

{  "timeout": 1440}

The example above makes the payment link valid for 24 hours.

The minimum timeout is 10 minutes.

If the customer opens the payment window and leaves it open, the active checkout session can expire before the payment link itself expires. If that happens, the customer can open the payment link again as long as the payment link is still valid.

Optional: Use URL templating

Redirect and notification URLs can include dynamic values from the session or transaction.

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.id}"}

Supported template values include:

KeyDescription
${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.

The payment link API supports additional settings for more advanced payment flows.

Examples include:

FieldDescription
scaModeControls how 3D Secure is handled
instantCaptureControls whether the payment should be captured instantly
exemptionsDefines exemptions such as Transaction Risk Analysis
textOnStatementSets the text shown on the cardholder's bank statement
attributesPass-through attributes returned in webhooks
customerIdMerchant-defined customer ID
maxAttemptsMaximum number of payment attempts
reportFailureEnables webhook notifications for failed transactions
dynamicAmountAllows the customer to enter the payment amount
preAuthUrlEnables pre-authorization webhooks
subscriptionCreates or updates a subscription for later payments

For your first payment link, start with the required fields and add advanced settings only when your payment flow needs them.

Common problems

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

Check that:

  • The payment link was created successfully
  • You are using the url from the response
  • The Point of Sale ID is correct
  • The amount and currency are valid
  • The payment link has not expired

The QR code does not work

Check that:

  • The QR code value is decoded as base64
  • The image is rendered with the correct MIME type
  • The payment link has not expired
  • The QR code has not been modified or truncated

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

Check that:

  • The timeout value is set in minutes
  • The timeout is at least 10 minutes
  • Your Point of Sale settings do not define another timeout
  • Your API request contains the timeout you expect

What you built

You have now created your first Link payment.

You created a payment link from your backend, received a payment URL and QR code, and completed a test payment through ePay Checkout.

Next steps

How is this guide?

On this page