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.
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
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 and return frontend-safe values
For Blocks, the important response fields are:
Prop
Type
sessionobject
Type
keystring
javascripturi
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.
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
| Name | Description |
|---|---|
default | The default ePay-inspired styling. |
Common variables
| Variable | Description |
|---|---|
colorText | The font color used for labels and input content |
borderRadius | The border radius for inputs and the hosted fields window |
borderColor | The default border color |
fontFamily | The font used for labels, input content and placeholders |
gridColumnSpacing | Horizontal spacing between input fields |
gridRowSpacing | Vertical spacing between input fields |
labelColor | The font color for labels |
inputBorderColor | The border color for input fields |
inputFocusBorderColor | The border color when an input field is focused |
inputBackgroundColor | The background color for input fields |
windowBackgroundColor | The background color of the hosted fields window |
colorDanger | The color used for validation errors |
colorPrimary | The 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:
| Name | Description |
|---|---|
clientReady | Dispatched once the client has verified the session ID and secret |
invalidSession | Dispatched if the client cannot verify the session ID and secret |
challengeIssued | Dispatched before a client redirect to the challenge page |
transactionAccepted | Dispatched when the payment succeeds |
transactionDeclined | Dispatched when the payment is rejected |
feeUpdated | Dispatched when the transaction fee is calculated |
clientRedirect | Dispatched before any client redirect |
invalidInput | Dispatched if the payment input cannot be used for processing |
inputValidity | Dispatched during payment input validity checks |
inputSubmit | Dispatched when the user submits the hosted fields directly |
sessionExpired | Dispatched if the session has expired |
error | Dispatched 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.
| Method | Description |
|---|---|
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.
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.