# SoftPOS only: Surfboard Payments integration path

Tap to Pay on iPhone and Android with no hardware: your app beside the CheckoutX app on the same device, then orders, refunds and receipts from your own app.

Source: https://www.surfboardpayments.com/developers/guides/paths/softpos
Guides: 13

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

Go with CheckoutX and the inter-app integration. CheckoutX is the certified payment application, so the PCI and Apple submissions and the compliance that follows sit with Surfboard, not with you, and your app is a normal app that hands off each payment with an app switch. The SDKs put Tap to Pay inside your own app instead, and that moves the submissions to PCI and Apple, the security reviews, and the ongoing compliance work onto your product.

- [CheckoutX SoftPOS](https://www.surfboardpayments.com/developers/guides/checkoutx-softpos): Pair the CheckoutX app with your own app on the same phone or tablet. No SDK, no card data in your app, nothing to certify.
- [Inter-App Integration](https://www.surfboardpayments.com/developers/guides/interapp-integration): The app-switch contract CheckoutX uses for registration, payment and NFC scanning, from a native app or a browser-based POS.

Also available, not on the recommended route:

- [Tap to Pay on iPhone SDK](https://www.surfboardpayments.com/developers/guides/tap-to-pay-iphone): The iOS SDK, if Tap to Pay has to live inside your own iPhone app and you are prepared to carry the Apple and PCI submissions yourself.
- [Android SoftPOS SDK](https://www.surfboardpayments.com/developers/guides/android-softpos-sdk): The Android SDK, on the same terms: your app becomes the payment application, and its compliance becomes yours.

### Payment flow

- [Create an Order](https://www.surfboardpayments.com/developers/guides/create-an-order): Orders with line items and VAT, with the payment initiated in the same call.
- [Payment Lifecycle](https://www.surfboardpayments.com/developers/guides/payment-lifecycle): The payment states your app has to handle.
- [Tips Configuration](https://www.surfboardpayments.com/developers/guides/tips-configuration): Tipping on the phone screen.
- [Refund an Order](https://www.surfboardpayments.com/developers/guides/refund-an-order): Refunds as return orders.
- [Receipts](https://www.surfboardpayments.com/developers/guides/receipts): Email the receipt or show a link as a QR code, since there is no printer.

### Merchants and operations

- [Merchant Onboarding](https://www.surfboardpayments.com/developers/guides/merchant-onboarding): Board merchants through the Partner API with a prefilled KYB link.
- [Store Management](https://www.surfboardpayments.com/developers/guides/store-management): Every phone is registered under a store.
- [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 for your merchants.

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

---

# CheckoutX SoftPOS

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

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

---
## Overview

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

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

## Two Ways to Accept Payments on Phones

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

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

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

## How It Works

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

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

## Setup

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

## The Configure Call

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

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

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

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

## Handling `PS_0025`, Terminal Not Connected

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

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

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

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

## Reference

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

---

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

---

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

---

# Tips Configuration

Configure tipping on Surfboard payment terminals at the merchant, store, or terminal level using a hierarchical override model.

Source: https://www.surfboardpayments.com/developers/guides/tips-configuration
Category: in-store
Tags: In-Store, API, Tips, Configuration, Terminal

---
## Overview

Surfboard Payments provides flexible tipping capabilities across all native Android payment terminals, including SurfTouch, SurfPad, SurfPrint, and SoftPOS. You can enable tips, define preset percentage options, allow custom amounts, and control how tip values are displayed to customers -- all through the API.

Tip settings follow a hierarchical model. Configuration set at a higher level acts as the default for everything below it, while lower-level settings override higher-level ones. This lets you define a baseline across your entire merchant account and then fine-tune individual stores or terminals as needed.

## Configuration Hierarchy

Settings cascade downward and lower levels always take precedence:

```
Partner (default)
  └── Merchant
        └── Store
              └── Terminal (highest priority)
```

**How the hierarchy works:**

- If a terminal has its own tip config, that config is used -- regardless of what is set at the store, merchant, or partner level.
- If a terminal has no config, the system checks the store level, then the merchant level, and finally falls back to the partner-level default.
- Each parameter is resolved independently. You can set `tipLevel1` at the merchant level and override only `tipLevel2` at a specific store.

## Configuration Parameters

All three levels (merchant, store, terminal) accept the same set of parameters:

| Parameter | Type | Description |
|-----------|------|-------------|
| `tipConfig` | string | Enable or disable tips. Values: `ENABLED`, `DISABLED`. |
| `tipLevel1` | number | First preset tip percentage shown to the customer (e.g., `10` for 10%). |
| `tipLevel2` | number | Second preset tip percentage (e.g., `20` for 20%). |
| `tipLevel3` | number | Third preset tip percentage (e.g., `30` for 30%). |
| `freeAmountEnabled` | boolean | When `true`, customers can enter a custom tip amount. |
| `defaultCustomAmount` | number | Pre-filled custom amount shown when `freeAmountEnabled` is `true`. |
| `displayCalculatedAmount` | string | Show the calculated tip in the local currency on screen. Values: `ENABLED`, `DISABLED`. |
| `tipDisplayFormat` | string | How tip options are presented. Values: `PERCENTAGE`, `AMOUNT`. |

> **Note:** All parameters are optional on every request. You can update a single field without resending the entire configuration. The system merges your changes with the existing config.

## Setting Merchant-Level Tips

Apply a tip configuration to all terminals registered under a merchant. This is the best starting point when you want a consistent tipping experience across every location.

```
PATCH /merchants/{merchantId}/tips
```

**Request body:**

```json
{
  "tipConfig": "ENABLED",
  "tipLevel1": 10,
  "tipLevel2": 15,
  "tipLevel3": 20,
  "freeAmountEnabled": true,
  "defaultCustomAmount": 50,
  "displayCalculatedAmount": "ENABLED",
  "tipDisplayFormat": "PERCENTAGE"
}
```

**Response:**

```json
{
  "status": "SUCCESS",
  "message": "Merchant tip configuration updated successfully"
}
```

### Fetching Merchant-Level Tips

Retrieve the current tip configuration for a merchant.

```
GET /merchants/{merchantId}/tips
```

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "tipConfig": "ENABLED",
    "tipLevel1": 10,
    "tipLevel2": 15,
    "tipLevel3": 20,
    "defaultCustomAmount": 50,
    "displayCalculatedAmount": "ENABLED",
    "tipDisplayFormat": "PERCENTAGE"
  },
  "message": "Merchant tip configuration fetched successfully"
}
```

## Setting Store-Level Tips

Override the merchant defaults for a specific store. Useful when different locations have different tipping norms -- for example, a restaurant store might offer higher preset percentages than a retail store under the same merchant.

```
PATCH /merchants/{merchantId}/stores/{storeId}/tips
```

**Request body:**

```json
{
  "tipConfig": "ENABLED",
  "tipLevel1": 15,
  "tipLevel2": 20,
  "tipLevel3": 25
}
```

**Response:**

```json
{
  "status": "SUCCESS",
  "message": "Store tip configuration updated successfully"
}
```

### Fetching Store-Level Tips

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

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "tipConfig": "ENABLED",
    "tipLevel1": 15,
    "tipLevel2": 20,
    "tipLevel3": 25,
    "defaultCustomAmount": 50,
    "displayCalculatedAmount": "ENABLED",
    "tipDisplayFormat": "PERCENTAGE"
  },
  "message": "Store tip configuration fetched successfully"
}
```

> **Note:** The response includes all effective values, including those inherited from the merchant level (such as `defaultCustomAmount` and `displayCalculatedAmount` in this example).

## Setting Terminal-Level Tips

Apply a tip configuration to a single terminal. Terminal-level settings have the highest priority and override everything above them.

```
PATCH /merchants/{merchantId}/terminals/{terminalId}/tips
```

**Request body:**

```json
{
  "tipConfig": "ENABLED",
  "tipLevel1": 5,
  "tipLevel2": 10,
  "tipLevel3": 15,
  "freeAmountEnabled": false,
  "tipDisplayFormat": "AMOUNT"
}
```

**Response:**

```json
{
  "status": "SUCCESS",
  "message": "Terminal tip configuration updated successfully"
}
```

### Fetching Terminal-Level Tips

```
GET /merchants/{merchantId}/terminals/{terminalId}/tips
```

**Response:**

```json
{
  "status": "SUCCESS",
  "data": {
    "tipConfig": "ENABLED",
    "tipLevel1": 5,
    "tipLevel2": 10,
    "tipLevel3": 15,
    "freeAmountEnabled": false,
    "displayCalculatedAmount": "ENABLED",
    "tipDisplayFormat": "AMOUNT"
  },
  "message": "Terminal tip configuration fetched successfully"
}
```

## Example: Multi-Level Configuration

Consider a restaurant chain with one merchant account, two stores, and several terminals. The merchant enables tips at 10/15/20%, the fine dining store overrides to 15/20/25%, and the bar terminal at that store switches to amount display with no custom entry:

```
Merchant "Nordic Bistro Group"  → ENABLED, 10/15/20%, PERCENTAGE
  └── Store "Casual Eatery"     → inherits merchant config
        └── Terminal "Checkout 1"  → 10% / 15% / 20%, PERCENTAGE
        └── Terminal "Checkout 2"  → 10% / 15% / 20%, PERCENTAGE
  └── Store "Fine Dining"       → overrides to 15/20/25%
        └── Terminal "Table POS"   → 15% / 20% / 25%, PERCENTAGE
        └── Terminal "Bar POS"     → 15% / 20% / 25%, AMOUNT, no custom
```

Each terminal resolves its effective config by merging all levels, with the most specific setting winning.

## API Quick Reference

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Set merchant tips | PATCH | `/merchants/{merchantId}/tips` |
| Fetch merchant tips | GET | `/merchants/{merchantId}/tips` |
| Set store tips | PATCH | `/merchants/{merchantId}/stores/{storeId}/tips` |
| Fetch store tips | GET | `/merchants/{merchantId}/stores/{storeId}/tips` |
| Set terminal tips | PATCH | `/merchants/{merchantId}/terminals/{terminalId}/tips` |
| Fetch terminal tips | GET | `/merchants/{merchantId}/terminals/{terminalId}/tips` |

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

---

# Refund an Order

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

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

---
## Overview

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

## When to Use Full Refund

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

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

## Step 1: Create a Refund Order

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

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

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

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

Key details:

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

## Payment Method for Refunds

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

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

For card refunds, the two card methods behave differently:

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

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

## Refund Processing Parameters

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

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

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

### Refund Reasons

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

When using `OTHER`, include the explanation:

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

## Step 2: Check Refund Status

Verify the refund completed:

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

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

## Adjustments in Refunds

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

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

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

## Refund via Partner Portal

You can also process refunds through the UI:

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

## Refund FAQ

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

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

## Reference

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

---

# Receipts

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

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

---
## Overview

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

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

## Prerequisites

Before working with receipts, make sure you have:

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

## Adding Receipt Information

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

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

**Request body:**

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

**Request parameters:**

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

**Response:**

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

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

## Emailing Receipts

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

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

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

**Request body:**

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

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

**Response:**

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

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

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

## Fetching a Receipt Link

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

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

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

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

**Response:**

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

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

## Printing Receipts on a Terminal

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

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

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

**Request body:**

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

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

**Response:**

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

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

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

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

## Custom ESC/POS Printing

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

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

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

**Request body:**

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

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

**Response:**

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

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

### Building ESC/POS Commands

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

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

**Workflow:**

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

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

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

## API Quick Reference

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

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

---

# Merchant Onboarding

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

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

---
## Overview

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

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

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

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

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

The typical flow is:

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

## Prerequisites

Before onboarding merchants:

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

Merchant applications in test and demo environments are approved automatically.

## Step 1: Create a Merchant Application

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

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

The body has three parts:

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

### The minimum request

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

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

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

### Create the first store in the same call

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

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

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

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

### Prefill the application

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

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

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

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

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

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

### Full example

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

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

### Response

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

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

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

### Key fields

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

### Pre-selecting terminals

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

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

### Linking a service provider

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

## Step 2: The Merchant Completes the Web KYB

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

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

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

## Step 3: Check Application Status

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

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

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

### Application statuses

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

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

## Step 4: Create Additional Stores

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

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

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

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

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

### Domain Verification (Online Stores)

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

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

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

## Notes and Behaviours

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

## Reference

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

---

# Store Management

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

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

---
## Overview

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

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

## Prerequisites

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

## Create an In-Store (Physical) Store

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

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

### Request

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

### Key Request Parameters

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

### Response

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

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

## Create an Online Store

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

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

### Request

```json
{
  "storeName": "Web Store",
  "email": "webstore@example.com",
  "phoneNumber": {
    "code": 46,
    "number": "701234567"
  },
  "address": "Drottninggatan 10",
  "city": "Stockholm",
  "zipCode": "103 16",
  "country": "SE",
  "onlineInfo": {
    "merchantWebshopURL": "https://shop.example.com",
    "paymentPageHostURL": "https://shop.example.com/payment",
    "termsAndConditionsURL": "https://shop.example.com/terms",
    "privacyPolicyURL": "https://shop.example.com/privacy"
  }
}
```

### Online Info Parameters

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

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

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

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

## Domain Verification

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

### Step 1: Set DNS TXT Records

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

### Step 2: Trigger Verification

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

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

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

### Check Verification Status

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

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

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

### Default Online Terminals

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

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

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

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

## Fetch Store Details

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

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

### Response

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

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

## List All Stores

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

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

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

## Update Store Details

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

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

### Request

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

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

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

## Fetch Store Terminals

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

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

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

### Response

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

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

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

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

## Deactivate a Store

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

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

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

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

### Response

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

## API Quick Reference

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

---

# Webhooks

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

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

---
## Overview

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

Surfboard supports two webhook mechanisms:

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

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

## Available Events

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

### Order and Payment Events

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

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

### Logistics Events

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

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

### Merchant Application Events

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

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

## Event Payload Details

### Order Customer Identity

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

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

**Payload example:**

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

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

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

### Order Terminal Event

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

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

**Payload example:**

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

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

**Terminal transaction statuses:**

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

## Getting Started

To set up webhooks via the Surfboard Console:

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

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

## Testing Webhooks

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

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

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

## Callback URL (Per-Order Webhook)

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

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

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

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

## Handling Duplicate Deliveries

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

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

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

## Handling Failures and Retries

### Retry Logic

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

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

### Failure Alerts and Automatic Disabling

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

### Failures on Surfboard's Side

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

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

## Verifying Webhook Signatures

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

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

### TypeScript

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

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

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

### PHP

```php
<?php

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

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

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

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

### Java

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

public class WebhookVerifier {

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

### .NET

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

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

### Python

```python
import base64
import hashlib
import hmac

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

### Go

```go
package main

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

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

## Best Practices

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

## Related Guides

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

## API Reference

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

---

# Settlements & Reporting

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

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

---
## Overview

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

This guide covers four related capabilities:

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

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

## Prerequisites

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

## Settlement Reports

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

### Fetch settlement reports

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

**Response:**

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

### Response fields

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

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

## Reading a Settlement Report

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

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

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

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

### Why the two totals differ

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

Take a merchant on daily payouts in May:

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

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

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

### Mapping a transaction to its report

One rule covers every case:

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

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

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

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

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

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

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

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

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

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

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

### Which figure answers which question

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

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

### Refunds land in the period they were processed

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

### Before escalating a mismatch

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

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

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

### Getting the numbers over the API

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

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

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

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

## Adjustments

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

### Fetch adjustments

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

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

**Response:**

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

### Response fields

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

## Merchant Charges

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

### Create a charge

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

**Response:**

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

### Create charge request fields

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

### Frequency options

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

### Fetch a charge by ID

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

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

**Key response fields:**

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

### Update a charge

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

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

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

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

### List all merchant charges

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

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

## Billing Plans

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

### Create billing plans

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

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

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

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

### Manage plans

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

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

## Customer Details

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

### Create a customer

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

**Response:**

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

### Customer fields

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

### Fetch a customer

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

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

## API Quick Reference

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

---

# Partner Branding

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

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

---
## Overview

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

## Set Partner Branding

Configure the visual appearance for your payment pages and terminals.

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

### Request

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

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

### Branding Parameters

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

### Response

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

### Via Partner Portal

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

## Fetch Partner Branding

Retrieve the current branding configuration for your partner account.

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

### Response

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

## How Branding Applies

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

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

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