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
How Link works
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
Example: Send a payment link or show it as a QR code
This is a simple example of how the returned payment link can be presented to the customer.
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.
Step 2: Create a payment link
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
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.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
sessionIduuid
urluri
qrCodestring
You can now send the url to the customer or display the qrCode.
Step 5: Send or display the payment link
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:
| 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: Advanced payment link settings
The payment link API supports additional settings for more advanced payment flows.
Examples include:
| Field | Description |
|---|---|
scaMode | Controls how 3D Secure is handled |
instantCapture | Controls whether the payment should be captured instantly |
exemptions | Defines exemptions such as Transaction Risk Analysis |
textOnStatement | Sets the text shown on the cardholder's bank statement |
attributes | Pass-through attributes returned in webhooks |
customerId | Merchant-defined customer ID |
maxAttempts | Maximum number of payment attempts |
reportFailure | Enables webhook notifications for failed transactions |
dynamicAmount | Allows the customer to enter the payment amount |
preAuthUrl | Enables pre-authorization webhooks |
subscription | Creates 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
I get unauthorized when creating the payment link
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
The payment link does not open
Check that:
- The payment link was created successfully
- You are using the
urlfrom 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
The payment link expires too soon
Check that:
- The
timeoutvalue 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.