Developers Guides Online Payment Link

Online Payment Link

Take a payment on the web without a checkout page of your own. Create an online store, verify its domains, create an order against the PaymentPage terminal it comes with, and send the customer the link it returns.

OnlineAPIPayment LinkPayment PageOrders

Add this to your codebase

Paste it into Claude Code, Codex, Cursor or any coding agent. It points the agent at this guide in machine-readable form, so it writes against the real API instead of a guess. Wire up the MCP server once and it can read the rest of the platform too.

Overview

An online payment link is an order created against an online terminal. The API returns a URL hosted by Surfboard; you send it to the customer by email, SMS, chat, or a redirect from your own site, and the card details never touch your infrastructure.

The order call is the same Create Order call you use in store. What changes is everything around it: the terminal must be an online terminal, that terminal must sit in an online store, and the store’s domains must be verified before Surfboard will approve it. This guide walks the whole path once, then covers the controlFunctions.online block that shapes the page the customer lands on.

If you want to render the card fields inside your own page instead of sending the customer away, use the Self-Hosted Checkout guide. If you want to charge a stored card from your backend with no customer present, see Server-to-Server API.

Prerequisites

  1. A developer account at the Developer Portal
  2. A merchant that has completed onboarding and KYB
  3. Control of the DNS for the webshop domain you are going to register

Demo environment: payment page mode is the only online terminal type supported in demo, and only test cards work there. Real cards used in demo are voided automatically after 30 minutes and never settle.

Step 1: Create an Online Store

Terminals live under stores, and an online terminal needs a store that carries an onlineInfo block. Create the store with the webshop details, or add onlineInfo to an existing physical store.

POST /partners/:partnerId/merchants/:merchantId/stores
{
  "storeName": "Web Store",
  "email": "webstore@example.com",
  "phoneNumber": { "code": 46, "number": "701234567" },
  "address": "Drottninggatan 10",
  "city": "Stockholm",
  "zipCode": "103 16",
  "country": "SE",
  "onlineInfo": {
    "merchantWebshopURL": "https://shop.example.com",
    "paymentPageHostURL": "https://shop.example.com/payment",
    "termsAndConditionsURL": "https://shop.example.com/terms",
    "privacyPolicyURL": "https://shop.example.com/privacy"
  }
}
// Response
{
  "status": "SUCCESS",
  "data": {
    "storeId": "81d64e7174dcb00b0f",
    "merchantId": "818712cdbcb670070e",
    "name": "Web Store",
    "merchantUrlDomainVerificationKey": "499470649f03b53fa1175659d4389743974710260b7f410313487e6062b3d559",
    "paymentPageUrlDomainVerificationKey": "2179beab4f5e8c3960615205f042939a2ccc6c51a6e5923c9c068b3d9a645590"
  },
  "message": "Store Created Successfully"
}
FieldRequiredNotes
onlineInfo.merchantWebshopURLYesThe shop the customer is buying from. Verified by DNS.
onlineInfo.termsAndConditionsURLYesMust include the refund policy. Rendered on the checkout page.
onlineInfo.privacyPolicyURLYesRendered on the checkout page.
onlineInfo.paymentPageHostURLNoOnly needed for SDK and iFrame modes. Verified by DNS when present.

Online info can only be set once. Get these URLs right before you send the call; they cannot be edited afterwards through the same route.

Terms, privacy policy and contact details must also be visible on the webshop itself. This is an acquiring requirement, not a Surfboard preference.

Step 2: Verify the Domains

The response carries one verification key per URL. Publish each as a TXT record on the matching domain, then ask Surfboard to check it:

POST /partners/:partnerId/merchants/:merchantId/stores/:storeId/verify
{
  "domainType": "MERCHANT_WEBSHOP_URL"
}

Repeat with "domainType": "PAYMENT_PAGE_HOST_URL" if you registered a payment page host. Surfboard also re-checks automatically every six hours, so a record published late is picked up without another call.

Verification is what unlocks the online terminal types: until the webshop domain passes, there is nothing to register a terminal against. Once it passes, the store goes through an internal approval step at Surfboard. Poll the store to see where it stands:

GET /partners/:partnerId/merchants/:merchantId/stores/:storeId/online

See Store Management for the full store lifecycle.

Step 3: Pick Up the Terminal You Already Have

An online terminal is a mode, not a device — and for payment links you do not have to create one. Creating the online store provisions two terminals by default: a PaymentPage terminal, which is the one this guide uses, and a MerchantInitiated terminal for backend charges against a stored token. List the store’s terminals and take the ID:

GET /partners/:partnerId/merchants/:merchantId/stores/:storeId/terminals
// Response
{
  "status": "SUCCESS",
  "data": [
    {
      "terminalId": "813ca2cb12ce400405",
      "terminalType": "PaymentPage",
      "terminalStatus": "ACTIVE",
      "storeId": "81d64e7174dcb00b0f"
    },
    {
      "terminalId": "813ca2cb12ce400406",
      "terminalType": "MerchantInitiated",
      "terminalStatus": "ACTIVE",
      "storeId": "81d64e7174dcb00b0f"
    }
  ],
  "message": "Terminals fetched successfully"
}

Store the PaymentPage terminalId against something identifiable in your system — it is the terminal$id every order in this guide is created against.

ModeUse it forProvisioned with the store
PaymentPagePayment links and hosted checkout. This guide.Yes
MerchantInitiatedBackend charges against a stored token, such as subscription renewals.Yes
SelfHostedPageCard fields rendered on your own page by the Online SDK. Returns a publicKey and sdkUrl.No
iFrameAn embedded payment frame inside your site.No

The two default terminals exist from the moment the store does, but they cannot take a payment until the domains verify and the store is approved. The other two modes are registered when you need them, and a store can hold as many as you like:

POST /merchants/:merchantId/stores/:storeId/online-terminals
{
  "onlineTerminalMode": "SelfHostedPage"
}
// Response
{
  "status": "SUCCESS",
  "data": {
    "terminalId": "813ca2cb12ce400407",
    "registrationStatus": "REGISTERED"
  },
  "message": "Terminal registered successfully"
}

Step 4: Create the Order

Now the familiar call. The terminal$id is the PaymentPage terminal from step 3, and the response carries the link.

POST /orders
{
  "terminal$id": "813ca2cb12ce400405",
  "referenceId": "order-2026-0418",
  "customer": {
    "person": {
      "name": { "firstName": "John", "lastName": "Doe" },
      "email": "john@example.com",
      "phoneNumber": { "code": "46", "number": "768100190" }
    }
  },
  "orderLines": [
    {
      "id": "ITEM-001",
      "name": "Annual Subscription",
      "quantity": 1,
      "amount": {
        "regular": 99900,
        "total": 99900,
        "currency": "752",
        "tax": [{ "amount": 19980, "percentage": 25, "type": "VAT" }]
      }
    }
  ],
  "totalOrderAmount": {
    "regular": 99900,
    "total": 99900,
    "currency": "752",
    "tax": [{ "amount": 19980, "percentage": 25, "type": "VAT" }]
  },
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD",
      "amount": 99900
    },
    "online": {
      "paymentPageValidFor": "2h",
      "enforce3DSecure": true,
      "generateShortLink": true,
      "payButtonType": "PAY",
      "redirectUrl": "https://shop.example.com/thanks",
      "failureRedirectUrl": "https://shop.example.com/checkout/failed"
    }
  }
}
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderId": "8455c12f9fd0620a010b",
    "paymentPageLink": "https://pay.withsurfboard.com/8455c12f9fd0620a010b?pi=Dr4GoyMXF0zHvjca_Oa0vHgxcT-OD1qp7KdokyI7dkTwRwYJt8nkXyQm3bT6vqCfgraOw50Bf5uOp__3ckbMWOV6L9QbiTiSEFS3YmF4Eb8lr5pTWP2KFjm9Ukmd0000&add=IzFlNDBhZg==",
    "shortLinkUrl": "https://srfb.se/Iq4JfPgHL"
  },
  "message": "Order created successfully"
}

paymentPageLink is the page to send the customer to. Treat it as opaque and pass it on whole: the query string carries the payment intent, and a link with a trimmed or re-encoded pi will not open. shortLinkUrl appears only when you asked for it with generateShortLink, and is the one to put in an SMS.

Keep the orderId. Everything afterwards is keyed on it, and unlike an in-store order there is no paymentId yet: nothing has been attempted until the customer opens the page. The paymentId arrives with the first attempt, in the status response and in the webhook.

Line items, tax, adjustments, customer objects and the order-level calculation rules all behave exactly as they do in store. Create an Order covers them in full.

Control Functions for Online Orders

Everything specific to the hosted page lives in controlFunctions.online.

FieldDescription
paymentPageValidForHow long the link works, as <number><unit> where the unit is m, h or d — for example 15m, 2h, 3d. Defaults to one day.
redirectUrlWhere the customer lands after a successful payment. The orderId is appended as a query parameter.
failureRedirectUrlWhere the customer lands after a failure. Also carries the orderId.
generateShortLinkReturns shortLinkUrl alongside the full link. Default false.
payButtonTypeThe label on the button: PAY, DONATE, BOOK, ORDER, CHECKOUT, CONTINUE, CONTRIBUTE, ADD_MONEY, RENT, SUPPORT, TIP, TOP_UP.
enforce3DSecureForce 3-D Secure where the issuer supports it.
relaxed3dsAllow relaxed 3-D Secure handling.
addressRequirementsAsk for an address on the page.
enforceTokenizationSave the card for later use, overriding the terminal configuration.
tokenisationIfPossibleTokenize when supported, but do not fail the payment if it is not.
errorIfTokenizationFailsFail the flow when the card cannot be tokenized.
subscriptionMark the order as recurring-capable.
generateOnlineLinkWithGenerate the link with a different terminal than the one the order was created against.
selfCardChargingLet the customer charge their own card.

These sit next to the order-level controls that are not online-specific but matter here:

FieldDescription
delayCaptureAuthorize now, capture later. See Capture a Payment.
authModeAUTH or PRE-AUTH. Choosing PRE-AUTH sets delayCapture for you.
lockToPaymentMethodsRestrict the page to the methods you list, e.g. ["CARD", "KLARNA"].
delayPayoutHold the payout for a period, as <number><unit>.
callBackUrlPer-order webhook URL for this order and its payments.

Recurring Orders

For a subscription, add the recurring object inside online and mark the order as one:

{
  "controlFunctions": {
    "online": {
      "subscription": true,
      "enforceTokenization": true,
      "recurring": {
        "subscriptionAmountType": "fixed",
        "frequency": "monthly",
        "numberOfPayments": 12,
        "uniqueReference": "sub-4471",
        "validation": "validated"
      }
    }
  }
}

The first payment is a customer-initiated transaction on the page, which is where the card is tokenized and 3-D Secure is satisfied. Every renewal after that is a merchant-initiated transaction against the stored token, charged from your backend through a MerchantInitiated terminal. Recurring Payments has the renewal side.

FieldDescription
subscriptionAmountTypefixed or variable.
maxAmountCeiling in minor units, for variable subscriptions only.
frequencydaily, twiceWeekly, weekly, tenDays, fortNightly, monthly, everyTwoMonths, trimester, quarterly, twiceYearly, annually, unscheduled. Required.
numberOfPaymentsHow many payments the schedule expects.
uniqueReferenceYour reference for the recurring order.
validationvalidated or notValidated.

Step 5: Confirm the Payment

The redirect back to your site tells you the customer finished, not that the money moved. Confirm server-side, either by polling or, better, by subscribing to the webhook.

GET /orders/:orderId/status
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderStatus": "PAYMENT_COMPLETED",
    "payments": [
      {
        "paymentId": "83a1ba3264bd500106",
        "paymentStatus": "PAYMENT_COMPLETED",
        "paymentMethod": "CARD",
        "amount": 99900
      }
    ]
  }
}

The states are the same as in store. An order sits in PENDING while the link is unused, and a failed or cancelled payment leaves it there, so the same orderId can be paid again without creating a new order.

Order statusMeaning
PENDINGThe link has not been paid yet, or the last attempt failed or was cancelled.
PAYMENT_COMPLETEDPaid in full. The order is closed.
PARTIAL_PAYMENT_COMPLETEDPart of the total is paid. See Partial Payments.
PAYMENT_CANCELLEDThe order was cancelled.

Subscribe to order.paymentcompleted and order.paymentfailed rather than polling on a timer. The payload carries the orderId, your referenceId, the amount and the transaction details, and each delivery carries a metadata.eventId you should use for idempotency — Surfboard retries a failed delivery twice, after 5 and 10 minutes.

{
  "eventType": "order.paymentcompleted",
  "metadata": {
    "eventId": "831fc2f040bf405fff",
    "created": 1745821536443,
    "retryAttempt": 0,
    "terminalId": "813ca2cb12ce400405"
  },
  "data": {
    "orderId": "83a1ba32774149710b",
    "referenceId": "order-2026-0418",
    "paymentId": "83a1ba3264bd500106",
    "paymentStatus": "PAYMENT_COMPLETED",
    "paymentMethod": "CARD",
    "amount": "99900",
    "type": "PURCHASE"
  }
}

See Webhooks for subscription and signature verification.

The link is a URL, so how it reaches the customer is your call:

  • Redirect from your own checkout, the closest thing to a hosted checkout flow.
  • Email or SMS for invoices, deposits and quotes. Use shortLinkUrl in an SMS and keep paymentPageValidFor short enough that a stale link cannot be paid by mistake.
  • QR code printed or shown on screen, for pay-at-table and self-service.

Two things to hold on to. Set paymentPageValidFor deliberately — a link that lives for three days is a link someone can pay three days late, after you have cancelled the order. And never treat the arrival at redirectUrl as proof of payment: a customer can reach that URL by other means. The webhook and the status call are the record.

Error Handling

Create-order failures come back as status: "ERROR" with an OR_* or PS_* code. The ones you will meet setting this up:

CodeCause
OR_0042Terminal not found. The terminal$id is wrong, or it belongs to another merchant.
OR_0037The total does not reconcile with the line items.
OR_0048Line items mix currencies.

If the store’s terminal list comes back without a PaymentPage entry, the store was created without onlineInfo — the two default terminals only come with an online store. A terminal that will not register, or one that is there but refuses a payment, usually means the store has not cleared domain verification or is still in approval. Check the store’s online status before you look at the terminal call.

The Create Order Error Codes reference lists the rest, including errors raised by the payment initiation that happens inside the same call.

Next Steps

Reference

Ready to get started?

Create a sandbox account and start building your integration today.