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

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

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

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

This example creates a payment session, mounts the secure payment fields, and shows a payment button in your own UI.

script.js
// -------------------------------// Hosted Fields Callback Functions// -------------------------------function clientReadyCallback(event) {  console.log("Hosted fields client is ready:", event);}function invalidSessionCallback(event) {  console.error("Invalid session detected:", event);  displayError("Invalid session detected", event);}function challengeIssuedCallback(event) {  console.log("A challenge has been issued:", event);}function transactionAcceptedCallback(event) {  onTransactionComplete(true);}function transactionDeclinedCallback(event) {  onTransactionComplete(false);}function clientRedirectCallback(event) {  return false; // Skipping redirects in the demo preview}function invalidInputCallback(event) {  console.error("Invalid input encountered:", event);  displayError("Invalid input encountered", event);}function inputValidityCallback(event) {  console.log("Input validity changed:", event);}function inputSubmitCallback(event) {  console.log("Input submitted:", event);}function sessionExpiredCallback(event) {  console.error("Session has expired:", event);  displayError("Session has expired", event);}function errorCallback(event) {  console.error("An error occurred in hosted fields:", event);  displayError("An error occurred in hosted fields", event);}// -------------------------------// Helper: Create UI Containers// -------------------------------function createUi() {  const fields = document.createElement("div");  fields.id = "fields";  const errorBlock = document.createElement("div");  errorBlock.id = "error-block";  document.body.appendChild(fields);  document.body.appendChild(errorBlock);  return { fields, errorBlock };}// -------------------------------// Helper: Display Errors// -------------------------------/** * Appends a formatted error message to the error block. * * @param {string} description - A human-readable error description. * @param {object} event - The event object containing error details. */function displayError(description, event) {  const errorBlock = document.getElementById("error-block");  errorBlock.style.display = "block";  let title = "";  if (event && event.type) {    title += event.type;  }  if (event && event.errorCode) {    title += title ? " - " + event.errorCode : event.errorCode;  }  if (!title) {    title = "Error";  }  if (event && event.event) {    title += ` (Event: ${event.event})`;  } else {    title += " (Event: N/A)";  }  const errorItem = document.createElement("div");  errorItem.classList.add("error-item");  const titleEl = document.createElement("div");  titleEl.classList.add("error-title");  titleEl.textContent = title;  const descriptionEl = document.createElement("div");  descriptionEl.classList.add("error-description");  descriptionEl.textContent =    description ||    (event && event.message ? event.message : "No further details provided.");  errorItem.appendChild(titleEl);  errorItem.appendChild(descriptionEl);  errorBlock.appendChild(errorItem);}// -------------------------------// Payment Request Setup// -------------------------------const 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}`);const paymentData = {  pointOfSaleId: "0195a362-5bf4-7bb9-a419-2367fda11e6f",  amount: 100, // minor amount  currency: "DKK",  scaMode: "SKIP",};const requestOptions = {  method: "POST",  headers: headers,  body: JSON.stringify(paymentData),  redirect: "follow",};// -------------------------------// Helper: Dynamically Load External Module// -------------------------------/** * Dynamically loads an external script module. * * @param {string} scriptUrl - The URL of the external module. * @param {Function} onLoadCallback - A callback function to run once the module loads. */function loadExternalModule(scriptUrl, onLoadCallback) {  const scriptEl = document.createElement("script");  scriptEl.type = "module";  scriptEl.src = scriptUrl;  scriptEl.onload = onLoadCallback;  document.body.appendChild(scriptEl);  console.log("Dynamically loading external script:", scriptUrl);}// -------------------------------// Helper: Create Payment Button// -------------------------------/** * Creates and appends a payment button to the DOM. * The button text is set dynamically using the converted amount (major) and currency. * * @param {number} amount - The payment amount (in minor units). * @param {string} currency - The currency code. */function createPaymentButton(amount, currency) {  const majorAmount = (amount / 100).toFixed(2);  const button = document.createElement("button");  button.type = "button";  button.id = "payment-button";  button.textContent = `Pay ${majorAmount} ${currency}`;  button.addEventListener("click", () => {    epay.createCardTransaction();  });  const fieldsContainer = document.getElementById("fields");  fieldsContainer.insertAdjacentElement("afterend", button);}// -------------------------------// Helper: Handle Transaction Completed// -------------------------------/** * Function to handle the display after a transaction is completed. * * @param {boolean} success - Indicates whether the transaction was successful. */function onTransactionComplete(success) {  const fieldsContainer = document.getElementById("fields");  const paymentButton = document.getElementById("payment-button");  fieldsContainer.style.display = "none";  paymentButton.style.display = "none";  const messageContainer = document.createElement("div");  messageContainer.id = "transaction-message";  messageContainer.style.marginTop = "20px";  if (success) {    messageContainer.innerHTML = `      <h2>Payment Success</h2>      <p>Your transaction has been completed successfully.</p>    `;  } else {    messageContainer.innerHTML = `      <h2>Payment Failed</h2>      <p>We're sorry, but your transaction could not be completed.</p>    `;  }  if (fieldsContainer && fieldsContainer.parentNode) {    fieldsContainer.parentNode.appendChild(messageContainer);  } else {    document.body.appendChild(messageContainer);  }}// -------------------------------// Payment Request & Hosted Fields Initialization// -------------------------------createUi();fetch("https://payments.epay.eu/public/api/v1/cit", requestOptions)  .then((response) => response.json())  .then((result) => {    console.log("Payment response:", result);    const sessionId = result.session.id;    const sessionKey = result.key;    const amount = result.session.amount;    const currency = result.session.currency;    loadExternalModule(result.javascript, () => {      epay        .setSessionId(sessionId)        .setSessionKey(sessionKey)        .setCallbacks({          clientReady: clientReadyCallback,          invalidSession: invalidSessionCallback,          challengeIssued: challengeIssuedCallback,          transactionAccepted: transactionAcceptedCallback,          transactionDeclined: transactionDeclinedCallback,          clientRedirect: clientRedirectCallback,          invalidInput: invalidInputCallback,          inputValidity: inputValidityCallback,          inputSubmit: inputSubmitCallback,          sessionExpired: sessionExpiredCallback,          error: errorCallback,        })        .init();      epay.mountFields("fields", {        // The configuration object can be added here when needed.      });      createPaymentButton(amount, currency);    });  })  .catch((error) => {    console.error("Error during payment request:", error);    displayError("Error during payment request", error);  });

Before you start

You need:

  • An API key
  • A Point of Sale ID
  • A backend endpoint that can create payment sessions
  • A frontend checkout page
  • ePay.js
  • A notification URL or webhook

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

Create an order in your own system before creating the payment session.

Set the order status to pending.

const order = await createOrder({  amount: 10000,  currency: "DKK",  status: "pending",});

Step 2: Create a payment session

Create the 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"})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})

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
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.

Step 4: Inspect the response and return frontend-safe values

For Blocks, the important response fields are:

Prop

Type

sessionobject
The created payment session. Use session.id as the client-side session identifier.

Type

See the session table on the Checkout page for the full shape.
keystring
The session-specific client secret used by ePay.js for embedded flows.
javascripturi
The ePay.js client URL for this exact session. Load this URL fresh for each new session.

Example:

{  "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:

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

Load ePay.js on the page where you want to show the payment form.

<script  type="module"  src="<JAVASCRIPT_URL>"></script>

Use the actual javascript URL returned by the payment session response.

ePay.js referenceOpen the full ePay.js documentationUse the full reference for the complete callback list, mountFields options, stored payment methods, wallet helpers, and advanced transaction methods.

Step 6: Add Blocks

Create the container where Blocks should be mounted.

<div id="fields"></div>

Then initialize the client and mount the hosted fields.

<script>  epay    .setSessionId("<SESSION_ID>")    .setSessionKey("<SESSION_KEY>")    .setCallbacks({      clientReady: clientReadyCallback,      invalidSession: invalidSessionCallback,      challengeIssued: challengeIssuedCallback,      transactionAccepted: transactionAcceptedCallback,      transactionDeclined: transactionDeclinedCallback,      feeUpdated: feeUpdatedCallback,      clientRedirect: clientRedirectCallback,      invalidInput: invalidInputCallback,      inputValidity: inputValidityCallback,      inputSubmit: inputSubmitCallback,      sessionExpired: sessionExpiredCallback,      error: errorCallback,    })    .init();  epay.mountFields("fields", {    theme: "default",    language: "da",    variables: {      colorText: "#2e3033",    },  });<\/script>

The actual card data is handled securely by ePay, so your own system should never handle raw card details.

Use the same sessionId and sessionKey returned in Step 4. The fields container id must match the id passed to epay.mountFields().

Optional: Show cardholder name, supported schemes, and a loader

By default, Blocks does not show a cardholder name field.

If you need more control, extend the mount configuration:

epay.mountFields("fields", {  theme: "default",  language: "da",  fields: {    name: { enabled: true, value: "" },    pan: {      focus: true,      showSupportedSchemes: true,      showBrandSelector: true,    },  },  loader: {    backgroundColor: "transparent",    borderStyle: "none",    height: "150px",  },});

You can prefill the cardholder name through fields.name.value.

Display the card brand selector when relevant. Some card flows require this for PSD2 compliance.

Step 7: Style Blocks

You can style the hosted fields to match your checkout branding.

To stay compliant and protect cardholder data, the styling options are strictly validated. Keep the configuration to simple values such as colors, fonts and sizing.

Theme

NameDescription
defaultThe default ePay-inspired styling.

Common variables

VariableDescription
colorTextThe font color used for labels and input content
borderRadiusThe border radius for inputs and the hosted fields window
borderColorThe default border color
fontFamilyThe font used for labels, input content and placeholders
gridColumnSpacingHorizontal spacing between input fields
gridRowSpacingVertical spacing between input fields
labelColorThe font color for labels
inputBorderColorThe border color for input fields
inputFocusBorderColorThe border color when an input field is focused
inputBackgroundColorThe background color for input fields
windowBackgroundColorThe background color of the hosted fields window
colorDangerThe color used for validation errors
colorPrimaryThe primary theme color

For the full list of supported languages, field options, loader settings and styling variables, use the ePay.js reference.

Step 8: Register JavaScript events

Use callbacks if you want to react to the current transaction state in your own UI.

The most important events are:

NameDescription
clientReadyDispatched once the client has verified the session ID and secret
invalidSessionDispatched if the client cannot verify the session ID and secret
challengeIssuedDispatched before a client redirect to the challenge page
transactionAcceptedDispatched when the payment succeeds
transactionDeclinedDispatched when the payment is rejected
feeUpdatedDispatched when the transaction fee is calculated
clientRedirectDispatched before any client redirect
invalidInputDispatched if the payment input cannot be used for processing
inputValidityDispatched during payment input validity checks
inputSubmitDispatched when the user submits the hosted fields directly
sessionExpiredDispatched if the session has expired
errorDispatched on any error not covered by the other events

All events have a default handler that ensures the payment flow works as expected. If you override the flow, return false from your handler to disable the default behavior for that event.

Step 9: Submit the payment

When the customer is ready to pay, call the card transaction method from your own button or checkout form.

<button  type="button"  onclick="epay.createCardTransaction()">  Pay</button>

If you use other payment methods, trigger the matching ePay.js method from your own checkout UI instead.

MethodDescription
epay.createCardTransaction()Start a card payment using hosted fields
epay.createVippsMobilePayTransaction()Start a Vipps MobilePay payment
epay.createApplePayTransaction()Start an Apple Pay payment
epay.createGooglePayTransaction()Start a Google Pay payment
epay.createAnydayTransaction()Start an Anyday payment
epay.createViabillTransaction()Start a ViaBill payment
epay.createSwishTransaction()Start a Swish payment
epay.createKlarnaTransaction()Start a Klarna payment

Apple Pay, Vipps MobilePay and Google Pay must use their official branded buttons to comply with provider guidelines.

epay.createTransaction(options) is also available when you want one call that merges values already set through epay.setPaymentOptions(), for example for stored cards or dynamic amounts.

Optional: Set payment options before creating the transaction

Use epay.setPaymentOptions() if you need to update dynamic values before the transaction starts.

epay.setPaymentOptions({  amount: 10000,  store: true,});

If you enable dynamicAmount, verify the shopper-selected amount on your backend before authorization begins, typically through preAuthUrl on the payment session. Most merchants should set the amount server-side when creating the payment session instead of using dynamic amounts.

Step 10: Handle the payment result server-side

Use your notification URL or webhook to update the order in your backend.

Do not mark the order as paid only because the browser fires transactionAccepted or the customer reaches your success page.

DocumentationHandle notification URLs and webhooks correctlyUse the full guide for webhook payloads, authorization validation, retries, duplicate handling, and safe order updates.

Common problems

The payment fields do not appear

Check that:

  • ePay.js is loaded
  • The session ID is valid
  • The session key is valid
  • The container selectors match your HTML
  • The payment session has not expired

The payment cannot be submitted

Check that:

  • All required fields are mounted
  • The payment fields are valid
  • The session belongs to the correct environment
  • The payment has not already been completed

The frontend says payment accepted, but my order is not paid

Check that:

  • Your notification URL is working
  • Your backend receives the payment result
  • Your backend matches the payment to the correct order
  • Your system updates the order server-side

What you built

You have now created your first payment with Blocks.

You created a payment session from your backend, mounted secure payment fields in your checkout and submitted a test payment with ePay.js.

Next steps

How is this guide?

On this page