# Hospitality: Surfboard Payments integration path

Hotels and venues: a deposit online, the balance at reception, pre-authorisation and later capture, stored cards for no-shows, and invoices for corporate guests.

Source: https://www.surfboardpayments.com/developers/guides/paths/hospitality
Guides: 22

## Reading order

### Get access

- [Getting Started](https://www.surfboardpayments.com/developers/guides/getting-started): Create the developer account, invite your team, take the API keys, and learn the demo and live environments before anything else.

### Payment application

- [EMV Terminal Integration](https://www.surfboardpayments.com/developers/guides/emv-terminal-integration): The reception terminal integration, from sandbox to live.
- [Device Registration](https://www.surfboardpayments.com/developers/guides/device-registration): Register each terminal to the property it belongs to.
- [CheckoutX SoftPOS](https://www.surfboardpayments.com/developers/guides/checkoutx-softpos): Take payment on a phone at the bar, the spa or the door with no extra hardware.

### Payment flow

- [Create an Order](https://www.surfboardpayments.com/developers/guides/create-an-order): Every charge starts as an order, whether it is taken at reception or online.
- [Payment Lifecycle](https://www.surfboardpayments.com/developers/guides/payment-lifecycle): Authorise, capture, void, cancel and refund, and when each applies to a stay.
- [Capture a Payment](https://www.surfboardpayments.com/developers/guides/capture-a-payment): Pre-authorise at check-in and capture the final amount at check-out.
- [Void a Payment](https://www.surfboardpayments.com/developers/guides/void-a-payment): Release a pre-authorisation that will not be captured.
- [Cancel a Payment](https://www.surfboardpayments.com/developers/guides/cancel-a-payment): Stop a payment the guest abandoned mid-flow.
- [Tokens](https://www.surfboardpayments.com/developers/guides/tokens): Keep the card from the booking to charge a no-show or incidentals later.
- [Recurring Payments](https://www.surfboardpayments.com/developers/guides/recurring-payments): Charge a stored card from your backend for instalments or memberships.
- [B2B Invoices](https://www.surfboardpayments.com/developers/guides/b2b-invoices): Bill a corporate guest on payment terms instead of a card.
- [Refund an Order](https://www.surfboardpayments.com/developers/guides/refund-an-order): Refund a cancelled booking against the original payment.
- [Receipts](https://www.surfboardpayments.com/developers/guides/receipts): Email the receipt to the guest or print it at reception.

### Online

- [Payment Page](https://www.surfboardpayments.com/developers/guides/payment-page): A hosted checkout for the booking deposit.
- [Online Payment Link](https://www.surfboardpayments.com/developers/guides/online-payment-link): Send a link for a deposit or an outstanding balance.
- [Self-Hosted Checkout](https://www.surfboardpayments.com/developers/guides/self-hosted-checkout): Card fields inside your own booking flow, with Surfboard carrying PCI.

### Merchants and operations

- [Merchant Onboarding](https://www.surfboardpayments.com/developers/guides/merchant-onboarding): Board each property through the Partner API.
- [Store Management](https://www.surfboardpayments.com/developers/guides/store-management): A store per property, or per outlet inside one.
- [Webhooks](https://www.surfboardpayments.com/developers/guides/webhooks-notifications): Payment outcomes pushed to your property management system.
- [Settlements & Reporting](https://www.surfboardpayments.com/developers/guides/settlements-reporting): Settlement reports per property for the finance team.

### Branding

- [Partner Branding](https://www.surfboardpayments.com/developers/guides/partner-branding): Put your colours, fonts and logo on the terminal screens, receipts and payment pages so the product reads as yours.

The guides follow in that order.

---

# Getting Started

The first hour of every Surfboard integration: create the developer account, invite your team, take the API keys, learn the demo and live environments, and pick the path of guides for what you are building.

Source: https://www.surfboardpayments.com/developers/guides/getting-started
Category: in-store
Tags: Getting Started, Developer Portal, API Keys, Sandbox, Onboarding

---
## Overview

Everything around the integration itself lives in the Developer Portal: your partner account, your team, the API keys, the playground, webhooks, logs, and later the switch to production. This guide gets you through that once, so every other guide can assume you have it.

It is the same list we send a new partner by email before the first call. If you would rather go through it together, [book a startup call](/contact) and we will set it up with you.

## Step 1: Create the Account and Invite Your Team

Sign up at the [Developer Portal](https://developers.surfboardpayments.com/sign-up). There is no approval queue: the account is live immediately, with a demo environment attached.

Then invite the developers who will work on the integration from [Console settings](https://developers.surfboardpayments.com/console/settings).

> **Use one email domain.** Team members are mapped to your partner ID by their email domain, so everyone should sign up with the same one, for example `@yourcompany.com`. If you use external contractors with other domains, tell us before they sign up and we will make sure they land in the same account.

The account is a partner account. Your `partnerId` is shown in the Console and is what you will pass when onboarding merchants and creating stores later.

## Step 2: Take the Keys and Try the API

Generate an API key and secret at [Console API keys](https://developers.surfboardpayments.com/console/api-keys). Every request carries them as headers, with the merchant you are acting for as a third:

```
API-KEY:      YOUR_API_KEY
API-SECRET:   YOUR_API_SECRET
MERCHANT-ID:  YOUR_MERCHANT_ID
```

Keys expire. When a call that worked yesterday returns `401 Unauthorized` today with nothing else changed, generate a new pair before you debug anything else.

Then try the API before you write code:

- **[Playground](https://developers.surfboardpayments.com/playground/)**: run real requests against the demo environment from the browser.
- **[API reference](https://developers.surfboardpayments.com/references/api/orders/create-order)**: every endpoint and payload. Create Order is the one to read first; the platform is orders-first, and the payment is initiated in the same call.
- **[Webhook reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)**: every event you can subscribe to.
- **Postman collection**: [download it](/files/Surfboard_Payments_API.postman_collection.json) and drop your keys into the environment.
- **MCP server**: `npx -y @surfboardpayments/surf-mcp` gives your coding agent the whole reference as tools. Setup is on the [MCP page](/developers/mcp).

Read [API Conventions](/developers/guides/api-conventions) once. Amounts are integers in the smallest currency unit, currencies are numeric ISO 4217 codes as strings, prices are tax-inclusive, and order endpoints are not merchant-scoped in the path. Each of those catches a first integration at least once.

## Step 3: Know the Environments

You start in the demo environment. Live credentials, and a different base URL, come after certification.

| Environment | Terminals | Cards |
|-------------|-----------|-------|
| **Demo** | All hardware terminals, the Terminal Tester app, Mobile Checkout | Live cards can be used. Transactions are voided immediately after payment. |
| **Live** | All hardware terminals, Mobile Checkout | Live cards. Transactions are settled and paid out. |

For in-store testing, the Terminal Tester app on Android simulates payments with built-in success and failure cards.

The base URL is issued to your account rather than published, and it changes between demo and live. Read it from the Console next to your keys and keep host, key and secret in configuration:

```
SURFBOARD_API_URL=
SURFBOARD_API_KEY=
SURFBOARD_API_SECRET=
SURFBOARD_MERCHANT_ID=
```

Never mix environments in one flow. A demo merchant against the live host, or live keys against the demo host, fails at the first call and the error will not say why.

## Step 4: Pick Your Path

The guides are written one topic at a time. The paths put them in reading order for a kind of build, and each path can be copied as one markdown file for your coding agent:

- [Food & Beverage](/developers/guides/paths/food-and-beverage): restaurant and café POS with tips, split bills and online orders.
- [Retail](/developers/guides/paths/retail): store POS with a product catalog, a Swedish cash register, returns and gift cards.
- [Hospitality](/developers/guides/paths/hospitality): deposits online, pre-authorisation at check-in, stored cards and invoices.
- [SoftPOS only](/developers/guides/paths/softpos): Tap to Pay on iPhone and Android with no hardware.

If none of them is your build, the assistant on the [guides page](/developers/guides) answers questions across all of them, and the [Developer Guides](/developers/guides) index lists every guide by topic.

## Step 5: Certification and Go Live

When the integration works end to end in demo:

1. Sign the contract and receive approval.
2. Complete an onboarding call where we test and certify the integration together. Some products carry their own certification on top, such as the cash register self-certification in [Build Your Own ECR](/developers/guides/electronic-cash-register).
3. Receive production credentials.
4. Switch the base URL from demo to production.
5. Start accepting live payments.

Branding is worth doing before the first merchant sees the product. [Partner Branding](/developers/guides/partner-branding) sets your colours, fonts and logo on the terminal screens, receipts and payment pages.

## Reference

- [Developer Portal](https://developers.surfboardpayments.com/)
- [Sign up](https://developers.surfboardpayments.com/sign-up)
- [Console settings](https://developers.surfboardpayments.com/console/settings) and [API keys](https://developers.surfboardpayments.com/console/api-keys)
- [Playground](https://developers.surfboardpayments.com/playground/)
- [API Conventions](/developers/guides/api-conventions)
- [Book a startup call](/contact)

---

# EMV Terminal Integration

Integrate traditional card-present terminals through Surfboard's unified API. From account setup to live payments in one guide.

Source: https://www.surfboardpayments.com/developers/guides/emv-terminal-integration
Category: in-store
Tags: EMV, Terminal, In-Store, API, Hardware

---
## Overview

Surfboard Payments lets you integrate traditional EMV card-present terminals through a single, unified API. Whether you are deploying countertop terminals, mobile POS devices, or kiosk setups, the integration follows the same workflow: create an account, get API credentials, build and test in the sandbox, then go live.

This guide walks you through the complete process from zero to accepting live in-store payments.

## Step 1: Create a Developer Account

Sign up at the [Surfboard Developer Portal](https://developers.surfboardpayments.com/sign-up) to get started. A developer account gives you:

- Access to the Console for managing your integration
- A sandbox environment for building and testing
- The path to certification and live payments

No approval process required -- you get instant sandbox access.

## Step 2: Generate API Credentials

After creating your account, open the **Console** in the Developer Portal. From there you can:

- Generate your **API-KEY** and **API-SECRET**
- Configure webhooks
- Access logs and monitoring
- Manage terminals and merchants

> **Tip:** You can also request test credentials through the Surfboard support team on Slack during onboarding.

## Step 3: Understand Environments

Surfboard provides different environments for building and testing your integration:

| Environment | Supported Terminals | Cards Supported |
|-------------|-------------------|-----------------|
| **Demo** | All hardware terminals, Terminal Tester App, Mobile Checkout | Live cards can be used. Transactions are voided immediately after payment. |
| **Live** | All hardware terminals, Mobile Checkout | Live cards. Transactions are settled and you receive payouts. |

By default, you gain access to the demo environment when you create a developer account. Use it to build and test your integration with the [Surfboard APIs](https://developers.surfboardpayments.com/) and SDKs.

> **Note:** For in-store payments, use the Terminal Tester App (available on Android) for payment simulations. It includes built-in success and failure test cards.

## Step 4: Build Your Integration

Complete these steps in the demo environment before going live:

### 4.1 Merchant Onboarding

Set up your merchant hierarchy using the [Merchants API](https://developers.surfboardpayments.com/api/merchants) and [Stores API](https://developers.surfboardpayments.com/api/stores). Each merchant can have multiple stores, and each store can have multiple terminals.

### 4.2 Terminal Registration

Register your hardware terminals through the [Terminals API](https://developers.surfboardpayments.com/api/terminals). When registering a terminal, you provide:

- The `registrationIdentifier` (printed on the terminal or provided during provisioning)
- The `storeId` for the store the terminal belongs to
- A human-readable `terminalName`

```json
POST /merchants/:merchantId/stores/:storeId/devices
{
  "registrationIdentifier": "250901",
  "terminalName": "Checkout 1"
}
```

### 4.3 Accept Payments

Create orders and initiate payments using the [Orders API](https://developers.surfboardpayments.com/api/orders). The Carbon API uses an orders-first workflow -- create an order and initiate payment in a single call:

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [
    {
      "id": "ITEM-001",
      "name": "Coffee",
      "quantity": 1,
      "amount": {
        "total": 4500,
        "currency": "752"
      }
    }
  ],
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD",
      "amount": 4500
    }
  }
}
```

The terminal displays the payment UI automatically. The customer taps, inserts, or swipes their card. You receive the result via the API response or webhooks.

### 4.4 Post-Payment Operations

After payments are accepted, integrate post-payment functionality:

- **Refunds** -- Use negative quantities in order lines
- **Receipts** -- Send digital receipts via the [Receipts API](https://developers.surfboardpayments.com/api/receipts)
- **Reporting** -- Query order and payment history

## Step 5: Certification & Go Live

Once your integration is built and tested in the demo environment:

1. Sign the contract and receive approval
2. Complete an onboarding call to test and certify your integration
3. Receive production credentials
4. Update your base URL from demo to production
5. Start accepting live payments

## Webhooks

Configure webhooks to receive real-time notifications about order and payment events. Key events include:

- `order.paymentcompleted` -- Payment was successful
- `order.paymentcancelled` -- Payment was cancelled
- `order.paymentfailed` -- Payment failed
- `order.terminal.event` -- Every terminal state during the transaction

Set up webhook endpoints in the Console under your developer account settings.

## API Quick Reference

| API | Purpose |
|-----|---------|
| [Merchants API](https://developers.surfboardpayments.com/api/merchants) | Create and manage merchants |
| [Stores API](https://developers.surfboardpayments.com/api/stores) | Create and manage stores |
| [Terminals API](https://developers.surfboardpayments.com/api/terminals) | Register and manage terminals |
| [Orders API](https://developers.surfboardpayments.com/api/orders) | Create orders and initiate payments |
| [Receipts API](https://developers.surfboardpayments.com/api/receipts) | Send digital receipts |
| [Branding API](https://developers.surfboardpayments.com/api/branding) | Customise terminal branding |

## Reference

- [Developer Portal](https://developers.surfboardpayments.com/)
- [Carbon API Documentation](https://developers.surfboardpayments.com/references/api/orders/create-order)
- [Webhook Reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)

---

# Device Registration

Register a terminal to a store before it can take payments or receive a partner POS app. Covers rotating codes, QR/registration links, pre-shipped codes, and in-app registration for SoftPOS.

Source: https://www.surfboardpayments.com/developers/guides/device-registration
Category: in-store
Tags: In-Store, Terminal, API, Device Management, Onboarding

---
## Overview

Before a terminal can accept payments -- or receive a partner POS app -- it must be **registered to a store** under a merchant. Registration links the physical (or software) device to your merchant hierarchy. Once a terminal is registered to a store, it cannot be repurposed by another merchant. You can still move it between stores under the same merchant using the [Change Store](/developers/guides/terminal-device-management) endpoint.

Surfboard supports several registration methods so you can pick the lowest-friction option for your setup. They are **not mutually exclusive** -- the same terminal can be registered through whichever path the merchant has available.

> If you integrate through one of our SDKs (Android SoftPOS, Tap to Pay on iPhone), the SDK handles registration for you and you do not need to call the registration APIs directly. See [Android SoftPOS SDK](/developers/guides/android-softpos-sdk) and [Tap to Pay on iPhone](/developers/guides/tap-to-pay-iphone).

## When Is Registration Needed?

| Terminal type | Registration required? | Typical method |
|---------------|------------------------|----------------|
| **EMV** (countertop, mobile POS, kiosk) | Yes -- the terminal must be registered before it can receive a partner POS app or take payments | Rotating code, QR/registration link, or pre-shipped code |
| **SoftPOS** (CheckoutX on Android) | Yes | In-app (interapp) registration, or any of the code-based methods |
| **SDK-based** (Android SoftPOS SDK, Tap to Pay on iPhone) | Handled by the SDK | N/A -- SDK methods cover registration |

## Registration Methods at a Glance

| Method | How the merchant registers | Code validity | Best for |
|--------|----------------------------|---------------|----------|
| **Rotating 6-digit code** | Reads a code from the terminal screen and enters it into the Surfboard merchant portal or the partner's portal/app | ~90 seconds (rotates) | Attended setup where someone is in front of both the terminal and a portal |
| **QR / registration link** | Taps the QR icon on the terminal's registration screen and scans a QR generated by the partner or Surfboard merchant portal | Short-lived | Fast, near zero-touch scan-to-register |
| **Pre-shipped code** | Enters a code provided ahead of time (e.g. a welcome email or SMS) directly on the terminal registration screen | Longer-lived | Unattended setup, or when the 90-second rotation is impractical |
| **In-app (interapp)** | Switches to CheckoutX, which registers the device automatically -- no code entry | n/a | SoftPOS / CheckoutX only |

---

## Method 1 -- Rotating 6-Digit Code

When a terminal starts up on its registration screen, it displays a **6-digit code that rotates roughly every 90 seconds**. The merchant reads this code and enters it into a registration screen -- either the **Surfboard merchant portal** or the **partner's own portal or app**.

Behind the scenes, the portal calls the **Register Device** API with the code as the `registrationIdentifier`:

```
POST /merchants/:merchantId/stores/:storeId/devices
```

```json
{
  "registrationIdentifier": "250901",
  "terminalName": "Kiosk One"
}
```

| Parameter | Required | Description |
|-----------|----------|-------------|
| `registrationIdentifier` | Yes | The 6-digit code shown on the terminal. For SurfPad and Printer devices, use the serial number printed on the back instead. |
| `terminalName` | No | A human-readable label to identify the terminal. |

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "terminalId": "813ca2cb12ce400405",
    "registrationStatus": "REGISTERED"
  },
  "message": "Terminal registered successfully"
}
```

`registrationStatus` is either `REGISTERED` (new device) or `ALREADY_REGISTERED` (device was previously linked). Store the returned `terminalId` -- you need it for all subsequent calls on this device.

> Because the code rotates every ~90 seconds, complete the entry promptly. If it expires, read the new code from the screen and try again.

See the [Register Device API reference](https://developers.surfboardpayments.com/references/api/terminals/register-device) for the full request/response and error codes.

---

## Method 2 -- QR Code / Registration Link

The terminal's registration screen also shows a **QR icon**. Tapping it opens the camera so the merchant can scan a QR code provided by the **partner** or by the **Surfboard merchant portal**. The QR encodes a `registrationLink` -- a deep link that registers the device automatically, with no manual code entry.

Generate the link with the **Get Device Registration Code** API:

```
GET /merchants/:merchantId/stores/:storeId/device-registration
```

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "registrationCode": "905788",
    "registrationLink": "checkoutx://com.surfboard.checkoutx/register?data=eyJyZWdpc3RyYXRpb25Db2RlIjoiOTA1Nzg4In0="
  },
  "message": "Registration Code Generated Successfully"
}
```

| Field | Description |
|-------|-------------|
| `registrationCode` | A 6-digit code the merchant can enter manually (see Method 3). |
| `registrationLink` | A deep link that, encoded as a QR code, registers the device when scanned. |

Render `registrationLink` as a QR code in your partner portal or app, or let the Surfboard merchant portal generate it for you. The merchant scans it from the terminal's registration screen and the device registers itself.

See the [Get Device Registration Code API reference](https://developers.surfboardpayments.com/references/api/terminals/get-device-registration-code).

---

## Method 3 -- Pre-Shipped Code

The same **Get Device Registration Code** API also returns a `registrationCode`. Unlike the rotating on-screen code, this code has **longer validity**, which makes it a good fit for setups where the ~90-second rotation is impractical.

A common pattern: a partner generates the code ahead of time and delivers it to the merchant out of band -- for example in a **welcome email or SMS** sent before the terminal ships. When the device arrives, the merchant simply enters the code on the terminal's registration screen and the device registers.

This supports a near zero-touch onboarding experience: the merchant never has to coordinate a live, time-limited code between the terminal and a portal.

---

## Method 4 -- In-App (Interapp) Registration for SoftPOS

For **SoftPOS** running CheckoutX, you can register a device by switching into the CheckoutX app -- **no code entry needed**. Your app opens CheckoutX via a deep link, CheckoutX registers the terminal, and control returns to your app with the resulting `terminalId`.

This is the smoothest option for SoftPOS and partner POS apps that already integrate with CheckoutX. It is **not available for EMV terminals**, which must use one of the code- or QR-based methods above.

For the full app-switch flow -- generating the interapp code, building the deep link, and handling the callback -- see the [Inter-App Integration](/developers/guides/interapp-integration) guide.

---

## After Registration

Once a terminal is registered:

- Use the returned `terminalId` to create orders and initiate payments. See [Create an Order](/developers/guides/create-an-order).
- Apply configuration (network, language, restart schedule, and more) -- see [Terminal & Device Management](/developers/guides/terminal-device-management).
- Move the terminal between stores under the same merchant with the Change Store endpoint, or reassign across merchants (partner-managed inventory) with Move Terminal -- both covered in [Terminal & Device Management](/developers/guides/terminal-device-management).

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Register a device | POST | `/merchants/:merchantId/stores/:storeId/devices` |
| Get registration code & link | GET | `/merchants/:merchantId/stores/:storeId/device-registration` |
| Get interapp code (SoftPOS) | GET | `/merchants/:merchantId/stores/:storeId/terminals/interapp` |

## Reference

- [Register Device API](https://developers.surfboardpayments.com/references/api/terminals/register-device)
- [Get Device Registration Code API](https://developers.surfboardpayments.com/references/api/terminals/get-device-registration-code)
- [Inter-App Integration](/developers/guides/interapp-integration)
- [EMV Terminal Integration](/developers/guides/emv-terminal-integration)
- [Terminal & Device Management](/developers/guides/terminal-device-management)
</content>
</invoke>

---

# CheckoutX SoftPOS

Accept in-person payments on smartphones and tablets by pairing the CheckoutX app with your own POS app, a dual-app setup that requires no SDK integration.

Source: https://www.surfboardpayments.com/developers/guides/checkoutx-softpos
Category: in-store
Tags: In-Store, Android, iOS, CheckoutX, SoftPOS, App Switch

---
## Overview

CheckoutX SoftPOS is the fastest way to accept in-person payments on a smartphone or tablet without integrating an SDK. You install the CheckoutX app alongside your own POS app on the same device, and your app hands off transactions to CheckoutX through a native app switch.

Use this setup when you want contactless acceptance on consumer hardware but don't want to embed and maintain a SoftPOS SDK inside your own app.

## Two Ways to Accept Payments on Phones

Surfboard gives you two routes for in-person payments on iOS and Android. Pick the one that fits your product:

| Option | What you do | When to pick it |
|--------|-------------|-----------------|
| **[Tap to Pay on iPhone SDK](/developers/guides/tap-to-pay-iphone)** / **[Android SoftPOS SDK](/developers/guides/android-softpos-sdk)** | Embed the Surfboard SoftPOS SDK directly inside your own app | You want a single, branded app with full control over the checkout UX |
| **CheckoutX SoftPOS (this guide)** | Install the CheckoutX app next to your POS app and use [Inter-App Integration](/developers/guides/interapp-integration) to hand off transactions | You want to ship faster, avoid SDK maintenance, or already have a working POS app |

Both approaches run on the same Surfboard platform, the difference is only where the payment UI lives.

## How It Works

1. **Install CheckoutX** from the App Store (iOS) or Google Play (Android) on the device running your POS app.
2. **Register CheckoutX** as a terminal once per device using the Inter-App flow.
3. **Initiate a payment** from your POS app, CheckoutX opens, accepts the tap, and returns the result to your app.

The underlying registration, payment, and tag-scanning flows are all documented in the [Inter-App Integration guide](/developers/guides/interapp-integration). CheckoutX SoftPOS is simply that flow running on a consumer phone or tablet instead of a dedicated terminal.

## Setup

1. **Get a Surfboard account** and register a store under your merchant.
2. **Download CheckoutX** on the target device.
3. **Follow [Inter-App Integration](/developers/guides/interapp-integration)** for terminal registration, payment, and tag-scanning deep link flows. The same API contract applies whether CheckoutX runs on a Surfboard terminal or on a phone in SoftPOS mode.

## The Configure Call

Before the first payment, and whenever the device has been idle, rebooted, or has lost its server session, call CheckoutX's configure route to prepare the terminal:

```
checkoutx://com.surfboard.checkoutx/configure?redirectUrl=REDIRECT_URL
```

Replace `REDIRECT_URL` with your base64-encoded app URL. CheckoutX opens, establishes its connection to the Surfboard server, and returns to your app with `isConfigured: true` when ready.

Running configure before the first transaction of a session gives the smoothest first-payment experience. See [Configure Terminal Before Payment](/developers/guides/interapp-integration#configure-terminal-before-payment) in the Inter-App guide for full details.

## Handling `PS_0025`, Terminal Not Connected

When you initiate a payment on SoftPOS, you may occasionally see:

```
PS_0025: Terminal is not connected to server so unable to send transactions
```

In these cases, run the configure call again and then re-initiate the payment. You can do this seamlessly on your end, the transaction may be slightly slower, but this is the easiest way to recover. No user action is needed.

This is specific to SoftPOS because consumer devices can go idle or lose their session to the server between transactions; a re-configure re-establishes the connection before the next payment.

## Reference

- [Inter-App Integration](/developers/guides/interapp-integration), full deep link flow
- [Tap to Pay on iPhone SDK](/developers/guides/tap-to-pay-iphone), iOS SDK alternative
- [Android SoftPOS SDK](/developers/guides/android-softpos-sdk), Android SDK alternative

---

# Create an Order

Learn how to create orders with line items, tax, customer details, and control functions. The starting point for accepting payments with the Surfboard API.

Source: https://www.surfboardpayments.com/developers/guides/create-an-order
Category: online
Tags: Online, API, Orders, In-Store

---
## Overview

An order is the starting point for every payment in Surfboard. You create an order against a `terminal$id`, include line items with pricing, and optionally initiate payment in the same call. The API returns an `orderId` and `paymentId` that you use for all subsequent operations.

This guide covers basic order creation, line items, customer details, tax handling, and common control functions.

## Prerequisites

1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. Complete onboarding (merchant and store setup)
3. A terminal to create the order against (any type -- in-store, PaymentPage, SelfHostedPage, or MerchantInitiated). In-store devices and SelfHostedPage are registered; an online store already carries a PaymentPage and a MerchantInitiated terminal, so fetch the store's terminals to find them.

## Basic Order

Order, payment, and receipt endpoints are **not** merchant-scoped in the path. The merchant travels in the `MERCHANT-ID` header alongside `API-KEY` and `API-SECRET`, so the path is `/orders`, not `/merchants/{merchantId}/orders`. See [API Conventions](/developers/guides/api-conventions) for the full header set.

Create an order with a single line item and initiate payment:

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [
    {
      "id": "ITEM-001",
      "name": "Nike Shoes",
      "quantity": 1,
      "amount": {
        "regular": 50000,
        "total": 50000,
        "currency": "752",
        "tax": [
          { "amount": 10000, "percentage": 25, "type": "VAT" }
        ]
      }
    }
  ],
  "totalOrderAmount": {
    "regular": 50000,
    "total": 50000,
    "currency": "752",
    "tax": [
      { "amount": 10000, "percentage": 25, "type": "VAT" }
    ]
  },
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD"
    }
  }
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderId": "83a1ba32774149710b",
    "paymentId": "83a1ba3264bd500106"
  },
  "message": "Order created successfully"
}
```

Store both `orderId` and `paymentId` -- you need them for status checks, captures, voids, and refunds.

## Line Items

Every order requires at least one line item in the `orderLines` array. Each line item must include:

| Field | Required | Description |
|-------|----------|-------------|
| `id` | Yes | Unique line item identifier |
| `name` | Yes | Product name |
| `quantity` | Yes | Quantity (negative for refunds) |
| `amount.regular` | Yes | Unit price in smallest currency unit |
| `amount.total` | Yes | **Unit** price after shipping and campaign (`regular + shipping - campaign`). Not the line total |
| `amount.currency` | Yes | Numeric ISO 4217 code (e.g., `"752"` for SEK) |
| `amount.tax` | Yes | Tax array for the line. Required even at zero rate -- send a `0` entry rather than omitting it |

Optional fields include `description`, `brand`, `imageUrl`, `gtin`, `categoryId`, `unit`, and `metadata`.

> **`amount.total` is per unit, not per line.** This is the single most common first-integration error, and it only shows up once a cart has a quantity above one. `total` must equal `regular + shipping - campaign` for **one** unit; the order total is `sum(total * quantity)`. Sending `unitPrice × quantity` returns `P_0001: Invalid item price for item id <id>`.

Two lines, one of them with a quantity above one:

```json
"orderLines": [
  {
    "id": "ITEM-001",
    "name": "Flat white",
    "quantity": 2,
    "amount": {
      "regular": 4500,
      "total": 4500,
      "currency": "752",
      "tax": [{ "amount": 900, "percentage": 25, "type": "VAT" }]
    }
  },
  {
    "id": "ITEM-002",
    "name": "Gift card",
    "quantity": 1,
    "amount": {
      "regular": 10000,
      "total": 10000,
      "currency": "752",
      "tax": [{ "amount": 0, "percentage": 0, "type": "VAT" }]
    }
  }
]
```

The first line contributes `4500 * 2 = 9000`, not `4500`. The order total is `19000`. The gift card is zero-rated and still carries a `tax` entry: omitting it returns `P_0001: Input data validation failed. Cannot read properties of undefined (reading 'vatValue')`.

> **Currency format:** All amounts use the smallest currency unit. For example, 10.00 SEK = `1000`, 5.00 EUR = `500`.

> **Prices include tax.** `amount.regular` and `amount.total` are gross. The `tax` array reports the VAT *contained within* that price, not an amount to add on top. See [API Conventions](/developers/guides/api-conventions) if you are coming from a sales-tax market.

## Customer, Billing, and Shipping

Include customer, billing, and shipping details when available:

```json
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "customer": {
    "person": {
      "name": { "firstName": "John", "lastName": "Doe" },
      "email": "john@example.com",
      "phoneNumber": { "code": "46", "number": "768100190" }
    },
    "company": {
      "vatId": "SE556026998601"
    }
  },
  "billing": {
    "name": { "firstName": "John", "lastName": "Doe" },
    "phoneNumber": { "code": "46", "number": "768100190" },
    "address": {
      "addressLine1": "Storgatan 1",
      "city": "Stockholm",
      "postalCode": "11122",
      "countryCode": "SE"
    }
  },
  "shipping": {
    "name": { "firstName": "John", "lastName": "Doe" },
    "phoneNumber": { "code": "46", "number": "768100190" },
    "address": {
      "addressLine1": "Storgatan 1",
      "city": "Stockholm",
      "postalCode": "11122",
      "countryCode": "SE"
    }
  },
  "orderLines": [...]
}
```

All customer fields are optional but recommended for invoice payments, fraud prevention, and receipt delivery.

## Order Line Level Calculation

The `orderLineLevelCalculation` control function changes how `totalOrderAmount` is computed from line items.

| Setting | Formula | Example |
|---------|---------|---------|
| `false` (default) | Sum of `(total * quantity)` per line | `(50 * 2) + (150 * 1) = 250` |
| `true` (recommended) | Sum of `((regular * quantity) - campaign + shipping)` per line | `((200 * 2) - 100 + 50) = 350` |

Enable it when your line items have campaigns or shipping costs:

```json
{
  "controlFunctions": {
    "orderLineLevelCalculation": true,
    "initiatePaymentsOptions": { "paymentMethod": "CARD" }
  }
}
```

## Adjustments

Adjustments modify the total order value for tips, donations, gift cards, or discounts:

```json
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [...],
  "adjustments": [
    { "type": "TIP", "value": 1000 }
  ],
  "totalOrderAmount": {
    "regular": 50000,
    "total": 51000,
    "currency": "752"
  },
  "controlFunctions": {
    "initiatePaymentsOptions": { "paymentMethod": "CARD" }
  }
}
```

The `totalOrderAmount.total` should reflect the adjusted amount (regular + adjustments).

## Delay Capture

To authorize payment now but capture funds later (e.g., at shipment), set `delayCapture: true`:

```json
{
  "controlFunctions": {
    "delayCapture": true,
    "initiatePaymentsOptions": { "paymentMethod": "CARD" }
  }
}
```

You can also use `authMode: "PRE-AUTH"` for pre-authorization flows, which automatically enables delayed capture and lets you capture a different amount than originally authorized.

See the [Capture a Payment](/developers/guides/capture-a-payment) guide for the full flow.

## Check Order Status

After creating an order, check its status at any time:

```json
GET /orders/:orderId/status
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderStatus": "PAYMENT_COMPLETED",
    "payments": [
      {
        "paymentId": "83a1ba3264bd500106",
        "paymentStatus": "PAYMENT_COMPLETED",
        "paymentMethod": "CARD",
        "amount": 50000
      }
    ],
    "paymentIds": ["83a1ba3264bd500106"]
  }
}
```

**Order statuses:** `PENDING` | `PAYMENT_COMPLETED` | `PAYMENT_CANCELLED` | `PARTIAL_PAYMENT_COMPLETED` | `PAYMENT_PROCESSED`

**Payment statuses:** `PAYMENT_INITIATED` | `PAYMENT_PROCESSING` | `PAYMENT_PROCESSED` | `PAYMENT_COMPLETED` | `PAYMENT_FAILED` | `PAYMENT_CANCELLED`

Every payment ends in one of three terminal states:

| Payment Status | Order Status | Description |
|----------------|--------------|-------------|
| `PAYMENT_COMPLETED` | `PAYMENT_COMPLETED` | Payment succeeded -- the order is closed. |
| `PAYMENT_CANCELLED` | `PENDING` | Payment was cancelled -- the order remains open and a new payment can be initiated using the existing `orderId`. |
| `PAYMENT_FAILED` | `PENDING` | Payment failed -- the order remains open and a new payment can be initiated using the existing `orderId`. |

## Error Handling

Create order responses return `status: "ERROR"` with a code in the `OR_*`, `PS_*`, `GC_*`, or `SP_*` prefix when validation or initiation fails. The most common ones are `OR_0042` (terminal not found), `OR_0037` (invalid total), `OR_0048` (mixed currencies), and `PS_0025` (terminal not connected -- retry after configure).

See the [Create Order Error Codes](/developers/guides/create-order-error-codes) reference for the full list, including errors thrown by the initiate payment step when both happen in the same call.

## Next Steps

Once you have an order created, you can:

- [Capture a Payment](/developers/guides/capture-a-payment) -- finalize a delayed-capture authorization
- [Cancel a Payment](/developers/guides/cancel-a-payment) -- stop an in-progress payment
- [Void a Payment](/developers/guides/void-a-payment) -- reverse a completed payment before settlement
- [Refund an Order](/developers/guides/refund-an-order) -- return funds after settlement
- [Partial Payments](/developers/guides/partial-payments) -- split an order across multiple payments

## Reference

- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Create Order Error Codes](/developers/guides/create-order-error-codes)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
- [Developer Portal](https://developers.surfboardpayments.com/)

---

# Payment Lifecycle

Manage the full payment lifecycle from order creation through capture, void, cancel, and refund operations using the Surfboard Payments API.

Source: https://www.surfboardpayments.com/developers/guides/payment-lifecycle
Category: online
Tags: Online, API, Payments, Refunds, Capture

---
## Overview

Every payment follows a lifecycle: create an order, authorize payment, capture funds, and settle. At each stage you can intervene -- void before settlement, cancel before completion, or refund after. This guide covers each operation with the API calls you need.

## Lifecycle at a Glance

| Operation | When to Use | Endpoint | Method |
|-----------|-------------|----------|--------|
| **Create Order** | Start a new payment | `/orders` | POST |
| **Capture** | Finalize a delayed-capture auth | `/payments/:paymentId/capture` | POST |
| **Void** | Reverse before settlement | `/payments/:paymentId/void` | POST |
| **Cancel** | Stop before completion | `/payments/:paymentId` | DELETE |
| **Refund** | Full return after settlement | `/orders` | POST |
| **Partial Refund** | Partial return after settlement | `/orders` | POST |

## Order and Payment Statuses

**Order statuses:** `PENDING` | `PAYMENT_COMPLETED` | `PAYMENT_CANCELLED` | `PARTIAL_PAYMENT_COMPLETED` | `PAYMENT_PROCESSED`

**Payment statuses:** `PAYMENT_INITIATED` | `PAYMENT_PROCESSING` | `PAYMENT_PROCESSED` | `PAYMENT_COMPLETED` | `PAYMENT_FAILED` | `PAYMENT_CANCELLED`

### Status Flow

A payment moves through progressive statuses before settling into one of three final (terminal) states. The happy path is:

```
PAYMENT_INITIATED → PAYMENT_PROCESSING → PAYMENT_PROCESSED → PAYMENT_COMPLETED
                                                           → PAYMENT_FAILED
                                                           → PAYMENT_CANCELLED
```

`PAYMENT_CANCELLED` and `PAYMENT_FAILED` can also occur **directly after** `PAYMENT_INITIATED` -- for example, if the customer abandons checkout or the payment is rejected before processing begins.

#### Progressive statuses

| Payment Status | Description |
|----------------|-------------|
| `PAYMENT_INITIATED` | Payment has been created on the order and is awaiting processing. Can transition to `PAYMENT_PROCESSING`, `PAYMENT_CANCELLED`, or `PAYMENT_FAILED`. |
| `PAYMENT_PROCESSING` | Payment is actively being processed by the network. |
| `PAYMENT_PROCESSED` | Payment has been authorised and processed, but is not yet in its final state (for example, awaiting capture or confirmation). |

#### Order-level intermediate status

| Order Status | Description |
|--------------|-------------|
| `PARTIAL_PAYMENT_COMPLETED` | Only set on the **order**, not on an individual payment. Indicates that one or more payments against the order have completed, but the full order amount has not yet been paid. |

### Terminal Payment States

Every payment ends in one of three terminal states. Once a payment reaches a terminal state, it is final and cannot change.

| Payment Status | Order Status | Description |
|----------------|--------------|-------------|
| `PAYMENT_COMPLETED` | `PAYMENT_COMPLETED` | Payment succeeded -- funds are captured and the order is closed. |
| `PAYMENT_CANCELLED` | `PENDING` | Payment was cancelled -- the order remains open and a new payment can be initiated using the existing `orderId`. |
| `PAYMENT_FAILED` | `PENDING` | Payment failed -- the order remains open and a new payment can be initiated using the existing `orderId`. |

> **Tip:** When a payment is cancelled or fails, you do not need to create a new order. Simply initiate a new payment against the same `orderId` to retry.

## Create an Order

Every payment starts with an order containing line items and a terminal ID.

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [{
    "id": "ITEM-001",
    "name": "Running Shoes",
    "quantity": 1,
    "amount": { "regular": 50000, "total": 50000, "currency": "752",
      "tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }] }
  }],
  "totalOrderAmount": { "regular": 50000, "total": 50000, "currency": "752",
    "tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }] },
  "controlFunctions": {
    "initiatePaymentsOptions": { "paymentMethod": "CARD" }
  }
}
```

```json
// Response
{ "status": "SUCCESS",
  "data": { "orderId": "83a1ba32774149710b", "paymentId": "83a1ba3264bd500106" },
  "message": "Order created successfully" }
```

Store both `orderId` and `paymentId` -- you need them for all subsequent operations.

### Delay Capture

To authorize now but capture later (e.g., charge at shipment), set `delayCapture: true` in `controlFunctions`. You can also use `authMode: "PRE-AUTH"` for pre-authorization flows, which automatically enables delayed capture.

## Capture a Payment

When an order uses `delayCapture: true`, explicitly capture to finalize the charge.

```json
POST /payments/:paymentId/capture
{ "amount": 50000 }
```

The `amount` field is only required for `PRE-AUTH` orders where you capture a different amount than authorized. For standard delayed capture, send an empty body `{}`.

```json
// Response
{ "status": "SUCCESS", "message": "Payment captured successfully" }
```

Check capture status with `GET /payments/:paymentId/capture`. Possible `captureStatus` values: `PENDING`, `SUCCESS`, `ERROR`.

## Void a Payment

Voiding reverses a completed payment **before settlement** -- no money moves.

```json
POST /payments/:paymentId/void
{}
```

```json
// Response
{ "status": "SUCCESS",
  "data": { "voidStatus": "VOIDED" },
  "message": "Payment voided successfully" }
```

Possible `voidStatus` values: `VOID_INITIATED`, `CANNOT_VOID`, `VOIDED`.

> **Important:** Voiding is only possible before 23:00 UTC on the transaction day, and only for completed payments. After settlement cutoff, use a refund instead.

## Cancel a Payment

Cancellation stops a payment **before it completes** -- for example, if the customer abandons checkout while payment is processing.

```json
DELETE /payments/:paymentId
```

```json
// Response
{ "status": "SUCCESS",
  "data": { "paymentStatus": "PAYMENT_CANCELLED" },
  "message": "Payment cancelled successfully" }
```

> **Cancel vs. Void:** Cancel applies to in-progress payments (before completion). Void applies to completed payments (before settlement).

## Refund an Order

A full refund is a **new order** with negative quantities and the original `orderId` as `purchaseOrderId` on each line item.

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [{
    "id": "ITEM-001",
    "purchaseOrderId": "ORIGINAL_ORDER_ID",
    "name": "Running Shoes",
    "quantity": -1,
    "amount": { "regular": 50000, "total": -50000, "currency": "752",
      "tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }] }
  }],
  "totalOrderAmount": { "regular": 50000, "total": -50000, "currency": "752",
    "tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }] },
  "controlFunctions": {
    "initiatePaymentsOptions": { "paymentMethod": "CARD" }
  }
}
```

Key details:

- Set `quantity` to a negative value to indicate a return
- Set `amount.total` to a negative value
- Include the original `purchaseOrderId` on each line item
- For card refunds, `CARD_NP` is the recommended payment method
- Transaction fees are charged again on refunds

## Partial Refund

Works the same as a full refund, but only include the specific items or reduced quantities you want to return.

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [{
    "id": "ITEM-002",
    "purchaseOrderId": "ORIGINAL_ORDER_ID",
    "name": "Water Bottle",
    "quantity": -1,
    "amount": { "regular": 15000, "total": -15000, "currency": "752",
      "tax": [{ "amount": 3000, "percentage": 25, "type": "VAT" }] }
  }],
  "totalOrderAmount": { "regular": -15000, "total": -15000, "currency": "752",
    "tax": [{ "amount": 3000, "percentage": 25, "type": "VAT" }] },
  "controlFunctions": {
    "initiatePaymentsOptions": { "paymentMethod": "CARD" }
  }
}
```

> **Note:** All payment methods except NSWISH, SVIPPS, and SMOBILEPAY support partial refunds.

## Checking Order Status

Query the current state of any order at any point:

```json
GET /orders/:orderId/status
```

```json
// Response
{ "status": "SUCCESS",
  "data": {
    "orderStatus": "PAYMENT_COMPLETED",
    "payments": [{ "paymentId": "83a1ba3264bd500106",
      "paymentStatus": "PAYMENT_COMPLETED", "paymentMethod": "CARD", "amount": 50000 }],
    "paymentIds": ["83a1ba3264bd500106"]
  } }
```

## Decision Guide

| Situation | Action |
|-----------|--------|
| Payment initiated but not completed | **Cancel** -- `DELETE /payments/:paymentId` |
| Payment completed, not yet settled (before 23:00 UTC) | **Void** -- `POST /payments/:paymentId/void` |
| Payment settled, need full reversal | **Full Refund** -- create order with negative quantities |
| Payment settled, need partial reversal | **Partial Refund** -- create order with specific negative items |
| Delayed-capture order, ready to charge | **Capture** -- `POST /payments/:paymentId/capture` |

## Reference

- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Developer Portal](https://developers.surfboardpayments.com/)

---

# Capture a Payment

Finalize a previously authorized payment by capturing funds. Covers delay capture and pre-authorization flows with step-by-step API examples.

Source: https://www.surfboardpayments.com/developers/guides/capture-a-payment
Category: online
Tags: Online, API, Payments, Capture, In-Store

---
## Overview

When you create an order with `delayCapture: true` or `authMode: "PRE-AUTH"`, funds are authorized but not immediately captured. This lets you verify inventory, confirm fulfillment, or adjust the final amount before charging the customer.

This guide walks through the full capture flow: create an authorized order, capture the payment, and verify the result.

## When to Use Delay Capture

| Scenario | Description |
|----------|-------------|
| **E-commerce fulfillment** | Authorize at checkout, capture at shipment |
| **Pre-authorization** | Hold a variable amount (e.g., hotel deposit), capture actual charge later |
| **Service bookings** | Authorize upfront, capture after service delivery |
| **Digital products** | Authorize, verify access, then capture |

## Step 1: Create an Order with Delay Capture

Create an order with `delayCapture: true` in `controlFunctions`:

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [
    {
      "id": "ITEM-001",
      "name": "Nike Shoes",
      "quantity": 1,
      "amount": {
        "regular": 50000,
        "total": 50000,
        "currency": "752",
        "tax": [
          { "amount": 10000, "percentage": 25, "type": "VAT" }
        ]
      }
    }
  ],
  "totalOrderAmount": {
    "regular": 50000,
    "total": 50000,
    "currency": "752",
    "tax": [
      { "amount": 10000, "percentage": 25, "type": "VAT" }
    ]
  },
  "controlFunctions": {
    "delayCapture": true,
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD"
    }
  }
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderId": "83ac302f7c5130810b",
    "paymentId": "83ac302f24bfb00b06"
  },
  "message": "Order created successfully"
}
```

Store the `paymentId` -- you need it to capture.

### Pre-Authorization Mode

For flows where the final capture amount may differ from the authorized amount, use `authMode: "PRE-AUTH"`. This automatically enables `delayCapture`:

```json
{
  "controlFunctions": {
    "delayCapture": true,
    "authMode": "PRE-AUTH",
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD"
    }
  }
}
```

## Step 2: Capture the Payment

Once ready to finalize, call the capture endpoint with the `paymentId`:

```json
POST /payments/:paymentId/capture
{}
```

For `PRE-AUTH` orders, you can specify a different capture amount:

```json
POST /payments/:paymentId/capture
{
  "amount": 45000
}
```

```json
// Response
{
  "status": "SUCCESS",
  "message": "Payment captured successfully"
}
```

> The `amount` field is only valid for `PRE-AUTH` orders. For standard `delayCapture`, send an empty body to capture the full authorized amount.

## Step 3: Check Capture Status

Verify the capture completed successfully:

```json
GET /payments/:paymentId/capture
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "captureStatus": "SUCCESS"
  }
}
```

**Possible `captureStatus` values:** `PENDING` | `SUCCESS` | `ERROR`

## Reference

- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Create an Order](/developers/guides/create-an-order)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)

---

# Void a Payment

Reverse a completed payment before settlement. Voiding stops funds from transferring to the merchant's account, avoiding incorrect transactions.

Source: https://www.surfboardpayments.com/developers/guides/void-a-payment
Category: online
Tags: Online, API, Payments, In-Store

---
## Overview

Voiding reverses a **completed** payment before it settles -- no money moves from the customer's account to the merchant's. This is the quickest way to reverse a transaction on the same day, avoiding refund processing fees.

> **Void vs. Cancel vs. Refund:**
> - **Cancel** -- payment is still in progress (not yet completed)
> - **Void** -- payment completed but not yet settled (same day, before 23:00 UTC)
> - **Refund** -- payment has settled (next day or later)

## When to Use Void

| Scenario | Description |
|----------|-------------|
| **Wrong amount charged** | Customer was overcharged, caught same day |
| **Duplicate transaction** | Same payment processed twice |
| **Customer changed mind** | Immediate post-purchase reversal |
| **Incorrect product** | Wrong item charged at point of sale |

## Step 1: Void the Payment

Call the void endpoint with the `paymentId`:

```json
POST /payments/:paymentId/void
{}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "voidStatus": "VOIDED"
  },
  "message": "Payment voided successfully"
}
```

**Possible `voidStatus` values:** `VOID_INITIATED` | `CANNOT_VOID` | `VOIDED`

> **Important:** Voiding is only possible before 23:00 UTC on the transaction day. After the settlement cutoff, you must process a [refund](/developers/guides/refund-an-order) instead.

## Step 2: Verify Order Status

Confirm the void was applied:

```json
GET /orders/:orderId/status
```

The order's transaction data will show `voided: true` for the affected transaction.

## Handling `CANNOT_VOID`

If the void returns `CANNOT_VOID`, the payment has either:
- Already been settled (past 23:00 UTC cutoff)
- Not yet completed (use [Cancel](/developers/guides/cancel-a-payment) instead)

In these cases, process a [full refund](/developers/guides/refund-an-order) or [partial refund](/developers/guides/partial-refund) as needed.

## Reference

- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Create an Order](/developers/guides/create-an-order)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)

---

# Cancel a Payment

Stop an in-progress payment before it completes. Use cancellation when a customer abandons checkout or a payment needs to be halted mid-process.

Source: https://www.surfboardpayments.com/developers/guides/cancel-a-payment
Category: online
Tags: Online, API, Payments, In-Store

---
## Overview

Cancellation stops a payment **before it completes** -- for example, if the customer abandons checkout while payment is processing, or you need to halt a transaction before funds are transferred.

> **Cancel vs. Void:** Cancel applies to in-progress payments (before completion). If the payment has already completed, use [Void a Payment](/developers/guides/void-a-payment) instead.

## When to Use Cancel

| Scenario | Description |
|----------|-------------|
| **Customer abandons checkout** | Payment initiated but customer leaves |
| **Timeout** | Payment processing takes too long |
| **Error detected** | Issue found after payment initiation |
| **Duplicate order** | Accidentally created a second payment |

## Step 1: Cancel the Payment

Call the delete endpoint with the `paymentId` from the original order:

```json
DELETE /payments/:paymentId
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "paymentStatus": "PAYMENT_CANCELLED"
  },
  "message": "Payment cancelled successfully"
}
```

**Possible `paymentStatus` values:** `PAYMENT_COMPLETED` | `PAYMENT_FAILED` | `PAYMENT_CANCELLED`

If the payment already completed before your cancel request was processed, the status will show `PAYMENT_COMPLETED` and you should use a [void](/developers/guides/void-a-payment) or [refund](/developers/guides/refund-an-order) instead.

## Step 2: Verify Order Status

Confirm the order reflects the cancellation:

```json
GET /orders/:orderId/status
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderStatus": "PAYMENT_CANCELLED",
    "payments": [
      {
        "paymentId": "83a1ba3264bd500106",
        "paymentStatus": "PAYMENT_CANCELLED",
        "paymentMethod": "CARD",
        "amount": 50000
      }
    ]
  }
}
```

## Decision Guide

| Payment State | Action |
|---------------|--------|
| In progress (not completed) | **Cancel** -- `DELETE /payments/:paymentId` |
| Completed, not settled (before 23:00 UTC) | [Void](/developers/guides/void-a-payment) -- `POST /payments/:paymentId/void` |
| Settled | [Refund](/developers/guides/refund-an-order) -- create order with negative quantities |

## Reference

- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Create an Order](/developers/guides/create-an-order)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)

---

# Tokens

Turn a card used once into a card you can charge again. Enable tokenization on an order, fetch the token it produced, and store it for subscriptions, repeat purchases and refunds.

Source: https://www.surfboardpayments.com/developers/guides/tokens
Category: online
Tags: Online, API, Tokenization, Cards, MIT

---
## Overview

A token is a reference to a card that Surfboard holds and you do not. The customer enters their details once, on a page or a terminal that is already in scope for PCI DSS, and you get back a `tokenId` you can charge later without the card ever touching your systems.

That is what makes the rest possible: subscription renewals, one-click repeat purchases, deposits settled after the fact, refunds routed back to the original card. This guide covers producing a token, reading it, and storing it. Charging one is [Server-to-Server API](/developers/guides/server-to-server-api), and scheduling the charges is [Recurring Payments](/developers/guides/recurring-payments).

| Where the card is entered | What tokenization gives you |
|---------------------------|------------------------------|
| Payment page or Online SDK | Card data handled inside Surfboard's PCI scope, never yours |
| MerchantInitiated terminal | A card you can charge with no customer present |
| Refunds | A reference back to the card that paid, without storing the card |

## Step 1: Ask for a Token

Set `enforceTokenization` when you create the order. On an online order it belongs inside `controlFunctions.online`:

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [
    {
      "id": "ITEM-001",
      "name": "First month",
      "quantity": 1,
      "amount": { "regular": 19900, "total": 19900, "currency": "752" }
    }
  ],
  "totalOrderAmount": { "regular": 19900, "total": 19900, "currency": "752" },
  "controlFunctions": {
    "initiatePaymentsOptions": { "paymentMethod": "CARD", "amount": 19900 },
    "online": {
      "enforceTokenization": true
    }
  }
}
```

Three flags decide how hard you insist, and the difference matters when the card or the issuer will not play along:

| Flag | Behaviour |
|------|-----------|
| `enforceTokenization` | Tokenize the card for future use. Overrides the terminal's own configuration. |
| `tokenisationIfPossible` | Tokenize where it is supported, and carry on quietly where it is not. |
| `errorIfTokenizationFails` | Fail the whole payment if the card cannot be tokenized. |

Pick by what breaks if there is no token. A subscription with no token to renew against is worse than a failed first payment, so `errorIfTokenizationFails` is right there. A shop offering "save this card for next time" should not lose the sale over it, so `tokenisationIfPossible` is right there.

For subscriptions, pair tokenization with the `recurring` block so the first payment is authorised as the start of a series rather than a one-off — see [Online Payment Link](/developers/guides/online-payment-link#recurring-orders).

## Step 2: Fetch the Token

The token exists once the payment succeeds. Read it from the order:

```
GET /orders/:orderId/tokens
```

```json
// Response
{
  "status": "SUCCESS",
  "data": [
    {
      "cardBrand": "VISA",
      "cardholderName": "Tom",
      "tokenId": "822d544dc48c200308",
      "createdAt": "2024-04-25T11:22:24.845Z",
      "expiryMonth": 7,
      "expiryYear": 2026,
      "truncatedPan": "8907",
      "cardArt": "iVBORw0KGgoAAAANSUhEUgAAAUQAAA......"
    }
  ],
  "message": "Fetched the card information."
}
```

| Field | What it is for |
|-------|----------------|
| `tokenId` | The handle you charge against. The only field that does anything. |
| `cardBrand`, `truncatedPan` | "Visa ending 8907" — how you show a saved card back to a customer. |
| `expiryMonth`, `expiryYear` | When the token stops working. Worth acting on before it does. |
| `cardholderName` | As given by the card. |
| `cardArt` | Base64 card image, if you want the saved card to look like the card. |

The response is an array. An order paid in parts, or retried on a second card, produces more than one token, so do not assume `data[0]`.

## Step 3: Store It

Store the `tokenId` against the customer in your own system, along with enough to describe it back to them — brand, last four, expiry. That pairing is the whole point: the token is meaningless without knowing whose card it is, and useless if the customer cannot tell which of their two Visas it is.

Store it where you would store an account identifier: your primary datastore, encrypted at rest, out of logs and analytics events. A token is not card data, but it is a bearer reference to someone's money.

## Charging a Stored Token

Pass it into a payment through `paymentMethodParams`:

```json
POST /payments
{
  "orderId": "83a1ba32774149710b",
  "paymentMethod": "CARD",
  "amount": 19900,
  "paymentMethodParams": {
    "tokenId": "822d544dc48c200308"
  }
}
```

A charge with no customer present is a Merchant Initiated Transaction, and it needs a terminal in `MerchantInitiated` mode — the terminal the customer paid on cannot do it. An online store is provisioned with one, so this is usually a matter of reading the store's terminal list rather than registering anything. [Server-to-Server API](/developers/guides/server-to-server-api) covers the setup and the rules that come with MIT.

## Expiry and Housekeeping

Tokens do not live forever, and the failure is silent until you try to charge:

- **The card expires.** You have `expiryMonth` and `expiryYear` at the point of tokenization, so you can warn a subscriber before the renewal that will fail rather than after it.
- **The card is replaced, lost or cancelled.** The token stops working with no notice to you. Handle the failure on the charge and ask the customer to re-authorise.
- **The customer asks you to forget them.** Delete your side of the mapping. A token with no customer attached is not usable and should not be kept.

Build the re-authorisation path before you need it: a link back to a payment page that tokenizes a fresh card and swaps the token on the subscription. It is the difference between a churned subscriber and a two-minute interruption.

## Reference

- [Fetch Tokens from Order](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Server-to-Server API](/developers/guides/server-to-server-api)
- [Recurring Payments](/developers/guides/recurring-payments)
- [Online Payment Link](/developers/guides/online-payment-link)

---

# Recurring Payments

Implement subscription billing and recurring charges using tokenization, recurring payment configuration, and Merchant Initiated Transactions.

Source: https://www.surfboardpayments.com/developers/guides/recurring-payments
Category: online
Tags: Online, API, Recurring, Subscriptions, Tokenization, MIT

---
## Overview

Recurring payments let you charge customers on a schedule without requiring them to re-enter card details each time. This guide covers the full lifecycle: tokenizing a card during the initial payment, configuring subscription parameters, charging with stored tokens, and managing the subscription over time.

This builds on the concepts introduced in the [Server-to-Server API](/guides/server-to-server-api) guide. If you are new to Merchant Initiated Transactions (MIT) and tokenization, start there first.

## How It Works

1. **Initial payment** -- The customer pays through a PaymentPage or SelfHostedPage terminal. Tokenization stores their card for future use.
2. **Subscription configuration** -- You define frequency, amount type, and payment count using the `recurring` object.
3. **Recurring charges** -- Your backend creates orders against a MerchantInitiated terminal and pays using the stored token.
4. **Lifecycle management** -- You handle renewals, cancellations, amount changes, and failed payment retries.

## Prerequisites

- A **PaymentPage** or **SelfHostedPage** terminal for the initial customer-present payment
- A **MerchantInitiated** terminal for subsequent server-to-server charges
- API credentials (API key, API secret, merchant ID)

Both `PaymentPage` and `MerchantInitiated` terminals are provisioned when the online store is created, so unless you are taking the first payment on your own page with `SelfHostedPage`, there is nothing to register — list the store's terminals and use the IDs it returns.

## Step 1: Create the Initial Tokenized Order

The first order collects card details and sets up the recurring agreement. Use `enforceTokenization` together with the `recurring` configuration and set `subscription` to `true`:

```json
POST /orders
{
  "terminal$id": "YOUR_PAYMENT_PAGE_TERMINAL_ID",
  "orderLines": [
    {
      "id": "SUB-001",
      "name": "Pro Plan - Monthly",
      "quantity": 1,
      "amount": {
        "regular": 9900,
        "total": 9900,
        "currency": "752"
      }
    }
  ],
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD",
      "amount": 9900
    },
    "online": {
      "enforceTokenization": true,
      "subscription": true,
      "recurring": {
        "subscriptionAmountType": "fixed",
        "frequency": "monthly",
        "numberOfPayments": 12,
        "uniqueReference": "cust-42-pro-monthly",
        "validation": "validated"
      }
    }
  }
}
```

The customer completes this payment on the payment page. Once the payment succeeds, the card is tokenized.

## Step 2: Retrieve and Store the Token

After the initial payment completes, fetch the token:

```
GET /orders/:orderId/tokens
```

Store the returned `tokenId` securely on your backend, associated with the customer record. This token is used for all future recurring charges.

> **Warning:** Never expose token IDs in client-side code or logs.

## Recurring Configuration Reference

The `controlFunctions.online.recurring` object controls how the subscription behaves:

| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `subscriptionAmountType` | Yes | string | `"fixed"` for same amount each cycle, `"variable"` for amounts that change |
| `maxAmount` | No | number | Maximum charge amount in smallest currency unit. Only used with `"variable"` amount type |
| `frequency` | Yes | string | Billing cycle. See frequency options below |
| `numberOfPayments` | No | number | Total number of payments for the subscription. Omit for indefinite |
| `uniqueReference` | No | string | Your unique identifier for this recurring agreement |
| `validation` | Yes | string | `"validated"` if the initial payment is authenticated (3DS), `"notValidated"` otherwise |

### Frequency Options

| Value | Cycle |
|-------|-------|
| `daily` | Every day |
| `twiceWeekly` | Twice per week |
| `weekly` | Every week |
| `tenDays` | Every 10 days |
| `fortNightly` | Every 2 weeks |
| `monthly` | Every month |
| `everyTwoMonths` | Every 2 months |
| `trimester` | Every 4 months |
| `quarterly` | Every 3 months |
| `twiceYearly` | Every 6 months |
| `annually` | Every year |
| `unscheduled` | No fixed schedule (usage-based or on-demand) |

## Step 3: Charge with the Stored Token

When a billing cycle is due, create an order on the MerchantInitiated terminal and pay with the token:

### Create the recurring order

```json
POST /orders
{
  "terminal$id": "YOUR_MIT_TERMINAL_ID",
  "referenceId": "sub-cust42-2026-02",
  "orderLines": [
    {
      "id": "SUB-002",
      "name": "Pro Plan - February 2026",
      "quantity": 1,
      "amount": {
        "regular": 9900,
        "total": 9900,
        "currency": "752"
      }
    }
  ]
}
```

### Initiate payment with the token

```json
POST /payments
{
  "orderId": "ORDER_ID_FROM_ABOVE",
  "paymentMethod": "CTOKEN",
  "tokenId": "STORED_TOKEN_ID"
}
```

### Verify the result

```
GET /orders/:orderId/status
```

A successful charge returns `orderStatus: "PAYMENT_COMPLETED"`.

## Variable-Amount Subscriptions

For metered billing or usage-based pricing, set `subscriptionAmountType` to `"variable"` and specify a `maxAmount`:

```json
"recurring": {
  "subscriptionAmountType": "variable",
  "maxAmount": 50000,
  "frequency": "monthly",
  "uniqueReference": "cust-42-usage",
  "validation": "validated"
}
```

Each recurring charge can then use a different amount (up to `maxAmount`) based on the customer's usage for that period.

## Handling Failed Payments

When a recurring charge fails, the order status will show `PAYMENT_FAILED` or `PAYMENT_CANCELLED`. Common reasons include expired cards, insufficient funds, or issuer declines.

**Retry strategy:**

1. Check the `failureReason` on the payment status response.
2. For soft declines (insufficient funds, temporary issuer issues), retry the same order by calling the Initiate Payment API again with the token.
3. Space retries over increasing intervals (e.g., 1 day, 3 days, 7 days).
4. After repeated failures, notify the customer to update their card details. Direct them to a new payment page order with `enforceTokenization: true` to capture a fresh token.
5. Replace the old token with the new one in your system.

## Managing the Subscription Lifecycle

| Action | How to implement |
|--------|-----------------|
| **Pause** | Stop creating new orders on your billing schedule. The token remains valid. |
| **Resume** | Start creating orders again using the same stored token. |
| **Cancel** | Stop billing. Optionally delete the stored token via your internal records. |
| **Upgrade / downgrade** | Change the amount on the next order you create. For variable subscriptions this works within `maxAmount`. For fixed subscriptions, create a new initial order with the updated recurring configuration. |
| **Update payment method** | Direct the customer to a new payment page order with tokenization enabled, then replace the stored token. |

## Reference

- [Server-to-Server API Guide](/guides/server-to-server-api) -- Tokenization and MIT fundamentals
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)
- [Token Management](https://developers.surfboardpayments.com/api/orders)

---

# B2B Invoices

Bill a business buyer on payment terms instead of charging a card. Create an order paid with B2BINV, distribute the invoice as an e-invoice or by email, and credit it when the goods come back.

Source: https://www.surfboardpayments.com/developers/guides/b2b-invoices
Category: online
Tags: Online, API, Invoice, B2B, Payment Methods

---
## Overview

Above a few hundred euros, a business buyer generally will not pay by card. Procurement expects an invoice on terms, approved by someone who was not in the room when the order was placed, and paid by bank transfer thirty days later. A checkout that only takes cards quietly loses that business.

B2B invoicing in Surfboard is a payment method, not a separate product. It is the same [Create Order](/developers/guides/create-an-order) call against the same online terminal, with `paymentMethod` set to `B2BINV` and an `invoice` block that says how the invoice is delivered and when it falls due. Surfboard issues the document, distributes it as an e-invoice or by email, chases it with reminders if you ask, and gives the buyer bank details to settle against.

What changes is the timing. A card payment moves money at checkout; an invoice raises a claim at checkout and moves money on the due date. Everything downstream — reconciliation, credit notes, reporting — follows from that.

> **Scope:** B2BINV is an online payment method. Raise invoices against an online terminal — the `PaymentPage` or `MerchantInitiated` terminal your online store is provisioned with. It is not available on physical terminals.

## Prerequisites

1. A developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. A merchant that has completed onboarding, with an approved online store — [Online Payment Link](/developers/guides/online-payment-link) covers store creation and domain verification
3. `b2binv` active on the merchant or the store
4. The `terminalId` of the store's online terminal

## Step 1: Activate B2B Invoicing

Card is on by default; `b2binv` is not. Activate it through the Payment Methods API:

```json
POST /merchants/:merchantId/payment-methods
{
  "b2binv": true
}
```

You can also activate it from the Partner Portal or the Merchant Portal — the three paths write to the same configuration, so pick whichever suits how the merchant is managed. To scope invoicing to one store rather than the whole merchant, use the store-level endpoint:

```
POST /merchants/:merchantId/stores/:storeId/payment-methods
```

See [Payment Methods](/developers/guides/payment-methods) for the full activation and deactivation flow. Until the method is active, the payment initiation inside Create Order will fail, so do this before you send the first invoice.

## Step 2: Identify the Buyer

This is the part that has no equivalent in a card payment. You are not charging a person, you are billing a legal entity, and the invoice has to name it correctly and say where to send it. Two blocks do that work: `customer`, which identifies who owes the money, and `billing`, which is the address the invoice is issued to.

### The Customer

Send both `person` and `company`:

```json
"customer": {
  "person": {
    "name": { "firstName": "Elin", "lastName": "Berg" },
    "email": "ap@radio-ocean.example",
    "phoneNumber": { "code": "46", "number": "701234567" }
  },
  "company": {
    "companyName": "Radio Ocean AB",
    "vatId": "SE556000000001",
    "registrationNumber": "5560000000"
  }
}
```

| Field | Notes |
|-------|-------|
| `company.companyName` | The legal name of the entity being billed, as it should appear on the invoice. |
| `company.vatId` | VAT registration number, including the country prefix. |
| `company.registrationNumber` | Company registration number. |
| `person.email` | Where an `EMAIL` invoice is delivered. Use the buyer's accounts-payable address, not the salesperson's. |
| `person.name`, `person.phoneNumber` | The contact on the buying side. |

The company details identify who owes the money and are what the invoice is issued against. Get them from the buyer at checkout rather than inferring them from an email domain — a wrong registration number is an invoice the buyer's finance team can reject.

### The Billing Address

`billing` is optional on a Create Order call in general. **For a B2B invoice it is mandatory** — an invoice is a document addressed to somewhere, and there is no sensible default.

```json
"billing": {
  "address": {
    "addressLine1": "Surfgatan 1",
    "city": "Stockholm",
    "postalCode": "11122",
    "countryCode": "SE"
  }
}
```

| Field | Required | Notes |
|-------|----------|-------|
| `billing.address.addressLine1` | Yes | Street address of the entity being billed. |
| `billing.address.city` | Yes | City. |
| `billing.address.postalCode` | Yes | Postal code. |
| `billing.address.countryCode` | Yes | ISO 3166-1 alpha-2, uppercase. |
| `billing.address.careOf` | No | Attention line — useful when invoices go to a named accounts-payable desk. |
| `billing.address.addressLine2`, `addressLine3` | No | Further address lines. |
| `billing.name`, `billing.email`, `billing.phoneNumber` | No | A billing contact distinct from `customer.person`. |

This is the buyer's registered billing address, which is not necessarily where the goods go. If you are shipping somewhere else, put that in `shipping` and leave `billing` as the address finance works from.

## Step 3: Create the Invoice Order

The call is Create Order with the invoice configuration carried in `controlFunctions.initiatePaymentsOptions.paymentMethodParams.invoice`:

```json
POST /orders
{
  "terminal$id": "813ca2cb12ce400405",
  "referenceId": "order-2026-0418",
  "billing": {
    "address": {
      "addressLine1": "Surfgatan 1",
      "city": "Stockholm",
      "postalCode": "11122",
      "countryCode": "SE"
    }
  },
  "orderLines": [
    {
      "id": "83dddf1596c8d03937",
      "name": "7'8 Radio Ocean Liner",
      "description": "7'8 Radio Ocean Liner surfboard",
      "quantity": 1,
      "amount": {
        "regular": 179200,
        "total": 179200,
        "currency": "752",
        "tax": [{ "type": "VAT", "percentage": 25, "amount": 35840 }]
      }
    }
  ],
  "customer": {
    "person": {
      "name": { "firstName": "Elin", "lastName": "Berg" },
      "email": "ap@radio-ocean.example",
      "phoneNumber": { "code": "46", "number": "701234567" }
    },
    "company": {
      "companyName": "Radio Ocean AB",
      "vatId": "SE556000000001",
      "registrationNumber": "5560000000"
    }
  },
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "B2BINV",
      "paymentMethodParams": {
        "invoice": {
          "invoiceDistribution": "EINVOICE",
          "dueDate": "30d",
          "reminder": false,
          "invoicePaymentMethods": ["BANK", "DIRECT_BANK"]
        }
      }
    }
  }
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderId": "845712d3b9674383020b",
    "paymentId": "845712d3b9675f900206",
    "interAppJWT": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "paymentPageLink": "https://pay.withsurfboard.com/845712d3b9674383020b?pi=ocaY_Xgzq9SqhTPR-ry9d8Ne2s3Cl9rB...",
    "invoiceDetails": {
      "invoiceId": 5100232680,
      "invoicePdfUrl": "https://b2b.payer.se/api/v1/receiptViewer/invoice/pdf/1946a1ab-141f-441c-938d-8044278922ed",
      "iban": "SE0000000000000000000000",
      "accountHolderName": "Surfboard Payments AB",
      "bic": "DNBASESX",
      "bankgiro": "0000000",
      "ocr": "00845712396759002065"
    }
  },
  "message": "Order created successfully"
}
```

Amounts follow the same rules as every other order: minor units, a numeric ISO 4217 `currency` (`752` is SEK), and tax stated per line. `totalOrderAmount` is optional, but when you send it, it has to reconcile with the lines.

Give every line a `description` as well as a `name`. On a card payment nobody reads the line items; on an invoice they are the document, and the person approving it may never have seen the order. A line that says only "Liner" is a line someone has to email you about.

### The Invoice Block

| Field | Required | Notes |
|-------|----------|-------|
| `invoiceDistribution` | Yes | `EINVOICE` or `EMAIL`. E-invoice is routed to the company; email goes to `customer.person.email`. |
| `dueDate` | Yes | A relative duration such as `30d` for Net 30, or an absolute date. |
| `invoicePaymentMethods` | Yes | How the buyer may settle: `BANK`, `DIRECT_BANK`, `CARD`. Send the ones you will accept. |
| `reminder` | Yes | Whether Surfboard sends reminders as the due date passes. |
| `debtCollectionEnabled` | No | Hands a past-due invoice to the debt-collection flow. |
| `contractReference` | No | Your reference for the underlying contract, carried on the invoice. |

`dueDate` is a commercial decision, not a technical one. Net 30 is the common default for procurement; longer terms are a financing choice the merchant is making on the buyer's behalf. Set `reminder` and `debtCollectionEnabled` deliberately too — both change what the buyer receives after the due date, and both should match what the merchant agreed with them.

### What Comes Back

Because the payment is initiated inside the same call, the response carries the invoice itself in `invoiceDetails`:

| Field | Type | Description |
|-------|------|-------------|
| `invoiceId` | number | The invoice identifier. Note it is a **number**, not a string — store it as one. |
| `invoicePdfUrl` | string | The invoice document. This is the thing the buyer's finance team will actually open. |
| `iban` | string | The account to transfer to, for international settlement. |
| `bic` | string | Bank identifier code for that account. |
| `accountHolderName` | string | The account holder shown on the invoice. |
| `bankgiro` | string | Swedish bankgiro number, for domestic transfers. |
| `ocr` | string | The OCR reference the buyer quotes on the payment so it reconciles automatically. |

`bankgiro` and `ocr` are how a Swedish buyer settles a `BANK` transfer, and the OCR is what matches their payment back to this invoice without anyone reading a bank statement. `iban` and `bic` cover payment from outside the country. All of them appear on the PDF, so you do not have to surface them yourself — but store `invoiceId` and `ocr` against your own accounts-receivable record, because those are what reconciliation is keyed on later.

The response also carries the usual `orderId` and `paymentId`, plus a `paymentPageLink` — the same hosted page a card order returns, where a buyer can settle through whichever of `DIRECT_BANK` or `CARD` you allowed in `invoicePaymentMethods`.

If you initiate the payment separately rather than inside Create Order, the same `invoiceDetails` block comes back from the [Initiate Payment](https://developers.surfboardpayments.com/api/payments) call instead.

## Step 4: Confirm and Reconcile

Order status works the same as for any other order:

```
GET /orders/:orderId/status
```

Subscribe to `order.paymentcompleted` and `order.paymentfailed` rather than polling on a timer — see [Webhooks](/developers/guides/webhooks-notifications) for subscriptions, retries and signature verification.

The thing to hold on to is that an invoice settles on its own timetable. The order tells you the invoice was raised and where it stands; the money arriving is a separate event on the buyer's terms. Reconcile invoice revenue against [Settlements & Reporting](/developers/guides/settlements-reporting) rather than treating order creation as cash in the bank.

## Crediting an Invoice

When an invoice was wrong or the goods come back, the buyer gets a credit invoice. **How you raise it depends on whether the original invoice has been paid**, and this is the one thing to get right before you write any code:

| The original invoice | What you do | One call or two |
|----------------------|-------------|-----------------|
| Raised, not yet paid | Cancel the order | One call, no body |
| Paid | Create a return order | A full order payload |

Because an invoice sits unpaid for the whole of its term by design, the unpaid case is the one you will hit most.

### Unpaid: Cancel the Order

There is nothing to give back yet, so this is a cancellation rather than a refund. Cancelling the order raises a credit invoice by default:

```
DELETE /orders/:orderId
```

The endpoint takes no request body.

```json
// Response
{
  "status": "SUCCESS",
  "message": "Order cancelled successfully"
}
```

That is the whole operation. Use it for the ordinary cases — wrong amount, wrong entity, the buyer walked away after the invoice went out.

### Paid: Create a Return Order

Once the buyer has settled, crediting is a refund and takes the standard return-order shape: a new order with negative quantities, each line pointing back at the order and the payment it credits.

```json
POST /orders
{
  "terminal$id": "813ca2cb12ce400405",
  "referenceId": "credit-2026-0418",
  "billing": {
    "address": {
      "addressLine1": "Surfgatan 1",
      "city": "Stockholm",
      "postalCode": "11122",
      "countryCode": "SE"
    }
  },
  "customer": {
    "person": {
      "name": { "firstName": "Elin", "lastName": "Berg" },
      "email": "ap@radio-ocean.example",
      "phoneNumber": { "code": "46", "number": "701234567" }
    },
    "company": {
      "companyName": "Radio Ocean AB",
      "vatId": "SE556000000001",
      "registrationNumber": "5560000000"
    }
  },
  "orderLines": [
    {
      "id": "83dddf1596c8d03937",
      "name": "7'8 Radio Ocean Liner",
      "description": "7'8 Radio Ocean Liner surfboard",
      "quantity": -1,
      "purchaseOrderId": "845712d3b9674383020b",
      "purchasePaymentId": "845712d3b9675f900206",
      "amount": {
        "regular": 179200,
        "total": 179200,
        "currency": "752",
        "tax": [{ "type": "VAT", "percentage": 25, "amount": 35840 }]
      }
    }
  ],
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "B2BINV",
      "paymentMethodParams": {
        "invoice": {
          "invoiceDistribution": "EMAIL",
          "dueDate": "30d",
          "reminder": false,
          "invoicePaymentMethods": ["BANK", "DIRECT_BANK"]
        }
      }
    }
  }
}
```

Key details:

- `quantity` goes negative on every credited line. `amount.total` stays positive.
- `purchaseOrderId` and `purchasePaymentId` both belong **on the line item**: the `orderId` and `paymentId` returned when the original invoice was created.
- Repeat the `billing` address and the `invoice` block. The credit is its own addressed document and can be distributed differently from the original — `EMAIL` here, where the original went out as `EINVOICE`.
- Credit only some of the lines and you have a partial credit. The mechanics are the same as [Partial Refund](/developers/guides/partial-refund).

> **`OR_0035: Cannot refund from purchase order that is not completed. Status: PENDING`** means exactly what the two paths above describe — the invoice has not been paid, so there is nothing to refund. Cancel the order instead.

The buyer keeps the original invoice and receives a credit against it. Both documents stand; the credit does not erase the original.

## Error Handling

Failures come back as `status: "ERROR"` with an `OR_*` or `PS_*` code, and the ones you will meet setting this up are mostly configuration rather than payload:

- **The method is not active.** `b2binv` has to be activated on the merchant or store before an invoice payment can initiate. This is the most common first failure.
- **The company block is missing.** A B2B invoice needs an entity to bill; a `customer` with only a `person` is not enough.
- **The billing address is missing.** `billing.address` is optional on Create Order generally and mandatory here. Easy to miss if you are adapting a working card payload.
- **The invoice block is incomplete.** `invoiceDistribution`, `dueDate`, `invoicePaymentMethods` and `reminder` are all required by the payment initiation, even though Create Order will accept the order without them.
- **You refunded an invoice nobody paid.** `OR_0035` on a return order means the original is still `PENDING`. Cancel it instead.
- **A credit line has no `purchaseOrderId`.** Every negative line must reference the order it credits, and carry the `purchasePaymentId` alongside it.

[Create Order Error Codes](/developers/guides/create-order-error-codes) lists the full set, including the errors raised by the payment initiation that happens inside the same call.

## Next Steps

- [Payment Methods](/developers/guides/payment-methods) — activating and deactivating `b2binv`
- [Create an Order](/developers/guides/create-an-order) — line items, tax and control functions in full
- [Refund an Order](/developers/guides/refund-an-order) — the card-side equivalent of a credit invoice
- [Settlements & Reporting](/developers/guides/settlements-reporting) — reconciling what has actually been paid
- [Online Payment Link](/developers/guides/online-payment-link) — the store and terminal setup this guide assumes

## Reference

- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Cancel an Order API](https://developers.surfboardpayments.com/api/orders) — the one-call credit for an unpaid invoice
- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)
- [Payment Methods API](https://developers.surfboardpayments.com/api/payment-methods)

---

# Refund an Order

Process a full refund by creating a return order with negative quantities. Covers the complete refund flow with API examples and payment method requirements.

Source: https://www.surfboardpayments.com/developers/guides/refund-an-order
Category: online
Tags: Online, API, Payments, Refunds, In-Store

---
## Overview

A full refund in Surfboard is processed by creating a **new order** with negative quantities and negative amounts, referencing the original order's `orderId` as the `purchaseOrderId` on each line item. When the payment completes, the full amount is returned to the customer.

## When to Use Full Refund

| Scenario | Description |
|----------|-------------|
| **Product return** | Customer returns all items |
| **Service not delivered** | Full service cancellation |
| **Order error** | Wrong order fulfilled entirely |
| **Post-settlement reversal** | Payment already settled, void no longer possible |

> If the payment hasn't settled yet (same day, before 23:00 UTC), consider using [Void a Payment](/developers/guides/void-a-payment) instead -- it's faster and avoids refund processing fees.

## Step 1: Create a Refund Order

Create a new order with negative `quantity` and negative `amount.total` for each line item. Include the original `orderId` as `purchaseOrderId`:

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "referenceId": "refund-order-001",
  "orderLines": [
    {
      "id": "ITEM-001",
      "purchaseOrderId": "ORIGINAL_ORDER_ID",
      "name": "Nike Shoes",
      "quantity": -2,
      "amount": {
        "regular": 10000,
        "total": -20000,
        "currency": "752",
        "tax": [
          { "amount": 4000, "percentage": 25, "type": "VAT" }
        ]
      }
    },
    {
      "id": "ITEM-002",
      "purchaseOrderId": "ORIGINAL_ORDER_ID",
      "name": "Apple Pods",
      "quantity": -1,
      "amount": {
        "regular": 20000,
        "total": -20000,
        "currency": "752",
        "tax": [
          { "amount": 4000, "percentage": 25, "type": "VAT" }
        ]
      }
    }
  ],
  "totalOrderAmount": {
    "regular": 30000,
    "total": -30000,
    "currency": "752",
    "tax": [
      { "amount": 8000, "percentage": 25, "type": "VAT" }
    ]
  },
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD_NP",
      "refundProcessingParams": {
        "purchasePaymentId": "ORIGINAL_PAYMENT_ID",
        "refundReason": "CUSTOMER_INITIATED_RETURN"
      }
    }
  }
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderId": "83b2ca45889a317b0b",
    "paymentId": "83b2ca4564bd500606"
  },
  "message": "Order created successfully"
}
```

The `terminal$id` only needs to be a **valid** terminal -- it does **not** have to be the same terminal that processed the original purchase.

Key details:

- Set `quantity` to a negative value to indicate a return
- Set `amount.total` to a negative value
- Include the original `purchaseOrderId` on each line item
- `totalOrderAmount.total` must be negative (the refund amount)

## Payment Method for Refunds

Set `paymentMethod` to either the method the customer originally paid with, or `CARD_NP`:

| Original Payment Method | Refund Method |
|------------------------|---------------|
| CARD | `CARD_NP` (recommended) or `CARD` |
| KLARNA | `KLARNA` |
| SWISH | `SWISH` |
| Other digital methods | Same as original |

For card refunds, the two card methods behave differently:

| Method | Behaviour |
|--------|-----------|
| `CARD_NP` | **Card not present.** Refunds straight back to the card that paid -- no terminal interaction. This is the recommended default for card refunds. |
| `CARD` | **Card present.** Triggers a card tap on the terminal, so a card must be physically presented to receive the refund. |

> **Note:** Transaction fees are charged again on refunds.

## Refund Processing Parameters

Pass refund metadata through `refundProcessingParams` inside `initiatePaymentsOptions`:

```json
{
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD_NP",
      "refundProcessingParams": {
        "purchasePaymentId": "ORIGINAL_PAYMENT_ID",
        "refundReason": "CUSTOMER_INITIATED_RETURN"
      }
    }
  }
}
```

| Parameter | Required | Description |
|-----------|----------|-------------|
| `purchasePaymentId` | No | The `paymentId` of the **original purchase** (returned when the original order was created). This is the payment-level reference, distinct from the `purchaseOrderId` you set on each line item. |
| `refundReason` | No | Why the refund is being issued. See the allowed values below. |
| `otherReason` | Conditional | Free-text explanation. **Required when `refundReason` is `OTHER`.** |

### Refund Reasons

| Value | Meaning |
|-------|---------|
| `CUSTOMER_INITIATED_RETURN` | The customer returned the goods or requested the refund. |
| `SUSPECTED_MALFUNCTION` | The product is suspected to be faulty or not working. |
| `SUSPECTED_FRAUD` | The transaction is suspected to be fraudulent. |
| `DUPLICATE_TRANSACTION` | The original charge was a duplicate. |
| `OTHER` | Any other reason -- requires a message in `otherReason`. |

When using `OTHER`, include the explanation:

```json
{
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD_NP",
      "refundProcessingParams": {
        "purchasePaymentId": "ORIGINAL_PAYMENT_ID",
        "refundReason": "OTHER",
        "otherReason": "Goodwill credit for delayed delivery"
      }
    }
  }
}
```

## Step 2: Check Refund Status

Verify the refund completed:

```json
GET /orders/:orderId/status
```

The order status will show `PAYMENT_COMPLETED` once the refund is processed. You can also track refund status via [webhooks](/developers/guides/webhooks-notifications).

## Adjustments in Refunds

If the original order included adjustments (tips, discounts), the refund includes them by default. Control this with `includeAdjustmentsForRefund`:

```json
{
  "controlFunctions": {
    "includeAdjustmentsForRefund": false,
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD"
    }
  }
}
```

For partial returns, by default the first refund order includes adjustments (`true`) and subsequent ones do not (`false`).

## Refund via Partner Portal

You can also process refunds through the UI:

1. Log in to **Partner Portal** > **Merchants** > select merchant > **Transactions**
2. Select the transaction to refund
3. Click **Create Refund** > **Full Refund** > **Process Refund**

## Refund FAQ

> **How long after a purchase can I issue a refund?**
> Refunds can be issued up to **90 days** after the original purchase. This limit is enforced by Surfboard across all payment methods -- there is no difference between card, Swish, Klarna, or other methods. If you need to reverse a transaction older than 90 days (e.g., an event ticket refund a year later), it cannot be processed through the API.

> **How long does it take for the customer to receive the refund?**
> Processing time depends on the payment method:
>
> | Payment Method | Refund Timeline |
> |----------------|-----------------|
> | **Card** (CARD, CARD_NP) | Up to 7 days. Depends on the issuer and acquirer fraud systems. |
> | **Swish** (SSWISH, NSWISH) | Instant |
> | **Vipps** (SVIPPS) | Instant |
> | **MobilePay** (SMOBILEPAY) | Up to 10 banking days |
> | **Klarna** (KLARNA) | Up to 10 days |

## Reference

- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Partial Refund](/developers/guides/partial-refund)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
- [Developer Portal](https://developers.surfboardpayments.com/)

---

# Receipts

Generate, email, print, and customise receipts for in-store transactions using the Surfboard Receipts API.

Source: https://www.surfboardpayments.com/developers/guides/receipts
Category: in-store
Tags: In-Store, API, Receipts, Printing, ESC/POS

---
## Overview

After a payment is completed, Surfboard gives you several ways to deliver receipts to customers. You can attach cash register details for regulatory compliance, email a digital copy, retrieve a shareable link, print directly on a Surfboard terminal, or send fully custom ESC/POS commands for branded receipt output.

All receipt endpoints accept a Transaction ID, Payment ID, or Order ID as the identifier, so you can work with whichever reference suits your integration.

## Prerequisites

Before working with receipts, make sure you have:

- A Surfboard developer account with valid API credentials (`API-KEY` and `API-SECRET`)
- At least one completed transaction, payment, or order
- For printing: a registered [terminal with a built-in printer](/products/?requirements=printer) or a [FinPrinter](/products/fins/finprinter)

## Adding Receipt Information

Use this endpoint to store cash register-specific details against an order. This data is used when generating receipt output and is often required for fiscal compliance in Nordic markets. If the register itself runs on Surfboard, see [Build Your Own ECR](/developers/guides/electronic-cash-register) for the full register lifecycle.

```
PUT /receipts/{orderId}
```

**Request body:**

```json
{
  "sequenceNumber": "1234567",
  "cashRegisterName": "Kassa 1",
  "controlUnitSerialNumber": "9876543",
  "cashierName": "Amanda",
  "customerName": "Tom"
}
```

**Request parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `sequenceNumber` | string | Yes | Receipt sequence number from your cash register. |
| `cashRegisterName` | string | Yes | Cash register designation or name. |
| `controlUnitSerialNumber` | string | Yes | Control unit or control system manufacturing number. |
| `cashierName` | string | No | Name of the cashier handling the transaction. |
| `customerName` | string | No | Name of the customer. |

**Response:**

```json
{
  "status": "SUCCESS",
  "message": "Receipt information added successfully"
}
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | `SUCCESS` or `ERROR`. |
| `message` | string | Human-readable status message. |

## Emailing Receipts

Send a digital receipt directly to a customer's email address. This is the simplest way to deliver post-payment confirmation without any printing hardware.

```
PUT /receipts/{id}/email
```

The `{id}` path parameter accepts a Transaction ID, Payment ID, or Order ID.

**Request body:**

```json
{
  "email": "customer@example.com"
}
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `email` | string | Yes | Email address to deliver the receipt to. |

**Response:**

```json
{
  "status": "SUCCESS",
  "message": "Receipt email sent successfully"
}
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | Status of the request. |
| `message` | string | Description of the result. |

> **Tip:** You can call this endpoint multiple times with different email addresses if the customer or merchant both need a copy.

## Fetching a Receipt Link

Retrieve a URL that points to a hosted digital receipt. This is useful when you want to display a QR code on the terminal screen, include a link in an SMS, or embed it in your own notification flow.

```
GET /receipts/{id}/link
```

The `{id}` path parameter accepts a Transaction ID, Payment ID, or Order ID.

**Request body:** None (empty `GET` request).

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "receiptURL": "https://receipts.surfboardpayments.com/r/abc123xyz"
  },
  "message": "Receipt link fetched successfully"
}
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | `SUCCESS` or `ERROR`. |
| `data.receiptURL` | string | URL to access the hosted digital receipt. |
| `message` | string | Description of the result. |

## Printing Receipts on a Terminal

Print a receipt directly on a Surfboard device with a printer: a [terminal with a built-in printer](/products/?requirements=printer), such as SurfPrint Pro, or a [FinPrinter](/products/fins/finprinter), the standalone cloud printer. Surfboard renders the receipt from the template configured in the Partner Portal, so the request carries an ID and a target device, nothing more.

```
POST /receipts/{id}/print
```

The `{id}` path parameter accepts a Transaction ID, Payment ID, or Order ID.

**Request body:**

```json
{
  "terminalId": "trm_abc123",
  "templateType": "TRANSACTION_RECEIPT"
}
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `terminalId` | string | No | Target a specific printing-enabled device. If omitted, prints on the terminal that handled the transaction. |
| `templateType` | string | No | `TRANSACTION_RECEIPT` (default) or `SALES_SUMMARY`. |

**Response:**

```json
{
  "status": "SUCCESS",
  "message": "Receipt sent to the printer successfully."
}
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | Status of the request. |
| `message` | string | Description of the result. |

> **Note:** A `SUCCESS` response means the print command was dispatched to the terminal. The terminal must be online and not processing another command for the receipt to print.

> **Note:** Which template renders is set in the Partner Portal at partner, merchant or store level, and the receipt content comes from the order's `orderLines`. The older `PUT /receipts/{id}/print` form with `templateId` and `language` still works but is superseded. For how templates are set up, how the FinPrinter fits in, and when to choose ESC/POS instead, see [Receipt Printing](/developers/guides/receipt-printing).

## Custom ESC/POS Printing

For full control over receipt layout and branding, send raw ESC/POS commands to a terminal's built-in printer. This lets you design completely custom receipts -- including logos, formatted tables, QR codes, and styled text -- using the industry-standard ESC/POS command set.

```
PUT /receipts/{terminalId}/escpos
```

Note that this endpoint uses the `terminalId` directly in the path, not a transaction or order ID.

**Request body:**

```json
{
  "escposCommands": "G0AbYQEbRQFTdXBlciBNYXJ0CjEyMyBNYWluIFN0ChtFABthAERhdGU6IDIwMjQvMTAvMDgKVGltZTogMTI6MDAgUE0KG0UBLS0tLS0tLS0tLQobRQAbYQBJdGVtIEE6IFdhdGVyClByaWNlOiAkMS4wMApJdGVtIEI6IEJyZWFkClByaWNlOiAkMi4wMAobRQEtLS0tLS0tLS0tClRvdGFsOiAkMy4wMAobRQAbYQFUaGFuayB5b3UhCgoKHVYA",
  "codePages": "UTF-8"
}
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `escposCommands` | string | Yes | A stream of ESC/POS commands encoded as a Base64 string. |
| `codePages` | string | No | Send `UTF-8` to opt in to the validated ESC/POS contract. Omitting it keeps the deprecated legacy flow. |

**Response:**

```json
{
  "status": "SUCCESS",
  "message": "ESC/POS receipt sent to terminal"
}
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | Status of the request. |
| `message` | string | Description of the result. |

### Building ESC/POS Commands

ESC/POS is a command protocol originally developed by Epson and now supported by most thermal receipt printers. A few common commands:

| Command | Hex | Description |
|---------|-----|-------------|
| Initialize printer | `1B 40` | Reset printer to default settings. |
| Bold on | `1B 45 01` | Enable bold text. |
| Bold off | `1B 45 00` | Disable bold text. |
| Center align | `1B 61 01` | Center-align subsequent text. |
| Left align | `1B 61 00` | Left-align subsequent text. |
| Cut paper | `1D 56 00` | Full cut of the receipt paper. |

**Workflow:**

1. Compose your ESC/POS byte stream (text interspersed with control commands).
2. Encode the entire byte stream as a Base64 string.
3. Send the Base64 string in the `escposCommands` field, with `"codePages": "UTF-8"`.

> **Note:** The table above is a starting point, not the full picture. Once you send `"codePages": "UTF-8"`, payloads are validated against a defined contract: UTF-8 text, a fixed command set, and line widths that vary by text size. Commands outside that set -- including `GS v 0` raster images and `ESC t` charset selection -- are rejected before they reach the terminal. See the [ESC/POS Printing](/developers/guides/escpos-printing) guide for the complete contract, a worked receipt, and a preflight validator.

> **Tip:** Generic ESC/POS libraries (`escpos` for Python, `node-escpos` for Node.js) can generate the byte stream for you, but their defaults often emit raster images and charset commands that the contract rejects. Check what your library actually produces before sending it.

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Add receipt information | PUT | `/receipts/{orderId}` |
| Email a receipt | PUT | `/receipts/{id}/email` |
| Fetch receipt link | GET | `/receipts/{id}/link` |
| Print receipt on terminal | POST | `/receipts/{id}/print` |
| Print custom ESC/POS receipt | PUT | `/receipts/{terminalId}/escpos` |

For full endpoint details, see the [Receipts API](https://developers.surfboardpayments.com/api/receipts) reference documentation. For a comparison of template printing and ESC/POS, see [Receipt Printing](/developers/guides/receipt-printing).

---

# Payment Page

Redirect customers to a Surfboard-hosted checkout page. The fastest way to accept online payments with minimal integration effort.

Source: https://www.surfboardpayments.com/developers/guides/payment-page
Category: online
Tags: Online, Payment Page, Hosted Checkout, API

---
## Overview

The Payment Page is the simplest way to accept online payments with Surfboard. Instead of building your own checkout form, you redirect customers to a Surfboard-hosted payment page. The customer completes payment there and is redirected back to your site.

This approach requires minimal frontend work -- you only need to create an order via the API and redirect the customer to the returned payment link.

## Prerequisites

Before accepting payments with the Payment Page:

1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. Complete onboarding (merchant and store setup)
3. The `terminalId` of the store's **PaymentPage** terminal — creating an online store provisions one for you, so fetch the store's terminals rather than registering a new one

## Payment Types

There are two primary types of payments:

1. **Customer Initiated Transaction (CIT):** Transactions initiated by customers on your webshop, such as e-commerce purchases.
2. **Merchant Initiated Transaction (MIT):** Transactions initiated by the merchant, such as subscription charges.

> **Note:** MIT payments can only be processed by terminals set to `MerchantInitiated`. See the [Server-to-Server API guide](/developers/guides/server-to-server-api) for details on MIT.

## Payment Process

### Step 1: Create an Order

Create an order using the [Create Order API](https://developers.surfboardpayments.com/api/orders). On success, you receive a **payment link** to share with the customer.

For the store, domain and terminal setup this call assumes, see [Online Payment Link](/developers/guides/online-payment-link).

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [
    {
      "id": "ITEM-001",
      "name": "Annual Subscription",
      "quantity": 1,
      "amount": {
        "total": 99900,
        "currency": "752"
      }
    }
  ],
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD",
      "amount": 99900
    }
  }
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderId": "8455c12f9fd0620a010b",
    "paymentPageLink": "https://pay.withsurfboard.com/8455c12f9fd0620a010b?pi=Dr4GoyMXF0zHvjca_Oa0vHgxcT-OD1qp7KdokyI7dkTwRwYJt8nkXyQm3bT6vqCfgraOw50Bf5uOp__3ckbMWOV6L9QbiTiSEFS3YmF4Eb8lr5pTWP2KFjm9Ukmd0000&add=IzFlNDBhZg=="
  },
  "message": "Order created successfully"
}
```

Pass `paymentPageLink` on whole -- the query string carries the payment intent, and a trimmed or re-encoded link will not open.

### Control Fields

The Payment Page supports additional control fields for fine-grained payment control:

| Field | Description |
|-------|-------------|
| `delayCapture` | Set to `true` to capture payment later after authorisation. Default: `false`. |
| `enforceTokenization` | Override tokenisation config -- control whether the card is saved for future use. |
| `enforce3DSecure` | Whether the customer goes through 3D Secure verification. |
| `paymentPageValidFor` | How long the payment link is valid. Default: one day. |
| `lockToPaymentMethod` | Force the customer to use a specific payment method. |
| `authMode` | `PREAUTH` or `AUTH`. Default: `AUTH`. If `PREAUTH`, `delayCapture` is set to `true` automatically. |
| `redirectUrl` | URL to redirect to after successful payment. Includes `orderId` as a query param. |
| `failureRedirectUrl` | URL to redirect to after failed payment. Includes `orderId` as a query param. |
| `generateShortLink` | Set to `true` to get a shortened payment URL. Default: `false`. |

### Recurring Payment Fields

For subscription-based payments, include these additional fields:

| Field | Description |
|-------|-------------|
| `subscriptionAmountType` | `FIXED` or `VARIABLE` |
| `maxAmount` | Maximum amount in minor units (for variable subscriptions) |
| `frequency` | `daily`, `weekly`, `monthly`, `quarterly`, `annually`, `unscheduled`, etc. |
| `numberOfPayments` | Total expected payments for this subscription |
| `uniqueReference` | Unique reference for the recurring order |

### Step 2: Check Order Status

Monitor the order status using the [Fetch Order Status API](https://developers.surfboardpayments.com/api/orders). When the status changes to `PAYMENT_COMPLETED` or `PAYMENT_CANCELLED`, you can view the transaction details.

You can also receive real-time updates via webhook notifications -- configure them in the Developer Portal Console.

## Integration Flow

Here is the typical integration flow:

1. Customer clicks "Pay" on your website
2. Your backend calls the Create Order API
3. You redirect the customer to the `paymentPageLink` from the response, or to `shortLinkUrl` if you asked for a short link
4. Customer completes payment on the Surfboard-hosted page
5. Customer is redirected to your `redirectUrl` (or `failureRedirectUrl`)
6. Your backend verifies the order status via the API or webhook

> **Tip:** Always verify the order status server-side after redirect. Do not rely solely on the redirect URL to confirm payment success.

## Reference

- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Fetch Order Status API](https://developers.surfboardpayments.com/api/orders)
- [Webhook Reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)
- [Developer Portal](https://developers.surfboardpayments.com/)

---

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

Source: https://www.surfboardpayments.com/developers/guides/online-payment-link
Category: online
Tags: Online, API, Payment Link, Payment Page, Orders

---
## 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](/developers/guides/create-an-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](/developers/guides/self-hosted-checkout) guide. If you want to charge a stored card from your backend with no customer present, see [Server-to-Server API](/developers/guides/server-to-server-api).

## Prerequisites

1. A developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
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.

```json
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"
  }
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "storeId": "81d64e7174dcb00b0f",
    "merchantId": "818712cdbcb670070e",
    "name": "Web Store",
    "merchantUrlDomainVerificationKey": "499470649f03b53fa1175659d4389743974710260b7f410313487e6062b3d559",
    "paymentPageUrlDomainVerificationKey": "2179beab4f5e8c3960615205f042939a2ccc6c51a6e5923c9c068b3d9a645590"
  },
  "message": "Store Created Successfully"
}
```

| Field | Required | Notes |
|-------|----------|-------|
| `onlineInfo.merchantWebshopURL` | Yes | The shop the customer is buying from. Verified by DNS. |
| `onlineInfo.termsAndConditionsURL` | Yes | Must include the refund policy. Rendered on the checkout page. |
| `onlineInfo.privacyPolicyURL` | Yes | Rendered on the checkout page. |
| `onlineInfo.paymentPageHostURL` | No | Only 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:

```json
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](/developers/guides/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
```

```json
// 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.

| Mode | Use it for | Provisioned with the store |
|------|------------|----------------------------|
| `PaymentPage` | Payment links and hosted checkout. This guide. | Yes |
| `MerchantInitiated` | Backend charges against a stored token, such as subscription renewals. | Yes |
| `SelfHostedPage` | Card fields rendered on your own page by the Online SDK. Returns a `publicKey` and `sdkUrl`. | No |
| `iFrame` | An 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:

```json
POST /merchants/:merchantId/stores/:storeId/online-terminals
{
  "onlineTerminalMode": "SelfHostedPage"
}
```

```json
// 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.

```json
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"
    }
  }
}
```

```json
// 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](/developers/guides/create-an-order) covers them in full.

## Control Functions for Online Orders

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

| Field | Description |
|-------|-------------|
| `paymentPageValidFor` | How long the link works, as `<number><unit>` where the unit is `m`, `h` or `d` — for example `15m`, `2h`, `3d`. Defaults to one day. |
| `redirectUrl` | Where the customer lands after a successful payment. The `orderId` is appended as a query parameter. |
| `failureRedirectUrl` | Where the customer lands after a failure. Also carries the `orderId`. |
| `generateShortLink` | Returns `shortLinkUrl` alongside the full link. Default `false`. |
| `payButtonType` | The label on the button: `PAY`, `DONATE`, `BOOK`, `ORDER`, `CHECKOUT`, `CONTINUE`, `CONTRIBUTE`, `ADD_MONEY`, `RENT`, `SUPPORT`, `TIP`, `TOP_UP`. |
| `enforce3DSecure` | Force 3-D Secure where the issuer supports it. |
| `relaxed3ds` | Allow relaxed 3-D Secure handling. |
| `addressRequirements` | Ask for an address on the page. |
| `enforceTokenization` | Save the card for later use, overriding the terminal configuration. |
| `tokenisationIfPossible` | Tokenize when supported, but do not fail the payment if it is not. |
| `errorIfTokenizationFails` | Fail the flow when the card cannot be tokenized. |
| `subscription` | Mark the order as recurring-capable. |
| `generateOnlineLinkWith` | Generate the link with a different terminal than the one the order was created against. |
| `selfCardCharging` | Let the customer charge their own card. |

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

| Field | Description |
|-------|-------------|
| `delayCapture` | Authorize now, capture later. See [Capture a Payment](/developers/guides/capture-a-payment). |
| `authMode` | `AUTH` or `PRE-AUTH`. Choosing `PRE-AUTH` sets `delayCapture` for you. |
| `lockToPaymentMethods` | Restrict the page to the methods you list, e.g. `["CARD", "KLARNA"]`. |
| `delayPayout` | Hold the payout for a period, as `<number><unit>`. |
| `callBackUrl` | Per-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:

```json
{
  "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](/developers/guides/recurring-payments) has the renewal side.

| Field | Description |
|-------|-------------|
| `subscriptionAmountType` | `fixed` or `variable`. |
| `maxAmount` | Ceiling in minor units, for `variable` subscriptions only. |
| `frequency` | `daily`, `twiceWeekly`, `weekly`, `tenDays`, `fortNightly`, `monthly`, `everyTwoMonths`, `trimester`, `quarterly`, `twiceYearly`, `annually`, `unscheduled`. Required. |
| `numberOfPayments` | How many payments the schedule expects. |
| `uniqueReference` | Your reference for the recurring order. |
| `validation` | `validated` 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
```

```json
// 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 status | Meaning |
|--------------|---------|
| `PENDING` | The link has not been paid yet, or the last attempt failed or was cancelled. |
| `PAYMENT_COMPLETED` | Paid in full. The order is closed. |
| `PARTIAL_PAYMENT_COMPLETED` | Part of the total is paid. See [Partial Payments](/developers/guides/partial-payments). |
| `PAYMENT_CANCELLED` | The 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.

```json
{
  "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](/developers/guides/webhooks-notifications) for subscription and signature verification.

## Sending the Link

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:

| Code | Cause |
|------|-------|
| `OR_0042` | Terminal not found. The `terminal$id` is wrong, or it belongs to another merchant. |
| `OR_0037` | The total does not reconcile with the line items. |
| `OR_0048` | Line 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](/developers/guides/create-order-error-codes) reference lists the rest, including errors raised by the payment initiation that happens inside the same call.

## Next Steps

- [Payment Page](/developers/guides/payment-page) — the hosted checkout redirect in more detail
- [Self-Hosted Checkout](/developers/guides/self-hosted-checkout) — keep the customer on your own page
- [Server-to-Server API](/developers/guides/server-to-server-api) — charge a stored card with no customer present
- [Capture a Payment](/developers/guides/capture-a-payment) — finalize a delayed-capture authorization
- [Refund an Order](/developers/guides/refund-an-order) — return funds after settlement

## Reference

- [Stores API](https://developers.surfboardpayments.com/api/stores)
- [Terminals API](https://developers.surfboardpayments.com/api/terminals)
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Developer Portal](https://developers.surfboardpayments.com/)

---

# Self-Hosted Checkout

Embed a payment form directly in your web app with the Surfboard Online SDK. Full UI control with Surfboard handling PCI compliance.

Source: https://www.surfboardpayments.com/developers/guides/self-hosted-checkout
Category: online
Tags: Online, SDK, JavaScript, Self-Hosted, Checkout

---
## Overview

The Self-Hosted Checkout (Online SDK) lets you embed payment fields directly in your website while Surfboard handles PCI compliance and payment processing. You get full control over the look and feel of your checkout page.

Surfboard renders secure input fields inside your page using the Online SDK. You control the layout, branding, and customer experience.

## Prerequisites

1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. Complete onboarding (merchant and store setup)
3. Register a terminal with the type set to **SelfHostedPage**
4. Note the terminal's `publicKey` from the registration response

> **Tip:** You can retrieve the terminal's public key later using the [Fetch Terminal by ID API](https://developers.surfboardpayments.com/api/terminals). It is returned as `terminalPublicKey` in the response.

## Initializing the SDK

To initialize the SDK, you need three parameters:

1. **`publicKey`** -- From terminal registration
2. **`orderId`** -- From the [Create Order API](https://developers.surfboardpayments.com/api/orders) response
3. **`nonce`** -- From the Create Order response (serves as access control)

```javascript
// Set up error handling
SurfboardOnlineSDK.errorCallback((code, message) => {
    console.error(`Error [${code}]: ${message}`);
});

// Listen for payment status changes
SurfboardOnlineSDK.paymentStatusCallback = function (data) {
    // data.paymentStatus: 'PAYMENT_INITIATED' | 'PAYMENT_COMPLETED' |
    //   'PAYMENT_CANCELLED' | 'PAYMENT_FAILED' | 'PAYMENT_PROCESSING'
    console.log("Payment status:", data.paymentStatus);
};

// Initialize
SurfboardOnlineSDK.initialiseOnlineSDK({
    publicKey: "YOUR_PUBLIC_KEY",
    orderId: "YOUR_ORDER_ID",
    nonce: "YOUR_NONCE",
});
```

To re-initialize for a different order without a full page reload:

```javascript
SurfboardOnlineSDK.remountOnlineSDK({
    publicKey: "YOUR_PUBLIC_KEY",
    orderId: "NEW_ORDER_ID",
    nonce: "NEW_NONCE",
});
```

### Error Codes

| Error Code | Message | Category |
|------------|---------|----------|
| -- | Surfboard SDK cannot function in the given environment | FATAL |
| -- | Surfboard SDK initialisation failed | FATAL |
| -- | Public key validation failed | FATAL |
| -- | Invalid Order ID | FATAL |
| -- | Invalid Nonce | FATAL |
| 401 | Invalid or Expired Link | FATAL |

## Available Data Objects

After successful initialization, the SDK exposes data objects on `SurfboardOnlineSDK`:

- **`order`** -- Order details, line items, and payment methods
- **`merchant`** -- Merchant name and organization number
- **`branding`** -- Colors, fonts, logos for your checkout styling
- **`store`** -- Store contact info, privacy policy, and terms URLs
- **`paymentMethods`** -- Supported payment methods for this terminal
- **`customer`** -- Customer details, saved cards, and addresses

> **Warning:** You are required to display the store contact information, privacy policy, and terms and conditions on your payment page.

## Payment Flow

### Updating Customer Information

Most payment methods require customer information. Provide it via the SDK or include it when creating the order.

| Payment Method | Required Fields |
|----------------|----------------|
| Card | Email, Phone, Address |
| Klarna | Email, Phone, Address, Shipping Address (physical goods) |
| Apple Pay | Email, Name, Phone, Postal Address |

```javascript
await SurfboardOnlineSDK.order.addCustomerInformation({
    name: "Jane Doe",
    email: "jane@example.com",
    phone: { countryCode: "+46", number: "701234567" },
    billingAddress: {
        addressLine1: "Main Street 1",
        city: "Stockholm",
        postalCode: "11122",
        countryCode: "SE",
    },
});
```

> **Tip:** Include the customer address in the Create Order API request when you have it. This pre-fills the address so the customer does not need to enter it manually.

### Card Payments

Mount the card input fields in your page:

```html
<div id="card-details"></div>
```

```javascript
SurfboardOnlineSDK.mount({
    mountCardWidget: "card-details",
});

// When the customer clicks "Pay":
await SurfboardOnlineSDK.order.initiatePayments("CARD");
```

### Swish Payments

```javascript
const paymentAttempt = await SurfboardOnlineSDK.order.initiatePayments("NSWISH");

// For mobile: redirect to Swish app
const redirectUrl = paymentAttempt.getSwishAppRedirectUrl("https://your-site.com/callback");

// For web: display QR code
const qrData = paymentAttempt.getSwishQRData;
```

### Apple Pay

```html
<div id="apple-pay"></div>
```

```javascript
SurfboardOnlineSDK.mount({
    mountApplePayWidget: "apple-pay",
});
// Payment is initiated automatically when the customer clicks the Apple Pay button
```

For Apple Pay, you must host the domain association file at `/.well-known/apple-developer-merchantid-domain-association` on your domain.

### Klarna

```javascript
await SurfboardOnlineSDK.order.addCustomerInformation({
    phone: { countryCode: "+46", number: "701234567" },
    name: "Jane Doe",
    email: "jane@example.com",
    billingAddress: {
        city: "Stockholm",
        postalCode: "11122",
        countryCode: "SE",
        addressLine1: "Main Street 1",
    },
});

await SurfboardOnlineSDK.order.initiatePayments("KLARNA");
```

## Payment Error Codes

| Code | Message | Category |
|------|---------|----------|
| ON_009 | Phone number required for Swish payment | Non Fatal |
| ON_010 | Payment method not supported for this order | Non Fatal |
| ON_011 | Payment already completed | Non Fatal |
| ON_012 | Error initiating payment -- retry | Non Fatal |
| ON_013 | Unknown error -- page reload may help | Non Fatal |
| ON_016 | Invalid card details | Non Fatal |
| ON_017 | Email required for this payment | Non Fatal |
| ON_018 | Billing address required for this payment | Non Fatal |

## Reference

- [React Sample App](https://github.com/surfboardpayments/react-next-online-sdk) for a complete working example
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Fetch Terminal API](https://developers.surfboardpayments.com/api/terminals)
- [Developer Portal](https://developers.surfboardpayments.com/)

---

# Merchant Onboarding

Create a merchant application through the Partner API and hand the merchant a prefilled web KYB link. Registry lookup, automatic business classification, people and signing, application status, and store setup.

Source: https://www.surfboardpayments.com/developers/guides/merchant-onboarding
Category: online
Tags: Onboarding, Merchant, KYB, Prefill, MCC, Store, Partners, API

---
## Overview

Merchant onboarding is the first step before accepting payments on Surfboard. A partner creates a merchant application through the API and receives a ready-to-use web onboarding link (**web KYB**, Know Your Business) that is handed to the merchant to finish. You can onboard merchants for both in-store and online payments using the same endpoint.

When you pre-enter the merchant's details, Surfboard does two things before returning the link:

1. **Registry prefill** -- the company's registry data (legal name, address, directors and beneficial owners where available) is resolved from the national business registry for the merchant's country.
2. **Business classification (MCC)** -- the free-text `businessDescription` is classified into a merchant category, which in turn determines the exact documents the merchant must supply (a taxi licence, association statutes, and so on) and any category-specific questions.

The link that comes back is therefore already populated. The merchant only has to add what a partner cannot know for them: their **bank account**, any **required documents** for their business category, and the **signing** (identity verification and e-signature) of the signatories and beneficial owners.

If any part of the prefill cannot be resolved, the call still succeeds and returns a working link. The merchant simply fills those sections in the web KYB flow as normal. Prefill is an accelerator, never a blocker.

The typical flow is:

1. **Create Merchant** -- submit the merchant's details and receive the web KYB link
2. **Merchant completes the web KYB** -- confirms the prefilled data, adds bank account and documents, signs
3. **Check Application Status** -- poll for the result or listen for webhooks
4. **Store Setup** -- optionally create additional stores after onboarding completes

## Prerequisites

Before onboarding merchants:

1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. Obtain your `partnerId` from the Developer Portal Console
3. Generate API credentials (API key and secret)

Merchant applications in test and demo environments are approved automatically.

## Step 1: Create a Merchant Application

Send a `POST` request to the Create Merchant endpoint. The same endpoint handles in-store and online merchants; the difference is whether you include `onlineInfo` in the store configuration.

```
POST /partners/{partnerId}/merchants
```

The body has three parts:

- `country` and `organisation` -- who the merchant is. Required.
- `controlFields` -- how the onboarding should behave (store, acquirer, flags). Optional.
- `controlFields.preEnteredInformation` -- the data you prefill on the merchant's behalf. Optional, but this is what unlocks the accelerated flow.

### The minimum request

Country and corporate ID are enough to create an application. Surfboard resolves the legal name and registered address from the business registry, and the merchant fills in everything else in the web KYB:

```json
{
  "country": "SE",
  "organisation": {
    "corporateId": "5591631360"
  }
}
```

`country` is one of `SE`, `NO`, `DK`, `FI`, `IE`, and the format of `corporateId` is validated per country. Add `localeSelected` (`sv`, `da`, `fi`, `en`) to set the language of the web KYB; it defaults to the country's language.

### Create the first store in the same call

Include `controlFields.store` to create the merchant's first store during onboarding. This is recommended, since the merchant needs a store before it can take payments. `paymentChannels` tells Surfboard where the merchant takes payments; at least one channel must be `true`, and `physicalSharePercent` (1-99) only matters when both are.

```json
{
  "country": "SE",
  "organisation": {
    "corporateId": "5591631360"
  },
  "controlFields": {
    "store": {
      "name": "Main Street Store",
      "email": "store@example.com",
      "phoneNumber": {
        "code": "46",
        "number": "701234567"
      },
      "address": {
        "addressLine1": "Main Street 123",
        "city": "Stockholm",
        "countryCode": "SE",
        "postalCode": "123 45"
      },
      "paymentChannels": { "physical": true, "online": false }
    }
  }
}
```

For online payments, add the `onlineInfo` object to the store with the webshop URL, terms and conditions, and privacy policy. To stop the merchant from changing those URLs in the web KYB, set `controlFields.disableFields.onlineInfo` to `true`; that then requires `merchantWebshopURL`, `termsAndConditionsURL` and `privacyPolicyURL` in the same request.

```json
{
  "country": "SE",
  "organisation": {
    "corporateId": "5591631360"
  },
  "controlFields": {
    "disableFields": { "onlineInfo": true },
    "store": {
      "name": "My Webshop",
      "email": "shop@example.com",
      "address": {
        "addressLine1": "Main Street 123",
        "city": "Stockholm",
        "countryCode": "SE",
        "postalCode": "123 45"
      },
      "paymentChannels": { "physical": false, "online": true },
      "onlineInfo": {
        "merchantWebshopURL": "https://shop.example.com",
        "paymentPageHostURL": "https://pay.example.com",
        "termsAndConditionsURL": "https://shop.example.com/terms",
        "privacyPolicyURL": "https://shop.example.com/privacy"
      }
    }
  }
}
```

### Prefill the application

Everything under `controlFields.preEnteredInformation` is optional. Supply what you know; anything you omit is collected from the merchant in the flow, and nothing you prefill is discarded.

**Describe the business.** `businessDescription` is what the merchant will *primarily use the payment solution for*: the specific activity that generates card transactions, not the general company purpose. "Selling coffee and pastries at our café" is right; "Food and beverage services" is not. Supplying it triggers automatic category (MCC) classification, which sets the required documents and category questions. If you already know the MCC, pass `organisation.mccCode` instead.

**Name the people.** You can prefill the `applicant` (the main contact), plus `signatories`, `ubos` (beneficial owners) and `chairpersons`. When you supply a person, give at least their `name` and `email`. A person can hold more than one role: the applicant is often both a signatory and a beneficial owner, which you express with `isSignatory` and `isUbo`.

> **Who receives a signing link:** signing invitations go only to the people who must sign, i.e. the signatories and the beneficial owners. Being the applicant or a chairperson alone does not trigger a signing link; that person signs only if they are also a signatory or UBO.

The ownership fields (`ownershipPercent`, `ownershipType`, `entityName`) describe beneficial ownership and only apply when a person is a UBO. `ownershipType` is `direct` or `indirect`; an indirect owner holds the shares through another company, and then `entityName` (the intermediary company) is required.

**Add trading details.** `openingInfo`, `giftcards`, `prePayments` and `fundsInfo` answer the questions the merchant would otherwise be asked in the flow. A few rules apply on the backend: `isOpenAllYear` and `isSeasonalOpen` must be opposites, `monthsOpen` is required when not open all year, and `reasonForOpeningAtNight` is required when `isStoreOpenAtNight` is `true`. Include `giftcards` and `prePayments` only if the merchant actually sells gift cards or takes prepayments.

### Full example

A Danish café, prefilled by the partner. The registry resolves the legal name and address; the business description classifies the merchant; the applicant is both signatory and sole direct owner, with a second, indirect owner listed under `ubos`.

```json
{
  "country": "DK",
  "localeSelected": "da",
  "organisation": {
    "corporateId": "12345678"
  },
  "controlFields": {
    "generateShortLink": true,
    "store": {
      "name": "Havnens Café",
      "email": "hello@havnenscafe.dk",
      "phoneNumber": { "code": "45", "number": "31234567" },
      "address": {
        "addressLine1": "Havnegade 12",
        "city": "København",
        "countryCode": "DK",
        "postalCode": "1058"
      },
      "paymentChannels": { "physical": true, "online": true, "physicalSharePercent": 80 }
    },
    "preEnteredInformation": {
      "businessDescription": "Selling coffee, pastries and light lunches at our harbourside café.",
      "applicant": {
        "email": "owner@havnenscafe.dk",
        "name": "Mette Jensen",
        "isSignatory": true,
        "isUbo": true,
        "ownershipPercent": 100,
        "ownershipType": "direct"
      },
      "ubos": [
        {
          "name": "Lars Holm",
          "email": "lars@example.dk",
          "ownershipPercent": 0,
          "ownershipType": "indirect",
          "entityName": "Holm Holding ApS"
        }
      ],
      "openingInfo": {
        "isOpenAllYear": true,
        "isSeasonalOpen": false,
        "monthsOpen": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
        "isStoreOpenAtNight": false,
        "reasonForOpeningAtNight": ""
      },
      "giftcards": { "revenueSharePercent": 15, "averageValidDays": 365 },
      "fundsInfo": {
        "averageTransactionValuePerDay": 4000,
        "estimatedAmountPerYear": 1200000,
        "priceOfMostExpensiveItemSold": 250,
        "estimatedAmountPerTransaction": 95,
        "estimatedFrequencyOfTransactions": "DAILY"
      }
    }
  }
}
```

### Response

A successful request returns the application ID and the web KYB link. `shortLinkUrl` is present when you asked for it with `generateShortLink`, and `storeId` when you supplied a store.

```json
{
  "status": "SUCCESS",
  "message": "Merchant application created successfully.",
  "data": {
    "applicationId": "845adba035abb00310",
    "webKybUrl": "https://onboarding.surfboard.se/845adba035abb00310?pi=…",
    "shortLinkUrl": "https://sb.fyi/abcd12",
    "validUntil": "2026-12-01T00:00:00.000Z",
    "merchantId": "83af75d53169b0070e",
    "storeId": "845adbc0a3f2b00711"
  }
}
```

Share `webKybUrl` (or `shortLinkUrl`) with the merchant. Treat it as sensitive: it grants access to the application. The link is valid until `validUntil`. Each call creates a new application, so do not call again for the same merchant to recover a link; the current link is always available from the status endpoint in Step 3.

### Key fields

| Field | Description |
|-------|-------------|
| `country` | Two-letter ISO country code: `SE`, `NO`, `DK`, `FI` or `IE`. Required. |
| `localeSelected` | Language of the web KYB (`sv`, `da`, `fi`, `en`). Defaults to the country's language. |
| `organisation.corporateId` | The merchant's corporate or organisation number. Required. |
| `organisation.legalName`, `organisation.address` | Resolved from the registry if omitted. Mandatory for Payment Facilitator (PF) partners. |
| `organisation.mccCode` | Merchant Category Code, if you already know it. Otherwise derived from `businessDescription`. |
| `controlFields.store` | Create a store during onboarding (recommended). |
| `controlFields.store.paymentChannels` | Whether the merchant takes payments in person, online, or both. |
| `controlFields.preEnteredInformation` | Business description, people and trading details to prefill. |
| `controlFields.disableFields.onlineInfo` | Lock the webshop URLs against merchant edits. Requires `store.onlineInfo`. |
| `controlFields.showProductCatalogue` | Show the terminal catalogue step. Requires the catalogue to be enabled for your programme. |
| `controlFields.preSelectProducts` | Pre-select terminals to ship automatically. |
| `controlFields.linkUsers` | Existing user IDs to link to the new merchant. |
| `controlFields.redirectUrl` | Where to send the merchant after they finish the web KYB. |
| `controlFields.generateShortLink` | Set `true` to also receive a shortened link. |
| `controlFields.merchantConfig.settlementFrequency` | Payout cadence: `daily`, `weekly`, `monthly` and more. |
| `controlFields.acquirerConfig`, `controlFields.directMerchantCreation` | PF programmes and direct acquirer agreements only. Leave unset otherwise. |

### Pre-selecting terminals

You can pre-select devices for automatic shipment using `preSelectProducts`, or let the merchant choose from a catalogue by setting `showProductCatalogue` to `true` and optionally filtering with `displayProducts`:

```json
{
  "controlFields": {
    "showProductCatalogue": true,
    "preSelectProducts": [
      {
        "productId": "PRODUCT_ID",
        "quantity": "2",
        "pricingPlanId": "PLAN_ID"
      }
    ]
  }
}
```

### Linking a service provider

If a service provider already exists when you onboard the merchant, you can link it and set its standing share in the same call under `controlFields.merchantConfig.serviceProvider`. See [Service Providers & Split Payouts](/developers/guides/service-providers).

## Step 2: The Merchant Completes the Web KYB

The merchant opens `webKybUrl` and, because you prefilled the rest, only needs to:

1. **Confirm the prefilled company and people**, already populated from the registry and your data
2. **Add their bank account** for settlement
3. **Upload any required documents** for their business category, determined automatically from `businessDescription`
4. **Complete signing**: each signatory and beneficial owner verifies their identity and e-signs

Signing invitations are sent by email to the signatories and beneficial owners you named, or that the registry returned. Once everyone has signed, the compliance team reviews the application, typically within 3-4 business days. Applications in test and demo environments are approved automatically.

## Step 3: Check Application Status

Poll the application status to track progress. The response also carries the current `webKybUrl` while the application is open, so you never need to store the link from the create call.

```
GET /partners/{partnerId}/merchants/{applicationId}/status
```

```json
{
  "status": "SUCCESS",
  "data": {
    "applicationId": "845adba035abb00310",
    "webKybUrl": "https://onboarding.surfboard.se/845adba035abb00310?pi=…",
    "applicationStatus": "APPLICATION_SUBMITTED",
    "merchantId": "83af75d53169b0070e",
    "storeId": "845adbc0a3f2b00711",
    "onlineOnboardingStatus": "PENDING",
    "billingPlans": [],
    "paymentMethods": [
      { "paymentMethod": "card", "enabledSchemes": ["VISA", "MASTERCARD"], "status": "ACTIVE" }
    ],
    "domainVerification": []
  },
  "message": "Application status fetched successfully"
}
```

### Application statuses

| Status | Description |
|--------|-------------|
| `APPLICATION_INITIATED` | Application created; the merchant has not started. |
| `APPLICATION_STARTED` | The merchant has opened the link and begun. |
| `APPLICATION_SUBMITTED` | The merchant has submitted all information. |
| `APPLICATION_PENDING_INFORMATION` | Awaiting additional information or documents from the merchant. |
| `APPLICATION_SIGNED` | All required signatories and beneficial owners have signed. |
| `APPLICATION_REJECTED` | Application rejected. |
| `APPLICATION_EXPIRED` | The link expired before the application was completed. Create a new application. |
| `APPLICATION_COMPLETED` | Compliance review passed; the merchant is being created. |
| `MERCHANT_CREATED` | The merchant is live and can transact. `merchantId` and `storeId` are returned. |

> **Tip:** You can also receive status updates via webhooks instead of polling. Configure webhooks in the Developer Portal Console. See [Webhooks & Notifications](/developers/guides/webhooks-notifications).

## Step 4: Create Additional Stores

A default store is typically created during onboarding. If the merchant needs additional stores, use the Create Store API:

```
POST /partners/{partnerId}/merchants/{merchantId}/stores
```

```json
{
  "storeName": "Second Location",
  "email": "store2@example.com",
  "phoneNumber": {
    "code": 46,
    "number": "709876543"
  },
  "address": "Second Street 456",
  "city": "Gothenburg",
  "zipCode": "411 01",
  "country": "SE"
}
```

For an online store, add `onlineInfo` with your webshop URLs:

```json
{
  "storeName": "Online Store",
  "email": "online@example.com",
  "phoneNumber": {
    "code": 46,
    "number": "709876543"
  },
  "address": "Main Street 123",
  "city": "Stockholm",
  "zipCode": "103 16",
  "country": "SE",
  "onlineInfo": {
    "merchantWebshopURL": "https://shop.example.com",
    "termsAndConditionsURL": "https://shop.example.com/terms",
    "privacyPolicyURL": "https://shop.example.com/privacy"
  }
}
```

### Domain Verification (Online Stores)

Online stores in production require domain verification before they can process payments:

1. **Get verification keys** -- returned in the Create Store response (`merchantURLDomainVerificationKey` and `paymentPageURLDomainVerificationKey`)
2. **Add DNS TXT record** -- add the verification key as a TXT record on your domain
3. **Trigger verification** -- Surfboard checks automatically every 6 hours, or use the Verify Domain API to trigger it manually
4. **Monitor status** -- use the Fetch Store Details API to check the `onlineOnboardingStatus` field

> **Note:** Domain verification is only required in production (not in demo/sandbox). A verified domain applies to all merchants under the same partner account.

## Notes and Behaviours

- **Prefill.** Registry data and category classification are resolved as part of the create call, so the returned link is already populated. If either cannot be resolved, the call still returns a valid link and the merchant completes those sections manually.
- **Documents are category-driven.** The `businessDescription` sets the merchant category, which sets exactly which documents are mandatory and any category-specific questions. You do not specify documents in the request.
- **Prefill is additive.** Anything you omit is collected from the merchant in the flow; nothing you prefill is discarded.
- **One application per call.** Each call creates a new application. Avoid duplicate calls for the same merchant. The current link for an application is always available from the status endpoint.

## Reference

- [Create Merchant API](https://developers.surfboardpayments.com/api/merchants)
- [Check Application Status API](https://developers.surfboardpayments.com/api/merchants)
- [Create Store API](https://developers.surfboardpayments.com/api/stores)
- [Verify Domain API](https://developers.surfboardpayments.com/api/stores)
- [Webhook Reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)
- [Service Providers & Split Payouts](/developers/guides/service-providers)
- [Developer Portal](https://developers.surfboardpayments.com/)

---

# Store Management

Create, update, verify, and manage in-store and online stores using the Surfboard Payments Store APIs.

Source: https://www.surfboardpayments.com/developers/guides/store-management
Category: online
Tags: Online, API, Stores, Domain Verification, Management

---
## Overview

Stores are the organizational units that sit beneath merchants in the Surfboard hierarchy. Every terminal, whether physical or online, is registered under a store. This guide covers the full store lifecycle: creating in-store and online stores, retrieving store details, updating store information, verifying domains for online payments, listing terminals, and deactivating stores you no longer need.

A default store is often created automatically during merchant onboarding. Both merchants and partners can create additional stores at any time through the API or the Partner Portal.

## Prerequisites

- A registered **partner** and **merchant** in the Surfboard system
- Your `partnerId` and `merchantId`
- API credentials (API key and API secret)

## Create an In-Store (Physical) Store

Use the Create Store endpoint to add a new physical store under a merchant. The store will be assigned a unique `storeId` on creation.

```
POST /partners/:partnerId/merchants/:merchantId/stores
```

### Request

```json
{
  "storeName": "Stockholm Flagship",
  "email": "flagship@example.com",
  "phoneNumber": {
    "code": 46,
    "number": "701234567"
  },
  "address": "Drottninggatan 10",
  "city": "Stockholm",
  "zipCode": "103 16",
  "country": "SE"
}
```

### Key Request Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `storeName` | string | Yes | Name of the store |
| `email` | string | No | Store email. Mandatory for online payment support |
| `phoneNumber.code` | number | Yes | International dialing code (e.g., `46` for Sweden) |
| `phoneNumber.number` | string | Yes | Phone number, 5-15 digits |
| `address` | string | Yes | Address line 1 |
| `city` | string | Yes | City name |
| `zipCode` | string | Yes | Postal code |
| `country` | string | Yes | Two-letter ISO country code (e.g., `SE`) |
| `acquirerMID` | string | No | Acquirer Merchant ID, required for PF partners with store-based acquiring |

### Response

The response includes the new `storeId` along with the full store object:

```json
{
  "status": "SUCCESS",
  "data": {
    "storeId": "store-abc-123",
    "merchantId": "merchant-xyz-789",
    "name": "Stockholm Flagship",
    "address": {
      "addressLine1": "Drottninggatan 10",
      "city": "Stockholm",
      "countryCode": "SE",
      "postalCode": "103 16"
    },
    "phone": "+46701234567",
    "email": "flagship@example.com"
  },
  "message": "Store created successfully"
}
```

## Create an Online Store

Online stores require additional properties in the `onlineInfo` object to enable e-commerce payment acceptance. You can either create a new online store directly or update an existing physical store to add online capabilities.

```
POST /partners/:partnerId/merchants/:merchantId/stores
```

### Request

```json
{
  "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"
  }
}
```

### Online Info Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `onlineInfo.merchantWebshopURL` | string | Yes | The merchant's webshop URL |
| `onlineInfo.paymentPageHostURL` | string | No | Payment page URL. Required for SDK mode integration |
| `onlineInfo.termsAndConditionsURL` | string | Yes | URL to terms and conditions (must include refund policy) |
| `onlineInfo.privacyPolicyURL` | string | Yes | URL to the privacy policy |

When an online store is created, the response includes two domain verification keys:

- `merchantURLDomainVerficationKey` -- used to verify ownership of the webshop domain
- `paymentPageURLDomainVerficationKey` -- used to verify the payment page domain (if provided)

You must complete domain verification before the store is approved for online payments.

## Domain Verification

After creating an online store, verify that you own the domains you provided. This is a two-step process.

### Step 1: Set DNS TXT Records

Take the verification keys returned during store creation and add them as **TXT records** on your domain's DNS configuration. Surfboard also performs automatic checks every 6 hours.

### Step 2: Trigger Verification

```
POST /partners/:partnerId/merchants/:merchantId/stores/:storeId/verify
```

```json
{
  "domainType": "MERCHANT_WEBSHOP_URL"
}
```

The `domainType` value specifies which domain to verify. Use `MERCHANT_WEBSHOP_URL` for the webshop domain or `PAYMENT_PAGE_HOST_URL` for the payment page domain.

### Check Verification Status

You can retrieve the current domain verification status at any time:

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

Once verification succeeds, the store enters an internal approval process. After approval, the store can take online payments.

### Default Online Terminals

Creating an online store provisions two online terminals automatically: a **PaymentPage** terminal, used for payment links and hosted checkout, and a **MerchantInitiated** terminal, used for backend charges against a stored token. You do not register either one — list the store's terminals to pick up their IDs. They exist as soon as the store does, but cannot take a payment until the domains verify and the store is approved.

**SelfHostedPage** and **iFrame** terminals are not provisioned. Register those with the [Register Online Terminal](https://developers.surfboardpayments.com/api/terminals) endpoint when you need them:

```
POST /merchants/:merchantId/stores/:storeId/online-terminals
```

```json
{
  "onlineTerminalMode": "SelfHostedPage"
}
```

## Fetch Store Details

Retrieve complete information about a specific store, including its status and online onboarding status.

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

### Response

```json
{
  "status": "SUCCESS",
  "data": {
    "storeId": "store-abc-123",
    "merchantId": "merchant-xyz-789",
    "name": "Web Store",
    "status": "ACTIVE",
    "onlineOnboardingStatus": "APPROVED",
    "address": {
      "addressLine1": "Drottninggatan 10",
      "city": "Stockholm",
      "countryCode": "SE",
      "postalCode": "103 16"
    },
    "phone": "+46701234567",
    "email": "webstore@example.com",
    "onlineInfo": {
      "merchantWebshopURL": "https://shop.example.com",
      "paymentPageHostURL": "https://shop.example.com/payment",
      "termsAndConditionsURL": "https://shop.example.com/terms",
      "privacyPolicyURL": "https://shop.example.com/privacy"
    }
  },
  "message": "Store details fetched successfully"
}
```

Store status values: `ACTIVE`, `DEACTIVATED`, `BLOCKED`, `INACTIVE`.
Online onboarding status values: `APPROVED`, `INITIATED`, `FAILED`.

## List All Stores

Retrieve every store registered under a merchant to get a complete overview.

```
GET /partners/:partnerId/merchants/:merchantId/stores
```

The response returns an array of store objects, each with the same structure as the single-store response above.

## Update Store Details

Modify an existing store's name, contact information, address, or add online capabilities. Send only the fields you want to change.

```
PUT /partners/:partnerId/merchants/:merchantId/stores/:storeId
```

### Request

```json
{
  "storeName": "Stockholm Flagship - Updated",
  "email": "new-email@example.com",
  "phoneNumber": {
    "code": 46,
    "number": "709876543"
  }
}
```

All parameters are optional. You can also add `onlineInfo` to convert a physical store into an online store. Note that online info can only be added once.

If you add `onlineInfo` during an update, the response will include the domain verification keys, and you must complete domain verification as described above.

## Fetch Store Terminals

Retrieve all terminals registered under a specific store. You can optionally filter by terminal type.

```
GET /partners/:partnerId/merchants/:merchantId/stores/:storeId/terminals
```

Optional query parameter: `terminalType` (e.g., `surfpad`, `PaymentPage`, `SelfHostedPage`, `MerchantInitiated`).

### Response

```json
{
  "status": "SUCCESS",
  "data": [
    {
      "terminalId": "terminal-001",
      "terminalType": "PaymentPage",
      "terminalStatus": "ACTIVE",
      "storeId": "store-abc-123",
      "terminalName": "Online Checkout",
      "startDate": "2025-06-15T10:00:00Z"
    },
    {
      "terminalId": "terminal-002",
      "terminalType": "MerchantInitiated",
      "terminalStatus": "ACTIVE",
      "storeId": "store-abc-123",
      "startDate": "2025-06-15T10:00:00Z"
    }
  ],
  "message": "Terminals fetched successfully"
}
```

This is the call that hands you the IDs of the `PaymentPage` and `MerchantInitiated` terminals an online store comes with. An online store returns both from the moment it is created, alongside any physical or SDK terminals you registered yourself.

Terminal types include: `surfpad`, `surftouch`, `surfprint`, `checkoutPro`, `checkoutX`, `PaymentPage`, `SelfHostedPage`, `MerchantInitiated`, `printer`, `surftester`.

Terminal statuses: `REGISTERED`, `ACTIVE`, `IN_ACTIVE`, `DE_REGISTERED`.

## Deactivate a Store

Remove a store that is no longer needed. You can deactivate immediately or schedule deactivation for a future date.

```
DELETE /partners/:partnerId/merchants/:merchantId/stores/:storeId
```

Optional query parameter: `deactivationDate` in `yyyy-mm-dd` format. If omitted, the store is deactivated immediately.

> **Important:** A store can only be deactivated if it has no terminals registered to it. If active terminals exist, you must first delink them or move them to another store under the same merchant. Remember that an online store carries its two default terminals, `PaymentPage` and `MerchantInitiated`, so the terminal list is never empty by default — deactivate those before you deactivate the store.

### Response

```json
{
  "status": "SUCCESS",
  "message": "Store deactivated successfully"
}
```

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create store | POST | `/partners/:partnerId/merchants/:merchantId/stores` |
| Fetch store details | GET | `/partners/:partnerId/merchants/:merchantId/stores/:storeId` |
| List all stores | GET | `/partners/:partnerId/merchants/:merchantId/stores` |
| Update store | PUT | `/partners/:partnerId/merchants/:merchantId/stores/:storeId` |
| Verify domain | POST | `/partners/:partnerId/merchants/:merchantId/stores/:storeId/verify` |
| Fetch domain status | GET | `/partners/:partnerId/merchants/:merchantId/stores/:storeId/online` |
| Fetch store terminals | GET | `/partners/:partnerId/merchants/:merchantId/stores/:storeId/terminals` |
| Deactivate store | DELETE | `/partners/:partnerId/merchants/:merchantId/stores/:storeId` |

---

# Webhooks

Receive real-time event notifications via webhooks. Subscribe to order, payment, logistics, and merchant application events with automatic retries and signature verification.

Source: https://www.surfboardpayments.com/developers/guides/webhooks-notifications
Category: online
Tags: Online, API, Webhooks, Events

---
## Overview

Webhooks enable you to receive real-time notifications for payment-related events in Surfboard, eliminating the need for repeated polling of the Surfboard APIs. When an event occurs, Surfboard sends an HTTP `POST` request to a URL on your server with the event details in the request body. All webhook messages include a signature for authenticity verification.

Surfboard supports two webhook mechanisms:

1. **Console webhooks:** Persistent, account-level subscriptions configured in the Surfboard Console. Support retries, failure alerts, and signature verification.
2. **Callback URL (per-order webhook):** A dynamic webhook URL set per order via `controlFunctions.callBackUrl`. Useful for order-level status updates during checkout.

Webhooks are also offered alongside other integration methods such as SSE (Server Sent Events) and event bus-based solutions (Kafka, Azure Event Stream, Google Pub/Sub, etc.).

## Available Events

You can subscribe to the following event categories to receive real-time updates within your platform.

### Order and Payment Events

Order and payment events provide real-time updates on order status and payment flow. These notifications help track orders, detect issues, and improve the checkout experience.

- **Order Updated** -- The order has been modified (e.g. order lines changed).
- **Order Payment Initiated** -- A payment attempt has started for the order.
- **Order Payment Processed** -- The payment is being processed by the payment provider.
- **Order Payment Completed** -- The payment has been successfully completed.
- **Order Payment Failed** -- The payment attempt has failed.
- **Order Payment Cancelled** -- The payment has been cancelled.
- **Order Cancelled** -- The entire order has been cancelled.
- **Order Customer Identity** -- A customer taps their card on the terminal, enabling you to identify the customer during a transaction and personalize the experience. Event type: `order.customer.identify`.
- **Order Terminal Event** -- Triggered for every state the terminal undergoes during a transaction (e.g. tip selection, card presented, PIN entry, authorizing). Also covers online terminal states such as page loaded, wallet SDK mounted, and payment initiated. Event type: `order.terminal.event`.

### Logistics Events

Logistics events notify you about updates on shipments, including terminals and accessories. These events help track order progress from placement to delivery.

- **Logistics Order Update** -- A logistics shipment status has changed.

### Merchant Application Events

Merchant application events provide updates during the onboarding process, from application creation to approval. These notifications help ensure smooth and timely onboarding for merchants.

- **Application Initiated** -- A new merchant application has been created.
- **Application Submitted** -- The application has been submitted for review.
- **Application Signed** -- The application has been signed by the merchant.
- **Application Started** -- Processing of the application has begun.
- **Application Pending Merchant Information** -- Additional information is required from the merchant.
- **Application Completed** -- The application review is complete.
- **Application Merchant Created** -- The merchant account has been created.
- **Application Expired** -- The application has expired.
- **Application Rejected** -- The application has been rejected.

## Event Payload Details

### Order Customer Identity

This event is triggered when a customer taps their card on the terminal, before the order is finalized or payment is processed. It enables customer identification early in the transaction flow.

**Event type:** `order.customer.identify`

**Payload example:**

```json
{
  "eventType": "order.customer.identify",
  "metadata": {
    "eventId": "832cf9fe1806581dff",
    "created": 1747553660038,
    "retryAttempt": 0,
    "webhookEventId": "81a214e74b107801ff"
  },
  "data": {
    "orderId": "832cf9f93d2fd0410b",
    "cardId": "c550c29e80908c887a"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `data.orderId` | string | Unique identifier for the order. |
| `data.cardId` | string | Tokenized identifier for the customer's card, used to recognize or link the customer to the order. |

> **Note:** The `cardId` is a tokenized representation and should be treated as sensitive data.

### Order Terminal Event

This event is triggered for every state the terminal undergoes during a transaction, including stages like tip selection, card presentation, PIN entry, authorization, and completion.

**Event type:** `order.terminal.event`

**Payload example:**

```json
{
  "eventType": "order.terminal.event",
  "metadata": {
    "eventId": "81a214e74b107801ff",
    "created": 1695793998732,
    "retryAttempt": 0,
    "webhookEventId": "81a214e7455ed01cff"
  },
  "data": {
    "orderId": "81b5f2624b16e0080b",
    "merchantId": "8248db4c5c8dd0130e",
    "paymentId": "81b5f26215e9583a06",
    "terminalTransactionStatus": "STARTED",
    "orderStatus": "PAYMENT_INITIATED"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `data.orderId` | string | Unique identifier for the order. |
| `data.merchantId` | string | Unique identifier of the merchant. |
| `data.paymentId` | string | Unique identifier for the payment. |
| `data.terminalTransactionStatus` | string | Current terminal state (see table below). |
| `data.orderStatus` | string | Current order status. |
| `data.metadata` | object | Optional metadata passed with the order creation. |

**Terminal transaction statuses:**

| Status | Description |
|--------|-------------|
| `STARTED` | Transaction initiated on the terminal. |
| `SELECT_TIP` | Tip selection screen displayed. |
| `AWAITING_CARD` | Waiting for card tap/insert. |
| `CARD_PRESENTED` | Customer has presented card. |
| `SELECT_APPLICATION` | Card has multiple applications; selection required. |
| `ENTER_PIN` | Customer needs to enter PIN. |
| `WRONG_PIN` | Wrong PIN entered. |
| `AUTHORIZING` | Payment authorization initiated. |
| `SUBMITTED` | Authorization submitted to the backend. |
| `AUTHORIZED` | Authorization complete. |
| `PAGE_LOADED` | Online only -- payment page fully loaded. |
| `SECURE_CHANNEL_INITIALISED` | Online only -- page ready for card details. |
| `GOOGLE_PAY_MOUNTED` | Online only -- Google Pay SDK mounted. |
| `APPLE_PAY_MOUNTED` | Online only -- Apple Pay SDK mounted. |
| `CUSTOMER_INTERACTION_IN_FORM` | Online only -- customer started entering information. |
| `CARD_PAYMENT_INITIATED` | Online only -- card payment initiated. |
| `APPLE_PAY_ATTEMPT_INITIATED` | Online only -- Apple Pay attempt initiated. |
| `GOOGLE_PAY_ATTEMPT_INITIATED` | Online only -- Google Pay attempt initiated. |
| `APPLE_PAY_PAYMENT_INITIATED` | Online only -- Apple Pay payment process initiated. |
| `GOOGLE_PAY_PAYMENT_INITIATED` | Online only -- Google Pay payment initiated. |

## Getting Started

To set up webhooks via the Surfboard Console:

1. Log in to the [Surfboard Developer Portal](https://developers.surfboardpayments.com).
2. Click **Add new Webhook**.
3. Enter a name and the URL of your webhook endpoint.
4. Enter an email address to receive notifications in case of webhook failures.
5. Choose which events you would like to receive.
6. Save the **webhook secret** that is displayed. This secret is used to verify that messages originate from Surfboard. It is only shown once -- store it securely.
7. Click **Test webhooks** to send a test notification to your endpoint and confirm it is working.

> **Note:** You can add multiple webhooks to listen to different events. You can also customise your URLs so that each endpoint receives only specific events -- useful for microservice or service-oriented architectures.

## Testing Webhooks

When you create or test a webhook in the Console, Surfboard sends a test message to verify your endpoint is reachable. The test message has the following structure:

```json
{
  "eventType": "test.webhook",
  "metadata": {
    "eventId": "string",
    "created": 1234567890,
    "retryAttempt": 0,
    "webhookEventId": "string"
  }
}
```

Your endpoint should return a `200` status code to acknowledge receipt.

## Callback URL (Per-Order Webhook)

In addition to Console webhooks, you can set a per-order callback URL when creating an order. This is useful for receiving status updates for a specific order during checkout.

Set `controlFunctions.callBackUrl` in the [Create Order API](https://developers.surfboardpayments.com/api/orders) request:

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "orderLines": [ ... ],
  "controlFunctions": {
    "callBackUrl": "https://your-server.com/webhooks/payments",
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD"
    }
  }
}
```

> **Note:** Retries and alert emails are not supported for callback URL webhooks. The validation process is the same as regular webhooks -- you can obtain the webhook certificate for signature validation from the [Surfboard Developer Portal](https://developers.surfboardpayments.com).

## Handling Duplicate Deliveries

> **Info:** Surfboard guarantees **at-least-once delivery** for webhook callbacks. Because the system operates in a distributed multi-cloud environment, your endpoint may receive duplicate notifications for the same event. Surfboard performs deduplication on its side, but you must also handle duplicates on yours.

Use the combination of `orderId` and `paymentId` as your idempotency key. When you receive a callback, update the payment status to the value in the payload rather than applying it as an incremental state change.

**Important:** Due to network conditions, callbacks may arrive out of order. Once a payment reaches a terminal state -- `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, or `PAYMENT_CANCELLED` -- do not overwrite it with an earlier status update. Your implementation should treat these three statuses as final and ignore any subsequent callbacks that would move the payment to a non-terminal state.

## Handling Failures and Retries

### Retry Logic

When a webhook delivery fails (your endpoint does not return a `200` status code), Surfboard retries automatically:

- **Attempts:** Up to 3 total delivery attempts.
- **First retry:** 5 minutes after the initial failure.
- **Second retry:** 10 minutes after the first retry.

### Failure Alerts and Automatic Disabling

- An **alert email** is sent on the first delivery failure.
- If the endpoint continues to fail, subsequent alerts are sent every 24 hours for up to 7 days.
- After 7 days of continuous failure with no action taken, the webhook is **automatically disabled**.
- To re-enable a disabled webhook, fix the underlying issue and re-run **Test Webhook** in the Console.

### Failures on Surfboard's Side

Surfboard guarantees to deliver events at least once. If Surfboard experiences an outage, all queued events are republished once the servers recover. Ensure your system can handle a burst of incoming events in this scenario.

> **Tip:** As a safety net for payment events, perform a status query via the API if you have not received a webhook within 60 seconds of initiating a payment. Do not rely solely on webhooks for critical payment status confirmation.

## Verifying Webhook Signatures

Every webhook event is signed using the secret key provided when you created the webhook. The signature is included in the `x-webhook-signature` header of the `POST` request. Always validate this signature to confirm that the message originates from Surfboard.

The signature is an HMAC-SHA512 hash of the JSON request body, encoded as Base64. Below are examples in several languages:

### TypeScript

```typescript
import { createHmac } from 'node:crypto';

function generateHMACSignature(certificate: string, message: string): string {
  return createHmac('sha512', certificate)
    .update(message)
    .digest()
    .toString('base64');
}

// Verify incoming webhook
function verifyWebhook(secret: string, body: string, receivedSignature: string): boolean {
  const expectedSignature = generateHMACSignature(secret, body);
  return expectedSignature === receivedSignature;
}
```

### PHP

```php
<?php

function generateHMACSignature($certificate, $message) {
    return base64_encode(hash_hmac('sha512', $message, $certificate, true));
}

// Verify incoming webhook
$certificate = 'YOUR_WEBHOOK_SECRET';
$body = file_get_contents('php://input');
$receivedSignature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];

$expectedSignature = generateHMACSignature($certificate, $body);

if ($expectedSignature === $receivedSignature) {
    // Signature is valid
    http_response_code(200);
} else {
    // Signature mismatch -- reject the request
    http_response_code(401);
}
```

### Java

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class WebhookVerifier {

    public static String generateHMACSignature(String certificate, String message) {
        try {
            Mac hmac = Mac.getInstance("HmacSHA512");
            SecretKeySpec secretKey = new SecretKeySpec(
                certificate.getBytes(StandardCharsets.UTF_8), "HmacSHA512"
            );
            hmac.init(secretKey);
            byte[] hash = hmac.doFinal(message.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(hash);
        } catch (Exception e) {
            throw new RuntimeException("Failed to generate HMAC signature", e);
        }
    }
}
```

### .NET

```csharp
using System;
using System.Security.Cryptography;
using System.Text;

public static class WebhookVerifier
{
    public static string GenerateHMACSignature(string certificate, string message)
    {
        using (HMACSHA512 hmac = new HMACSHA512(Encoding.UTF8.GetBytes(certificate)))
        {
            byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
            return Convert.ToBase64String(hash);
        }
    }
}
```

### Python

```python
import base64
import hashlib
import hmac

def generate_hmac_signature(certificate, message):
    signature = hmac.new(certificate.encode(), message.encode(), hashlib.sha512)
    return base64.b64encode(signature.digest()).decode()
```

### Go

```go
package main

import (
    "crypto/hmac"
    "crypto/sha512"
    "encoding/base64"
)

func generateHMACSignature(certificate, message string) string {
    key := []byte(certificate)
    h := hmac.New(sha512.New, key)
    h.Write([]byte(message))
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
```

## Best Practices

- **Return 200 quickly.** Process webhook payloads asynchronously. Return a `200` response before performing business logic to avoid timeouts and retries.
- **Always verify signatures.** Validate the `x-webhook-signature` header on every incoming webhook to confirm it originates from Surfboard.
- **Handle duplicates idempotently.** Use `orderId` + `paymentId` as your idempotency key and treat terminal payment statuses as final.
- **Plan for retries.** Your endpoint may receive the same event multiple times. Ensure your processing logic is idempotent.
- **Query on timeout.** If you have not received a payment event within 60 seconds, query the order status via the API as a fallback.
- **Use HTTPS.** Always use HTTPS URLs for webhook endpoints to protect data in transit.
- **Monitor your endpoints.** Watch for failure alert emails and resolve issues promptly to avoid your webhook being disabled after 7 days.

## Related Guides

- [Notification Subscriptions](/developers/guides/notification-subscriptions) -- Set up persistent email, Slack, and SFTP delivery for settlement reports and operational alerts.

## API Reference

| Action | Method | Endpoint |
|--------|--------|----------|
| Set per-order webhook | POST | `/orders` (via `controlFunctions.callBackUrl`) |

---

# Settlements & Reporting

Retrieve settlement reports, view adjustments, manage merchant charges, and register customer profiles for reconciliation and billing.

Source: https://www.surfboardpayments.com/developers/guides/settlements-reporting
Category: online
Tags: Online, API, Settlements, Reporting, Charges, Adjustments

---
## Overview

Once transactions are processed, you need visibility into what was settled, what fees were applied, and how to bill merchants for additional services. The Settlements and Reporting APIs give you that visibility.

This guide covers four related capabilities:

1. **Settlement reports** -- retrieve payout summaries for a merchant over a given period.
2. **Adjustments** -- view tips, surcharges, insurance, and other amounts applied to orders.
3. **Merchant charges** -- create, update, and list one-time or recurring charges billed to a merchant.
4. **Customer details** -- register customer profiles with addresses, contact information, and linked payment cards.

It also covers [reading a settlement report](#reading-a-settlement-report): why a monthly total and the payouts inside that month rarely match, and which figure answers which merchant question.

## Prerequisites

- A valid `partnerId` and `merchantId`
- API credentials (API key, API secret)

## Settlement Reports

Settlement reports summarize a merchant's settled transactions for a selected time period. Reports can be configured as `DAILY` or `MONTHLY` depending on the merchant's setup.

### Fetch settlement reports

```
GET /partners/:partnerId/merchants/:merchantId/reports
```

**Response:**

```json
{
  "status": "SUCCESS",
  "data": [
    {
      "payoutId": "po_83a1f...",
      "merchantId": "m_91b2c...",
      "transactionStartDate": "2026-01-01",
      "transactionEndDate": "2026-01-31",
      "settlementDate": "2026-02-03",
      "reportType": "MONTHLY",
      "url": "https://reports.surfboardpayments.com/settlements/po_83a1f...",
      "totalSale": 1250000,
      "totalRefund": 35000,
      "fee": 18750,
      "payout": 1196250
    }
  ],
  "message": "Settlement reports fetched successfully"
}
```

### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `payoutId` | string | Identifies this specific payout |
| `transactionStartDate` | string | First transaction date covered (`YYYY-MM-DD`) |
| `transactionEndDate` | string | Last transaction date covered (`YYYY-MM-DD`) |
| `settlementDate` | string | Date the payout was issued (`YYYY-MM-DD`) |
| `reportType` | string | `MONTHLY` or `DAILY` |
| `url` | string | Direct link to view the full report |
| `totalSale` | number | Total sales amount in smallest currency unit |
| `totalRefund` | number | Total refunded amount |
| `fee` | number | Total fees deducted |
| `payout` | number | Net payout to the merchant |

Use the `url` field to download or redirect merchants to a detailed breakdown of every transaction in the settlement period.

## Reading a Settlement Report

This is the part support gets asked about most, so it is worth understanding before a merchant asks you.

A monthly report carries two fee totals, and they are usually different numbers:

- The **header figure** is the fee on transactions that happened in that calendar month. It is on a **transaction-date** basis.
- The **fee column in the payouts breakdown** sums the fees of the payouts issued during that month. It is on a **payout-date** basis.

Both are correct. They measure different things, and at a month boundary they cannot agree.

### Why the two totals differ

Payouts lag transactions by two to three days. A payout issued on 1 May settles transactions from the end of April, and the transactions from the last days of May are paid out in June. So the payout-date total borrows from the previous month at one end and loses to the next month at the other.

Take a merchant on daily payouts in May:

| Transactions | Paid out | Fee |
|---|---|---|
| 29--30 April | 1--2 May | 43.50 |
| 1--28 May | during May | 1,196.50 |
| 29--31 May | 1--3 June | 87.20 |

The monthly report header reads **1,283.70**, the fee on May's transactions: `1,196.50 + 87.20`. The fee column of the payouts breakdown reads **1,240.00**, the fee on May's payouts: `43.50 + 1,196.50`. Nothing has been charged twice, and neither figure is wrong.

The same shift applies to the sales and payout columns, not just fees. It is simply most visible on fees, because that is the number merchants ask about.

### Mapping a transaction to its report

One rule covers every case:

> **The monthly report follows the transaction date. The payouts breakdown follows the payout date.**

Every transaction is therefore counted in two places, and at a month boundary those two places are different months:

| Transaction happened | Paid out | Counted in the monthly report for | Appears in the payouts breakdown for |
|---|---|---|---|
| 30 April | 2 May | **April** | **May** |
| 15 May | 17 May | May | May |
| 31 May | 2 June | **May** | **June** |

The middle row is what people expect. The first and last rows are what the questions are about.

Drawn on a calendar, the two views are the same trading, shifted by the settlement lag:

```
transactions  │ 29 Apr  30 Apr │ 01 May  ...  30 May  31 May │
paid out      │ 01 May  02 May │ 03 May  ...  01 Jun  02 Jun │
                └──────┬───────┘              └──────┬──────┘
                 April's trading,              May's trading,
                 inside May's payouts          inside June's payouts
```

To show a merchant where a specific transaction went, take its date, add the settlement lag, and read off both columns. That is the whole mapping.

### The fee is not taken out of the payout

A payout settles transactions. The Surfboard fee for the period is collected separately, once the month has closed, rather than being netted off each payout as it goes.

That matters when a merchant reconciles a bank statement. They see payouts arriving through the month, then one fee deduction afterwards. **The deduction that lands in early June is May's fees, and it matches the May monthly report header, not the sum of the May payout rows.** A merchant who compares the June deduction against the May payout breakdown is comparing two different periods and will always find a gap.

If you do see a fee deducted from an individual payout, that is not the normal arrangement -- check the merchant's billing setup before explaining it as expected behaviour.

### Which figure answers which question

| The merchant asks | Use |
|---|---|
| "What were my fees for May?" | The **monthly report header** fee. Transaction basis, the month they actually traded. |
| "What was deducted from my account in June?" | The **May monthly report** fee total. Fees are collected after the month closes. |
| "Why was this payout this amount?" | The **payout row**, or the daily report for that settlement date. |
| "What did I sell in May?" | The **monthly report header** sales figure, not the sum of May's payouts. |

The short version to give a merchant: *your monthly report tells you what you traded and what it cost you that month; your payouts tell you what arrived in the bank and when. The two are offset by a couple of days at each end of the month.*

### Refunds land in the period they were processed

A refund processed in June against a May sale reduces June's payouts. It does not reopen May. A merchant looking for a refund in the month of the original sale will not find it, and the monthly totals are not wrong for lacking it.

### Before escalating a mismatch

Work through this first -- it resolves most reports of a mismatch:

1. Take the two figures and subtract. Does the difference equal the fees or sales of the days either side of the month boundary? If so, the report is right and this is the transaction-date versus payout-date offset.
2. Is a refund or an adjustment sitting in a different period from its original sale?
3. Is the merchant comparing a fee deduction against the payouts of the same month rather than the month before?

If none of those explain it, raise it with support with the `payoutId` values and the two figures you are comparing. Both come from the same [settlement reports endpoint](#fetch-settlement-reports), so quoting the IDs is faster than describing the rows.

### Getting the numbers over the API

The report list gives you both bases without downloading a file. `transactionStartDate` and `transactionEndDate` are the transaction basis; `settlementDate` is the payout basis. Filter on the pair you mean:

```
GET /partners/:partnerId/merchants/:merchantId/reports
```

- Fees a merchant incurred in May: the `MONTHLY` report whose `transactionStartDate` falls in May.
- Fees inside payouts issued in May: sum `fee` across the reports whose `settlementDate` falls in May.

Reading those two into a support tool, side by side and labelled, answers the question before it gets asked.

## Adjustments

Adjustments represent additional amounts applied to orders during a transaction -- tips, surcharges, insurance payments, and similar line items. The Adjustments API lets you retrieve all adjustments at the merchant level for tracking and reconciliation.

### Fetch adjustments

```
GET /partners/:partnerId/merchants/:merchantId/adjustments?startDate=2026-01-01&endDate=2026-01-31
```

Both `startDate` and `endDate` are required query parameters in `YYYY-MM-DD` format.

**Response:**

```json
{
  "status": "SUCCESS",
  "data": [
    {
      "adjustmentId": "adj_44c2e...",
      "adjustmentType": "TIP",
      "amount": "2500"
    },
    {
      "adjustmentId": "adj_55d3f...",
      "adjustmentType": "SURCHARGE",
      "amount": "1500"
    }
  ],
  "message": "Adjustments fetched successfully"
}
```

### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `adjustmentId` | string | Unique identifier for the adjustment |
| `adjustmentType` | string | Type of adjustment (e.g. `TIP`, `SURCHARGE`, `INSURANCE`) |
| `amount` | string | Adjustment amount in smallest currency unit |

## Merchant Charges

Merchant charges let partners bill merchants for services, fees, or subscriptions. A charge can be one-time or recurring, and supports VAT.

### Create a charge

```json
POST /partners/:partnerId/merchants/:merchantId/charges
{
  "description": "Monthly platform fee",
  "currency": "752",
  "amount": 5000000,
  "vat": 35,
  "billingDate": "2026-03-01",
  "recurring": {
    "frequency": "monthly",
    "billingEndDate": "2027-03-01"
  }
}
```

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "chargeId": "chg_72a4d..."
  },
  "message": "Charge created successfully"
}
```

### Create charge request fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `description` | string | Yes | Brief description of the charge |
| `currency` | string | Yes | Three-digit ISO currency code |
| `amount` | number | Yes | Charge amount in smallest currency unit |
| `vat` | number | No | VAT amount |
| `billingDate` | string | No | Effective date (`YYYY-MM-DD`) |
| `recurring.frequency` | string | No | Billing frequency (see table below) |
| `recurring.billingEndDate` | string | No | When to stop recurring charges (`YYYY-MM-DD`) |

### Frequency options

| Value | Cycle |
|-------|-------|
| `daily` | Every day |
| `twiceWeekly` | Twice per week |
| `weekly` | Every week |
| `tenDays` | Every 10 days |
| `fortNightly` | Every 2 weeks |
| `monthly` | Every month |
| `everyTwoMonths` | Every 2 months |
| `trimester` | Every 4 months |
| `quarterly` | Every 3 months |
| `twiceYearly` | Every 6 months |
| `annually` | Every year |
| `unscheduled` | No fixed schedule |

### Fetch a charge by ID

```
GET /partners/:partnerId/merchants/:merchantId/charges/:chargeId
```

The response includes subscription details, VAT, frequency, billing dates, and any associated `subCharges`. Sub-charges are individual billing instances generated from a recurring charge.

**Key response fields:**

| Field | Type | Description |
|-------|------|-------------|
| `isSubscriptionCharge` | boolean | Whether this is a recurring charge |
| `description` | string | Charge description |
| `amount` | number | Charge amount in smallest currency unit |
| `vat` | number | VAT applied |
| `frequency` | string | Billing frequency |
| `billingStartDate` | string | Start date (ISO 8601) |
| `billingEndDate` | string | End date (ISO 8601) |
| `subCharges` | array | Individual billing instances with their own `chargeId`, `amount`, `status`, and `billingDate` |

### Update a charge

Modify an existing charge's amount, VAT, or recurring configuration:

```json
PUT /partners/:partnerId/merchants/:merchantId/charges/:chargeId
{
  "amount": 650000,
  "vat": 15,
  "recurring": {
    "updateType": "onlyNext",
    "billingEndDate": "2027-10-23"
  }
}
```

The `recurring.updateType` field controls the scope of the update:

| Value | Behaviour |
|-------|-----------|
| `onlyNext` | Apply the change only to the next billing cycle |
| `allFuture` | Apply the change to all future billing cycles |

### List all merchant charges

```
GET /partners/:partnerId/merchants/:merchantId/charges
```

Returns a paginated list of all charges (one-time and recurring) for the merchant, including `chargeId`, `description`, `amount`, `vat`, `status`, `billingDate`, and whether the charge is subscription-based.

## Billing Plans

A merchant charge is what a merchant is billed. A billing plan is the pricing behind it: the rates that apply to a card brand, a payment method and a terminal type, broken down by where the card comes from and what kind of card it is. Plans are defined once at partner level and then assigned to merchants.

### Create billing plans

```json
POST /partners/:partnerId/billing-plans
{
  "plans": [
    {
      "id": "SP_STANDARD_CARD",
      "paymentMethod": "CARD",
      "cardBrand": "VISA",
      "terminalType": "STANDARD",
      "planType": "FIXED",
      "description": "Standard card pricing 2026",
      "domesticDebitNonCommercial": 0.6,
      "domesticCreditNonCommercial": 0.9,
      "eeaDebitNonCommercial": 0.8,
      "eeaCreditNonCommercial": 1.1,
      "internationalDebitNonCommercial": 1.9,
      "internationalCreditNonCommercial": 2.3,
      "fixedCost": 30,
      "vatPercentage": 25
    }
  ]
}
```

`plans` is an array, so a full price list goes up in one call.

| Field | Description |
|-------|-------------|
| `id` | Your identifier for the plan. |
| `paymentMethod`, `cardBrand`, `terminalType` | What the plan applies to. One plan per combination. |
| `planType` | `FIXED` for a flat percentage or amount, `VARIABLE` for pricing that depends on transaction type. |
| `domestic*`, `eea*`, `international*` | Percentage rates, split by debit or credit and commercial or non-commercial. |
| `minimumCeiling` | Minimum amount for the rate to apply. |
| `fixedCost` | Fixed cost per transaction, in minor units. |
| `fixedPercentage` | Flat percentage across the board. |
| `vatPercentage` | VAT applied to the plan. |

The twelve rate fields are not padding. Interchange differs by card origin and card type, so a single blended rate either loses money on international commercial cards or overcharges on domestic debit. Price the grid.

### Manage plans

```
GET    /partners/:partnerId/billing-plans
GET    /partners/:partnerId/billing-plans/:id
DELETE /partners/:partnerId/billing-plans/:id
GET    /partners/:partnerId/merchants/:merchantId/plans
```

The last one is the useful one in support: it returns the plans actually assigned to a merchant, which is the answer to "why was I charged this". Plans are attached to a merchant during onboarding through the `transactionPricingPlan` and `displayProducts` control fields — see [Merchant Onboarding](/developers/guides/merchant-onboarding) and [Order and Return Terminals](/developers/guides/terminal-logistics).

## Customer Details

The Customer API lets you create and retrieve customer profiles. Profiles store personal information, addresses, contact details, and linked payment cards, enabling richer order data and streamlined checkout experiences.

### Create a customer

```json
POST /customers
{
  "firstName": "John",
  "middleName": "Doe",
  "birthDate": "1990/03/04",
  "countryCode": "SE",
  "address": [
    {
      "addressLine1": "Storgatan 12",
      "city": "Stockholm",
      "countryCode": "SE",
      "postalCode": "111 23",
      "role": "shipping"
    }
  ],
  "phoneNumbers": [
    {
      "phoneNumber": {
        "code": "46",
        "number": "701234567"
      },
      "role": "own"
    }
  ],
  "emails": [
    {
      "email": "john.doe@example.com",
      "role": "personal"
    }
  ],
  "cardIds": [
    "824c514bfe001805f0"
  ]
}
```

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "customerId": "cust_61e3b..."
  },
  "message": "Customer created successfully"
}
```

### Customer fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `firstName` | string | No | Customer's first name |
| `lastName` | string | No | Customer's last name |
| `birthDate` | string | No | Date of birth (`YYYY/MM/DD`) |
| `countryCode` | string | No | Two-letter ISO country code |
| `address` | array | No | Array of address objects (shipping, billing, etc.) |
| `address.addressLine1` | string | Yes | Primary address line |
| `address.city` | string | Yes | City |
| `address.countryCode` | string | Yes | Two-letter ISO country code |
| `address.postalCode` | string | Yes | Postal code |
| `address.role` | string | No | Address purpose (`shipping`, `billing`) |
| `emails` | array | No | Array of email objects with `email` and `role` |
| `phoneNumbers` | array | No | Array of phone objects with nested `phoneNumber` (`code`, `number`) and `role` |
| `cardIds` | array | No | Payment card identifiers to associate with the customer |

### Fetch a customer

```
GET /customers/:customerId
```

Returns the full customer profile including all addresses, emails, phone numbers, and linked card IDs.

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Fetch settlement reports | GET | `/partners/:partnerId/merchants/:merchantId/reports` |
| Fetch adjustments | GET | `/partners/:partnerId/merchants/:merchantId/adjustments` |
| Create merchant charge | POST | `/partners/:partnerId/merchants/:merchantId/charges` |
| Fetch charge by ID | GET | `/partners/:partnerId/merchants/:merchantId/charges/:chargeId` |
| Update merchant charge | PUT | `/partners/:partnerId/merchants/:merchantId/charges/:chargeId` |
| List all merchant charges | GET | `/partners/:partnerId/merchants/:merchantId/charges` |
| Create billing plans | POST | `/partners/:partnerId/billing-plans` |
| Fetch billing plans | GET | `/partners/:partnerId/billing-plans` |
| Fetch billing plan by ID | GET | `/partners/:partnerId/billing-plans/:id` |
| Remove billing plan | DELETE | `/partners/:partnerId/billing-plans/:id` |
| Fetch a merchant's plans | GET | `/partners/:partnerId/merchants/:merchantId/plans` |
| Create customer | POST | `/customers` |
| Fetch customer by ID | GET | `/customers/:customerId` |

---

# Partner Branding

Configure white-label branding for terminals and payment pages. Set colors, fonts, logos, and cover images at the partner level via API or Partner Portal.

Source: https://www.surfboardpayments.com/developers/guides/partner-branding
Category: online
Tags: Online, API, Branding, White-Label

---
## Overview

Surfboard is fully white-label. Use the Branding API to configure colors, fonts, logos, and images that apply to all terminals and customizable pages under your partner account. Branding can be set at the partner level and inherited by all merchants and stores beneath it.

## Set Partner Branding

Configure the visual appearance for your payment pages and terminals.

```
PATCH /partners/:partnerId/branding
```

### Request

```json
{
  "backgroundColor": "#071132",
  "brandColor": "#0e44e1",
  "accentColor": "#00ffa7",
  "footerColor": "#071132",
  "rectShape": "rounded",
  "fontType": "sans-serif",
  "logoUrl": "https://your-cdn.com/logo.svg",
  "iconUrl": "https://your-cdn.com/icon.png",
  "primaryCoverImage": "https://your-cdn.com/cover-primary.jpg",
  "secondaryCoverImage": "https://your-cdn.com/cover-secondary.jpg"
}
```

All fields are optional -- only include the ones you want to update.

### Branding Parameters

| Parameter | Description |
|-----------|-------------|
| `backgroundColor` | Background color for pages (hex) |
| `brandColor` | Primary brand color for buttons and accents (hex) |
| `accentColor` | Secondary color that complements the brand color (hex) |
| `footerColor` | Footer background color (hex) |
| `rectShape` | Button shape: `rounded`, `pill`, or `edgy` |
| `fontType` | Font family: `sans-serif`, `serif`, or `mono` |
| `logoUrl` | URL to your logo image |
| `iconUrl` | URL to your icon/favicon image |
| `primaryCoverImage` | URL to the primary cover image |
| `secondaryCoverImage` | URL to the secondary cover image |

### Response

```json
{
  "status": "SUCCESS",
  "message": "Branding updated successfully"
}
```

### Via Partner Portal

Navigate to **Settings** > **Set Partner Branding Config**, enter your branding values, and click **Save Changes**.

## Fetch Partner Branding

Retrieve the current branding configuration for your partner account.

```
GET /partners/:partnerId/branding
```

### Response

```json
{
  "status": "SUCCESS",
  "data": {
    "backgroundColour": "#071132",
    "brandColor": "#0e44e1",
    "accentColor": "#00ffa7",
    "footerColor": "#071132",
    "rectShape": "rounded",
    "fontType": "sans-serif",
    "logoUrl": "https://your-cdn.com/logo.svg",
    "iconUrl": "https://your-cdn.com/icon.png",
    "primaryCoverImage": "https://your-cdn.com/cover-primary.jpg",
    "secondaryCoverImage": "https://your-cdn.com/cover-secondary.jpg"
  },
  "message": "Branding retrieved successfully"
}
```

## How Branding Applies

Partner-level branding is the default for all merchants and stores under your account. It applies to:

- **Payment pages** -- hosted checkout UI
- **Terminals** -- on-screen branding for smart terminals
- **Receipts** -- logo and styling on digital receipts

This means your merchants' customers see your brand, not Surfboard's, across all payment touchpoints.