# Retail: Surfboard Payments integration path

Store POS on Surfboard terminals: product catalog with barcodes and stock, a compliant cash register for Sweden, returns, gift cards and receipts.

Source: https://www.surfboardpayments.com/developers/guides/paths/retail
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 card-present terminal integration end to end, from sandbox to certified and live.
- [Device Registration](https://www.surfboardpayments.com/developers/guides/device-registration): Register each till terminal to its store: rotating code, QR link or pre-shipped code.
- [Inter-App Integration](https://www.surfboardpayments.com/developers/guides/interapp-integration): If your POS runs on the terminal itself, this is the app switch that hands each sale to the payment app.

### Products and the cash register

- [Product Catalog](https://www.surfboardpayments.com/developers/guides/product-catalog): Mirror your ERP into a catalog per store: articles, barcodes, prices, VAT, variants and stock.
- [Build Your Own ECR](https://www.surfboardpayments.com/developers/guides/electronic-cash-register): The Swedish cash register on the platform: control unit, Skatteverket declaration, shifts, cash handling, Z-reports, and the certification steps.

### Payment flow

- [Create an Order](https://www.surfboardpayments.com/developers/guides/create-an-order): Ring up a sale as an order with line items from the catalog and the payment in the same call.
- [Payment Lifecycle](https://www.surfboardpayments.com/developers/guides/payment-lifecycle): What each payment state means for the till.
- [Refund an Order](https://www.surfboardpayments.com/developers/guides/refund-an-order): Full returns as a return order against the original sale.
- [Partial Refund](https://www.surfboardpayments.com/developers/guides/partial-refund): Return one item from a basket of many.
- [Gift Cards & Promotions](https://www.surfboardpayments.com/developers/guides/gift-cards-promotions): Issue and redeem gift cards, and run promotions on the catalog.
- [Receipts](https://www.surfboardpayments.com/developers/guides/receipts): Digital receipt links, email, and printing on the terminal.
- [Receipt Printing](https://www.surfboardpayments.com/developers/guides/receipt-printing): Let Surfboard render the receipt from a template, or build it yourself. How to choose.
- [ESC/POS Printing](https://www.surfboardpayments.com/developers/guides/escpos-printing): Your own receipt layout when the default templates are not enough.

### Online sales

- [Online Payment Link](https://www.surfboardpayments.com/developers/guides/online-payment-link): Sell over the phone or by message with a payment link from the same platform.
- [Payment Page](https://www.surfboardpayments.com/developers/guides/payment-page): A hosted checkout for the web shop, settled together with the store.

### Merchants and operations

- [Merchant Onboarding](https://www.surfboardpayments.com/developers/guides/merchant-onboarding): Board each retailer through the Partner API.
- [Store Management](https://www.surfboardpayments.com/developers/guides/store-management): Stores are where catalogs, terminals and cash registers live.
- [Terminal & Device Management](https://www.surfboardpayments.com/developers/guides/terminal-device-management): Move, rename, configure and reboot terminals across a merchant’s stores.
- [Webhooks](https://www.surfboardpayments.com/developers/guides/webhooks-notifications): Payment outcomes pushed to your backend.
- [Settlements & Reporting](https://www.surfboardpayments.com/developers/guides/settlements-reporting): Settlement reports and the reconciliation a retailer’s finance team asks for.

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

---

# Inter-App Integration

Integrate your POS app with CheckoutX using native app switch. Register terminals, process payments, and scan NFC tags through a bi-directional deep link flow -- from a native app or a browser-based POS.

Source: https://www.surfboardpayments.com/developers/guides/interapp-integration
Category: in-store
Tags: In-Store, Android, iOS, Web, CheckoutX, App Switch

---
## Overview

Surfboard's CheckoutX app handles payment acceptance on Android payment terminals and as a SoftPOS solution. If you have your own POS or business app, you can integrate with CheckoutX through **native app switch** -- your app opens CheckoutX to process a payment, and CheckoutX returns control to your app when done.

This guide covers terminal registration, the payment flow, and NFC tag scanning -- all through deep links.

> **Important:** Surfboard terminals operate in full online mode. All data exchange happens through APIs and deep link parameters -- no offline data passing is supported.

## How It Works

The inter-app flow is a bi-directional app switch:

1. **Your app -> CheckoutX** -- initiate a task (registration, payment, or tag scan)
2. **CheckoutX -> Your app** -- return the result via your redirect URL

There are three flows:

| Flow | Purpose | Frequency |
|------|---------|-----------|
| **Terminal Registration** | Link CheckoutX to a terminal | Once per device |
| **Payment** | Process a payment via CheckoutX | Every transaction |
| **Tag Scanning** | Read NFC product tags | As needed |

## Setting Up Your App for App Switch

Configure your app to receive the callback from CheckoutX after a task completes.

### Android

Register a deep link intent filter in your `AndroidManifest.xml`:

```xml
<activity android:name=".YourActivity">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="posapp" android:host="hello" />
  </intent-filter>
</activity>
```

### iOS

Register a custom URL scheme in your `Info.plist` or Xcode project settings. Add your scheme (e.g., `posapp`) under **URL Types**.

### Browser-based POS

A web app has no scheme of its own to register. The switch out to CheckoutX works the same way, but the return needs a redirect URL that names the operator's browser -- see [Browser-Based POS (Web Apps)](#browser-based-pos-web-apps).

## Configure Terminal Before Payment

Before the first payment (especially after a device reboot), call the configuration route to prepare CheckoutX:

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

Replace `REDIRECT_URL` with your base64-encoded app URL. CheckoutX will open, configure itself, and return with `isConfigured: true` when ready.

Use this step before starting the payment flow for optimal performance on the first transaction.

### Handling `PS_0025`, Terminal Not Connected

When initiating a payment you may occasionally see:

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

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, and no user action is needed. This is most common in SoftPOS setups where consumer devices can go idle or lose their server session between transactions; re-configuring re-establishes the connection before the next payment.

## Terminal Registration (One-Time Setup)

Register a terminal with CheckoutX once per device. This links your Surfboard terminal to the CheckoutX app.

### Step 1: Get an Interapp Code

Call the API to generate a registration code:

```json
GET /merchants/:merchantId/stores/:storeId/terminals/interapp
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "registrationCode": "abc123..."
  },
  "message": "Interapp code generated successfully"
}
```

> The registration code is valid for **120 seconds**. Complete the app switch before it expires.

### Step 2: App Switch to Register

Build the registration deep link with the code:

```
checkoutx://com.surfboard.checkoutx/register?redirectUrl=REDIRECT_URL&data=REGISTRATION_CODE
```

- `REDIRECT_URL` -- your base64-encoded app callback URL
- `REGISTRATION_CODE` -- base64-encoded JSON: `{"registrationCode": "GENERATED_CODE"}`

### Step 3: Handle the Callback

After registration, CheckoutX calls your redirect URL with a `data` query parameter containing the `terminalId`:

```
posapp://hello/order?orderRef=...&data=<base64_encoded_data>
```

Decode the base64 `data` parameter to get the terminal ID:

```kotlin
// Kotlin
val data = String(Base64.getUrlDecoder().decode(uri.getQueryParameter("data")))
val jsonObject = serializer.fromJson(data, JsonObject::class.java)
val terminalId = jsonObject["terminalId"].asString
```

```swift
// Swift
guard let base64String = URLComponents(url: url, resolvingAgainstBaseURL: false)?
    .queryItems?.first(where: { $0.name == "data" })?.value,
    let jsonData = Data(base64Encoded: base64String),
    let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
    let terminalId = json["terminalId"] as? String
else { return }
```

Store the `terminalId` -- you need it for all future payments on this device.

### Step 4: Verify Registration

Confirm the registration status via API:

```json
GET /merchants/:merchantId/stores/:storeId/terminals/interapp/:interappCode
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "registrationStatus": "REGISTERED",
    "terminalId": "83abab731f6fb00704"
  }
}
```

**Possible `registrationStatus` values:** `REGISTERED` | `NOT_REGISTERED`

## Payment Flow

Once the terminal is registered, process payments through app switch.

### Step 1: Create an Order via API

Create an order using the [Create Order API](/developers/guides/create-an-order) with the `terminalId` from registration. The response includes a `paymentId` and an `interAppJWT`:

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

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderId": "83a1ba32774149710b",
    "paymentId": "83a1ba3264bd500106",
    "interAppJWT": "eyJhbGciOiJIUzI1NiIs..."
  }
}
```

### Step 2: App Switch to CheckoutX

Build the transaction deep link:

```
checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=REDIRECT_URL&data=REQUIRED_DATA
```

- `REDIRECT_URL` -- your base64-encoded callback URL
- `REQUIRED_DATA` -- base64-encoded JSON containing the terminal ID and the `interAppJWToken`:

```json
{
  "terminalId": "YOUR_TERMINAL_ID",
  "interAppJWToken": "eyJhbGciOiJIUzI1NiIs..."
}
```

> **Required on both Android and iOS:** Include the `interAppJWToken` -- the `interAppJWT` value returned in the order response -- in the data parameter on every app switch transaction. This is required for the app switch flow on both platforms, not an iOS-only step.

### Step 3: Perform the App Switch

```kotlin
// Kotlin
val url = "checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=$encodedRedirectUrl&data=$encodedData"
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
startActivity(intent)
```

```swift
// Swift
let url = "checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=\(encodedRedirectUrl)&data=\(encodedData)"
if let deepLink = URL(string: url) {
    UIApplication.shared.open(deepLink)
}
```

```dart
// Flutter
String url = "checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=$encodedRedirectUrl&data=$encodedData";
Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
    await launchUrl(uri);
}
```

### Step 4: Handle the Result

CheckoutX calls your redirect URL with the result. Check the order status via API to confirm payment completion:

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

## Framing the Redirect URL

The redirect URL follows the format:

```
<YOUR_SCHEME>://<YOUR_HOST>/<ROUTES>?<QUERY_PARAMS>
```

For example, if your scheme is `posapp` and host is `hello`:

```
posapp://hello/order?orderRef=6ba7b7db-519f-4ed9-9f6b-a834140466f7
```

This URL must be **base64-encoded** before passing it as the `redirectUrl` parameter:

```kotlin
// Kotlin
val url = "posapp://hello/order?orderRef=6ba7b7db-519f-4ed9-9f6b-a834140466f7"
val encoded = Base64.getUrlEncoder().encodeToString(url.toByteArray())
```

```swift
// Swift
let url = "posapp://hello/order?orderRef=6ba7b7db-519f-4ed9-9f6b-a834140466f7"
let encoded = Data(url.utf8).base64EncodedString()
```

```js
// JavaScript -- URL-safe base64, no padding
const encoded = btoa(url).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
```

For a browser-based POS the redirect URL names the operator's browser instead of your app -- see [Browser-Based POS (Web Apps)](#browser-based-pos-web-apps).

## Browser-Based POS (Web Apps)

If your POS runs in a browser rather than as an installed app -- a web POS on an Android tablet, for instance -- the switch **out** to CheckoutX works exactly as described above. A `checkoutx://` deep link is just a link, and the browser hands it to CheckoutX.

The difference is the way **back**. A web app has no custom scheme to register, and an `https://` redirect URL does not return the operator to their browser: CheckoutX opens it in its own in-app browser, leaving the POS session behind in a tab nobody is looking at.

### Launching CheckoutX from a page

Build and encode the deep link exactly as elsewhere in this guide, then follow it:

```js
const deepLink =
  `checkoutx://com.surfboard.checkoutx/transaction` +
  `?redirectUrl=${encodedRedirectUrl}&data=${encodedData}`;

// A temporary anchor click is more reliable than assigning window.location,
// which some in-app browsers and webviews intercept
const a = document.createElement("a");
a.href = deepLink;
a.style.display = "none";
document.body.appendChild(a);
a.click();
setTimeout(() => a.remove(), 100);
```

### Returning to the browser

Point the redirect URL at the **browser**, not at a page. On Android Chrome:

```
googlechrome://com.android.chrome
```

Base64-encode it like any other redirect URL. Chrome comes to the front on the tab the flow started in -- nothing is navigated and nothing reloads, so the POS keeps its state.

If the return has to land on a specific page instead:

```
googlechrome://navigate?url=<percent-encoded https URL>
```

This works too, but opens a **new tab** on every return and leaves the original behind. Prefer the first form unless a specific landing URL is essential.

> A `?url=` parameter on the first form is silently dropped -- `com.android.chrome` is a host Chrome ignores rather than a navigate endpoint. There is no same-tab-with-landing-URL variant.

### The return carries no data

Because the redirect names a browser rather than a URL, nothing comes back in it -- no `data` parameter to decode. That is not a limitation to work around: the API is the source of truth for the result in every flow, and a browser POS simply leans on it entirely.

- **Payment:** poll `GET /orders/:orderId/status` until it reaches a terminal state.
- **Registration:** poll `GET /merchants/:merchantId/stores/:storeId/terminals/interapp/:interappCode` until `registrationStatus` is `REGISTERED`, then store the returned `terminalId`.

Since the tab is never reloaded, becoming visible again is the signal that the operator is back:

```js
document.addEventListener("visibilitychange", () => {
  if (!document.hidden && pendingOrderId) {
    checkOrderStatus(pendingOrderId); // re-check immediately, then keep polling
  }
});
```

Persist the pending `orderId` (and the registration code) in `localStorage` as well. The tab is not reloaded on the way back with the redirect above, but it can still be evicted while backgrounded, and the `navigate` form reloads by design.

### Other browsers

The mechanism is not Chrome-specific: **any browser that registers a launch scheme can be named in the redirect URL the same way.** Chrome on Android is simply the combination we verified end to end.

| Redirect URL | Behaviour |
|---|---|
| `googlechrome://com.android.chrome` | Chrome to the front, original tab, no reload -- verified on an Android tablet |
| `googlechrome://navigate?url=<encoded>` | Chrome opens the given URL in a new tab -- verified |
| Another browser's scheme | Same shape, verify per browser |

To check what a given browser answers to on your target device:

```
adb shell am start -a android.intent.action.VIEW -d "<scheme>://"
```

If the browser comes to the front, that scheme works as a redirect URL. Confirm on the device and browser your merchants actually use -- schemes differ between browsers and vendors, and some register none at all. Where a browser registers nothing, the flows still complete: polling reports the result, and the operator returns to the browser manually.

### What does not work from a browser

Measured against CheckoutX on Android, so you do not have to retry them:

| Redirect URL | Result |
|---|---|
| `intent://…#Intent;package=com.android.chrome;end` | No return at all, with or without `action=` and extras -- the redirect is not parsed as an intent URI |
| `https://your-pos.example.com/...` | Opens in CheckoutX's in-app browser rather than the operator's browser |
| An `https://` page that re-launches an `intent://` URI | Ignored as well -- the in-app browser does not follow it |

## NFC Tag Scanning

Scan product NFC tags through CheckoutX before or during a sale:

```
checkoutx://com.surfboard.checkoutx/scanProducts?redirectUrl=REDIRECT_URL&data=REQUIRED_DATA
```

The `REQUIRED_DATA` is a base64-encoded JSON specifying the read mode:

```json
{ "readMode": "SINGLE" }
```

| Read Mode | Description |
|-----------|-------------|
| `SINGLE` | Scan one product tag |
| `MULTIPLE_EDITABLE` | Scan multiple tags, allow editing scanned data |
| `MULTIPLE_NONEDITABLE` | Scan multiple tags, no editing allowed |

The redirect URL and app switch mechanics are identical to the payment flow.

## Example Repositories

- [Android Example App (Kotlin)](https://github.com/surfboardpayments/surfboard-interapp-kotlin-simple)

## Reference

- [Terminals API](https://developers.surfboardpayments.com/api/terminals)
- [Create an Order](/developers/guides/create-an-order)
- [Tap to Pay on iPhone](/developers/guides/tap-to-pay-iphone)
- [NFC Tag Reading](/developers/guides/nfc-tag-reading)
- [Developer Portal](https://developers.surfboardpayments.com/)

---

# Product Catalog

Create and manage product catalogs, products, variants, inventory levels, and analytics through the Catalog API.

Source: https://www.surfboardpayments.com/developers/guides/product-catalog
Category: online
Tags: Online, API, Products, Catalog, Inventory

---
## Overview

The Product Catalog API lets you build a structured product hierarchy for your stores. You can create catalogs, add products with pricing and tax configuration, define variants (such as sizes and colours), manage stock levels, and pull sales statistics -- all through a single set of REST endpoints.

This guide walks through every operation in the catalog lifecycle, from creating an empty catalog to pulling performance analytics.

## Prerequisites

- A configured **store** with a valid `storeId`
- API credentials (API key, API secret)

## Step 1: Create a Product Catalog

A catalog is the top-level container that groups products for a store. Each store can have one or more catalogs.

### Create catalog

```json
POST /catalog
{
  "storeId": "8136a645a2c2d1bb0f"
}
```

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "productcatalogId": "cat_91a3f..."
  },
  "message": "Product catalog created successfully"
}
```

### List catalogs

Retrieve all catalogs that exist under the store:

```
GET /catalog
```

The response returns `data.productcatalogId` as an array of catalog IDs associated with the store.

## Step 2: Add Products

With a catalog in place, add products to it. Each product requires a name, type, pricing, tax, and descriptive metadata.

```json
POST /catalog/:catalogId/products
{
  "storeId": "8136a645a2c2d1bb0f",
  "name": "SurfPad Purple Logo",
  "type": "PRODUCT",
  "unitType": "FIXED",
  "costPrice": 20,
  "sellingPrice": 45,
  "currencyCode": "752",
  "tax": [
    {
      "type": "VAT",
      "percentage": "3"
    }
  ],
  "description": "SurfPad Payment Terminal in Purple",
  "category": "electronics",
  "unit": "nos",
  "productImages": [
    "https://example.com/images/surfpad-purple.png"
  ],
  "hsnCode": "723453",
  "barCode": "7812123454323"
}
```

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "productId": "prod_82f4a..."
  },
  "message": "Product created successfully"
}
```

### Product types and unit types

| Field | Values | Description |
|-------|--------|-------------|
| `type` | `PRODUCT`, `SERVICE` | Whether the item is a physical product or a service |
| `unitType` | `FIXED`, `VARIABLE`, `FREE_AMOUNT` | How quantity and pricing are determined |

### Fetch a single product

```
GET /catalog/:catalogId/products/:productId
```

Pass `storeId` as a query parameter. The response includes the full product object with pricing, tax, attributes, and inventory status.

### List all products in a catalog

```
GET /catalog/:catalogId/products
```

Returns an array of products including their variants, inventory, billing plans, campaign info, and tax breakdown.

## Step 3: Add Product Variants

Variants represent different versions of a product, such as colour or size options. Attach them to an existing product.

```json
POST /catalog/:catalogId/products/:productId/variants
{
  "storeId": "8136a645a2c2d1bb0f",
  "variants": [
    {
      "name": "SurfPad Blue Variant",
      "description": "Blue variant of SurfPad",
      "costPrice": 10,
      "sellingPrice": 12,
      "currencyCode": "752",
      "productImages": [
        "https://example.com/images/surfpad-blue.png"
      ],
      "hsnCode": "123453",
      "barCode": "1212123454323",
      "attributeValues": [
        {
          "attributeKey": "colour",
          "displayName": "blue",
          "value": "#0000FF"
        },
        {
          "attributeKey": "size",
          "displayName": "medium",
          "value": "M"
        }
      ]
    }
  ]
}
```

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "variants": ["var_73b1c..."]
  },
  "message": "Variants added successfully"
}
```

Each variant in the `attributeValues` array uses an `attributeKey` (e.g. `colour`, `size`) paired with a `displayName` and `value` so the storefront can render selectable options.

## Step 4: Link Related Products

Drive cross-sell and upsell opportunities by associating related products with a primary product.

```json
POST /catalog/:catalogId/products/:productId/related-products
{
  "storeId": "8136a645a2c2d1bb0f",
  "relatedProducts": [
    {
      "productId": "prod_82f4a...",
      "relatedProductId": "prod_55d2b..."
    }
  ]
}
```

The API returns a `SUCCESS` status when the association is saved.

## Step 5: Update Products and Variants

### Update a product

Use `PATCH` to modify any product field. Only the fields you include will be changed.

```json
PATCH /catalog/:catalogId/products/:productId
{
  "storeId": "8136a645a2c2d1bb0f",
  "name": "SurfPad Black Logo",
  "sellingPrice": 15,
  "description": "SurfPad Payment Terminal in Black"
}
```

### Update a variant

The same partial-update approach works for variants:

```json
PATCH /catalog/:catalogId/products/:productId/variants/:variantId
{
  "storeId": "8136a645a2c2d1bb0f",
  "name": "SurfPad Black Logo - Large",
  "sellingPrice": 18,
  "description": "SurfPad Payment Terminal in Black - Large Size"
}
```

Both endpoints return `{ "status": "SUCCESS" }` on success.

## Step 6: Manage Inventory

Track stock at both the product level and the individual variant level.

### Update product inventory

```json
PATCH /catalog/:catalogId/products/:productId/inventory
{
  "storeId": "8136a645a2c2d1bb0f",
  "inventory": {
    "productId": "prod_82f4a...",
    "inventory": {
      "quantity": 10,
      "reorderLevel": 5,
      "reorderQuantity": 10
    }
  }
}
```

### Update variant inventory

```json
PATCH /catalog/:catalogId/products/:productId/variants/:variantId/inventory
{
  "storeId": "8136a645a2c2d1bb0f",
  "operation": "STOCK_UP",
  "quantity": 15,
  "unit": "nos"
}
```

The `operation` field controls how stock is modified (e.g. `STOCK_UP` to add inventory). The `unit` field accepts standard measurement units such as `nos`, `kg`, `l`, `m`, and many others.

## Step 7: View Statistics

### Product statistics

Get sales performance, inventory levels, and VAT breakdowns for a single product:

```
GET /catalog/:catalogId/products/:productId/statistics
```

Optionally pass `startDate` and `endDate` query parameters in `YYYY-MM-DD` format to filter by date range. The response includes:

- **Sales by currency** -- units sold, units returned, revenue, VAT, campaign discounts, order count, and average order value
- **Inventory status** -- current stock, stock in, stock out
- **VAT breakdown** -- amount and taxable total per VAT percentage
- **Variant-level stats** -- the same metrics broken down per variant

### Catalog statistics

Get an aggregate view across the entire catalog:

```
GET /catalog/:catalogId/products/statistics
```

This returns:

- **Summary** -- total products, total variants, and aggregated sales metrics by currency
- **VAT breakdown** -- catalog-wide tax totals
- **Top-selling products** -- ranked by units sold and revenue, with per-currency breakdowns

Both endpoints support optional `startDate` and `endDate` query parameters.

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create catalog | POST | `/catalog` |
| List catalogs | GET | `/catalog` |
| Create product | POST | `/catalog/:catalogId/products` |
| Fetch product by ID | GET | `/catalog/:catalogId/products/:productId` |
| List all products | GET | `/catalog/:catalogId/products` |
| Update product | PATCH | `/catalog/:catalogId/products/:productId` |
| Add variants | POST | `/catalog/:catalogId/products/:productId/variants` |
| Update variant | PATCH | `/catalog/:catalogId/products/:productId/variants/:variantId` |
| Add related products | POST | `/catalog/:catalogId/products/:productId/related-products` |
| Update product inventory | PATCH | `/catalog/:catalogId/products/:productId/inventory` |
| Update variant inventory | PATCH | `/catalog/:catalogId/products/:productId/variants/:variantId/inventory` |
| Product statistics | GET | `/catalog/:catalogId/products/:productId/statistics` |
| Catalog statistics | GET | `/catalog/:catalogId/products/statistics` |

---

# Build Your Own ECR

Turn your ERP or business system into a compliant point of sale for Sweden. Product catalog, fiscal cash register with control unit, Skatteverket declaration, shifts, cash handling, orders, receipts, and Z-reports on the Surfboard platform.

Source: https://www.surfboardpayments.com/developers/guides/electronic-cash-register
Category: in-store
Tags: In-Store, ECR, Cash Register, POS, Skatteverket, Z-report, Sweden, API

---
## Overview

If you sell software to Swedish merchants, you already know the shape of the problem. Card, cash and Swish sales have to land in a certified cash register with a control unit, the register has to be declared to Skatteverket, and every trading day ends with a Z-report. Doing that in your own product means a control unit supplier, a certification cycle, and a fiscal journal you are responsible for from then on.

On Surfboard the register is a platform resource. You create it under a store, the platform pairs it with a control unit, keeps the journal, produces the Z-reports, X-reports and journal memory, and mails them to the merchant. Your side is the POS: the checkout, the product data, the stock and the customer records that already live in your ERP.

This guide is the end-to-end integration: catalog sync, register creation, the Skatteverket declaration, shifts and cash handling, orders and receipts, reports, and the certification steps before a merchant goes live. It assumes you are comfortable with the Surfboard order and terminal APIs; where it leans on them, it links to the guide that covers them.

> **Two meanings of "ECR".** Elsewhere in the Surfboard docs, "ECR" can also mean an external POS that drives a payment terminal over a cable or the API. This guide is about the fiscal cash register: the register record, its control unit, shifts, and reports. The API section for it is the [Electronic Cash Register (ECR) V2 API](https://developers.surfboardpayments.com/api/electronic-cash-register).

### What Surfboard provides, and what you build

| Layer | Surfboard provides | You build |
|-------|-------------------|-----------|
| Payments | Terminals (SurfTouch, SurfPad, SurfPrint, CheckoutX SoftPOS), card, Swish, Klarna, gift cards, refunds | The checkout flow that creates orders |
| Products | Product Catalog API: catalogs, products, variants, barcodes, prices, VAT, inventory | The sync from your ERP into the catalog |
| Fiscal register | Cash register record, paired control unit, register and control-unit details for Skatteverket | The declaration workflow your merchant follows |
| Daily operation | Open and close shift, petty cash, cash withdrawals, End of Day, scheduled close, register state | The cashier screens that call these |
| Receipts | Receipt data on the order, digital receipt link, email, terminal printing, ESC/POS | Where in your flow receipts are offered |
| Reporting | Z-report, X-report, journal memory files per register, SIE bookkeeping files per merchant, portal views | Any push into your own back office |

## How the Pieces Fit

The cash register sits inside the same hierarchy as everything else on the platform:

```
Partner (you)
└── Merchant (your customer, onboarded and KYB-approved)
    └── Store (a physical location)
        ├── Cash registers (one or more per store, each with a cashRegisterId)
        ├── Terminals (registered devices, each with a terminalId)
        └── Product catalog (products, variants, inventory for the store)
```

Cash registers belong to the store, not to a terminal. A store can have several. The link between a register and a terminal is made when a shift is opened on that terminal, and it is removed again when the shift is closed. Between shifts a register has no terminal, and a terminal has no register. This is a change from the first version of the ECR API, where a register was tied permanently to one terminal.

Your POS application can live in either of two places:

- **On the Surfboard terminal.** SurfTouch and SurfPrint run Android. Your app runs on the device beside the payment application and hands off payments with a native app switch. See [Inter-App Integration](/developers/guides/interapp-integration).
- **On your own hardware.** A tablet, a PC, or your cloud backend creates orders over the API against the terminal, and the terminal takes the payment. See [Create an Order](/developers/guides/create-an-order).

Either way the cash register is the same record, operated with the same twelve endpoints, and every sale is an order created against the terminal that currently holds an open shift on the register. A phone running CheckoutX SoftPOS is a terminal like any other here: it opens shifts, takes sales and closes shifts on a register exactly as the hardware terminals do.

## Prerequisites

1. A partner account with API credentials from the [Developer Portal](https://developers.surfboardpayments.com/), and the base URL, key and secret in configuration as described in [API Conventions](/developers/guides/api-conventions).
2. ECR enabled for your partner account. [Get Partner Config](https://developers.surfboardpayments.com/api/partners) returns `ecrEnabled`, which states whether the partner has access to the cash-register features. If it is `false`, ask your Surfboard contact to enable it before you start.
3. A Swedish merchant onboarded through [Merchant Onboarding](/developers/guides/merchant-onboarding), with a store. You need the `merchantId` and `storeId`.
4. A terminal registered to that store, so you have its `terminalId`. See [Device Registration](/developers/guides/device-registration). You can list a store's terminals with `GET /merchants/:merchantId/stores/:storeId/terminals`.

> **Note:** A charge applies for every cash register created under a merchant. Create the registers a store actually needs, not one per test run.

All ECR endpoints are merchant-scoped and take the standard header set:

```
Content-Type: application/json
API-KEY:      YOUR_API_KEY
API-SECRET:   YOUR_API_SECRET
MERCHANT-ID:  YOUR_MERCHANT_ID
```

## Step 1: Sync Your ERP into the Product Catalog

Your ERP is the source of truth for articles, barcodes, prices and stock. Mirror it into a Surfboard product catalog so the terminal, the merchant portal and the receipts all show the same products, and so sales statistics and inventory are tracked per store.

Create one catalog per store:

```json
POST /catalog
{
  "storeId": "8136a645a2c2d1bb0f"
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "catalogId": "8219688f18ebb8020a"
  },
  "message": "Product catalog created successfully"
}
```

Then create each article as a product. This is the request example from the reference, with the fields that matter for a POS sync:

```json
POST /catalog/:catalogId/products
{
  "storeId": "8136a645a2c2d1bb0f",
  "name": "SurfPad Purple Logo",
  "type": "PRODUCT",
  "unitType": "FIXED_UNIT",
  "costPrice": 2000,
  "sellingPrice": 4500,
  "currencyCode": "752",
  "tax": [
    {
      "type": "VAT",
      "percentage": "25"
    }
  ],
  "description": "SurfPad Payment Terminal in Purple",
  "category": "electronics",
  "unit": "nos",
  "productImages": [
    "https://res.cloudinary.com/martinsurf/image/upload/v1619101937/surfboardpayments/surfboard-icon.svg"
  ],
  "hsnCode": "723453",
  "barcode": "7812123454323"
}
```

How the ERP fields map:

| Your ERP holds | Catalog field | Notes |
|----------------|---------------|-------|
| Article name | `name` | Required. |
| Physical good or service | `type` | `PRODUCT` or `SERVICE`. Required. |
| Sold per piece, by weight, or open price | `unitType` | `FIXED_UNIT`, `VARIABLE_UNIT` or `FREE_UNIT`. Required. |
| Sales price | `sellingPrice` | Required. Integer in the smallest unit, so 45.00 SEK is `4500`. Tax-inclusive. |
| Cost price | `costPrice` | Optional, same unit rules. |
| VAT rate | `tax[].type` and `tax[].percentage` | `VAT` with the rate as a string. |
| Barcode / EAN | `barcode` | Scanned at the till, and carried onto the order line as `gtin`. |
| Unit of measure | `unit` | Required. `nos` for pieces, `kg`, `l`, and the other listed units. |
| Product group | `category` or `categoryId` | Free text, or an existing category identifier. |
| Your own article number or anything else | `metadata` | Free-form key/value object for your own use. |
| Sizes, colours | Variants | Add with `POST /catalog/:catalogId/products/:productId/variants`. |

`productImages` is required by the endpoint, so send at least one image URL per product.

Keep stock in step with your ERP with the inventory endpoint. Send a `STOCK_UP` when goods arrive and a `STOCK_DOWN` for shrinkage or manual corrections:

```json
PATCH /catalog/:catalogId/products/:productId/inventory
{
  "storeId": "8136a645a2c2d1bb0f",
  "operation": "STOCK_UP",
  "quantity": 10,
  "unit": "nos"
}
```

The full catalog lifecycle, including variants, related products and statistics, is in [Product Catalog](/developers/guides/product-catalog).

## Step 2: Create the Cash Registers

A cash register is created under a store. Create as many as the store needs: typically one per till position, each with a name the cashier will recognise when choosing a register at shift open.

```json
POST /merchants/:merchantId/stores/:storeId/cash-register
{
  "cashRegisterName": "Main Register",
  "deviceId": "{{terminalId}}",
  "emails": [
    "store.manager@example.com"
  ]
}
```

| Parameter | Required | Description |
|-----------|----------|-------------|
| `cashRegisterName` | Yes | Name of the register, letters and digits only. Spaces and punctuation are rejected by the control unit provider's validation, so `Kassa1` works and `Kassa 1` does not. This is the name the merchant will see on reports. |
| `deviceId` | Yes | A terminal or device identifier. The reference lists it as required at creation. The terminal that actually operates the register is set when a shift is opened, see Step 4. |
| `emails` | No | Addresses that receive this register's reports, in addition to the merchant's own configured email. |

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "cashRegister": {
      "cashRegisterId": "{{cashRegisterId}}"
    }
  },
  "message": "Cash register subscribed successfully"
}
```

Store `cashRegisterId` in your system. Every shift, close, and report call is addressed to it, and it is what the cashier chooses from when opening a shift.

Creating the register also pairs it with a control unit. There is no separate control-unit call: the pairing is done by the platform, and the control unit's details come back in the register record in the next step. The control unit is shared by all of a merchant's registers, so a second or third register under the same merchant reuses it.

> **Note:** Report recipients can be added but not removed through the portal or the API. If an address is entered by mistake, write to Surfboard to have it removed.

To verify what exists under a merchant, or to rebuild your mapping after a reinstall, list the registers:

```
GET /merchants/:merchantId/cash-register
```

Each entry carries `cashRegisterId`, `storeId`, `terminalId`, `merchantId` and `cashRegisterName`. Filter on `storeId` to get the registers for one store. `terminalId` is the terminal currently linked by an open shift, and it is `null` while no shift is open on that register. This list is what your POS shows the cashier when they pick a register at shift open.

## Step 3: Declare the Register with Skatteverket

A cash register must be reported to Skatteverket before it is taken into use. The merchant does this in Skatteverket's e-service *Anmäl, ändra, felanmäl och avanmäl kassaregister* (report, change, report faults, and deregister cash registers). Only an authorised representative of the company can file, or a registration agent (*registreringsombud*) the company has appointed with Skatteverket's form. You as the partner cannot file on the merchant's behalf unless you have been appointed that way, so the declaration step belongs in your merchant onboarding flow, not in your backend.

Everything the form asks for about the register and its control unit is in the register record:

```
GET /merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "cashRegisterId": "{{cashRegisterId}}",
    "cashRegisterName": "Main Register",
    "activationDate": "2026-04-15",
    "closingTime": "23:00",
    "designation": "Register 1",
    "modelOrProgram": "Surfboard ECR 1.0",
    "address": "Main Street 1, Stockholm",
    "email": [
      "store.manager@example.com"
    ],
    "controlUnitAddress": "Main Street 1, Stockholm",
    "controlUnitManufacturer": "Surfboard",
    "controlUnitType": "SOFTWARE",
    "controlUnitModel": "CU-100",
    "controlUnitSerialNumber": "{{controlUnitSerialNumber}}",
    "notifications": [
      {
        "email": "store.manager@example.com",
        "reports": [
          "Z_REPORT"
        ]
      }
    ]
  },
  "message": "Fetched cash register successfully"
}
```

What Skatteverket asks for, and where it comes from:

| Skatteverket asks for | Field in the register record |
|-----------------------|------------------------------|
| Cash register designation (*beteckning*) | `designation` |
| Model or program (*modell eller program*) | `modelOrProgram` |
| Address where the register is used | `address` |
| Control unit manufacturer | `controlUnitManufacturer` |
| Control unit type | `controlUnitType` |
| Control unit model | `controlUnitModel` |
| Control unit serial number (*tillverkningsnummer*) | `controlUnitSerialNumber` |
| Control unit address | `controlUnitAddress` |

The same details are shown on the register's **Info** tab in the partner portal, where a **Declare with Skatteverket** button opens the e-service. See [Cash Registers (ECR)](/partner-portal/cash-registers) in the partner portal guide.

A practical onboarding flow for your merchants:

1. Create the register as soon as the store exists.
2. Show the merchant the register and control-unit details from the record above, or point them to the Info tab in the portal.
3. Have the merchant file the declaration in the e-service. Skatteverket sends a confirmation and later a registration certificate.
4. Only then open the first shift.

Declare production registers only. A register created in the demo environment carries demo control-unit details, and those are not valid for a Skatteverket declaration.

Changes must be reported to Skatteverket within two weeks. Moving the register to another address, replacing it, or taking it out of service are all changes, so pair every delete in Step 8 with a deregistration.

> **The 2027 deadline.** Skatteverket's regulation SKVFS 2021:17 applies to all cash registers from 1 January 2027, and cloud control systems certified under SKVFS 2020:9 are the alternative to a physical control unit. The register and control unit Surfboard provisions are what you declare; you do not source or certify a control unit yourself. Skatteverket's own pages cover [how to report a cash register](https://www.skatteverket.se/servicelankar/otherlanguages/englishengelska/businessesandemployers/startingandrunningaswedishbusiness/cashregisters/howtoreportacashregister.4.57cadbbd15a3688ff44de5a.html) and the [exemptions from the cash register requirement](https://www.skatteverket.se/servicelankar/otherlanguages/englishengelska/businessesandemployers/startingandrunningaswedishbusiness/cashregisters/exemptionsfromthecashregisterrequirement.4.57cadbbd15a3688ff44deba.html).

## Step 4: Run the Business Day

The ECR API follows the shape of a trading day. Your POS needs four screens for it: open shift, cash in and out, close shift, and end of day. Read the register state before each so the UI only offers what the register will accept.

The terminal enters the picture at shift open. Opening a shift links the terminal the POS is using to the register the cashier chose, and closing the shift removes that link. While the shift is open, the register is addressed either by its `cashRegisterId` or, for the cash calls, by the `terminalId` linked to it.

The rules that follow from that model, and that your POS has to respect:

- **One open shift per register.** Opening a second shift on a register that already has one is rejected.
- **One register per terminal at a time.** A terminal with an open shift cannot open a shift on another register until that shift is closed. The error names the register it is still mapped to.
- **No shift, no sales.** An order created on a terminal with no open shift is rejected.
- **Two tills trading at once need two registers.** A shop counter and a market stall selling at the same time is two terminals and two registers, each with its own shift and its own Z-report.
- **A terminal can move between registers.** Close the shift on one register, open a shift on another. The same phone can run the store register one day and the market register the next.

### Check the register state

```
GET /merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/state-info
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "cashRegisterState": "CASH_REGISTER_OPEN",
    "cashRegisterId": "{{cashRegisterId}}",
    "cashRegisterName": "Main Register",
    "currencyCode": "SEK",
    "shiftInfo": {
      "shiftNumber": 1,
      "shiftState": "SHIFT_OPEN",
      "cashierName": "John Doe",
      "shiftOpenedAt": "2026-04-15T12:00:00Z",
      "shiftClosedAt": null
    },
    "pettyCash": 5000,
    "openedAt": "2026-04-15T08:00:00Z",
    "closedAt": null
  },
  "message": "Fetched cash register state successfully"
}
```

`cashRegisterState` is the register's lifecycle state and `shiftInfo.shiftState` the current or most recent shift. `pettyCash` is the cash float currently held. Use this call when the POS starts, after a crash, and before every shift action.

| Field | Values you will see | Meaning |
|-------|---------------------|---------|
| `cashRegisterState` | `CASH_REGISTER_OPEN` | Register is open for the trading day |
| | `CASH_REGISTER_CLOSED` | Register is closed, after End of Day or before its first shift |
| `shiftInfo.shiftState` | `SHIFT_OPEN` | A shift is active and the register accepts sales |
| | `SHIFT_CLOSED` | No active shift |

### Open a shift

A shift must be open before the register accepts payments. Your POS lists the store's registers, the cashier picks one, counts the float into the drawer, and you open the shift on that register with the ID of the terminal the POS is using. From this point the terminal is linked to the register.

```json
PUT /merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/open-shift
{
  "terminalId": "{{terminalId}}",
  "pettyCash": 5000,
  "cashierName": "John Doe"
}
```

| Parameter | Required | Description |
|-----------|----------|-------------|
| `terminalId` | Yes | The terminal to link to this register for the shift. |
| `pettyCash` | Yes | Opening cash float for the shift. |
| `cashierName` | No | Name of the cashier. It is also what appears as the cashier on the shift record. |

```json
// Response
{
  "status": "SUCCESS",
  "data": {},
  "message": "Shift opened successfully"
}
```

### Deposit and withdraw cash

Every movement of cash in or out of the drawer that is not a sale goes through these two calls, so the fiscal journal and the drawer agree at close. Both are addressed by the terminal rather than the register: pass the `terminalId` that opened the shift, and the platform resolves the register linked to it.

Adding a float top-up:

```json
POST /merchants/:merchantId/stores/:storeId/cash-register/deposit-petty-cash
{
  "terminalId": "{{terminalId}}",
  "amount": 1000
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "cashRegisterId": "{{cashRegisterId}}",
    "newPettyCash": 6000
  },
  "message": "Petty cash inserted successfully"
}
```

Taking cash out, for a bank drop or a paid-out:

```json
PUT /merchants/:merchantId/stores/:storeId/cash-register/withdraw
{
  "amount": 500,
  "terminalId": "{{terminalId}}"
}
```

```json
// Response
{
  "status": "SUCCESS",
  "message": "Cash withdrawn successfully"
}
```

Both require an active shift.

### Close a shift

When the cashier goes off duty, close the shift. This also de-links the terminal from the register, so the terminal is free to open a shift on another register, and the register is free to be opened from another terminal. The response returns the shift number and the petty-cash balance at close, which is the figure to reconcile against the counted drawer:

```
PUT /merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/close-shift
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "cashRegisterId": "{{cashRegisterId}}",
    "shiftNumber": 1,
    "pettyCash": 5000
  },
  "message": "Shift closed successfully"
}
```

A register can run several shifts in one day; open the next one with a fresh float, from the same terminal or a different one.

### End of Day

End of Day closes the register for the trading day and generates the Z-report. It runs automatically at the register's scheduled close time, or you trigger it from the POS when the store closes:

```
PUT /merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/end-of-day
```

```json
// Response
{
  "status": "SUCCESS",
  "message": "Cash register closed successfully"
}
```

> **Warning:** End of Day is a one-time action and cannot be undone. Once it has been called, the register cannot accept payments for the rest of the day, and it cannot be reopened until the next day. Put a confirmation step in front of it, and never wire it to an automatic "logout".

The automatic close runs at the scheduled close time whether or not anyone called End of Day, and whether or not a shift is still open. From the next day the register is available again, and the first Open-Shift of the day starts the new trading day. A register that was closed by End of Day during the day stays closed for that day.

### Set the scheduled close time

Every register has an automatic End of Day. The reference documents the default as 00:00 local time. A store that trades past midnight, or one that wants its Z-report cut at closing time, sets its own:

```json
PATCH /merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/closing-time
{
  "closingTime": "23:00"
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "cashRegisterId": "{{cashRegisterId}}",
    "newClosingTime": "23:00",
    "shiftCloseType": "CUSTOM",
    "scheduledClosingTime": "2026-04-16T23:00:00Z"
  },
  "message": "Cash register closing time updated successfully"
}
```

Sending a closing time sets the register's shift close type to `CUSTOM`. The current setting is also returned as `closingTime` on the register record.

### The day, end to end

| Moment | Call | What the POS shows |
|--------|------|--------------------|
| POS starts | Fetch Cash Registers, then Get Cash Register State | The store's registers, each with its status and float |
| Store opens | Open-Shift on the chosen register with this terminal's ID and the counted float | Terminal linked to the register, sales enabled |
| Float top-up or bank drop | Deposit Petty Cash / Withdraw Cash | Updated float |
| Cashier change | Close-Shift, then Open-Shift | Shift summary to reconcile, terminal de-linked and re-linked |
| Store closes | End of Day, or wait for the scheduled close | Z-report on its way to the report recipients |

## Step 5: Ring Up Sales

A sale on your POS is an order created against the terminal that has an open shift on the register. The order lines come straight from your catalog sync, and the order is flagged for the cash register with `controlFunctions.ecrEnabled`. The reference lists `ecrEnabled` as "whether ECR (electronic cash register) mode is enabled" on the order.

The example below follows the Create Order reference exactly. Two units of the same article, tax-inclusive prices, the barcode carried as `gtin`, your ERP line reference as `externalItemId`, and the cashier's name passed through to the receipt:

```json
POST /orders
{
  "terminal$id": "{{terminalId}}",
  "referenceId": "POS-2026-000123",
  "orderLines": [
    {
      "id": "1",
      "externalItemId": "ART-10442",
      "name": "SurfPad Purple Logo",
      "quantity": 2,
      "gtin": "7812123454323",
      "amount": {
        "regular": 4500,
        "total": 4500,
        "currency": "752",
        "tax": [
          {
            "amount": 900,
            "percentage": 25,
            "type": "VAT"
          }
        ]
      }
    }
  ],
  "totalOrderAmount": {
    "regular": 9000,
    "total": 9000,
    "currency": "752",
    "tax": [
      {
        "amount": 1800,
        "percentage": 25,
        "type": "VAT"
      }
    ]
  },
  "controlFunctions": {
    "ecrEnabled": true,
    "receipt": {
      "cashierName": "John Doe"
    },
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD"
    }
  }
}
```

The response returns `orderId` and `paymentId`. Store both against your POS receipt so refunds and receipt lookups can find them later.

Three rules from [Create an Order](/developers/guides/create-an-order) that matter more on a till than anywhere else:

- **`amount.total` is per unit, not per line.** The line above is two units at 45.00 SEK. `total` stays `4500`; only `totalOrderAmount` carries the `9000`.
- **Prices are tax-inclusive.** The `tax` array states how much VAT is inside the price, never an amount to add on top. Sweden's 25, 12 and 6 percent rates each go on their own lines with their own `tax` entries.
- **Currencies are numeric ISO 4217 codes as strings.** SEK is `"752"`.

Fields under `controlFunctions.receipt` are printed on the receipt. Besides `cashierName` you can send `autoPrintReceipt` and `autoSendReceiptIfEmailAvailable`. If you prefer to add receipt details after the payment, use `PUT /orders/:orderId/receipt` as described in [Receipts](/developers/guides/receipts).

> **The receipt number is the register's, not yours.** On an order that goes through a Surfboard register, the receipt number shown on the receipt is generated by the register in line with the regulations, and the series is per cash register: two registers give two series. A `sequenceNumber` you send is stored on the order for your own reference but does not change the printed number. The `cashRegisterName` and `controlUnitSerialNumber` receipt fields are for the bring-your-own-control-unit path in Step 6, not for Surfboard registers. Keep the `orderId` on your side; it is printed on every Surfboard receipt and is the key to find a sale when a customer comes back with one.

### Returns and refunds

A return is an order with negative quantities. Set `purchaseOrderId` on each returned line to the `orderId` of the original sale; the reference marks it as mandatory for a return item. The full flow, including refunding to the original payment, is in [Refund an Order](/developers/guides/refund-an-order) and [Partial Refund](/developers/guides/partial-refund).

### Cash sales

Cash is a payment method like any other; a cash sale is still an order on the register, so it lands in the fiscal journal and on the Z-report. The [Partial Payments](/developers/guides/partial-payments) guide shows a second payment taken with the `CASH` method. Cash movements that are not sales, such as floats and bank drops, are the petty-cash calls in Step 4.

### Split and mixed payments

Card plus gift card, or card plus cash, are handled with partial payments on the same order. See [Partial Payments](/developers/guides/partial-payments) and [Gift Cards & Promotions](/developers/guides/gift-cards-promotions).

## Step 6: Receipts

Once the payment completes, the receipt options are the same as for any Surfboard payment:

- **Digital receipt link.** `GET /receipts/{id}/link` returns a hosted receipt URL to show as a QR code or send in an SMS.
- **Email.** `PUT /receipts/{id}/email` sends the receipt to the customer.
- **Print on the terminal.** `POST /receipts/{id}/print` prints from a Surfboard template on a terminal with a built-in printer or on a FinPrinter. See [Receipt Printing](/developers/guides/receipt-printing).
- **Your own layout.** `PUT /receipts/{terminalId}/escpos` prints a receipt you have rendered yourself. See [ESC/POS Printing](/developers/guides/escpos-printing).

All of them are covered in [Receipts](/developers/guides/receipts).

> **Already have a certified control unit?** If your existing POS keeps its own control unit and you only use Surfboard for payments and receipts, you can attach that unit's data to the receipt instead of creating a Surfboard register. `POST /receipts/:id` in the [Receipts API](https://developers.surfboardpayments.com/api/receipts) takes `receiptNumber`, `cashRegisterName`, `controlUnitSerial` and `cashierName`. That path is for partners who bring their own fiscal setup; everything else in this guide assumes Surfboard's.

## Step 7: Reports and Bookkeeping

Every End of Day produces a Z-report for the register, and the register also keeps X-reports and journal memory. They are emailed to the merchant's configured address and to every address in the register's notification list, and they can be fetched by API for your own back office:

```
GET /merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/files?reportType=value&startDate=value&endDate=value
```

All three query parameters are optional and each is applied only when present. `reportType` filters to one report type, `startDate` and `endDate` bound the period.

```json
// Response
{
  "status": "SUCCESS",
  "data": [
    {
      "fileName": "z-report-2026-04-15.pdf",
      "fileType": "application/pdf",
      "reportType": "Z_REPORT",
      "sequence": 1,
      "fileUrl": "https://files.example.com/z-report-2026-04-15.pdf"
    }
  ],
  "message": "Cash register files fetched successfully."
}
```

`sequence` is the file's ordinal within its report type, so a gap in the Z-report sequence in your archive means a day you have not fetched yet. The partner portal offers the same files under the register's **Reports** tab, filtered by *Z report*, *X report* or *Journal Memory* and a date range.

For accounting, the merchant can also receive **SIE files**. These are generated per merchant, not per register, and sent for every payout Surfboard makes to the merchant. They are switched on with **Enable bookkeeping** on the merchant's Cash-Register tab in the partner portal. Settlement and payout reporting for the same money is in [Settlements & Reporting](/developers/guides/settlements-reporting).

## Step 8: Decommission a Register

When a till is retired or a store closes, delete the register:

```
DELETE /merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId
```

```json
// Response
{
  "status": "SUCCESS",
  "message": "Cash register unsubscribed successfully."
}
```

> **Warning:** Deleting a register cannot be undone. Fetch and archive its files first, and remind the merchant to deregister it in Skatteverket's e-service within two weeks.

## Releasing Your ECR

The build order that gets a first merchant live with the least rework:

1. **Build in demo.** Demo credentials come with your developer account. Create a test merchant and store, register a terminal, and run the full day: create register, open shift, sales, cash movements, close shift, End of Day, fetch the Z-report. Demo terminals run in payment page mode and take test cards only, so exercise the register flow there and the terminal hardware once you have live credentials. See [API Conventions](/developers/guides/api-conventions) for the environments.
2. **Wire the catalog sync.** Decide what triggers a product update in your ERP and push it to the catalog on that event. A nightly full sync plus event-driven price changes is the usual shape.
3. **Put the state check first.** Every register screen in your POS starts with Fetch Cash Registers for the store and Get Cash Register State for the chosen one. It is what keeps a cashier from opening a shift on a register that is already open elsewhere, or calling End of Day twice.
4. **Make End of Day deliberate.** Confirmation dialog, and a visible note that it cannot be undone. Set the scheduled close time per store so a forgotten End of Day still happens.
5. **Listen for payment events.** Subscribe to order and payment webhooks so the POS learns the outcome of a payment without polling. See [Webhooks](/developers/guides/webhooks-notifications).
6. **Register your POS with the control unit provider.** Before the first production register can be created, Surfboard registers your POS software with the control unit provider. Send your Surfboard contact:
   - Company name and organisation number. The company name cannot be changed once the application is submitted, so use the entity that will own the product.
   - A unique cash register software identifier in reverse-domain form, 1 to 64 characters, for example `se.yourcompany`.
   - An application name that includes `SE`, for example `YourPOS SE 1.0.0`. Regulation requires the market code in the name. It is visible on internal dashboards only, not on receipts.
   - A point of contact: name, email and phone number.
7. **Self-certify.** Surfboard sends a test-case sheet covering the register flow and the payment flow. You run each case and fill in the IDs it asks for, and walk the full flow with Surfboard on a call. Two things are hard requirements from day one: support for every Swedish VAT rate including 12 percent, even if the merchant you start with never uses it, and a receipt for every sale. Paperless is fine: attach the digital receipt link as evidence and note that you do not print. Receipt copies are handled by Surfboard's receipt product, so your POS does not track copy limits or the "KOPIA" marking.
8. **Go live.** Live credentials and a new base URL are issued after certification. Keep host, key and secret in configuration so the switch is a config change.
9. **Roll out per merchant.** For each merchant: onboard, create the store, register the terminals, create the store's registers, hand the merchant each register's control-unit details, wait for the Skatteverket declarations, then open the first shift from a terminal.

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create cash register | POST | `/merchants/:merchantId/stores/:storeId/cash-register` |
| Fetch cash registers | GET | `/merchants/:merchantId/cash-register` |
| Get cash register by ID | GET | `/merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId` |
| Get cash register state | GET | `/merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/state-info` |
| Open shift | PUT | `/merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/open-shift` |
| Close shift | PUT | `/merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/close-shift` |
| End of day | PUT | `/merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/end-of-day` |
| Deposit petty cash | POST | `/merchants/:merchantId/stores/:storeId/cash-register/deposit-petty-cash` |
| Withdraw cash | PUT | `/merchants/:merchantId/stores/:storeId/cash-register/withdraw` |
| Update scheduled close time | PATCH | `/merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/closing-time` |
| Get cash register files | GET | `/merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId/files` |
| Delete cash register | DELETE | `/merchants/:merchantId/stores/:storeId/cash-register/:cashRegisterId` |
| Create order on the register | POST | `/orders` with `controlFunctions.ecrEnabled` |
| Add receipt information | PUT | `/orders/:orderId/receipt` |

## Reference

- [Electronic Cash Register (ECR) V2 API](https://developers.surfboardpayments.com/api/electronic-cash-register)
- [Orders API](https://developers.surfboardpayments.com/api/orders)
- [Receipts API](https://developers.surfboardpayments.com/api/receipts)
- [Cash Registers (ECR) in the partner portal](/partner-portal/cash-registers)
- [Product Catalog](/developers/guides/product-catalog)
- [Create an Order](/developers/guides/create-an-order)
- [Receipts](/developers/guides/receipts)
- [Device Registration](/developers/guides/device-registration)
- [Merchant Onboarding](/developers/guides/merchant-onboarding)
- [Skatteverket: How to report a cash register](https://www.skatteverket.se/servicelankar/otherlanguages/englishengelska/businessesandemployers/startingandrunningaswedishbusiness/cashregisters/howtoreportacashregister.4.57cadbbd15a3688ff44de5a.html)

---

# 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/)

---

# 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/)

---

# Partial Refund

Refund specific items or a reduced amount from a completed order. Process partial returns by creating a return order with only the items to be refunded.

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

---
## Overview

A partial refund returns a portion of the order amount to the customer. Like a full refund, it works by creating a **new order** with negative quantities -- but only for the specific items being returned.

## When to Use Partial Refund

| Scenario | Description |
|----------|-------------|
| **Single item return** | Customer returns one item from a multi-item order |
| **Partial quantity** | Customer returns 1 of 3 identical items |
| **Price adjustment** | Discount applied after purchase |
| **Damaged goods** | Partial compensation for a defective item |

## Step 1: Create a Partial Refund Order

Include only the line items being refunded, with negative `quantity` and negative `amount.total`. Reference the original order's `orderId` as `purchaseOrderId`:

```json
POST /orders
{
  "terminal$id": "YOUR_TERMINAL_ID",
  "referenceId": "partial-refund-001",
  "orderLines": [
    {
      "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": -20000,
    "total": -20000,
    "currency": "752",
    "tax": [
      { "amount": 4000, "percentage": 25, "type": "VAT" }
    ]
  },
  "controlFunctions": {
    "initiatePaymentsOptions": {
      "paymentMethod": "CARD_NP",
      "refundProcessingParams": {
        "purchasePaymentId": "ORIGINAL_PAYMENT_ID",
        "refundReason": "CUSTOMER_INITIATED_RETURN"
      }
    }
  }
}
```

```json
// Response
{
  "status": "SUCCESS",
  "data": {
    "orderId": "83c4db56990b428c0b",
    "paymentId": "83c4db5674ce610706"
  },
  "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.

## 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:** All payment methods except NSWISH, SVIPPS, and SMOBILEPAY support partial 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 partial refund completed:

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

Track refund status via the API response or through [webhooks](/developers/guides/webhooks-notifications).

## Partial Refund via Partner Portal

1. Log in to **Partner Portal** > **Merchants** > select merchant > **Transactions**
2. Select the transaction to refund
3. Click **Create Refund** > **Partial Refund**
4. Choose **Select Line Items** or **Enter Custom Amount**
5. **Process Refund** with a refund reason

## Multiple Partial Refunds

You can issue multiple partial refunds against the same original order. Each refund creates a separate return order referencing the same `purchaseOrderId`.

When the original order included adjustments (tips, discounts), the first partial refund includes adjustments by default. Subsequent partial refunds do not. Override this with `includeAdjustmentsForRefund` in `controlFunctions`.

## Reference

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

---

# Gift Cards & Promotions

Issue and manage gift cards, track transactions, and create marketing promotions using the Surfboard Payments APIs.

Source: https://www.surfboardpayments.com/developers/guides/gift-cards-promotions
Category: online
Tags: Online, API, Gift Cards, Promotions, Commerce

---
## Overview

Surfboard Payments provides APIs for two complementary commerce features: **gift cards** for stored-value and entitlement-based programs, and **promotions** for marketing campaigns displayed across merchant channels. This guide covers creating and managing both, with full API details and request/response examples.

## Gift Cards

Gift cards in Surfboard come in two types:

- **FUND** -- A stored monetary balance. Customers spend down the balance over one or more transactions.
- **ENTITLEMENT** -- A usage-limited card. Instead of a cash value, the card grants a fixed number of redemptions (e.g., "5 free coffees").

### Create a Gift Card

```
POST /gift-cards
```

#### FUND Type Request

```json
{
  "cardType": "FUND",
  "amount": 500,
  "currency": "SEK",
  "name": "Holiday Gift Card",
  "accessControl": "OPEN",
  "expiryDate": "12/31/2026",
  "note": "Happy Holidays!"
}
```

#### ENTITLEMENT Type Request

```json
{
  "cardType": "ENTITLEMENT",
  "redemptionLimit": 10,
  "name": "Loyalty Reward Card",
  "accessControl": "OPEN",
  "expiryDate": "06/30/2027",
  "note": "Thank you for being a valued customer"
}
```

#### Request Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `cardType` | string | Yes | `FUND` or `ENTITLEMENT` |
| `amount` | number | Conditional | Monetary amount in smallest currency unit. Required for `FUND` type |
| `redemptionLimit` | number | Conditional | Number of allowed uses. Required for `ENTITLEMENT` type |
| `currency` | string | No | ISO currency code (e.g., `SEK`, `EUR`) |
| `name` | string | No | Display name for the gift card |
| `accessControl` | string | No | Access control level (e.g., `OPEN`) |
| `expiryDate` | string | No | Expiry date in `mm/dd/yyyy` or `mm-dd-yyyy` format |
| `note` | string | No | Optional note or message |

#### Response

```json
{
  "status": "SUCCESS",
  "data": {
    "giftCardId": "gc-abc-123",
    "pan": "6789012345678901",
    "name": "Holiday Gift Card",
    "cardType": "FUND",
    "amount": 500,
    "currency": "SEK",
    "accessControl": "OPEN",
    "status": "ACTIVE",
    "expiryDate": "12/31/2026",
    "shareableLink": "https://giftcards.surfboardpayments.com/gc-abc-123",
    "formats": {
      "qrCode": "data:image/png;base64,...",
      "nfcData": "NFC_ENCODED_DATA",
      "barcode": "data:image/png;base64,..."
    },
    "externalId": "ext-001",
    "externalIdType": "CUSTOM"
  },
  "message": "Gift card created successfully"
}
```

The response includes multiple format representations (QR code, NFC data, barcode) for flexible distribution. The `shareableLink` provides a URL that can be sent directly to the recipient.

### List All Gift Cards

Retrieve a paginated list of all gift cards for a merchant, with optional filtering.

```
GET /gift-cards
```

#### Query Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `type` | string | No | Filter by card type: `FUND` or `ENTITLEMENT` |
| `status` | string | No | Filter by card status |

#### Response

```json
{
  "status": "SUCCESS",
  "data": [
    {
      "giftCardId": "gc-abc-123",
      "pan": "6789012345678901",
      "name": "Holiday Gift Card",
      "cardType": "FUND",
      "amount": 500,
      "currentAmount": 350,
      "usageCount": 2,
      "currency": "SEK",
      "accessControl": "OPEN",
      "status": "ACTIVE",
      "expiryDate": "12/31/2026",
      "lastTransactionAt": "2026-01-15T14:30:00Z",
      "transactionCount": 2,
      "totalRedeemed": 150
    }
  ],
  "message": "Gift cards fetched successfully"
}
```

Note the tracking fields: `currentAmount` shows the remaining balance for FUND cards, `usageCount` tracks how many times the card has been used, and `totalRedeemed` shows the cumulative amount spent.

### Get Gift Card Details

Retrieve full details for a single gift card, including customer information and format representations.

```
GET /gift-cards/:id
```

#### Response

```json
{
  "status": "SUCCESS",
  "data": {
    "giftCardId": "gc-abc-123",
    "pan": "6789012345678901",
    "name": "Holiday Gift Card",
    "cardType": "FUND",
    "amount": 500,
    "currentAmount": 350,
    "usageCount": 2,
    "currency": "SEK",
    "status": "ACTIVE",
    "expiryDate": "12/31/2026",
    "lastTransactionAt": "2026-01-15T14:30:00Z",
    "transactionCount": 2,
    "totalRedeemed": 150,
    "customerDetails": {
      "customerId": "cust-456",
      "firstName": "Anna",
      "surname": "Svensson",
      "countryCode": "SE",
      "emails": [{ "email": "anna@example.com" }],
      "phoneNumbers": [
        {
          "phoneNumber": {
            "countryCode": "46",
            "number": "701234567"
          }
        }
      ]
    },
    "shareableLink": "https://giftcards.surfboardpayments.com/gc-abc-123",
    "formats": {
      "qrCode": "data:image/png;base64,...",
      "nfcData": "NFC_ENCODED_DATA",
      "barcode": "data:image/png;base64,..."
    }
  },
  "message": "Gift card details fetched successfully"
}
```

### Get Gift Card Transactions

View the transaction history for a specific gift card. Supports filtering by transaction type and pagination via the `x-page-number` header.

```
GET /gift-cards/:giftCardId/transactions
```

#### Query Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `transactionType` | string | No | Filter by type: `ISSUED`, `CREDIT`, or `DEBIT` |

#### Response

```json
{
  "status": "SUCCESS",
  "data": [
    {
      "paymentId": "pay-789",
      "transactionType": "DEBIT",
      "transactionAmount": 150,
      "currency": "SEK",
      "valueBefore": 500,
      "valueAfter": 350,
      "orderId": "order-456",
      "merchantId": "merchant-xyz-789",
      "storeId": "store-abc-123",
      "metadata": {}
    },
    {
      "paymentId": "pay-001",
      "transactionType": "ISSUED",
      "transactionAmount": 500,
      "currency": "SEK",
      "valueBefore": 0,
      "valueAfter": 500,
      "merchantId": "merchant-xyz-789",
      "metadata": {}
    }
  ],
  "message": "Transactions fetched successfully"
}
```

Each transaction record shows the `valueBefore` and `valueAfter` fields, giving a clear audit trail of the gift card balance over time.

## Promotions

Promotions let you create and manage marketing campaigns that appear across merchant channels, such as on payment terminals, receipts, and idle screens. Each promotion is scoped to a specific merchant and store.

### Create a Promotion

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

#### Request

```json
{
  "title": "Summer Sale",
  "name": "summer-sale-2026",
  "description": "50% off all summer items",
  "assetUrl": "https://cdn.example.com/promo-summer.png",
  "type": "RECEIPT_BIG",
  "assetOpacity": "0.8",
  "backgroundColor": "#1e3a5f",
  "contentTextColor": "#ffffff",
  "endProductUrl": "https://shop.example.com/summer",
  "endProduct": "SUMMER-COLLECTION",
  "buttonLabel": "Shop Now",
  "priority": 1,
  "startDate": "06-01-2026",
  "endDate": "08-31-2026"
}
```

#### Request Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Unique name for the promotion |
| `type` | string | Yes | Promotion type (e.g., `RECEIPT_BIG`, `RECEIPT_SMALL`) |
| `priority` | number | Yes | Display priority. Lower numbers = higher priority |
| `startDate` | string | Yes | Start date in `MM-DD-YYYY` format |
| `endDate` | string | Yes | End date in `MM-DD-YYYY` format |
| `title` | string | No | Display title for the promotion |
| `description` | string | No | Brief description of the promotion |
| `assetUrl` | string | No | URL of the promotional image |
| `assetOpacity` | string | No | Image opacity, `0` (transparent) to `1` (opaque) |
| `backgroundColor` | string | No | Background color in hex format |
| `contentTextColor` | string | No | Text color in hex format |
| `endProductUrl` | string | No | URL of the promoted product |
| `endProduct` | string | No | Product ID linked to the promotion |
| `buttonLabel` | string | No | Label for the call-to-action button |

#### Response

```json
{
  "status": "SUCCESS",
  "data": {
    "promotionId": "promo-abc-456"
  },
  "message": "Promotion created successfully"
}
```

### List All Promotions

Retrieve all promotions for a merchant's store to view, manage, and track active and past campaigns.

```
GET /merchants/:merchantId/stores/:storeId/promotions
```

#### Response

```json
{
  "status": "SUCCESS",
  "data": [
    {
      "promotionId": "promo-abc-456",
      "merchantId": "merchant-xyz-789",
      "storeId": "store-abc-123",
      "name": "summer-sale-2026",
      "title": "Summer Sale",
      "description": "50% off all summer items",
      "assetUrl": "https://cdn.example.com/promo-summer.png",
      "endProduct": "SUMMER-COLLECTION",
      "buttonLabel": "Shop Now",
      "startDate": "2026-06-01T00:00:00Z",
      "endDate": "2026-08-31T00:00:00Z",
      "priority": "1",
      "assetOpacity": "0.8",
      "backgroundColor": "#1e3a5f",
      "contentTextColor": "#ffffff",
      "endProductUrl": "https://shop.example.com/summer"
    }
  ],
  "message": "Promotions fetched successfully"
}
```

### Get Promotion by ID

Retrieve the full configuration and current state of a single promotion.

```
GET /merchants/:merchantId/stores/:storeId/promotions/:promotionId
```

The response structure is identical to a single item in the list response above.

### Update a Promotion

Modify any attributes of an existing promotion. Send only the fields you want to change.

```
PUT /merchants/:merchantId/stores/:storeId/promotions/:promotionId
```

#### Request

```json
{
  "description": "Up to 60% off all summer items - extended!",
  "endDate": "09-30-2026",
  "priority": 1
}
```

All fields are optional. The response confirms the update:

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

### Delete a Promotion

Permanently remove a promotion and its associated data.

```
DELETE /merchants/:merchantId/stores/:storeId/promotions/:promotionId
```

#### Response

```json
{
  "status": "SUCCESS",
  "message": "Promotion deleted successfully"
}
```

> **Warning:** Deletion is permanent. The promotion will no longer be active or visible on any channel.

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create gift card | POST | `/gift-cards` |
| List all gift cards | GET | `/gift-cards` |
| Get gift card details | GET | `/gift-cards/:id` |
| Get gift card transactions | GET | `/gift-cards/:giftCardId/transactions` |
| Create promotion | POST | `/merchants/:merchantId/stores/:storeId/promotions` |
| List all promotions | GET | `/merchants/:merchantId/stores/:storeId/promotions` |
| Get promotion by ID | GET | `/merchants/:merchantId/stores/:storeId/promotions/:promotionId` |
| Update promotion | PUT | `/merchants/:merchantId/stores/:storeId/promotions/:promotionId` |
| Delete promotion | DELETE | `/merchants/:merchantId/stores/:storeId/promotions/:promotionId` |

---

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

---

# Receipt Printing

Two ways to print a receipt on a terminal with a built-in printer or on the FinPrinter: let Surfboard render it from a template, or build the layout yourself and send ESC/POS. How to choose, and how to set up each.

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

---
## Overview

Surfboard prints on two kinds of device: [terminals with a built-in printer](/products/?requirements=printer), such as SurfPrint Pro, and the [FinPrinter](/products/fins/finprinter), the standalone cloud printer for a counter, a pickup point or a kitchen. Whichever device you print on, there are two ways to get a receipt onto paper, and you pick whichever fits your POS better.

| | Printing templates | ESC/POS |
|---|---|---|
| Who renders the receipt | Surfboard | You |
| What you send | An ID: `paymentId`, `transactionId` or `orderId` | The complete ESC/POS byte stream |
| Layout control | Add or remove pre-defined receipt elements | Full control, line by line |
| Where the layout lives | Partner Portal, at partner, merchant or store level | Your code |
| Requirement | `orderLines` on the order | UTF-8 text inside the [ESC/POS contract](/developers/guides/escpos-printing) |
| Fonts | Handled by the template | One font, with bold and size variations |
| Maintenance | Surfboard | You |
| Prints more than receipts | No | Yes: kitchen slips, bongs, pickup tickets |

If you already send `orderLines` on every order and want a receipt that looks right without owning its layout, use templates. If you need a layout Surfboard does not offer, or you want to print things that are not receipts at all, use ESC/POS. The two are not exclusive: many integrations print the customer receipt from a template and a kitchen slip from ESC/POS on the same device.

> **Note:** This guide is about paper. For email receipts, hosted receipt links and the fiscal receipt fields, see the [Receipts](/developers/guides/receipts) guide.

## Prerequisites

- A Surfboard developer account with valid API credentials (`API-KEY` and `API-SECRET`)
- A [terminal with a built-in printer](/products/?requirements=printer) or a [FinPrinter](/products/fins/finprinter), registered to a merchant and store, and its `terminalId`
- For templates: orders created with `orderLines`, and access to the Partner Portal

## The printers

Both kinds of device are registered the same way and get a `terminalId`, so the print endpoints do not care which one they are talking to. The device must be online for a job to print; a `SUCCESS` response means the job was dispatched, not that paper came out.

| Device | Printer | Paper | Normal-text columns | Typical placement |
|---|---|---|---|---|
| [Terminals with a built-in printer](/products/?requirements=printer) | Built in | 58 mm | 32 | Handheld: prints at the table, on the floor or at the counter |
| [FinPrinter](/products/fins/finprinter) | Standalone, cloud-connected | 80 mm | 48 | Counter, pickup point or kitchen, with no terminal beside it |

**Terminals with a built-in printer** print where the payment happens. The terminal that took the payment is the default target of the print call, so a receipt on the device the customer just tapped needs no `terminalId` at all. The current line-up is on the [products page](/products/?requirements=printer), filtered to devices with a printer.

**FinPrinter** has no screen and takes no payments. It connects over Wi-Fi or Ethernet, and you register it with the same device registration call as a terminal, using the serial number on the back of the unit as the registration identifier. See [Device Registration](/developers/guides/device-registration). Once registered it shows up as a device next to the merchant's terminals in the portals and the API, and both print endpoints in this guide accept its `terminalId`. Because every job routes through Surfboard's cloud, your POS needs no driver and no cable to the printer.

The column counts matter only on the ESC/POS path, where you format every line yourself. Templates adapt to the paper width on their own.

## Path 1: Printing templates

On this path you never render a receipt. You describe what the receipt should contain by choosing from Surfboard's pre-defined receipt fields, Surfboard lays it out, and at print time you send an ID. Surfboard maps that ID to the order, fills in the template and prints.

### Set up the template

Templates are configured in the Partner Portal. A template is a list of the receipt elements you want, built from the pre-defined fields: merchant details, the order lines with quantities and prices, VAT breakdown, the payment details from the card scheme, footer text and so on. Add and remove elements to fit what you and the merchant need.

A template can be set at three levels:

| Level | Use it for |
|---|---|
| Partner | The default for every merchant you board |
| Merchant | A merchant whose receipt should differ from your default |
| Store | One location that needs its own variant, for example a different footer or fiscal line |

The most specific level wins, so the usual setup is one partner-level default and overrides only where a merchant or store asks for one. If you would rather set the merchant default from code, the merchant configuration endpoint takes a `terminalReceiptTemplateId`:

```
PATCH /merchants/{merchantId}/configuration
```

```json
{
  "terminalReceiptTemplateId": "<template id from the Partner Portal>"
}
```

### Send order lines

The one requirement on this path is that the receipt data comes from the order, so `orderLines` must be present on the [create order](/developers/guides/create-an-order) call. An order created without lines has nothing for the template to render. If your POS already sends lines, which it does if the terminal shows a basket, there is no extra work beyond setting up the template.

The fiscal fields that Nordic regulators expect on a receipt, such as the register name and control unit serial, are stored against the order separately with `PUT /receipts/{orderId}`. That call is covered in [Receipts](/developers/guides/receipts), and the template picks the fields up when they are present.

### Print

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

The `{id}` is a `paymentId`, `transactionId` or `orderId`, whichever your POS has to hand. Surfboard resolves it to the order and its receipt data.

**Request body:**

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

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `terminalId` | string | No | The device to print on. Defaults to the terminal that processed the transaction. Set it to send the receipt to a FinPrinter at the counter, or to a different terminal than the one that took the payment. |
| `templateType` | string | No | `TRANSACTION_RECEIPT` (default) prints the customer receipt. `SALES_SUMMARY` prints a summary instead. |

**Response:**

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

Which template renders is decided by the configuration in the Partner Portal, resolved from the store up through the merchant to the partner. Nothing in the print call names a template, which is what lets you change a merchant's receipt without touching the POS.

> **Note:** The older `PUT /receipts/{id}/print` form, which took a `templateId` and a `language` in the body, still works but is superseded by the `POST` above. New integrations should use `POST`.

For a register built on Surfboard, the order itself can carry `autoPrintReceipt` under `controlFunctions.receipt`, so the receipt prints when the payment completes with no print call at all. See [Build Your Own ECR](/developers/guides/electronic-cash-register).

## Path 2: ESC/POS

On this path you build the whole receipt: every line, every column, every bold header. You send the finished ESC/POS byte stream to a specific printer, and Surfboard validates it and passes it through. This is the path when you already have a receipt renderer in your POS, when the templates cannot express the layout you need, or when the thing you are printing is not a receipt.

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

Note that the path takes a `terminalId`, not a payment or order ID. Nothing ties the job to an order, which is what makes it usable for kitchen slips and pickup tickets.

**Request body:**

```json
{
  "escposCommands": "<base64 ESC/POS>",
  "codePages": "UTF-8"
}
```

Three things to know before you commit to this path:

- **One font.** The printers support a single typeface, with bold, a condensed variant and two larger sizes. If a design calls for anything else, it has to be sent as an image, and images are capped at 384 dots wide.
- **The contract is enforced.** With `"codePages": "UTF-8"` set, the stream is validated before it reaches hardware. Text must be UTF-8, only a fixed set of commands is accepted, and an invalid stream is rejected whole with `PR_0006` rather than printed halfway. The full rules, a worked receipt and a preflight validator are in [ESC/POS Printing](/developers/guides/escpos-printing).
- **Formatting and maintenance sit with you.** Line widths differ by device and by text size, 32 columns on a 58 mm terminal against 48 on the FinPrinter, so a layout built for one printer needs checking on the other. When a merchant wants a different footer, that is a change in your code, not in a portal.

The trade is the same one a template makes in the other direction: you own the layout, so you own the work of keeping it right.

## Choosing, in practice

- **A POS that already sends `orderLines` and has no receipt renderer.** Templates. Set one at partner level and you are done.
- **A POS with its own receipt engine, migrating to Surfboard.** ESC/POS, which lets you keep the receipts your merchants already know. Read the contract first; generic ESC/POS libraries often emit raster images and charset commands it rejects.
- **Customer receipt plus kitchen slip.** Both. Templates for the receipt on the terminal that took the payment, ESC/POS for the bong on a FinPrinter in the kitchen.
- **A receipt that must carry something the template fields do not have.** Check the available elements in the Partner Portal first; if the field is not there, ESC/POS.

## Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| `SUCCESS` but nothing printed | Device offline, or busy with another command | Check the device in the Merchant Portal and retry |
| Template receipt is empty or missing lines | Order was created without `orderLines` | Send lines on the create order call; they cannot be added after the fact |
| Receipt prints on the wrong device | `terminalId` omitted, so it defaulted to the terminal that took the payment | Pass the `terminalId` of the printer you want |
| Template changes do not show | A more specific level overrides yours | Check for a merchant or store template overriding the partner default |
| `PR_0006` on the ESC/POS path | A command outside the contract, or non-UTF-8 text | The message names the byte offset. See the [ESC/POS Printing](/developers/guides/escpos-printing) error table |

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Print from a template | POST | `/receipts/{id}/print` |
| Print a custom ESC/POS job | PUT | `/receipts/{terminalId}/escpos` |
| Set a merchant's default template | PATCH | `/merchants/{merchantId}/configuration` |
| Register a FinPrinter | POST | `/merchants/{merchantId}/stores/{storeId}/devices` |

For the full endpoint reference, see the [Receipts API](https://developers.surfboardpayments.com/api/receipts) documentation. For the ESC/POS contract itself, see [ESC/POS Printing](/developers/guides/escpos-printing); for email, links and fiscal receipt data, see [Receipts](/developers/guides/receipts).

---

# ESC/POS Printing

Send custom ESC/POS receipts to any Surfboard terminal using the UTF-8 contract: opt in with one field, get validated payloads and consistent output across current and legacy hardware.

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

---
## Overview

Surfboard is standardising ESC/POS printing across every payment terminal. You opt in by adding a single field to the print request you already send. In return, your payload is validated before it reaches hardware -- so you get a clear API error instead of a garbled receipt -- and the output is adapted automatically to every terminal model, current and legacy.

The opt-in is deliberately small. The work is in the rules the contract enforces, which this guide covers: UTF-8 text, a fixed set of supported commands, and line widths that depend on the text size you select.

> **Note:** This guide covers the ESC/POS contract specifically. If you have not yet decided whether to render receipts yourself or let Surfboard print them from a template, start with [Receipt Printing](/developers/guides/receipt-printing), which compares the two paths and covers the printers, including the FinPrinter. For email, hosted links and fiscal receipt data, see the [Receipts](/developers/guides/receipts) guide.

## Prerequisites

- A Surfboard developer account with valid API credentials (`API-KEY` and `API-SECRET`)
- A registered [terminal with a built-in printer](/products/?requirements=printer) or a [FinPrinter](/products/fins/finprinter)
- The terminal's `terminalId`

## Opting in

Add `codePages` to your existing ESC/POS request:

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

**Request body:**

```json
{
  "escposCommands": "<base64 ESC/POS>",
  "codePages": "UTF-8"
}
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `escposCommands` | string | Yes | Your ESC/POS byte stream, Base64-encoded. |
| `codePages` | string | No | `UTF-8` is the only accepted value. Omitting it keeps the legacy flow. |

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

Three things follow from opting in:

- **Validation is atomic.** An invalid stream is rejected whole with `PR_0006`, quoting the byte offset and the offending command. Nothing partial ever prints.
- **Request bodies cap at 75 KB**, which is roughly 55 KB of decoded ESC/POS.
- **The legacy flow is deprecated** and will be removed once migration completes. Omitting `codePages` keeps you on it for now.

## Character encoding

All text must be UTF-8. Do not send Latin-1 or CP1252 bytes, and do not send any codepage or charset selection command -- the platform manages character encoding per device, which is precisely what makes one payload work across mixed hardware.

The quickest way to confirm your encoding is to check a single character. `ä` must be two bytes:

```
ä  =  C3 A4     ✅ UTF-8
ä  =  E4        ❌ Latin-1, will be rejected
```

For reference, the Nordic characters most likely to appear on a receipt:

| Lowercase | UTF-8 bytes | Uppercase | UTF-8 bytes |
|---|---|---|---|
| `ä` | `C3 A4` | `Ä` | `C3 84` |
| `å` | `C3 A5` | `Å` | `C3 85` |
| `ö` | `C3 B6` | `Ö` | `C3 96` |

If you are migrating from a charset-based integration, translate national characters to their real UTF-8 form rather than relying on the old substitutions -- `{` meaning `ä`, `}` meaning `å`, and so on. Those substitutions depended on a charset command that is no longer accepted.

## Line widths

Line width depends on the terminal's paper and on the text size active at that point in the stream. Count characters, not bytes: `Örsundsbro väg 1` is 16 characters even though it is 18 bytes.

| Context | Payment terminals (58mm) | Printer terminals (80mm) |
|---|---|---|
| Normal text (Font A) | **32** | **48** |
| Fine print (Font B) | **42** | -- |
| Big text (2×) | **16** | **24** |
| Biggest text (3×) | **10** | **16** |

Lines longer than the budget wrap mid-word, which is almost never what you want on a receipt. Because the fonts are monospace, align columns by padding with spaces rather than tabs.

Images are capped at **384 dots wide on every device**, regardless of paper width.

## Supported commands

| Purpose | Command | Bytes (hex) | Notes |
|---|---|---|---|
| Initialise | `ESC @` | `1B 40` | Required as the first bytes of every job |
| Line feed | `LF` | `0A` | Ends and prints a line |
| Feed n lines | `ESC d n` | `1B 64 n` | Use `1B 64 04` at the end so the receipt clears the tear bar |
| Paper cut | `GS V 0` | `1D 56 00` | Cuts on 80mm printer terminals; safely ignored on handheld terminals, which have no cutter. `1D 56 42 n` (feed-then-cut) also accepted |
| Bold on/off | `ESC ! 08` / `ESC ! 00` | `1B 21 08` / `1B 21 00` | `ESC E 1` / `ESC E 0` (`1B 45 n`) also accepted. One bold level only |
| Fine print (Font B) | `ESC ! 01` / `ESC ! 00` | `1B 21 01` / `1B 21 00` | `ESC M n` (`1B 4D n`) also accepted |
| Bold + Font B | `ESC ! 09` | `1B 21 09` | `ESC !` bits: `0x01` Font B, `0x08` bold |
| Text size | `GS ! n` | `1D 21 00` / `1D 21 11` / `1D 21 22` | Normal / Big (2×) / Biggest (3×) |
| Underline on/off | `ESC - 1` / `ESC - 0` | `1B 2D n` | Renders on current terminals; legacy prints the text without the underline. Send *after* any `ESC !` on the same line |
| Alignment | `ESC a n` | `1B 61 00/01/02` | Left / centre / right. Text only -- images always print left-aligned |
| Line spacing | `ESC 3 24` / `ESC 2` | `1B 33 18` / `1B 32` | For wrapping image stripes only |
| QR code | `GS ( k` | see below | The only QR method |
| Image / logo | `ESC *` mode 33 | see below | The only image format |

Two rules are worth stating separately, because they are the ones existing integrations most often break:

- **Size is set only via `GS !`**, never via the size bits of `ESC !`. Nothing above `GS ! 22` is accepted.
- **Anything not in the table above is rejected.** That includes `GS v 0` (raster images), `ESC t` / `ESC u` / `ESC R` (charset selection), `GS B` (reverse print), and any unknown command. These can corrupt or damage terminals, so the API blocks them before they reach hardware.

## QR codes

Send the data as a string and let the terminal render it. Five commands, in order:

```
1D 28 6B 04 00 31 41 32 00              # model 2
1D 28 6B 03 00 31 43 06                 # module size: 6 dots (1-16)
1D 28 6B 03 00 31 45 31                 # error correction: 30=L 31=M 32=Q 33=H
1D 28 6B pL pH 31 50 30 <data bytes>    # store data; pL + pH*256 = len(data) + 3
1D 28 6B 03 00 31 51 30                 # print
```

The only part that varies is the fourth line's length prefix. For 20 bytes of data, `pL + pH*256` is 23, so `pL = 17` (hex) and `pH = 00`.

## Images and logos

Monochrome only, maximum 384 dots wide. Encode the image as 24-dot-tall horizontal stripes in column format:

```
1B 33 18                                # line spacing = 24 dots, so stripes butt together
for each 24-row stripe:
  1B 2A 21 nL nH <data>                 # nL + nH*256 = width in dots; data = width × 3 bytes
  0A                                    # LF after each stripe
1B 32                                   # restore default line spacing
```

In column format each of the `width` columns contributes 3 bytes, making 24 vertical dots, with the most significant bit at the top. Pixel `(x, y)` within a stripe lives in byte `x*3 + y/8` at bit `7 - y%8`.

Images always print left-aligned, so `ESC a` will not centre them. To centre one, prepend blank columns: `pad = (384 - width) / 2`.

## A complete receipt

This builds a 259-byte receipt with real Swedish characters, correct column alignment, and a fine-print tax line. Every line is inside its budget for the size active on it.

```python
ESC, GS = 0x1B, 0x1D
out = bytearray()
raw = out.extend
txt = lambda s: out.extend(s.encode("utf-8"))

raw(bytes([ESC, 0x40]))              # init
raw(bytes([ESC, 0x61, 0x01]))        # centre
raw(bytes([GS, 0x21, 0x11]))         # big (2x)
raw(bytes([ESC, 0x21, 0x08]))        # bold
txt("Kaffebaren\n")                  # 10 chars, budget 16 at 2x
raw(bytes([GS, 0x21, 0x00]))         # size back to normal
raw(bytes([ESC, 0x21, 0x00]))        # style off
txt("Örsundsbro väg 1\n")            # 16 chars, budget 32
raw(bytes([ESC, 0x61, 0x00]))        # left
txt("-" * 32 + "\n")
txt(f"{'Bryggkaffe':<26}{'29,00':>6}\n")
txt(f"{'Kanelbulle':<26}{'35,00':>6}\n")
txt("-" * 32 + "\n")
raw(bytes([ESC, 0x21, 0x08]))        # bold
txt(f"{'Totalt':<22}{'64,00 SEK':>10}\n")
raw(bytes([ESC, 0x21, 0x00]))
raw(bytes([ESC, 0x21, 0x01]))        # fine print (Font B, 42 cols)
txt("Moms 12% ingår med 6,86 SEK\n")
raw(bytes([ESC, 0x21, 0x00]))
raw(bytes([ESC, 0x64, 0x04]))        # feed out past the tear bar

import base64
print(base64.b64encode(bytes(out)).decode())
```

Which prints as (the first line is double-width, so its ten characters occupy twenty of the thirty-two columns):

```
      Kaffebaren
        Örsundsbro väg 1
--------------------------------
Bryggkaffe                 29,00
Kanelbulle                 35,00
--------------------------------
Totalt                 64,00 SEK
Moms 12% ingår med 6,86 SEK
```

And sends as:

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

**Response:**

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

## Validating before you send

`PR_0006` tells you exactly what is wrong, but catching it in your own test suite is faster than catching it against a terminal. This preflight checks the three things that actually break integrations -- an out-of-contract command, non-UTF-8 text, and an over-long line:

```python
ESC, GS, LF = 0x1B, 0x1D, 0x0A
WIDTH = {0x00: 32, 0x11: 16, 0x22: 10}   # GS ! n -> chars per line, 58mm Font A
SKIP  = {0x40: 2, 0x64: 3, 0x61: 3, 0x21: 3, 0x45: 3, 0x4D: 3, 0x2D: 3, 0x33: 3, 0x32: 2}

def preflight(data):
    errs, i, size, font_b, line, start = [], 0, 0x00, False, bytearray(), 0
    if data[:2] != bytes([ESC, 0x40]):
        errs.append("stream must begin with ESC @ (1B 40)")
    while i < len(data):
        b = data[i]
        if b == LF:
            if line:
                try:
                    s = line.decode("utf-8")
                    budget = 42 if (font_b and size == 0x00) else WIDTH[size]
                    if len(s) > budget:
                        errs.append(f"byte {start}: {len(s)} chars exceeds {budget}: {s!r}")
                except UnicodeDecodeError:
                    errs.append(f"byte {start}: line is not valid UTF-8")
            line, i = bytearray(), i + 1
            continue
        if b == ESC and data[i+1] in SKIP:
            n = data[i+1]
            if n == 0x21:
                if data[i+2] & ~0x09:
                    errs.append(f"byte {i}: ESC ! sets size bits; use GS ! for size")
                font_b = bool(data[i+2] & 0x01)
            i += SKIP[n]
            continue
        if b == GS and data[i+1] == 0x21:
            if data[i+2] not in WIDTH:
                errs.append(f"byte {i}: GS ! {data[i+2]:02X} must be 00, 11 or 22")
            else:
                size = data[i+2]
            i += 3
            continue
        if b in (ESC, GS):
            errs.append(f"byte {i}: unsupported command {b:02X} {data[i+1]:02X}")
            i += 2
            continue
        if not line:
            start = i
        line.append(b)
        i += 1
    return errs
```

Handle `ESC *` images, `GS ( k` QR blocks, and `GS V` cuts before this runs, or extend it to skip over them -- their payloads contain arbitrary bytes that would otherwise be read as text.

## Errors

| Code | Meaning | Action |
|---|---|---|
| `PR_0006` | Out-of-contract command. The message names the command, its byte offset, and what to use instead: `unsupported ESC/POS command 1D 76 at byte offset 214` | Replace the command. `1D 76` is raster imaging -- use `ESC *` mode 33 |
| `codePages must be 'UTF-8' when provided` | Wrong opt-in value | Send exactly `"UTF-8"` |

> **Note:** Because validation is atomic, a `PR_0006` means nothing printed at all. There is no half-receipt to clear from the printer.

## Migration checklist

- [ ] Add `"codePages": "UTF-8"` to your print requests
- [ ] Encode all text as UTF-8 -- test that `ä` is two bytes (`C3 A4`), not one (`E4`)
- [ ] Remove charset commands (`ESC t`, `ESC u`, `ESC R`) and translate national characters to real UTF-8
- [ ] Replace `GS v 0` raster images with `ESC *` mode 33 stripes
- [ ] Keep `GS V 0` and `GS V 66 n` cuts; remove any other cut variant (`ESC i`, `ESC m`)
- [ ] Set size only with `GS ! 00`, `11`, or `22`, and respect the width for each
- [ ] Format normal-text lines to 32 characters
- [ ] End every job with `ESC d 4` so the receipt clears the tear bar

> **Tip:** Generic ESC/POS libraries are a common source of rejections, because their defaults often emit raster images and charset commands. Check what your library actually produces before assuming it is in contract -- the preflight above will tell you.

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Print custom ESC/POS receipt | PUT | `/receipts/{terminalId}/escpos` |

For the full endpoint reference, see the [Receipts API](https://developers.surfboardpayments.com/api/receipts) documentation. To print from a Surfboard template instead of rendering the receipt yourself, see [Receipt Printing](/developers/guides/receipt-printing). For the other receipt delivery methods, see the [Receipts](/developers/guides/receipts) guide.

---

# 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/)

---

# 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/)

---

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

---

# Terminal & Device Management

Manage payment terminals and devices via the Surfboard API. Register in-store and online terminals, configure settings, and handle device operations.

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

---
## Overview

Once merchants are onboarded and stores are created, the next step is registering and managing terminals. Surfboard supports both physical in-store devices and online payment terminals. This guide covers registration, configuration, and ongoing operations like moving or rebooting terminals.

## Terminal Types

| Type | Category | Description |
|------|----------|-------------|
| **EMV** | In-Store | Traditional card-present terminals (countertop, mobile POS, kiosk) |
| **SoftPOS** | In-Store | Tap-to-pay on Android smartphones or tablets |
| **PaymentPage** | Online | Surfboard-hosted checkout page with a payment link |
| **SelfHostedPage** | Online | Surfboard renders card fields on your own web page via the Online SDK |
| **iFrame** | Online | Embedded payment frame within your site |
| **MerchantInitiated** | Online | Server-to-server payments using stored card tokens (subscriptions, recurring) |

## Registering In-Store Terminals

Register physical terminals by providing the device's registration code and the store it belongs to.

```
POST /merchants/{merchantId}/stores/{storeId}/devices
```

```json
{
  "registrationIdentifier": "250901",
  "terminalName": "Checkout 1"
}
```

The `registrationIdentifier` is a 6-digit code displayed when you power on the terminal. For SurfPad and Printer devices, use the serial number printed on the back instead.

**Response:**

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

The `registrationStatus` will be either `REGISTERED` (new device) or `ALREADY_REGISTERED` (device was previously linked).

## Registering Online Terminals

Two online terminals arrive with the store. Creating an online store provisions a `PaymentPage` terminal, for payment links and hosted checkout, and a `MerchantInitiated` terminal, for backend charges against a stored token. Neither takes a registration call — fetch the store's terminals to get their IDs:

```
GET /merchants/{merchantId}/stores/{storeId}/terminals
```

The remaining modes, `SelfHostedPage` and `iFrame`, are registered per store with the mode named in the body:

```
POST /merchants/{merchantId}/stores/{storeId}/online-terminals
```

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

> **Note:** The default terminals exist from the moment the online store does, but no online terminal can take a payment until the store's domains are verified and the store is approved.

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "terminalId": "trm_xyz789",
    "publicKey": "pk_live_...",
    "registrationStatus": "REGISTERED"
  },
  "message": "Terminal registered successfully"
}
```

For `SelfHostedPage` terminals, the response includes a `publicKey` used to initialize the Online SDK on your checkout page.

## Terminal Configuration

Terminal settings follow a hierarchy: merchant-level defaults cascade down to store-level, which cascade down to terminal-level. Terminal-level settings always take precedence.

### Setting Terminal Config

```
PATCH /merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}
```

```json
{
  "wifiSsid": "StoreNetwork",
  "wifiPassword": "securepass",
  "preferredNetwork": "WIFI",
  "preferredRestartTime": "03:00",
  "language": "en",
  "showReceipt": true,
  "alwaysShowMinorUnits": 1
}
```

Key configuration options:

| Parameter | Description |
|-----------|-------------|
| `preferredRestartTime` | Scheduled restart in HH:MM format (default `02:00`). Terminals restart within a 1-hour window. |
| `preferredNetwork` | `WIFI` or `GSM` |
| `language` | ISO language code: `en`, `fi`, `da`, `se` |
| `autoSleep` | Sleep timeout in seconds (battery-powered devices only) |
| `showStatusBar` | Show/hide the status bar (SurfPad only) |
| `openPosOnReboot` | `enabled` or `disabled` -- auto-launch POS after restart |
| `enableRefundLock` | Require PIN for refunds (Android terminals only) |

### Fetching Terminal Config

```
GET /merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}/config
```

Returns all active configuration values for the terminal, including inherited settings from merchant and store levels.

## Device Operations

### Change Store

Move a terminal between stores under the same merchant. The terminal ID stays the same.

```
POST /terminals/change
```

```json
{
  "terminal$id": "trm_abc123",
  "storeId": "str_newstore456"
}
```

> Terminals can only be moved between stores belonging to the same merchant. To reassign across merchants, use the Move Terminal endpoint.

### Move Terminal (Cross-Merchant)

Partners who manage terminal inventory in bulk can reassign a hardware terminal to a different merchant entirely.

```
PUT /partners/{partnerId}/terminals/{terminalSerialNo}/move
```

```json
{
  "targetMerchantId": "mrc_target789"
}
```

This endpoint uses the terminal's serial number rather than its terminal ID.

### Reboot Terminal

Remotely restart a terminal for troubleshooting or to apply firmware updates.

```
POST /terminals/{terminalId}/reboot
```

```json
{}
```

**Response:**

```json
{
  "status": "SUCCESS",
  "message": "Reboot command published successfully"
}
```

A `SUCCESS` status means the reboot command was sent. It does not guarantee the terminal has rebooted -- the device must be connected to the network and not processing another command.

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Register in-store device | POST | `/merchants/{merchantId}/stores/{storeId}/devices` |
| Register online terminal | POST | `/merchants/{merchantId}/stores/{storeId}/online-terminals` |
| Set terminal config | PATCH | `/merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}` |
| Fetch terminal config | GET | `/merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}/config` |
| Change store | POST | `/terminals/change` |
| Move terminal | PUT | `/partners/{partnerId}/terminals/{terminalSerialNo}/move` |
| Reboot terminal | POST | `/terminals/{terminalId}/reboot` |

For full endpoint details, see the [Terminals API](https://developers.surfboardpayments.com/api/terminals) and [Stores API](https://developers.surfboardpayments.com/api/stores) reference documentation.

---

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