{
  "totalGuides": 46,
  "lastUpdated": "2026-09-04",
  "categories": [
    "in-store",
    "online"
  ],
  "guides": [
    {
      "title": "Tap to Pay on iPhone SDK",
      "slug": "tap-to-pay-iphone",
      "url": "/developers/guides/tap-to-pay-iphone",
      "description": "Accept contactless payments directly on iPhone. Complete integration guide for Surfboard's iOS SoftPOS SDK -- from setup to production.",
      "category": "in-store",
      "tags": [
        "iOS",
        "Swift",
        "SoftPOS",
        "In-Store",
        "XCFramework"
      ],
      "content": "## Overview\n\nThe Surfboard Tap to Pay on iPhone SDK turns any compatible iPhone into a payment terminal. The SDK ships as a prebuilt `tap_to_pay_apple.xcframework` that you link into your iOS app.\n\n- **SDK binary:** `tap_to_pay_apple.xcframework` (static framework, module name `tap_to_pay_apple`).\n- **Entry point:** The `Gap` class is the single facade your app interacts with.\n- **Configuration:** `GapCredentials` encapsulates your Surfboard credentials and connection blob. Network details (Director URLs, certificates) are embedded in the blob -- you do not configure them manually.\n- **Backend responsibilities:** Your server handles auth token issuance, order creation, and credential storage. The app never holds long-lived secrets.\n- **App responsibilities:** Initialize the SDK, register the terminal, start transactions, and handle events.\n\n> **Note:** The canonical reference implementation is the [Gap Example App for iOS](https://github.com/surfboardpayments/ios-gap-example), which demonstrates the full integration flow.\n\n## Availability\n\nTap to Pay on iPhone with Surfboard is available in the following countries:\n\n<div class=\"not-prose my-6\">\n  <div class=\"flex flex-wrap gap-2\">\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇸🇪 Sweden</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇫🇮 Finland</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇩🇰 Denmark</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇳🇴 Norway</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇬🇧 United Kingdom</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇫🇷 France</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇮🇪 Ireland</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇪🇪 Estonia</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇱🇻 Latvia</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇱🇹 Lithuania</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇵🇱 Poland</span>\n    <span class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-50 text-green-800 text-sm font-medium border border-green-200\">🇭🇺 Hungary</span>\n  </div>\n</div>\n\n> **Note:** Availability depends on Apple enabling Tap to Pay on iPhone in each market and Surfboard's payment processing coverage. Contact Surfboard if you need support for a country not listed here.\n\n## Supported Devices\n\nTap to Pay on iPhone requires specific hardware and software:\n\n- **Device:** iPhone XS or later (models with NFC capability).\n- **iOS version:** iOS 16.0 or later. Some features (such as PIN entry) require iOS 16.4+. Check [Apple's ProximityReader documentation](https://developer.apple.com/documentation/proximityreader) for the latest version requirements.\n- **Beta iOS:** Beta versions of iOS are **not supported** for Tap to Pay on iPhone. Always test on stable iOS releases.\n- **Simulator:** The iOS Simulator supports SDK initialization and terminal registration, but cannot perform real NFC card reads. Use a physical device for end-to-end payment testing.\n\n## Prerequisites\n\nBefore you start, confirm you have the following:\n\n| Category | Requirement |\n|----------|-------------|\n| Apple Developer | Active account with Tap to Pay on iPhone permission |\n| Surfboard credentials | `CONNECTION_BLOB` -- opaque config string provided by Surfboard |\n| Surfboard credentials | `TMS_PUBLIC_KEY` -- retrieved from Developer Portal after registering your SDK app under Console → SDK Apps |\n| Merchant identifiers | `MERCHANT_ID` and `STORE_ID` from your Surfboard merchant onboarding |\n| Auth provider | `providerId` and `providerCertificate` issued by Surfboard for your backend |\n| Backend | A server that calls Surfboard's Client Auth Tokens API (`/api/auth`) and creates orders via the Orders API |\n| iOS device | iPhone running iOS 17.4+ with NFC (required for real payments) |\n| Tooling | Xcode 15+ |\n| Simulator note | Simulator supports SDK init and registration flows, but not real card reads |\n\n## Apple Developer Setup\n\n<div class=\"not-prose my-6 p-4 bg-yellow-100 border-l-4 border-yellow-500 rounded-r-md text-sm\">\n  <div class=\"font-semibold font-display text-yellow-800 mb-1\">Warning</div>\n  <div class=\"text-yellow-900\">Implementing Tap to Pay on iPhone is a complex process that requires submitting your app to Apple for approval. Plan extra time for the entitlement request and review process, which can take several weeks.</div>\n</div>\n\nFour steps to configure your Apple Developer account and Xcode project for Tap to Pay on iPhone.\n\n### Step 1: Register your App ID\n\nIn Apple's Certificates, Identifiers & Profiles, create or select an App ID and note the Bundle ID (e.g. `com.yourcompany.yourapp`). Use the same Bundle ID for test and production -- the Surfboard SDK controls environments through the connection blob and backend URLs, not the bundle identifier.\n\n### Step 2: Enable the capability\n\nEdit the App ID, find **Tap to Pay on iPhone** under Capabilities, and enable it. Apple may require additional contracts before this is available.\n\n### Step 3: Update provisioning profiles\n\nRegenerate your development and distribution profiles for the updated App ID and install them in Xcode.\n\n### Step 4: Add the entitlement in Xcode\n\nIn your app target under Signing & Capabilities, click \"+ Capability\" and add **Tap to Pay on iPhone**. Xcode creates the entitlements file automatically. Also add any required `Info.plist` usage descriptions per Apple's ProximityReader documentation.\n\nThe entitlement key that must be present in your `.entitlements` file:\n\n| Entitlement Key | Type | Value |\n|-----------------|------|-------|\n| `com.apple.developer.proximity-reader.payment.acceptance` | Boolean | `true` |\n\n### Further reading\n\n- [Apple: Setting up the entitlement for Tap to Pay on iPhone](https://developer.apple.com/documentation/proximityreader/setting-up-the-entitlement-for-tap-to-pay-on-iphone)\n- [Apple: Tap to Pay on iPhone for Developers](https://developer.apple.com/tap-to-pay/)\n\n> **Tip:** The Surfboard SDK does not use different bundle IDs for environments. Environments are controlled by the connection blob and backend URLs, not by the bundle ID.\n\n## SDK Installation\n\nThe SDK is delivered as a prebuilt `tap_to_pay_apple.xcframework`. Surfboard provides separate staging and production builds.\n\n1. Copy `tap_to_pay_apple.xcframework` into your repo (e.g., `YourApp/SDKs/tap_to_pay_apple/`).\n2. In Xcode, go to your app target > General > Frameworks, Libraries, and Embedded Content.\n3. Click + > Add Other > Add Files, then select the xcframework.\n4. Set embedding to **\"Do Not Embed\"** (the SDK is a static framework).\n5. Build to verify linking.\n\nImport in Swift:\n\n```swift\nimport tap_to_pay_apple\n```\n\n> **Note:** Swap the xcframework binary to switch between staging and production. Store both variants in your repo (e.g., `staging/` and `release/` subdirectories) and link only one at a time.\n\n## Credentials & Initialization\n\nThree objects to set up before processing payments: a logger, credentials, and the SDK instance.\n\n### GapLogger\n\nImplement the `GapLogger` protocol to route SDK logs into your logging stack. The protocol requires three methods: `addLog`, `addErrorLog`, and `addDebugLog`.\n\n```swift\nimport os\nimport tap_to_pay_apple\n\nfinal class AppGapLogger: GapLogger {\n    private let logger = Logger(subsystem: \"YourApp\", category: \"TapToPay\")\n\n    func addLog(logText: String) {\n        logger.log(\"\\(logText, privacy: .public)\")\n    }\n\n    func addErrorLog(logText: String) {\n        logger.error(\"\\(logText, privacy: .public)\")\n    }\n\n    func addDebugLog(logText: String) {\n        logger.debug(\"\\(logText, privacy: .public)\")\n    }\n}\n```\n\n### GapCredentials\n\nBuild `GapCredentials` once, typically at app startup, using values provided by Surfboard:\n\n```swift\nlet credentials = GapCredentials(\n    connectionBlob: connectionBlob,        // from Surfboard\n    versionNumber: appVersion,\n    tmsPublicKey: tmsPublicKey,            // from Surfboard Developer Portal\n    merchantId: merchantId,                // from merchant onboarding\n    storeId: storeId,                      // from merchant onboarding\n    applicationBundleId: Bundle.main.bundleIdentifier ?? \"\"\n)\n```\n\n### Gap instance\n\nCreate a single, long-lived `Gap` instance. Subscribe to events, initialize the SDK, and set the auth token:\n\n```swift\nlet gap = try Gap(logger: logger, credentials: credentials)\n\ngap.subscribeToPublicEvents { wrapper in\n    // Handle wrapper.event and wrapper.data\n}\n\ntry await gap.initializeSdk()\n\nlet authToken = try await fetchAuthTokenFromBackend()\ngap.setAuthToken(authToken: authToken)\n```\n\n> **Warning:** Auth tokens are short-lived. Refresh them before expiry and call `setAuthToken(authToken:)` again. Never generate tokens on the device -- always fetch them from your backend.\n\n## Terminal Lifecycle\n\nThe terminal represents a logical payment endpoint on the Surfboard side. You register it once per device, then open it for each session.\n\n### Register (first run)\n\n```swift\nlet result = await gap.registerTerminal()\n\nswitch result {\ncase .success:\n    print(\"Terminal ID: \\(gap.terminalId)\")\ncase .failure(let error):\n    print(\"Registration failed: \\(error.code.details.message)\")\n}\n```\n\nRegistration is a one-time operation per device or installation. After success, `gap.terminalId` contains the terminal identifier.\n\n### Open and initialize (each session)\n\nOn subsequent app launches, skip registration and go straight to opening:\n\n```swift\nlet openResult = await gap.openTerminal()\nlet readerResult = await gap.initializeReader()\ntry await gap.getReadyForTransaction()\n```\n\nCall these three methods in sequence before starting any payment.\n\n### Cleanup\n\nFor logout or troubleshooting flows, use `disposeTerminal()` to close the session and `clean()` to reset SDK state. After calling `clean()`, you need to re-register the terminal.\n\n## Payment Flow\n\nEvery payment follows three steps: create an order on your backend, start a transaction through the SDK, and handle the result.\n\n### Step 1: Create an order\n\nFrom your iOS app, call your backend to create an order via the Surfboard Orders API. Pass the amount in **minor units** (e.g., `1000` for 10.00 SEK), the ISO numeric currency code (e.g., `\"752\"` for SEK), and `gap.terminalId` as the terminal identifier. Your backend returns an `orderId`.\n\n### Step 2: Start the transaction\n\n```swift\nlet parameters = GapPaymentParameters(\n    amount: NSNumber(value: amountInMinorUnits),\n    type: \"PURCHASE\",\n    currency: \"SEK\",\n    orderId: orderId,\n    aidPreference: nil\n)\n\nlet paymentId = try await gap.startTransaction(parameters: parameters)\n```\n\nThe SDK takes over and presents Apple's Tap to Pay UI. The customer taps their card. The SDK emits events throughout -- use them to update your UI (\"Present card\", \"Processing\", \"Approved\", \"Declined\").\n\n### Step 3: Complete or cancel\n\nAfter the transaction resolves:\n\n```swift\n// Confirm completion\ngap.sendCompletedEvent(paymentId: paymentId, approved: true)\n\n// Or cancel an in-progress transaction\nlet cancelResult = await gap.cancelTransaction(paymentId: paymentId)\n```\n\n## Cardholder Verification & PIN\n\nTap to Pay on iPhone supports on-device PIN entry starting with **iOS 16.4**. When a contactless transaction requires cardholder verification, the SDK presents a secure PIN input screen on the iPhone.\n\n### When is PIN required?\n\n- **NFC wallet payments** (Apple Pay, Google Pay) typically do not require PIN -- the cardholder authenticates via Face ID or passcode on their own device.\n- **Physical contactless cards** may require PIN depending on the transaction amount, card issuer policy, and regional regulations.\n\n### Regional considerations\n\n| Region | Consideration |\n|--------|--------------|\n| United Kingdom | Strong Customer Authentication (SCA) may require card insertion for verification. If the card only supports offline PIN, the transaction will decline with an `offline_pin_required` error. |\n| Canada & Finland | Cards that only support offline PIN are not compatible with Tap to Pay on iPhone. These transactions will be declined. |\n\n<div class=\"not-prose my-6 p-4 bg-blue-50 border-l-4 border-blue-500 rounded-r-md text-sm\">\n  <div class=\"font-semibold font-display text-blue-800 mb-1\">Recommendation</div>\n  <div class=\"text-blue-900\">If a transaction declines due to PIN or verification issues, ask the customer to try a different card or use an alternative payment method such as a <a href=\"/products/payment-links\" class=\"text-green-600 hover:underline\">Payment Link</a> or a traditional card reader.</div>\n</div>\n\nFor more information on contactless transaction limits by country, see [Visa's contactless transaction limits](https://www.visa.co.uk/dam/VCOM/regional/ve/unitedkingdom/PDF/visa-contactless-transaction-limit.pdf).\n\n## Events & Monitoring\n\nSubscribe to the SDK event stream to drive your UI and logging:\n\n```swift\ngap.subscribeToPublicEvents { wrapper in\n    switch wrapper.event {\n    case .INITIALIZED:           // SDK ready\n    case .TERMINAL_CREATED:      // Registration succeeded\n    case .TERMINAL_CREATION_ERROR: // Registration failed\n    case .TRANSACTION_COMPLETED: // Payment finished\n    case .TRANSACTION_FAILED:    // Payment failed\n    default: break\n    }\n}\n```\n\n### Key status properties\n\n| Property | Purpose |\n|----------|---------|\n| `gap.isInitialized` | SDK has completed initialization |\n| `gap.terminalId` | Terminal ID after successful registration |\n| `gap.isReadyForTransactions` | Terminal and reader are ready for payments |\n| `gap.canMakePayments()` | Device supports Tap to Pay and is in a usable state |\n\nUse these for health checks and to enable or disable payment UI elements.\n\n## Best Practices\n\nFollow these recommendations to deliver a reliable and polished Tap to Pay experience.\n\n### Reader connection\n\n- **Connect early:** Initialize the SDK and connect to the reader in the background during app startup, so the terminal is ready when the merchant needs to accept a payment.\n- **Automatic reconnection:** When your app returns to the foreground, check `gap.isReadyForTransactions` and re-initialize the reader if needed. This ensures the terminal is always available after the app has been backgrounded.\n\n### User experience\n\n- Follow Apple's [Human Interface Guidelines for Tap to Pay on iPhone](https://developer.apple.com/design/human-interface-guidelines/tap-to-pay-on-iphone) to provide a consistent and intuitive payment experience.\n- On **iOS 18+**, use Apple's `ProximityReaderDiscovery` API to display localized educational content that helps merchants and customers understand how Tap to Pay works.\n\n### Marketing & branding\n\n- When promoting Tap to Pay on iPhone in your app or marketing materials, follow Apple's [Tap to Pay on iPhone Marketing Guidelines](https://developer.apple.com/tap-to-pay/marketing-guidelines/) for correct branding, terminology, and asset usage.\n\n## Testing\n\n**Simulator:** Supports SDK initialization and terminal registration. Card presentation is simulated -- no real NFC reads. Use the simulator to validate your integration flow and UI before moving to a device.\n\n**Physical device:** Required for end-to-end payment validation. Needs an iPhone running iOS 17.4+ with NFC. Test at least one full transaction on a real device before release.\n\n### Common issues and checks\n\n- **SDK not initializing:** Verify credentials (connection blob, TMS public key, merchant/store IDs). Check network connectivity. Inspect `GapLogger` output.\n- **Registration fails:** Confirm `initializeSdk()` completed. Verify a valid `authToken` is set. Check that merchant/store IDs match the target environment.\n- **Payments fail:** Confirm `gap.terminalId` is set. Verify order creation succeeds on your backend. Check that amount uses minor units and currency uses the correct ISO code. Inspect transaction events to distinguish declines from technical errors.\n\n## Release Checklist\n\nVerify these items before shipping your app with Tap to Pay on iPhone.\n\n### Apple configuration\n\n- App ID has Tap to Pay on iPhone capability enabled\n- Provisioning profiles are up to date and installed\n- Required `Info.plist` usage descriptions are present\n\n### Surfboard configuration\n\n- Using the correct `connectionBlob` and `tmsPublicKey` for the target environment\n- Backend calls the correct Surfboard endpoints (staging vs. production) for auth tokens and orders\n- `merchantId` and `storeId` match the target environment\n\n### SDK wiring\n\n- `tap_to_pay_apple.xcframework` is linked to the correct app target\n- Only one xcframework variant (staging or production) is linked at a time\n- `Gap` is instantiated once and reused across the app lifecycle\n- Auth tokens are refreshed before expiry via `setAuthToken(authToken:)`\n\n### Functional validation\n\n- `gap.isInitialized` returns `true` after startup\n- `gap.terminalId` is populated after registration\n- At least one full transaction completes on a real device\n- Event stream fires expected events: `INITIALIZED`, `TERMINAL_CREATED`, `TRANSACTION_COMPLETED`\n\n## Reference\n\n- [iOS Example App on GitHub](https://github.com/surfboardpayments/ios-gap-example)\n- [Client Auth Token API](https://developers.surfboardpayments.com/references/api/client-auth-token/create-token)\n- [Orders API](https://developers.surfboardpayments.com/api/orders)\n\n## Disclaimer\n\n<div class=\"not-prose my-6 p-4 bg-gray-50 border-l-4 border-gray-400 rounded-r-md text-sm\">\n  <div class=\"text-gray-700\">While this document aims to assist users in the application process, it is ultimately the user's responsibility to meet Apple's requirements, and the final decision to approve or decline an application lies with Apple.</div>\n</div>"
    },
    {
      "title": "Android SoftPOS SDK",
      "slug": "android-softpos-sdk",
      "url": "/developers/guides/android-softpos-sdk",
      "description": "Turn Android devices into payment terminals with the Surfboard Android SoftPOS SDK. Complete integration guide from setup to production.",
      "category": "in-store",
      "tags": [
        "Android",
        "Kotlin",
        "SoftPOS",
        "In-Store",
        "NFC"
      ],
      "content": "## Overview\n\nThe Surfboard Android SoftPOS SDK enables Android applications to process tap-to-pay transactions using NFC-enabled devices. It provides terminal management, transaction processing, and security -- supporting seamless integration for contactless payments.\n\nThe SDK is distributed via Surfboard-hosted Maven repositories and ships as separate debug and release variants.\n\n> **Note:** The canonical reference implementation is the [Android GAP Example App](https://github.com/surfboardpayments/android-gap-example), which demonstrates the full integration flow.\n\n## Prerequisites\n\nBefore starting integration, confirm you have the following:\n\n### Development Environment\n\n- **Android Studio** 4.1 or higher\n- **Minimum SDK**: API level 29 (Android 10.0)\n- **Java**: 11 or higher\n- **Kotlin**: 2.0.21 or higher\n\n### Surfboard Account & Credentials\n\n- Registered Surfboard Partner account\n- Active merchant setup with at least one store\n- Surfboard-issued SoftPOS configuration values:\n  - `connectionBlob` -- provided by Surfboard during onboarding\n  - `tmsPublicKey` -- retrieved from the Developer Portal after registering your SDK app under **Console > SDK Apps**\n- Merchant and store identifiers:\n  - `merchantId`\n  - `storeId`\n- Access to the [Client Auth Token API](https://developers.surfboardpayments.com/references/api/client-auth-token/create-token) for obtaining bearer tokens\n\n> **Tip:** You do not generate `connectionBlob` or `tmsPublicKey` yourself. Surfboard shares them as part of onboarding. If you are missing any values, contact Surfboard integrations support.\n\n## SDK Setup & Installation\n\n### 1. Add softpos.properties\n\nCreate a `softpos.properties` file in your Android project root (same level as `settings.gradle.kts`):\n\n```properties\nsoftpos1.mavenurl = MAVEN_URL1\nsoftpos1.mavenusername = MAVEN_USERNAME1\nsoftpos1.mavenpassword = MAVEN_PASSWORD1\n\nsoftpos2.mavenurl = MAVEN_URL2\nsoftpos2.mavenusername = MAVEN_USERNAME2\nsoftpos2.mavenpassword = MAVEN_PASSWORD2\n```\n\nAdd it to `.gitignore`:\n\n```text\nsoftpos.properties\n```\n\n### 2. Configure Gradle Repositories\n\nIn your root `build.gradle.kts`, load the properties and configure repositories:\n\n```kotlin\nimport java.util.Properties\nimport java.io.FileInputStream\n\nval localProperties = Properties()\nval localPropertiesFile = rootProject.file(\"softpos.properties\")\n\nif (localPropertiesFile.exists()) {\n    localProperties.load(FileInputStream(localPropertiesFile))\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n        mavenLocal()\n\n        maven {\n            url = uri(localProperties.getProperty(\"softpos1.mavenurl\"))\n            credentials {\n                username = localProperties.getProperty(\"softpos1.mavenusername\")\n                password = localProperties.getProperty(\"softpos1.mavenpassword\")\n            }\n        }\n\n        maven {\n            url = uri(localProperties.getProperty(\"softpos2.mavenurl\"))\n            credentials {\n                username = localProperties.getProperty(\"softpos2.mavenusername\")\n                password = localProperties.getProperty(\"softpos2.mavenpassword\")\n            }\n            authentication {\n                create<BasicAuthentication>(\"basic\")\n            }\n        }\n    }\n}\n```\n\n### 3. Add Dependencies\n\nIn `gradle/libs.versions.toml`:\n\n```toml\n[versions]\nsoftposSDK = \"1.1.4\"\n\n[libraries]\nsoftposSDKDebug = { module = \"com.surfboardpayments:gapsdk-debug\", version.ref = \"softposSDK\" }\nsoftposSDKRelease = { module = \"com.surfboardpayments:gapsdk\", version.ref = \"softposSDK\" }\n```\n\nIn `app/build.gradle.kts`:\n\n```kotlin\ndependencies {\n    debugImplementation(libs.softposSDKDebug)\n    releaseImplementation(libs.softposSDKRelease)\n}\n```\n\n### 4. App Module Configuration\n\n```kotlin\nandroid {\n    compileSdk = 35\n\n    defaultConfig {\n        applicationId = \"com.your.app.id\"\n        minSdk = 29\n        targetSdk = 35\n        multiDexEnabled = true\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_11\n        targetCompatibility = JavaVersion.VERSION_11\n    }\n\n    kotlinOptions {\n        jvmTarget = \"11\"\n    }\n}\n```\n\n### 5. Android Manifest\n\nAdd the required permissions and features to `AndroidManifest.xml`:\n\n```xml\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <uses-feature android:name=\"android.hardware.nfc\" android:required=\"true\"/>\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.NFC\" />\n    <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n    <uses-permission android:name=\"android.permission.CAMERA\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.VIBRATE\" />\n</manifest>\n```\n\n### 6. Verify Installation\n\n```kotlin\nimport com.surfboardpayments.gapsdk.Gap\n// If this import compiles without errors, the SDK is properly installed\n```\n\n## Credentials & SDK Initialization\n\n### 1. Create Logger\n\nThe SDK requires a logger implementation:\n\n```kotlin\nclass AppLogger : GapLogger() {\n    override fun addDebugLog(log: String) {\n        Log.d(\"SoftPOS\", log)\n    }\n\n    override fun addErrorLog(log: String) {\n        Log.e(\"SoftPOS\", log)\n    }\n\n    override fun addLog(log: String) {\n        Log.i(\"SoftPOS\", log)\n    }\n}\n```\n\n### 2. Create SDK Instance\n\n```kotlin\nval softposSDK = Gap(\n    logger = AppLogger(),\n    gapCredentials = GapCredentials(\n        connectionBlob = BuildConfig.connectionBlob,\n        versionNumber = \"YOUR_APP_VERSION\",\n        tmsPublicKey = \"YOUR_TMS_PUBLIC_KEY\",\n        merchantId = BuildConfig.merchantId,\n        storeId = BuildConfig.storeId,\n        applicationBundleId = \"YOUR_BUNDLE_ID\"\n    ),\n    context = applicationContext\n)\n```\n\n### 3. Set Authentication Token\n\nSet the bearer token before any SDK operations. Tokens are fetched from your backend, which calls the [Client Auth Token API](https://developers.surfboardpayments.com/references/api/client-auth-token/create-token):\n\n```kotlin\nval bearerToken = fetchTokenFromBackend()\nsoftposSDK.setAuthToken(bearerToken)\n```\n\n> **Warning:** Bearer tokens expire every 60 minutes. Implement automatic refresh. Never generate tokens on the device -- always fetch them from your backend.\n\n### 4. Subscribe to Events\n\n```kotlin\nsoftposSDK.subscribeToPublicEvents { wrapper ->\n    when (wrapper.event) {\n        GapPublicEvent.INITIALIZED -> { /* SDK ready */ }\n        GapPublicEvent.TRANSACTION_APPROVED -> { /* Payment approved */ }\n        GapPublicEvent.TRANSACTION_DECLINED -> { /* Payment declined */ }\n        // Handle other events\n    }\n}\n```\n\n## Terminal Lifecycle & Payment Flow\n\n### Step 1: Register Terminal\n\nOne-time operation per device. Returns a `terminalId` used for creating orders:\n\n```kotlin\nval result = softposSDK.registerTerminal().await()\nresult.fold(\n    { error -> Log.e(\"SoftPOS\", \"Registration failed: ${error.explainError()}\") },\n    { terminalId -> Log.i(\"SoftPOS\", \"Terminal registered: $terminalId\") }\n)\n```\n\n### Step 2: Initialize SDK\n\n```kotlin\nsoftposSDK.initializeGapSDK()\n// Wait for INITIALIZED event\n```\n\n### Step 3: Fetch Prerequisites (First Run Only)\n\nDownloads merchant branding, EMV configs, and currency data:\n\n```kotlin\nsoftposSDK.fetchPrerequisites()\n```\n\n> **Note:** Only required on first run. Skip on subsequent launches unless you change environment or credentials.\n\n### Step 4: Open Terminal Session\n\nCall every time the app comes to the foreground:\n\n```kotlin\nsoftposSDK.openTerminal().onRight { _ ->\n    Log.i(\"SoftPOS\", \"Terminal session opened\")\n}\n```\n\n### Step 5: Prepare for Transaction\n\nPrepares transaction keys. Valid for 120 seconds -- call right before starting a payment:\n\n```kotlin\nval result = softposSDK.getReadyForTransaction().await()\n```\n\n### Step 6: Create Order\n\nCreate an order via the [Surfboard Orders API](https://developers.surfboardpayments.com/api/orders) from your backend. Pass the amount in minor units, the ISO numeric currency code, and the `terminalId`.\n\n### Step 7: Start Transaction\n\n```kotlin\nval result = softposSDK.startTransaction(\n    GapInitiatePayment(\n        amount = amountInMinorUnits,\n        type = \"PURCHASE\",\n        orderId = orderId,\n        currency = Currency.getInstance(\"SEK\")\n    )\n).await()\n\nresult.fold(\n    { error -> Log.e(\"SoftPOS\", \"Transaction failed: ${error.explainError()}\") },\n    { paymentId -> Log.i(\"SoftPOS\", \"Transaction started: $paymentId\") }\n)\n```\n\n## Transaction Events\n\nDuring a transaction, handle these events to drive your UI:\n\n```kotlin\nwhen (event) {\n    GapPublicEvent.TRANSACTION_STARTED -> { /* Show payment UI */ }\n    GapPublicEvent.PRESENT_CARD -> { /* \"Tap your card\" */ }\n    GapPublicEvent.HOLD_CARD -> { /* \"Hold card still\" */ }\n    GapPublicEvent.CARD_READ -> { /* Card read successfully */ }\n    GapPublicEvent.TRANSACTION_ENTER_PIN -> { /* PIN screen appears */ }\n    GapPublicEvent.TRANSACTION_AUTHORIZING -> { /* Processing */ }\n    GapPublicEvent.TRANSACTION_APPROVED -> { /* Show success */ }\n    GapPublicEvent.TRANSACTION_DECLINED -> { /* Show declined */ }\n    GapPublicEvent.TRANSACTION_COMPLETED -> { /* Show receipt */ }\n    GapPublicEvent.TRANSACTION_CANCELLED -> { /* Clean up */ }\n}\n```\n\nAfter displaying the receipt, notify the SDK:\n\n```kotlin\nsoftposSDK.sendCompletedEvent(paymentId, true)\n```\n\n## Production Requirements\n\nBefore releasing your app:\n\n1. **Google Play Services** must be enabled\n2. **Play Integrity** must be enabled\n3. **Application signing** must match the registered SHA-256 hash\n4. **Debug mode** must be disabled\n5. **Card scheme logos** must be displayed during transactions (Visa, Mastercard)\n6. **Battery level** recommended above 10%\n7. **Screen recording** not allowed during transactions\n\n## Reference\n\n- [Android GAP Example App](https://github.com/surfboardpayments/android-gap-example)\n- [Client Auth Token API](https://developers.surfboardpayments.com/references/api/client-auth-token/create-token)\n- [Orders API](https://developers.surfboardpayments.com/api/orders)"
    },
    {
      "title": "CheckoutX SoftPOS",
      "slug": "checkoutx-softpos",
      "url": "/developers/guides/checkoutx-softpos",
      "description": "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.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "Android",
        "iOS",
        "CheckoutX",
        "SoftPOS",
        "App Switch"
      ],
      "content": "## Overview\n\nCheckoutX 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.\n\nUse 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.\n\n## Two Ways to Accept Payments on Phones\n\nSurfboard gives you two routes for in-person payments on iOS and Android. Pick the one that fits your product:\n\n| Option | What you do | When to pick it |\n|--------|-------------|-----------------|\n| **[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 |\n| **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 |\n\nBoth approaches run on the same Surfboard platform, the difference is only where the payment UI lives.\n\n## How It Works\n\n1. **Install CheckoutX** from the App Store (iOS) or Google Play (Android) on the device running your POS app.\n2. **Register CheckoutX** as a terminal once per device using the Inter-App flow.\n3. **Initiate a payment** from your POS app, CheckoutX opens, accepts the tap, and returns the result to your app.\n\nThe 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.\n\n## Setup\n\n1. **Get a Surfboard account** and register a store under your merchant.\n2. **Download CheckoutX** on the target device.\n3. **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.\n\n## The Configure Call\n\nBefore 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:\n\n```\ncheckoutx://com.surfboard.checkoutx/configure?redirectUrl=REDIRECT_URL\n```\n\nReplace `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.\n\nRunning 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.\n\n## Handling `PS_0025`, Terminal Not Connected\n\nWhen you initiate a payment on SoftPOS, you may occasionally see:\n\n```\nPS_0025: Terminal is not connected to server so unable to send transactions\n```\n\nIn 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.\n\nThis 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.\n\n## Reference\n\n- [Inter-App Integration](/developers/guides/interapp-integration), full deep link flow\n- [Tap to Pay on iPhone SDK](/developers/guides/tap-to-pay-iphone), iOS SDK alternative\n- [Android SoftPOS SDK](/developers/guides/android-softpos-sdk), Android SDK alternative"
    },
    {
      "title": "EMV Terminal Integration",
      "slug": "emv-terminal-integration",
      "url": "/developers/guides/emv-terminal-integration",
      "description": "Integrate traditional card-present terminals through Surfboard's unified API. From account setup to live payments in one guide.",
      "category": "in-store",
      "tags": [
        "EMV",
        "Terminal",
        "In-Store",
        "API",
        "Hardware"
      ],
      "content": "## Overview\n\nSurfboard Payments lets you integrate traditional EMV card-present terminals through a single, unified API. Whether you are deploying countertop terminals, mobile POS devices, or kiosk setups, the integration follows the same workflow: create an account, get API credentials, build and test in the sandbox, then go live.\n\nThis guide walks you through the complete process from zero to accepting live in-store payments.\n\n## Step 1: Create a Developer Account\n\nSign up at the [Surfboard Developer Portal](https://developers.surfboardpayments.com/sign-up) to get started. A developer account gives you:\n\n- Access to the Console for managing your integration\n- A sandbox environment for building and testing\n- The path to certification and live payments\n\nNo approval process required -- you get instant sandbox access.\n\n## Step 2: Generate API Credentials\n\nAfter creating your account, open the **Console** in the Developer Portal. From there you can:\n\n- Generate your **API-KEY** and **API-SECRET**\n- Configure webhooks\n- Access logs and monitoring\n- Manage terminals and merchants\n\n> **Tip:** You can also request test credentials through the Surfboard support team on Slack during onboarding.\n\n## Step 3: Understand Environments\n\nSurfboard provides different environments for building and testing your integration:\n\n| Environment | Supported Terminals | Cards Supported |\n|-------------|-------------------|-----------------|\n| **Demo** | All hardware terminals, Terminal Tester App, Mobile Checkout | Live cards can be used. Transactions are voided immediately after payment. |\n| **Live** | All hardware terminals, Mobile Checkout | Live cards. Transactions are settled and you receive payouts. |\n\nBy default, you gain access to the demo environment when you create a developer account. Use it to build and test your integration with the [Surfboard APIs](https://developers.surfboardpayments.com/) and SDKs.\n\n> **Note:** For in-store payments, use the Terminal Tester App (available on Android) for payment simulations. It includes built-in success and failure test cards.\n\n## Step 4: Build Your Integration\n\nComplete these steps in the demo environment before going live:\n\n### 4.1 Merchant Onboarding\n\nSet up your merchant hierarchy using the [Merchants API](https://developers.surfboardpayments.com/api/merchants) and [Stores API](https://developers.surfboardpayments.com/api/stores). Each merchant can have multiple stores, and each store can have multiple terminals.\n\n### 4.2 Terminal Registration\n\nRegister your hardware terminals through the [Terminals API](https://developers.surfboardpayments.com/api/terminals). When registering a terminal, you provide:\n\n- The `registrationIdentifier` (printed on the terminal or provided during provisioning)\n- The `storeId` for the store the terminal belongs to\n- A human-readable `terminalName`\n\n```json\nPOST /merchants/:merchantId/stores/:storeId/devices\n{\n  \"registrationIdentifier\": \"250901\",\n  \"terminalName\": \"Checkout 1\"\n}\n```\n\n### 4.3 Accept Payments\n\nCreate orders and initiate payments using the [Orders API](https://developers.surfboardpayments.com/api/orders). The Carbon API uses an orders-first workflow -- create an order and initiate payment in a single call:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"Coffee\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"total\": 4500,\n        \"currency\": \"752\"\n      }\n    }\n  ],\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\",\n      \"amount\": 4500\n    }\n  }\n}\n```\n\nThe terminal displays the payment UI automatically. The customer taps, inserts, or swipes their card. You receive the result via the API response or webhooks.\n\n### 4.4 Post-Payment Operations\n\nAfter payments are accepted, integrate post-payment functionality:\n\n- **Refunds** -- Use negative quantities in order lines\n- **Receipts** -- Send digital receipts via the [Receipts API](https://developers.surfboardpayments.com/api/receipts)\n- **Reporting** -- Query order and payment history\n\n## Step 5: Certification & Go Live\n\nOnce your integration is built and tested in the demo environment:\n\n1. Sign the contract and receive approval\n2. Complete an onboarding call to test and certify your integration\n3. Receive production credentials\n4. Update your base URL from demo to production\n5. Start accepting live payments\n\n## Webhooks\n\nConfigure webhooks to receive real-time notifications about order and payment events. Key events include:\n\n- `order.paymentcompleted` -- Payment was successful\n- `order.paymentcancelled` -- Payment was cancelled\n- `order.paymentfailed` -- Payment failed\n- `order.terminal.event` -- Every terminal state during the transaction\n\nSet up webhook endpoints in the Console under your developer account settings.\n\n## API Quick Reference\n\n| API | Purpose |\n|-----|---------|\n| [Merchants API](https://developers.surfboardpayments.com/api/merchants) | Create and manage merchants |\n| [Stores API](https://developers.surfboardpayments.com/api/stores) | Create and manage stores |\n| [Terminals API](https://developers.surfboardpayments.com/api/terminals) | Register and manage terminals |\n| [Orders API](https://developers.surfboardpayments.com/api/orders) | Create orders and initiate payments |\n| [Receipts API](https://developers.surfboardpayments.com/api/receipts) | Send digital receipts |\n| [Branding API](https://developers.surfboardpayments.com/api/branding) | Customise terminal branding |\n\n## Reference\n\n- [Developer Portal](https://developers.surfboardpayments.com/)\n- [Carbon API Documentation](https://developers.surfboardpayments.com/references/api/orders/create-order)\n- [Webhook Reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)"
    },
    {
      "title": "Payment Page",
      "slug": "payment-page",
      "url": "/developers/guides/payment-page",
      "description": "Redirect customers to a Surfboard-hosted checkout page. The fastest way to accept online payments with minimal integration effort.",
      "category": "online",
      "tags": [
        "Online",
        "Payment Page",
        "Hosted Checkout",
        "API"
      ],
      "content": "## Overview\n\nThe Payment Page is the simplest way to accept online payments with Surfboard. Instead of building your own checkout form, you redirect customers to a Surfboard-hosted payment page. The customer completes payment there and is redirected back to your site.\n\nThis approach requires minimal frontend work -- you only need to create an order via the API and redirect the customer to the returned payment link.\n\n## Prerequisites\n\nBefore accepting payments with the Payment Page:\n\n1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)\n2. Complete onboarding (merchant and store setup)\n3. The `terminalId` of the store's **PaymentPage** terminal — creating an online store provisions one for you, so fetch the store's terminals rather than registering a new one\n\n## Payment Types\n\nThere are two primary types of payments:\n\n1. **Customer Initiated Transaction (CIT):** Transactions initiated by customers on your webshop, such as e-commerce purchases.\n2. **Merchant Initiated Transaction (MIT):** Transactions initiated by the merchant, such as subscription charges.\n\n> **Note:** MIT payments can only be processed by terminals set to `MerchantInitiated`. See the [Server-to-Server API guide](/developers/guides/server-to-server-api) for details on MIT.\n\n## Payment Process\n\n### Step 1: Create an Order\n\nCreate an order using the [Create Order API](https://developers.surfboardpayments.com/api/orders). On success, you receive a **payment link** to share with the customer.\n\nFor the store, domain and terminal setup this call assumes, see [Online Payment Link](/developers/guides/online-payment-link).\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"Annual Subscription\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"total\": 99900,\n        \"currency\": \"752\"\n      }\n    }\n  ],\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\",\n      \"amount\": 99900\n    }\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderId\": \"8455c12f9fd0620a010b\",\n    \"paymentPageLink\": \"https://pay.withsurfboard.com/8455c12f9fd0620a010b?pi=Dr4GoyMXF0zHvjca_Oa0vHgxcT-OD1qp7KdokyI7dkTwRwYJt8nkXyQm3bT6vqCfgraOw50Bf5uOp__3ckbMWOV6L9QbiTiSEFS3YmF4Eb8lr5pTWP2KFjm9Ukmd0000&add=IzFlNDBhZg==\"\n  },\n  \"message\": \"Order created successfully\"\n}\n```\n\nPass `paymentPageLink` on whole -- the query string carries the payment intent, and a trimmed or re-encoded link will not open.\n\n### Control Fields\n\nThe Payment Page supports additional control fields for fine-grained payment control:\n\n| Field | Description |\n|-------|-------------|\n| `delayCapture` | Set to `true` to capture payment later after authorisation. Default: `false`. |\n| `enforceTokenization` | Override tokenisation config -- control whether the card is saved for future use. |\n| `enforce3DSecure` | Whether the customer goes through 3D Secure verification. |\n| `paymentPageValidFor` | How long the payment link is valid. Default: one day. |\n| `lockToPaymentMethod` | Force the customer to use a specific payment method. |\n| `authMode` | `PREAUTH` or `AUTH`. Default: `AUTH`. If `PREAUTH`, `delayCapture` is set to `true` automatically. |\n| `redirectUrl` | URL to redirect to after successful payment. Includes `orderId` as a query param. |\n| `failureRedirectUrl` | URL to redirect to after failed payment. Includes `orderId` as a query param. |\n| `generateShortLink` | Set to `true` to get a shortened payment URL. Default: `false`. |\n\n### Recurring Payment Fields\n\nFor subscription-based payments, include these additional fields:\n\n| Field | Description |\n|-------|-------------|\n| `subscriptionAmountType` | `FIXED` or `VARIABLE` |\n| `maxAmount` | Maximum amount in minor units (for variable subscriptions) |\n| `frequency` | `daily`, `weekly`, `monthly`, `quarterly`, `annually`, `unscheduled`, etc. |\n| `numberOfPayments` | Total expected payments for this subscription |\n| `uniqueReference` | Unique reference for the recurring order |\n\n### Step 2: Check Order Status\n\nMonitor the order status using the [Fetch Order Status API](https://developers.surfboardpayments.com/api/orders). When the status changes to `PAYMENT_COMPLETED` or `PAYMENT_CANCELLED`, you can view the transaction details.\n\nYou can also receive real-time updates via webhook notifications -- configure them in the Developer Portal Console.\n\n## Integration Flow\n\nHere is the typical integration flow:\n\n1. Customer clicks \"Pay\" on your website\n2. Your backend calls the Create Order API\n3. You redirect the customer to the `paymentPageLink` from the response, or to `shortLinkUrl` if you asked for a short link\n4. Customer completes payment on the Surfboard-hosted page\n5. Customer is redirected to your `redirectUrl` (or `failureRedirectUrl`)\n6. Your backend verifies the order status via the API or webhook\n\n> **Tip:** Always verify the order status server-side after redirect. Do not rely solely on the redirect URL to confirm payment success.\n\n## Reference\n\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Fetch Order Status API](https://developers.surfboardpayments.com/api/orders)\n- [Webhook Reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Inter-App Integration",
      "slug": "interapp-integration",
      "url": "/developers/guides/interapp-integration",
      "description": "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.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "Android",
        "iOS",
        "Web",
        "CheckoutX",
        "App Switch"
      ],
      "content": "## Overview\n\nSurfboard'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.\n\nThis guide covers terminal registration, the payment flow, and NFC tag scanning -- all through deep links.\n\n> **Important:** Surfboard terminals operate in full online mode. All data exchange happens through APIs and deep link parameters -- no offline data passing is supported.\n\n## How It Works\n\nThe inter-app flow is a bi-directional app switch:\n\n1. **Your app -> CheckoutX** -- initiate a task (registration, payment, or tag scan)\n2. **CheckoutX -> Your app** -- return the result via your redirect URL\n\nThere are three flows:\n\n| Flow | Purpose | Frequency |\n|------|---------|-----------|\n| **Terminal Registration** | Link CheckoutX to a terminal | Once per device |\n| **Payment** | Process a payment via CheckoutX | Every transaction |\n| **Tag Scanning** | Read NFC product tags | As needed |\n\n## Setting Up Your App for App Switch\n\nConfigure your app to receive the callback from CheckoutX after a task completes.\n\n### Android\n\nRegister a deep link intent filter in your `AndroidManifest.xml`:\n\n```xml\n<activity android:name=\".YourActivity\">\n  <intent-filter>\n    <action android:name=\"android.intent.action.VIEW\" />\n    <category android:name=\"android.intent.category.DEFAULT\" />\n    <category android:name=\"android.intent.category.BROWSABLE\" />\n    <data android:scheme=\"posapp\" android:host=\"hello\" />\n  </intent-filter>\n</activity>\n```\n\n### iOS\n\nRegister a custom URL scheme in your `Info.plist` or Xcode project settings. Add your scheme (e.g., `posapp`) under **URL Types**.\n\n### Browser-based POS\n\nA 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).\n\n## Configure Terminal Before Payment\n\nBefore the first payment (especially after a device reboot), call the configuration route to prepare CheckoutX:\n\n```\ncheckoutx://com.surfboard.checkoutx/configure?redirectUrl=REDIRECT_URL\n```\n\nReplace `REDIRECT_URL` with your base64-encoded app URL. CheckoutX will open, configure itself, and return with `isConfigured: true` when ready.\n\nUse this step before starting the payment flow for optimal performance on the first transaction.\n\n### Handling `PS_0025`, Terminal Not Connected\n\nWhen initiating a payment you may occasionally see:\n\n```\nPS_0025: Terminal is not connected to server so unable to send transactions\n```\n\nRun 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.\n\n## Terminal Registration (One-Time Setup)\n\nRegister a terminal with CheckoutX once per device. This links your Surfboard terminal to the CheckoutX app.\n\n### Step 1: Get an Interapp Code\n\nCall the API to generate a registration code:\n\n```json\nGET /merchants/:merchantId/stores/:storeId/terminals/interapp\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"registrationCode\": \"abc123...\"\n  },\n  \"message\": \"Interapp code generated successfully\"\n}\n```\n\n> The registration code is valid for **120 seconds**. Complete the app switch before it expires.\n\n### Step 2: App Switch to Register\n\nBuild the registration deep link with the code:\n\n```\ncheckoutx://com.surfboard.checkoutx/register?redirectUrl=REDIRECT_URL&data=REGISTRATION_CODE\n```\n\n- `REDIRECT_URL` -- your base64-encoded app callback URL\n- `REGISTRATION_CODE` -- base64-encoded JSON: `{\"registrationCode\": \"GENERATED_CODE\"}`\n\n### Step 3: Handle the Callback\n\nAfter registration, CheckoutX calls your redirect URL with a `data` query parameter containing the `terminalId`:\n\n```\nposapp://hello/order?orderRef=...&data=<base64_encoded_data>\n```\n\nDecode the base64 `data` parameter to get the terminal ID:\n\n```kotlin\n// Kotlin\nval data = String(Base64.getUrlDecoder().decode(uri.getQueryParameter(\"data\")))\nval jsonObject = serializer.fromJson(data, JsonObject::class.java)\nval terminalId = jsonObject[\"terminalId\"].asString\n```\n\n```swift\n// Swift\nguard let base64String = URLComponents(url: url, resolvingAgainstBaseURL: false)?\n    .queryItems?.first(where: { $0.name == \"data\" })?.value,\n    let jsonData = Data(base64Encoded: base64String),\n    let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],\n    let terminalId = json[\"terminalId\"] as? String\nelse { return }\n```\n\nStore the `terminalId` -- you need it for all future payments on this device.\n\n### Step 4: Verify Registration\n\nConfirm the registration status via API:\n\n```json\nGET /merchants/:merchantId/stores/:storeId/terminals/interapp/:interappCode\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"registrationStatus\": \"REGISTERED\",\n    \"terminalId\": \"83abab731f6fb00704\"\n  }\n}\n```\n\n**Possible `registrationStatus` values:** `REGISTERED` | `NOT_REGISTERED`\n\n## Payment Flow\n\nOnce the terminal is registered, process payments through app switch.\n\n### Step 1: Create an Order via API\n\nCreate 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`:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"Running Shoes\",\n      \"quantity\": 1,\n      \"amount\": { \"regular\": 50000, \"total\": 50000, \"currency\": \"752\" }\n    }\n  ],\n  \"totalOrderAmount\": { \"regular\": 50000, \"total\": 50000, \"currency\": \"752\" },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": { \"paymentMethod\": \"CARD\" }\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderId\": \"83a1ba32774149710b\",\n    \"paymentId\": \"83a1ba3264bd500106\",\n    \"interAppJWT\": \"eyJhbGciOiJIUzI1NiIs...\"\n  }\n}\n```\n\n### Step 2: App Switch to CheckoutX\n\nBuild the transaction deep link:\n\n```\ncheckoutx://com.surfboard.checkoutx/transaction?redirectUrl=REDIRECT_URL&data=REQUIRED_DATA\n```\n\n- `REDIRECT_URL` -- your base64-encoded callback URL\n- `REQUIRED_DATA` -- base64-encoded JSON containing the terminal ID and the `interAppJWToken`:\n\n```json\n{\n  \"terminalId\": \"YOUR_TERMINAL_ID\",\n  \"interAppJWToken\": \"eyJhbGciOiJIUzI1NiIs...\"\n}\n```\n\n> **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.\n\n### Step 3: Perform the App Switch\n\n```kotlin\n// Kotlin\nval url = \"checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=$encodedRedirectUrl&data=$encodedData\"\nval intent = Intent(Intent.ACTION_VIEW)\nintent.data = Uri.parse(url)\nstartActivity(intent)\n```\n\n```swift\n// Swift\nlet url = \"checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=\\(encodedRedirectUrl)&data=\\(encodedData)\"\nif let deepLink = URL(string: url) {\n    UIApplication.shared.open(deepLink)\n}\n```\n\n```dart\n// Flutter\nString url = \"checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=$encodedRedirectUrl&data=$encodedData\";\nUri uri = Uri.parse(url);\nif (await canLaunchUrl(uri)) {\n    await launchUrl(uri);\n}\n```\n\n### Step 4: Handle the Result\n\nCheckoutX calls your redirect URL with the result. Check the order status via API to confirm payment completion:\n\n```json\nGET /orders/:orderId/status\n```\n\n## Framing the Redirect URL\n\nThe redirect URL follows the format:\n\n```\n<YOUR_SCHEME>://<YOUR_HOST>/<ROUTES>?<QUERY_PARAMS>\n```\n\nFor example, if your scheme is `posapp` and host is `hello`:\n\n```\nposapp://hello/order?orderRef=6ba7b7db-519f-4ed9-9f6b-a834140466f7\n```\n\nThis URL must be **base64-encoded** before passing it as the `redirectUrl` parameter:\n\n```kotlin\n// Kotlin\nval url = \"posapp://hello/order?orderRef=6ba7b7db-519f-4ed9-9f6b-a834140466f7\"\nval encoded = Base64.getUrlEncoder().encodeToString(url.toByteArray())\n```\n\n```swift\n// Swift\nlet url = \"posapp://hello/order?orderRef=6ba7b7db-519f-4ed9-9f6b-a834140466f7\"\nlet encoded = Data(url.utf8).base64EncodedString()\n```\n\n```js\n// JavaScript -- URL-safe base64, no padding\nconst encoded = btoa(url).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n```\n\nFor 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).\n\n## Browser-Based POS (Web Apps)\n\nIf 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.\n\nThe 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.\n\n### Launching CheckoutX from a page\n\nBuild and encode the deep link exactly as elsewhere in this guide, then follow it:\n\n```js\nconst deepLink =\n  `checkoutx://com.surfboard.checkoutx/transaction` +\n  `?redirectUrl=${encodedRedirectUrl}&data=${encodedData}`;\n\n// A temporary anchor click is more reliable than assigning window.location,\n// which some in-app browsers and webviews intercept\nconst a = document.createElement(\"a\");\na.href = deepLink;\na.style.display = \"none\";\ndocument.body.appendChild(a);\na.click();\nsetTimeout(() => a.remove(), 100);\n```\n\n### Returning to the browser\n\nPoint the redirect URL at the **browser**, not at a page. On Android Chrome:\n\n```\ngooglechrome://com.android.chrome\n```\n\nBase64-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.\n\nIf the return has to land on a specific page instead:\n\n```\ngooglechrome://navigate?url=<percent-encoded https URL>\n```\n\nThis 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.\n\n> 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.\n\n### The return carries no data\n\nBecause 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.\n\n- **Payment:** poll `GET /orders/:orderId/status` until it reaches a terminal state.\n- **Registration:** poll `GET /merchants/:merchantId/stores/:storeId/terminals/interapp/:interappCode` until `registrationStatus` is `REGISTERED`, then store the returned `terminalId`.\n\nSince the tab is never reloaded, becoming visible again is the signal that the operator is back:\n\n```js\ndocument.addEventListener(\"visibilitychange\", () => {\n  if (!document.hidden && pendingOrderId) {\n    checkOrderStatus(pendingOrderId); // re-check immediately, then keep polling\n  }\n});\n```\n\nPersist 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.\n\n### Other browsers\n\nThe 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.\n\n| Redirect URL | Behaviour |\n|---|---|\n| `googlechrome://com.android.chrome` | Chrome to the front, original tab, no reload -- verified on an Android tablet |\n| `googlechrome://navigate?url=<encoded>` | Chrome opens the given URL in a new tab -- verified |\n| Another browser's scheme | Same shape, verify per browser |\n\nTo check what a given browser answers to on your target device:\n\n```\nadb shell am start -a android.intent.action.VIEW -d \"<scheme>://\"\n```\n\nIf 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.\n\n### What does not work from a browser\n\nMeasured against CheckoutX on Android, so you do not have to retry them:\n\n| Redirect URL | Result |\n|---|---|\n| `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 |\n| `https://your-pos.example.com/...` | Opens in CheckoutX's in-app browser rather than the operator's browser |\n| An `https://` page that re-launches an `intent://` URI | Ignored as well -- the in-app browser does not follow it |\n\n## NFC Tag Scanning\n\nScan product NFC tags through CheckoutX before or during a sale:\n\n```\ncheckoutx://com.surfboard.checkoutx/scanProducts?redirectUrl=REDIRECT_URL&data=REQUIRED_DATA\n```\n\nThe `REQUIRED_DATA` is a base64-encoded JSON specifying the read mode:\n\n```json\n{ \"readMode\": \"SINGLE\" }\n```\n\n| Read Mode | Description |\n|-----------|-------------|\n| `SINGLE` | Scan one product tag |\n| `MULTIPLE_EDITABLE` | Scan multiple tags, allow editing scanned data |\n| `MULTIPLE_NONEDITABLE` | Scan multiple tags, no editing allowed |\n\nThe redirect URL and app switch mechanics are identical to the payment flow.\n\n## Example Repositories\n\n- [Android Example App (Kotlin)](https://github.com/surfboardpayments/surfboard-interapp-kotlin-simple)\n\n## Reference\n\n- [Terminals API](https://developers.surfboardpayments.com/api/terminals)\n- [Create an Order](/developers/guides/create-an-order)\n- [Tap to Pay on iPhone](/developers/guides/tap-to-pay-iphone)\n- [NFC Tag Reading](/developers/guides/nfc-tag-reading)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Self-Hosted Checkout",
      "slug": "self-hosted-checkout",
      "url": "/developers/guides/self-hosted-checkout",
      "description": "Embed a payment form directly in your web app with the Surfboard Online SDK. Full UI control with Surfboard handling PCI compliance.",
      "category": "online",
      "tags": [
        "Online",
        "SDK",
        "JavaScript",
        "Self-Hosted",
        "Checkout"
      ],
      "content": "## Overview\n\nThe Self-Hosted Checkout (Online SDK) lets you embed payment fields directly in your website while Surfboard handles PCI compliance and payment processing. You get full control over the look and feel of your checkout page.\n\nSurfboard renders secure input fields inside your page using the Online SDK. You control the layout, branding, and customer experience.\n\n## Prerequisites\n\n1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)\n2. Complete onboarding (merchant and store setup)\n3. Register a terminal with the type set to **SelfHostedPage**\n4. Note the terminal's `publicKey` from the registration response\n\n> **Tip:** You can retrieve the terminal's public key later using the [Fetch Terminal by ID API](https://developers.surfboardpayments.com/api/terminals). It is returned as `terminalPublicKey` in the response.\n\n## Initializing the SDK\n\nTo initialize the SDK, you need three parameters:\n\n1. **`publicKey`** -- From terminal registration\n2. **`orderId`** -- From the [Create Order API](https://developers.surfboardpayments.com/api/orders) response\n3. **`nonce`** -- From the Create Order response (serves as access control)\n\n```javascript\n// Set up error handling\nSurfboardOnlineSDK.errorCallback((code, message) => {\n    console.error(`Error [${code}]: ${message}`);\n});\n\n// Listen for payment status changes\nSurfboardOnlineSDK.paymentStatusCallback = function (data) {\n    // data.paymentStatus: 'PAYMENT_INITIATED' | 'PAYMENT_COMPLETED' |\n    //   'PAYMENT_CANCELLED' | 'PAYMENT_FAILED' | 'PAYMENT_PROCESSING'\n    console.log(\"Payment status:\", data.paymentStatus);\n};\n\n// Initialize\nSurfboardOnlineSDK.initialiseOnlineSDK({\n    publicKey: \"YOUR_PUBLIC_KEY\",\n    orderId: \"YOUR_ORDER_ID\",\n    nonce: \"YOUR_NONCE\",\n});\n```\n\nTo re-initialize for a different order without a full page reload:\n\n```javascript\nSurfboardOnlineSDK.remountOnlineSDK({\n    publicKey: \"YOUR_PUBLIC_KEY\",\n    orderId: \"NEW_ORDER_ID\",\n    nonce: \"NEW_NONCE\",\n});\n```\n\n### Error Codes\n\n| Error Code | Message | Category |\n|------------|---------|----------|\n| -- | Surfboard SDK cannot function in the given environment | FATAL |\n| -- | Surfboard SDK initialisation failed | FATAL |\n| -- | Public key validation failed | FATAL |\n| -- | Invalid Order ID | FATAL |\n| -- | Invalid Nonce | FATAL |\n| 401 | Invalid or Expired Link | FATAL |\n\n## Available Data Objects\n\nAfter successful initialization, the SDK exposes data objects on `SurfboardOnlineSDK`:\n\n- **`order`** -- Order details, line items, and payment methods\n- **`merchant`** -- Merchant name and organization number\n- **`branding`** -- Colors, fonts, logos for your checkout styling\n- **`store`** -- Store contact info, privacy policy, and terms URLs\n- **`paymentMethods`** -- Supported payment methods for this terminal\n- **`customer`** -- Customer details, saved cards, and addresses\n\n> **Warning:** You are required to display the store contact information, privacy policy, and terms and conditions on your payment page.\n\n## Payment Flow\n\n### Updating Customer Information\n\nMost payment methods require customer information. Provide it via the SDK or include it when creating the order.\n\n| Payment Method | Required Fields |\n|----------------|----------------|\n| Card | Email, Phone, Address |\n| Klarna | Email, Phone, Address, Shipping Address (physical goods) |\n| Apple Pay | Email, Name, Phone, Postal Address |\n\n```javascript\nawait SurfboardOnlineSDK.order.addCustomerInformation({\n    name: \"Jane Doe\",\n    email: \"jane@example.com\",\n    phone: { countryCode: \"+46\", number: \"701234567\" },\n    billingAddress: {\n        addressLine1: \"Main Street 1\",\n        city: \"Stockholm\",\n        postalCode: \"11122\",\n        countryCode: \"SE\",\n    },\n});\n```\n\n> **Tip:** Include the customer address in the Create Order API request when you have it. This pre-fills the address so the customer does not need to enter it manually.\n\n### Card Payments\n\nMount the card input fields in your page:\n\n```html\n<div id=\"card-details\"></div>\n```\n\n```javascript\nSurfboardOnlineSDK.mount({\n    mountCardWidget: \"card-details\",\n});\n\n// When the customer clicks \"Pay\":\nawait SurfboardOnlineSDK.order.initiatePayments(\"CARD\");\n```\n\n### Swish Payments\n\n```javascript\nconst paymentAttempt = await SurfboardOnlineSDK.order.initiatePayments(\"NSWISH\");\n\n// For mobile: redirect to Swish app\nconst redirectUrl = paymentAttempt.getSwishAppRedirectUrl(\"https://your-site.com/callback\");\n\n// For web: display QR code\nconst qrData = paymentAttempt.getSwishQRData;\n```\n\n### Apple Pay\n\n```html\n<div id=\"apple-pay\"></div>\n```\n\n```javascript\nSurfboardOnlineSDK.mount({\n    mountApplePayWidget: \"apple-pay\",\n});\n// Payment is initiated automatically when the customer clicks the Apple Pay button\n```\n\nFor Apple Pay, you must host the domain association file at `/.well-known/apple-developer-merchantid-domain-association` on your domain.\n\n### Klarna\n\n```javascript\nawait SurfboardOnlineSDK.order.addCustomerInformation({\n    phone: { countryCode: \"+46\", number: \"701234567\" },\n    name: \"Jane Doe\",\n    email: \"jane@example.com\",\n    billingAddress: {\n        city: \"Stockholm\",\n        postalCode: \"11122\",\n        countryCode: \"SE\",\n        addressLine1: \"Main Street 1\",\n    },\n});\n\nawait SurfboardOnlineSDK.order.initiatePayments(\"KLARNA\");\n```\n\n## Payment Error Codes\n\n| Code | Message | Category |\n|------|---------|----------|\n| ON_009 | Phone number required for Swish payment | Non Fatal |\n| ON_010 | Payment method not supported for this order | Non Fatal |\n| ON_011 | Payment already completed | Non Fatal |\n| ON_012 | Error initiating payment -- retry | Non Fatal |\n| ON_013 | Unknown error -- page reload may help | Non Fatal |\n| ON_016 | Invalid card details | Non Fatal |\n| ON_017 | Email required for this payment | Non Fatal |\n| ON_018 | Billing address required for this payment | Non Fatal |\n\n## Reference\n\n- [React Sample App](https://github.com/surfboardpayments/react-next-online-sdk) for a complete working example\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Fetch Terminal API](https://developers.surfboardpayments.com/api/terminals)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Server-to-Server API",
      "slug": "server-to-server-api",
      "url": "/developers/guides/server-to-server-api",
      "description": "Process online payments entirely from your backend with Merchant Initiated Transactions. Full control over recurring payments, subscriptions, and tokenized card flows.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Server-to-Server",
        "MIT",
        "Tokenization"
      ],
      "content": "## Overview\n\nMerchant Initiated Transactions (MIT) allow you to initiate payments on behalf of customers entirely from your backend. This is the foundation for subscription billing, recurring charges, and any scenario where you need to charge a stored card without the customer being present.\n\nThe flow has two stages: the customer completes an initial payment (which tokenizes their card), then you use the stored token for subsequent charges.\n\n## Prerequisites\n\n1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)\n2. Complete onboarding (merchant and store setup)\n3. Two terminal IDs:\n   - A **PaymentPage** or **SelfHostedPage** terminal for the initial customer payment\n   - A **MerchantInitiated** terminal for subsequent server-to-server payments\n\nAn online store comes with both a **PaymentPage** and a **MerchantInitiated** terminal already provisioned, so for the default setup this is a fetch, not a registration — list the store's terminals and take the two IDs. Only **SelfHostedPage** needs [registering](/developers/guides/terminal-device-management), and only if you are collecting the first payment on your own page.\n\n## Stage 1: Initial Customer Payment\n\nThe first payment must be initiated by the customer. This step collects and tokenizes the card details.\n\n### Step 1: Create Order with Tokenization\n\nUse the [Create Order API](https://developers.surfboardpayments.com/api/orders) with `enforceTokenization` set to `true`:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_PAYMENT_PAGE_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"SUB-001\",\n      \"name\": \"Monthly Subscription\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"total\": 29900,\n        \"currency\": \"752\"\n      }\n    }\n  ],\n  \"controlFunctions\": {\n    \"enforceTokenization\": true,\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\",\n      \"amount\": 29900\n    }\n  }\n}\n```\n\n### Step 2: Customer Completes Payment\n\nThe customer enters their card details on the payment page or your self-hosted checkout. Once payment completes, the card is tokenized and saved against the order.\n\n### Step 3: Retrieve the Token\n\nAfter the payment completes, call the [Fetch Tokens from Orders API](https://developers.surfboardpayments.com/api/orders) to retrieve the `tokenId` and card information:\n\n```json\nGET /orders/:orderId/tokens\n```\n\nStore the `tokenId` securely against the customer in your system. You will use it for all future charges.\n\n> **Warning:** Store tokens securely on your backend. Never expose token IDs to the client or include them in frontend code.\n\n## Stage 2: Merchant Initiated Payments\n\nWith the `tokenId` stored, you can now charge the customer from your backend at any time.\n\n### Step 1: Create an Order\n\nCreate a new order using the **MerchantInitiated** terminal:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_MIT_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"SUB-002\",\n      \"name\": \"Monthly Subscription - February\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"total\": 29900,\n        \"currency\": \"752\"\n      }\n    }\n  ]\n}\n```\n\n### Step 2: Initiate Payment with Token\n\nUse the [Initiate Payment API](https://developers.surfboardpayments.com/api/payments) with the stored `tokenId`:\n\n```bash\ncurl -X POST YOUR_API_URL/payments \\\n  -H 'Content-Type: application/json' \\\n  -H 'API-KEY: YOUR_API_KEY' \\\n  -H 'API-SECRET: YOUR_API_SECRET' \\\n  -H 'MERCHANT-ID: YOUR_MERCHANT_ID' \\\n  -d '{\n    \"orderId\": \"YOUR_ORDER_ID\",\n    \"paymentMethod\": \"CTOKEN\",\n    \"tokenId\": \"YOUR_TOKEN_ID\"\n  }'\n```\n\n### Step 3: Check Order Status\n\nVerify the payment result using the [Fetch Order Status API](https://developers.surfboardpayments.com/api/orders):\n\n```json\nGET /orders/:orderId/status\n```\n\nThe status will be `PAYMENT_COMPLETED` on success or `PAYMENT_CANCELLED` / `PAYMENT_FAILED` otherwise. You can also receive real-time updates via webhooks.\n\n## Common Use Cases\n\n| Use Case | Description |\n|----------|-------------|\n| **Subscriptions** | Charge customers monthly/yearly on a schedule |\n| **Metered billing** | Charge variable amounts based on usage |\n| **Retry failed payments** | Re-attempt a charge after a soft decline |\n| **Installments** | Split a large payment into scheduled charges |\n\n## Post-Payment Operations\n\nPost-payment operations (refunds, receipts, reporting) work the same way as other online payment modes. Use the standard APIs:\n\n- [Receipts API](https://developers.surfboardpayments.com/api/receipts) for sending digital receipts\n- [Orders API](https://developers.surfboardpayments.com/api/orders) for refunds and order management\n\n## Reference\n\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)\n- [Token Management](https://developers.surfboardpayments.com/api/orders)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Create an Order",
      "slug": "create-an-order",
      "url": "/developers/guides/create-an-order",
      "description": "Learn how to create orders with line items, tax, customer details, and control functions. The starting point for accepting payments with the Surfboard API.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Orders",
        "In-Store"
      ],
      "content": "## Overview\n\nAn 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.\n\nThis guide covers basic order creation, line items, customer details, tax handling, and common control functions.\n\n## Prerequisites\n\n1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)\n2. Complete onboarding (merchant and store setup)\n3. 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.\n\n## Basic Order\n\nOrder, 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.\n\nCreate an order with a single line item and initiate payment:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"Nike Shoes\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 50000,\n        \"total\": 50000,\n        \"currency\": \"752\",\n        \"tax\": [\n          { \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }\n        ]\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 50000,\n    \"total\": 50000,\n    \"currency\": \"752\",\n    \"tax\": [\n      { \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }\n    ]\n  },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\"\n    }\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderId\": \"83a1ba32774149710b\",\n    \"paymentId\": \"83a1ba3264bd500106\"\n  },\n  \"message\": \"Order created successfully\"\n}\n```\n\nStore both `orderId` and `paymentId` -- you need them for status checks, captures, voids, and refunds.\n\n## Line Items\n\nEvery order requires at least one line item in the `orderLines` array. Each line item must include:\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `id` | Yes | Unique line item identifier |\n| `name` | Yes | Product name |\n| `quantity` | Yes | Quantity (negative for refunds) |\n| `amount.regular` | Yes | Unit price in smallest currency unit |\n| `amount.total` | Yes | **Unit** price after shipping and campaign (`regular + shipping - campaign`). Not the line total |\n| `amount.currency` | Yes | Numeric ISO 4217 code (e.g., `\"752\"` for SEK) |\n| `amount.tax` | Yes | Tax array for the line. Required even at zero rate -- send a `0` entry rather than omitting it |\n\nOptional fields include `description`, `brand`, `imageUrl`, `gtin`, `categoryId`, `unit`, and `metadata`.\n\n> **`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>`.\n\nTwo lines, one of them with a quantity above one:\n\n```json\n\"orderLines\": [\n  {\n    \"id\": \"ITEM-001\",\n    \"name\": \"Flat white\",\n    \"quantity\": 2,\n    \"amount\": {\n      \"regular\": 4500,\n      \"total\": 4500,\n      \"currency\": \"752\",\n      \"tax\": [{ \"amount\": 900, \"percentage\": 25, \"type\": \"VAT\" }]\n    }\n  },\n  {\n    \"id\": \"ITEM-002\",\n    \"name\": \"Gift card\",\n    \"quantity\": 1,\n    \"amount\": {\n      \"regular\": 10000,\n      \"total\": 10000,\n      \"currency\": \"752\",\n      \"tax\": [{ \"amount\": 0, \"percentage\": 0, \"type\": \"VAT\" }]\n    }\n  }\n]\n```\n\nThe 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')`.\n\n> **Currency format:** All amounts use the smallest currency unit. For example, 10.00 SEK = `1000`, 5.00 EUR = `500`.\n\n> **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.\n\n## Customer, Billing, and Shipping\n\nInclude customer, billing, and shipping details when available:\n\n```json\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"customer\": {\n    \"person\": {\n      \"name\": { \"firstName\": \"John\", \"lastName\": \"Doe\" },\n      \"email\": \"john@example.com\",\n      \"phoneNumber\": { \"code\": \"46\", \"number\": \"768100190\" }\n    },\n    \"company\": {\n      \"vatId\": \"SE556026998601\"\n    }\n  },\n  \"billing\": {\n    \"name\": { \"firstName\": \"John\", \"lastName\": \"Doe\" },\n    \"phoneNumber\": { \"code\": \"46\", \"number\": \"768100190\" },\n    \"address\": {\n      \"addressLine1\": \"Storgatan 1\",\n      \"city\": \"Stockholm\",\n      \"postalCode\": \"11122\",\n      \"countryCode\": \"SE\"\n    }\n  },\n  \"shipping\": {\n    \"name\": { \"firstName\": \"John\", \"lastName\": \"Doe\" },\n    \"phoneNumber\": { \"code\": \"46\", \"number\": \"768100190\" },\n    \"address\": {\n      \"addressLine1\": \"Storgatan 1\",\n      \"city\": \"Stockholm\",\n      \"postalCode\": \"11122\",\n      \"countryCode\": \"SE\"\n    }\n  },\n  \"orderLines\": [...]\n}\n```\n\nAll customer fields are optional but recommended for invoice payments, fraud prevention, and receipt delivery.\n\n## Order Line Level Calculation\n\nThe `orderLineLevelCalculation` control function changes how `totalOrderAmount` is computed from line items.\n\n| Setting | Formula | Example |\n|---------|---------|---------|\n| `false` (default) | Sum of `(total * quantity)` per line | `(50 * 2) + (150 * 1) = 250` |\n| `true` (recommended) | Sum of `((regular * quantity) - campaign + shipping)` per line | `((200 * 2) - 100 + 50) = 350` |\n\nEnable it when your line items have campaigns or shipping costs:\n\n```json\n{\n  \"controlFunctions\": {\n    \"orderLineLevelCalculation\": true,\n    \"initiatePaymentsOptions\": { \"paymentMethod\": \"CARD\" }\n  }\n}\n```\n\n## Adjustments\n\nAdjustments modify the total order value for tips, donations, gift cards, or discounts:\n\n```json\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [...],\n  \"adjustments\": [\n    { \"type\": \"TIP\", \"value\": 1000 }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 50000,\n    \"total\": 51000,\n    \"currency\": \"752\"\n  },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": { \"paymentMethod\": \"CARD\" }\n  }\n}\n```\n\nThe `totalOrderAmount.total` should reflect the adjusted amount (regular + adjustments).\n\n## Delay Capture\n\nTo authorize payment now but capture funds later (e.g., at shipment), set `delayCapture: true`:\n\n```json\n{\n  \"controlFunctions\": {\n    \"delayCapture\": true,\n    \"initiatePaymentsOptions\": { \"paymentMethod\": \"CARD\" }\n  }\n}\n```\n\nYou 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.\n\nSee the [Capture a Payment](/developers/guides/capture-a-payment) guide for the full flow.\n\n## Check Order Status\n\nAfter creating an order, check its status at any time:\n\n```json\nGET /orders/:orderId/status\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderStatus\": \"PAYMENT_COMPLETED\",\n    \"payments\": [\n      {\n        \"paymentId\": \"83a1ba3264bd500106\",\n        \"paymentStatus\": \"PAYMENT_COMPLETED\",\n        \"paymentMethod\": \"CARD\",\n        \"amount\": 50000\n      }\n    ],\n    \"paymentIds\": [\"83a1ba3264bd500106\"]\n  }\n}\n```\n\n**Order statuses:** `PENDING` | `PAYMENT_COMPLETED` | `PAYMENT_CANCELLED` | `PARTIAL_PAYMENT_COMPLETED` | `PAYMENT_PROCESSED`\n\n**Payment statuses:** `PAYMENT_INITIATED` | `PAYMENT_PROCESSING` | `PAYMENT_PROCESSED` | `PAYMENT_COMPLETED` | `PAYMENT_FAILED` | `PAYMENT_CANCELLED`\n\nEvery payment ends in one of three terminal states:\n\n| Payment Status | Order Status | Description |\n|----------------|--------------|-------------|\n| `PAYMENT_COMPLETED` | `PAYMENT_COMPLETED` | Payment succeeded -- the order is closed. |\n| `PAYMENT_CANCELLED` | `PENDING` | Payment was cancelled -- the order remains open and a new payment can be initiated using the existing `orderId`. |\n| `PAYMENT_FAILED` | `PENDING` | Payment failed -- the order remains open and a new payment can be initiated using the existing `orderId`. |\n\n## Error Handling\n\nCreate 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).\n\nSee 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.\n\n## Next Steps\n\nOnce you have an order created, you can:\n\n- [Capture a Payment](/developers/guides/capture-a-payment) -- finalize a delayed-capture authorization\n- [Cancel a Payment](/developers/guides/cancel-a-payment) -- stop an in-progress payment\n- [Void a Payment](/developers/guides/void-a-payment) -- reverse a completed payment before settlement\n- [Refund an Order](/developers/guides/refund-an-order) -- return funds after settlement\n- [Partial Payments](/developers/guides/partial-payments) -- split an order across multiple payments\n\n## Reference\n\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Payments API](https://developers.surfboardpayments.com/api/payments)\n- [Create Order Error Codes](/developers/guides/create-order-error-codes)\n- [Payment Lifecycle](/developers/guides/payment-lifecycle)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Merchant Onboarding",
      "slug": "merchant-onboarding",
      "url": "/developers/guides/merchant-onboarding",
      "description": "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.",
      "category": "online",
      "tags": [
        "Onboarding",
        "Merchant",
        "KYB",
        "Prefill",
        "MCC",
        "Store",
        "Partners",
        "API"
      ],
      "content": "## Overview\n\nMerchant 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.\n\nWhen you pre-enter the merchant's details, Surfboard does two things before returning the link:\n\n1. **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.\n2. **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.\n\nThe 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.\n\nIf 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.\n\nThe typical flow is:\n\n1. **Create Merchant** -- submit the merchant's details and receive the web KYB link\n2. **Merchant completes the web KYB** -- confirms the prefilled data, adds bank account and documents, signs\n3. **Check Application Status** -- poll for the result or listen for webhooks\n4. **Store Setup** -- optionally create additional stores after onboarding completes\n\n## Prerequisites\n\nBefore onboarding merchants:\n\n1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)\n2. Obtain your `partnerId` from the Developer Portal Console\n3. Generate API credentials (API key and secret)\n\nMerchant applications in test and demo environments are approved automatically.\n\n## Step 1: Create a Merchant Application\n\nSend 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.\n\n```\nPOST /partners/{partnerId}/merchants\n```\n\nThe body has three parts:\n\n- `country` and `organisation` -- who the merchant is. Required.\n- `controlFields` -- how the onboarding should behave (store, acquirer, flags). Optional.\n- `controlFields.preEnteredInformation` -- the data you prefill on the merchant's behalf. Optional, but this is what unlocks the accelerated flow.\n\n### The minimum request\n\nCountry 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:\n\n```json\n{\n  \"country\": \"SE\",\n  \"organisation\": {\n    \"corporateId\": \"5591631360\"\n  }\n}\n```\n\n`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.\n\n### Create the first store in the same call\n\nInclude `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.\n\n```json\n{\n  \"country\": \"SE\",\n  \"organisation\": {\n    \"corporateId\": \"5591631360\"\n  },\n  \"controlFields\": {\n    \"store\": {\n      \"name\": \"Main Street Store\",\n      \"email\": \"store@example.com\",\n      \"phoneNumber\": {\n        \"code\": \"46\",\n        \"number\": \"701234567\"\n      },\n      \"address\": {\n        \"addressLine1\": \"Main Street 123\",\n        \"city\": \"Stockholm\",\n        \"countryCode\": \"SE\",\n        \"postalCode\": \"123 45\"\n      },\n      \"paymentChannels\": { \"physical\": true, \"online\": false }\n    }\n  }\n}\n```\n\nFor 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.\n\n```json\n{\n  \"country\": \"SE\",\n  \"organisation\": {\n    \"corporateId\": \"5591631360\"\n  },\n  \"controlFields\": {\n    \"disableFields\": { \"onlineInfo\": true },\n    \"store\": {\n      \"name\": \"My Webshop\",\n      \"email\": \"shop@example.com\",\n      \"address\": {\n        \"addressLine1\": \"Main Street 123\",\n        \"city\": \"Stockholm\",\n        \"countryCode\": \"SE\",\n        \"postalCode\": \"123 45\"\n      },\n      \"paymentChannels\": { \"physical\": false, \"online\": true },\n      \"onlineInfo\": {\n        \"merchantWebshopURL\": \"https://shop.example.com\",\n        \"paymentPageHostURL\": \"https://pay.example.com\",\n        \"termsAndConditionsURL\": \"https://shop.example.com/terms\",\n        \"privacyPolicyURL\": \"https://shop.example.com/privacy\"\n      }\n    }\n  }\n}\n```\n\n### Prefill the application\n\nEverything 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.\n\n**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.\n\n**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`.\n\n> **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.\n\nThe 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.\n\n**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.\n\n### Full example\n\nA 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`.\n\n```json\n{\n  \"country\": \"DK\",\n  \"localeSelected\": \"da\",\n  \"organisation\": {\n    \"corporateId\": \"12345678\"\n  },\n  \"controlFields\": {\n    \"generateShortLink\": true,\n    \"store\": {\n      \"name\": \"Havnens Café\",\n      \"email\": \"hello@havnenscafe.dk\",\n      \"phoneNumber\": { \"code\": \"45\", \"number\": \"31234567\" },\n      \"address\": {\n        \"addressLine1\": \"Havnegade 12\",\n        \"city\": \"København\",\n        \"countryCode\": \"DK\",\n        \"postalCode\": \"1058\"\n      },\n      \"paymentChannels\": { \"physical\": true, \"online\": true, \"physicalSharePercent\": 80 }\n    },\n    \"preEnteredInformation\": {\n      \"businessDescription\": \"Selling coffee, pastries and light lunches at our harbourside café.\",\n      \"applicant\": {\n        \"email\": \"owner@havnenscafe.dk\",\n        \"name\": \"Mette Jensen\",\n        \"isSignatory\": true,\n        \"isUbo\": true,\n        \"ownershipPercent\": 100,\n        \"ownershipType\": \"direct\"\n      },\n      \"ubos\": [\n        {\n          \"name\": \"Lars Holm\",\n          \"email\": \"lars@example.dk\",\n          \"ownershipPercent\": 0,\n          \"ownershipType\": \"indirect\",\n          \"entityName\": \"Holm Holding ApS\"\n        }\n      ],\n      \"openingInfo\": {\n        \"isOpenAllYear\": true,\n        \"isSeasonalOpen\": false,\n        \"monthsOpen\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],\n        \"isStoreOpenAtNight\": false,\n        \"reasonForOpeningAtNight\": \"\"\n      },\n      \"giftcards\": { \"revenueSharePercent\": 15, \"averageValidDays\": 365 },\n      \"fundsInfo\": {\n        \"averageTransactionValuePerDay\": 4000,\n        \"estimatedAmountPerYear\": 1200000,\n        \"priceOfMostExpensiveItemSold\": 250,\n        \"estimatedAmountPerTransaction\": 95,\n        \"estimatedFrequencyOfTransactions\": \"DAILY\"\n      }\n    }\n  }\n}\n```\n\n### Response\n\nA 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.\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Merchant application created successfully.\",\n  \"data\": {\n    \"applicationId\": \"845adba035abb00310\",\n    \"webKybUrl\": \"https://onboarding.surfboard.se/845adba035abb00310?pi=…\",\n    \"shortLinkUrl\": \"https://sb.fyi/abcd12\",\n    \"validUntil\": \"2026-12-01T00:00:00.000Z\",\n    \"merchantId\": \"83af75d53169b0070e\",\n    \"storeId\": \"845adbc0a3f2b00711\"\n  }\n}\n```\n\nShare `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.\n\n### Key fields\n\n| Field | Description |\n|-------|-------------|\n| `country` | Two-letter ISO country code: `SE`, `NO`, `DK`, `FI` or `IE`. Required. |\n| `localeSelected` | Language of the web KYB (`sv`, `da`, `fi`, `en`). Defaults to the country's language. |\n| `organisation.corporateId` | The merchant's corporate or organisation number. Required. |\n| `organisation.legalName`, `organisation.address` | Resolved from the registry if omitted. Mandatory for Payment Facilitator (PF) partners. |\n| `organisation.mccCode` | Merchant Category Code, if you already know it. Otherwise derived from `businessDescription`. |\n| `controlFields.store` | Create a store during onboarding (recommended). |\n| `controlFields.store.paymentChannels` | Whether the merchant takes payments in person, online, or both. |\n| `controlFields.preEnteredInformation` | Business description, people and trading details to prefill. |\n| `controlFields.disableFields.onlineInfo` | Lock the webshop URLs against merchant edits. Requires `store.onlineInfo`. |\n| `controlFields.showProductCatalogue` | Show the terminal catalogue step. Requires the catalogue to be enabled for your programme. |\n| `controlFields.preSelectProducts` | Pre-select terminals to ship automatically. |\n| `controlFields.linkUsers` | Existing user IDs to link to the new merchant. |\n| `controlFields.redirectUrl` | Where to send the merchant after they finish the web KYB. |\n| `controlFields.generateShortLink` | Set `true` to also receive a shortened link. |\n| `controlFields.merchantConfig.settlementFrequency` | Payout cadence: `daily`, `weekly`, `monthly` and more. |\n| `controlFields.acquirerConfig`, `controlFields.directMerchantCreation` | PF programmes and direct acquirer agreements only. Leave unset otherwise. |\n\n### Pre-selecting terminals\n\nYou 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`:\n\n```json\n{\n  \"controlFields\": {\n    \"showProductCatalogue\": true,\n    \"preSelectProducts\": [\n      {\n        \"productId\": \"PRODUCT_ID\",\n        \"quantity\": \"2\",\n        \"pricingPlanId\": \"PLAN_ID\"\n      }\n    ]\n  }\n}\n```\n\n### Linking a service provider\n\nIf 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).\n\n## Step 2: The Merchant Completes the Web KYB\n\nThe merchant opens `webKybUrl` and, because you prefilled the rest, only needs to:\n\n1. **Confirm the prefilled company and people**, already populated from the registry and your data\n2. **Add their bank account** for settlement\n3. **Upload any required documents** for their business category, determined automatically from `businessDescription`\n4. **Complete signing**: each signatory and beneficial owner verifies their identity and e-signs\n\nSigning 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.\n\n## Step 3: Check Application Status\n\nPoll 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.\n\n```\nGET /partners/{partnerId}/merchants/{applicationId}/status\n```\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"applicationId\": \"845adba035abb00310\",\n    \"webKybUrl\": \"https://onboarding.surfboard.se/845adba035abb00310?pi=…\",\n    \"applicationStatus\": \"APPLICATION_SUBMITTED\",\n    \"merchantId\": \"83af75d53169b0070e\",\n    \"storeId\": \"845adbc0a3f2b00711\",\n    \"onlineOnboardingStatus\": \"PENDING\",\n    \"billingPlans\": [],\n    \"paymentMethods\": [\n      { \"paymentMethod\": \"card\", \"enabledSchemes\": [\"VISA\", \"MASTERCARD\"], \"status\": \"ACTIVE\" }\n    ],\n    \"domainVerification\": []\n  },\n  \"message\": \"Application status fetched successfully\"\n}\n```\n\n### Application statuses\n\n| Status | Description |\n|--------|-------------|\n| `APPLICATION_INITIATED` | Application created; the merchant has not started. |\n| `APPLICATION_STARTED` | The merchant has opened the link and begun. |\n| `APPLICATION_SUBMITTED` | The merchant has submitted all information. |\n| `APPLICATION_PENDING_INFORMATION` | Awaiting additional information or documents from the merchant. |\n| `APPLICATION_SIGNED` | All required signatories and beneficial owners have signed. |\n| `APPLICATION_REJECTED` | Application rejected. |\n| `APPLICATION_EXPIRED` | The link expired before the application was completed. Create a new application. |\n| `APPLICATION_COMPLETED` | Compliance review passed; the merchant is being created. |\n| `MERCHANT_CREATED` | The merchant is live and can transact. `merchantId` and `storeId` are returned. |\n\n> **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).\n\n## Step 4: Create Additional Stores\n\nA default store is typically created during onboarding. If the merchant needs additional stores, use the Create Store API:\n\n```\nPOST /partners/{partnerId}/merchants/{merchantId}/stores\n```\n\n```json\n{\n  \"storeName\": \"Second Location\",\n  \"email\": \"store2@example.com\",\n  \"phoneNumber\": {\n    \"code\": 46,\n    \"number\": \"709876543\"\n  },\n  \"address\": \"Second Street 456\",\n  \"city\": \"Gothenburg\",\n  \"zipCode\": \"411 01\",\n  \"country\": \"SE\"\n}\n```\n\nFor an online store, add `onlineInfo` with your webshop URLs:\n\n```json\n{\n  \"storeName\": \"Online Store\",\n  \"email\": \"online@example.com\",\n  \"phoneNumber\": {\n    \"code\": 46,\n    \"number\": \"709876543\"\n  },\n  \"address\": \"Main Street 123\",\n  \"city\": \"Stockholm\",\n  \"zipCode\": \"103 16\",\n  \"country\": \"SE\",\n  \"onlineInfo\": {\n    \"merchantWebshopURL\": \"https://shop.example.com\",\n    \"termsAndConditionsURL\": \"https://shop.example.com/terms\",\n    \"privacyPolicyURL\": \"https://shop.example.com/privacy\"\n  }\n}\n```\n\n### Domain Verification (Online Stores)\n\nOnline stores in production require domain verification before they can process payments:\n\n1. **Get verification keys** -- returned in the Create Store response (`merchantURLDomainVerificationKey` and `paymentPageURLDomainVerificationKey`)\n2. **Add DNS TXT record** -- add the verification key as a TXT record on your domain\n3. **Trigger verification** -- Surfboard checks automatically every 6 hours, or use the Verify Domain API to trigger it manually\n4. **Monitor status** -- use the Fetch Store Details API to check the `onlineOnboardingStatus` field\n\n> **Note:** Domain verification is only required in production (not in demo/sandbox). A verified domain applies to all merchants under the same partner account.\n\n## Notes and Behaviours\n\n- **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.\n- **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.\n- **Prefill is additive.** Anything you omit is collected from the merchant in the flow; nothing you prefill is discarded.\n- **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.\n\n## Reference\n\n- [Create Merchant API](https://developers.surfboardpayments.com/api/merchants)\n- [Check Application Status API](https://developers.surfboardpayments.com/api/merchants)\n- [Create Store API](https://developers.surfboardpayments.com/api/stores)\n- [Verify Domain API](https://developers.surfboardpayments.com/api/stores)\n- [Webhook Reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)\n- [Service Providers & Split Payouts](/developers/guides/service-providers)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Device Registration",
      "slug": "device-registration",
      "url": "/developers/guides/device-registration",
      "description": "Register a terminal to a store before it can take payments or receive a partner POS app. Covers rotating codes, QR/registration links, pre-shipped codes, and in-app registration for SoftPOS.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "Terminal",
        "API",
        "Device Management",
        "Onboarding"
      ],
      "content": "## Overview\n\nBefore a terminal can accept payments -- or receive a partner POS app -- it must be **registered to a store** under a merchant. Registration links the physical (or software) device to your merchant hierarchy. Once a terminal is registered to a store, it cannot be repurposed by another merchant. You can still move it between stores under the same merchant using the [Change Store](/developers/guides/terminal-device-management) endpoint.\n\nSurfboard supports several registration methods so you can pick the lowest-friction option for your setup. They are **not mutually exclusive** -- the same terminal can be registered through whichever path the merchant has available.\n\n> If you integrate through one of our SDKs (Android SoftPOS, Tap to Pay on iPhone), the SDK handles registration for you and you do not need to call the registration APIs directly. See [Android SoftPOS SDK](/developers/guides/android-softpos-sdk) and [Tap to Pay on iPhone](/developers/guides/tap-to-pay-iphone).\n\n## When Is Registration Needed?\n\n| Terminal type | Registration required? | Typical method |\n|---------------|------------------------|----------------|\n| **EMV** (countertop, mobile POS, kiosk) | Yes -- the terminal must be registered before it can receive a partner POS app or take payments | Rotating code, QR/registration link, or pre-shipped code |\n| **SoftPOS** (CheckoutX on Android) | Yes | In-app (interapp) registration, or any of the code-based methods |\n| **SDK-based** (Android SoftPOS SDK, Tap to Pay on iPhone) | Handled by the SDK | N/A -- SDK methods cover registration |\n\n## Registration Methods at a Glance\n\n| Method | How the merchant registers | Code validity | Best for |\n|--------|----------------------------|---------------|----------|\n| **Rotating 6-digit code** | Reads a code from the terminal screen and enters it into the Surfboard merchant portal or the partner's portal/app | ~90 seconds (rotates) | Attended setup where someone is in front of both the terminal and a portal |\n| **QR / registration link** | Taps the QR icon on the terminal's registration screen and scans a QR generated by the partner or Surfboard merchant portal | Short-lived | Fast, near zero-touch scan-to-register |\n| **Pre-shipped code** | Enters a code provided ahead of time (e.g. a welcome email or SMS) directly on the terminal registration screen | Longer-lived | Unattended setup, or when the 90-second rotation is impractical |\n| **In-app (interapp)** | Switches to CheckoutX, which registers the device automatically -- no code entry | n/a | SoftPOS / CheckoutX only |\n\n---\n\n## Method 1 -- Rotating 6-Digit Code\n\nWhen a terminal starts up on its registration screen, it displays a **6-digit code that rotates roughly every 90 seconds**. The merchant reads this code and enters it into a registration screen -- either the **Surfboard merchant portal** or the **partner's own portal or app**.\n\nBehind the scenes, the portal calls the **Register Device** API with the code as the `registrationIdentifier`:\n\n```\nPOST /merchants/:merchantId/stores/:storeId/devices\n```\n\n```json\n{\n  \"registrationIdentifier\": \"250901\",\n  \"terminalName\": \"Kiosk One\"\n}\n```\n\n| Parameter | Required | Description |\n|-----------|----------|-------------|\n| `registrationIdentifier` | Yes | The 6-digit code shown on the terminal. For SurfPad and Printer devices, use the serial number printed on the back instead. |\n| `terminalName` | No | A human-readable label to identify the terminal. |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"terminalId\": \"813ca2cb12ce400405\",\n    \"registrationStatus\": \"REGISTERED\"\n  },\n  \"message\": \"Terminal registered successfully\"\n}\n```\n\n`registrationStatus` is either `REGISTERED` (new device) or `ALREADY_REGISTERED` (device was previously linked). Store the returned `terminalId` -- you need it for all subsequent calls on this device.\n\n> Because the code rotates every ~90 seconds, complete the entry promptly. If it expires, read the new code from the screen and try again.\n\nSee the [Register Device API reference](https://developers.surfboardpayments.com/references/api/terminals/register-device) for the full request/response and error codes.\n\n---\n\n## Method 2 -- QR Code / Registration Link\n\nThe terminal's registration screen also shows a **QR icon**. Tapping it opens the camera so the merchant can scan a QR code provided by the **partner** or by the **Surfboard merchant portal**. The QR encodes a `registrationLink` -- a deep link that registers the device automatically, with no manual code entry.\n\nGenerate the link with the **Get Device Registration Code** API:\n\n```\nGET /merchants/:merchantId/stores/:storeId/device-registration\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"registrationCode\": \"905788\",\n    \"registrationLink\": \"checkoutx://com.surfboard.checkoutx/register?data=eyJyZWdpc3RyYXRpb25Db2RlIjoiOTA1Nzg4In0=\"\n  },\n  \"message\": \"Registration Code Generated Successfully\"\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `registrationCode` | A 6-digit code the merchant can enter manually (see Method 3). |\n| `registrationLink` | A deep link that, encoded as a QR code, registers the device when scanned. |\n\nRender `registrationLink` as a QR code in your partner portal or app, or let the Surfboard merchant portal generate it for you. The merchant scans it from the terminal's registration screen and the device registers itself.\n\nSee the [Get Device Registration Code API reference](https://developers.surfboardpayments.com/references/api/terminals/get-device-registration-code).\n\n---\n\n## Method 3 -- Pre-Shipped Code\n\nThe same **Get Device Registration Code** API also returns a `registrationCode`. Unlike the rotating on-screen code, this code has **longer validity**, which makes it a good fit for setups where the ~90-second rotation is impractical.\n\nA common pattern: a partner generates the code ahead of time and delivers it to the merchant out of band -- for example in a **welcome email or SMS** sent before the terminal ships. When the device arrives, the merchant simply enters the code on the terminal's registration screen and the device registers.\n\nThis supports a near zero-touch onboarding experience: the merchant never has to coordinate a live, time-limited code between the terminal and a portal.\n\n---\n\n## Method 4 -- In-App (Interapp) Registration for SoftPOS\n\nFor **SoftPOS** running CheckoutX, you can register a device by switching into the CheckoutX app -- **no code entry needed**. Your app opens CheckoutX via a deep link, CheckoutX registers the terminal, and control returns to your app with the resulting `terminalId`.\n\nThis is the smoothest option for SoftPOS and partner POS apps that already integrate with CheckoutX. It is **not available for EMV terminals**, which must use one of the code- or QR-based methods above.\n\nFor the full app-switch flow -- generating the interapp code, building the deep link, and handling the callback -- see the [Inter-App Integration](/developers/guides/interapp-integration) guide.\n\n---\n\n## After Registration\n\nOnce a terminal is registered:\n\n- Use the returned `terminalId` to create orders and initiate payments. See [Create an Order](/developers/guides/create-an-order).\n- Apply configuration (network, language, restart schedule, and more) -- see [Terminal & Device Management](/developers/guides/terminal-device-management).\n- Move the terminal between stores under the same merchant with the Change Store endpoint, or reassign across merchants (partner-managed inventory) with Move Terminal -- both covered in [Terminal & Device Management](/developers/guides/terminal-device-management).\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Register a device | POST | `/merchants/:merchantId/stores/:storeId/devices` |\n| Get registration code & link | GET | `/merchants/:merchantId/stores/:storeId/device-registration` |\n| Get interapp code (SoftPOS) | GET | `/merchants/:merchantId/stores/:storeId/terminals/interapp` |\n\n## Reference\n\n- [Register Device API](https://developers.surfboardpayments.com/references/api/terminals/register-device)\n- [Get Device Registration Code API](https://developers.surfboardpayments.com/references/api/terminals/get-device-registration-code)\n- [Inter-App Integration](/developers/guides/interapp-integration)\n- [EMV Terminal Integration](/developers/guides/emv-terminal-integration)\n- [Terminal & Device Management](/developers/guides/terminal-device-management)\n</content>\n</invoke>"
    },
    {
      "title": "Payment Lifecycle",
      "slug": "payment-lifecycle",
      "url": "/developers/guides/payment-lifecycle",
      "description": "Manage the full payment lifecycle from order creation through capture, void, cancel, and refund operations using the Surfboard Payments API.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Payments",
        "Refunds",
        "Capture"
      ],
      "content": "## Overview\n\nEvery 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.\n\n## Lifecycle at a Glance\n\n| Operation | When to Use | Endpoint | Method |\n|-----------|-------------|----------|--------|\n| **Create Order** | Start a new payment | `/orders` | POST |\n| **Capture** | Finalize a delayed-capture auth | `/payments/:paymentId/capture` | POST |\n| **Void** | Reverse before settlement | `/payments/:paymentId/void` | POST |\n| **Cancel** | Stop before completion | `/payments/:paymentId` | DELETE |\n| **Refund** | Full return after settlement | `/orders` | POST |\n| **Partial Refund** | Partial return after settlement | `/orders` | POST |\n\n## Order and Payment Statuses\n\n**Order statuses:** `PENDING` | `PAYMENT_COMPLETED` | `PAYMENT_CANCELLED` | `PARTIAL_PAYMENT_COMPLETED` | `PAYMENT_PROCESSED`\n\n**Payment statuses:** `PAYMENT_INITIATED` | `PAYMENT_PROCESSING` | `PAYMENT_PROCESSED` | `PAYMENT_COMPLETED` | `PAYMENT_FAILED` | `PAYMENT_CANCELLED`\n\n### Status Flow\n\nA payment moves through progressive statuses before settling into one of three final (terminal) states. The happy path is:\n\n```\nPAYMENT_INITIATED → PAYMENT_PROCESSING → PAYMENT_PROCESSED → PAYMENT_COMPLETED\n                                                           → PAYMENT_FAILED\n                                                           → PAYMENT_CANCELLED\n```\n\n`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.\n\n#### Progressive statuses\n\n| Payment Status | Description |\n|----------------|-------------|\n| `PAYMENT_INITIATED` | Payment has been created on the order and is awaiting processing. Can transition to `PAYMENT_PROCESSING`, `PAYMENT_CANCELLED`, or `PAYMENT_FAILED`. |\n| `PAYMENT_PROCESSING` | Payment is actively being processed by the network. |\n| `PAYMENT_PROCESSED` | Payment has been authorised and processed, but is not yet in its final state (for example, awaiting capture or confirmation). |\n\n#### Order-level intermediate status\n\n| Order Status | Description |\n|--------------|-------------|\n| `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. |\n\n### Terminal Payment States\n\nEvery payment ends in one of three terminal states. Once a payment reaches a terminal state, it is final and cannot change.\n\n| Payment Status | Order Status | Description |\n|----------------|--------------|-------------|\n| `PAYMENT_COMPLETED` | `PAYMENT_COMPLETED` | Payment succeeded -- funds are captured and the order is closed. |\n| `PAYMENT_CANCELLED` | `PENDING` | Payment was cancelled -- the order remains open and a new payment can be initiated using the existing `orderId`. |\n| `PAYMENT_FAILED` | `PENDING` | Payment failed -- the order remains open and a new payment can be initiated using the existing `orderId`. |\n\n> **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.\n\n## Create an Order\n\nEvery payment starts with an order containing line items and a terminal ID.\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [{\n    \"id\": \"ITEM-001\",\n    \"name\": \"Running Shoes\",\n    \"quantity\": 1,\n    \"amount\": { \"regular\": 50000, \"total\": 50000, \"currency\": \"752\",\n      \"tax\": [{ \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }] }\n  }],\n  \"totalOrderAmount\": { \"regular\": 50000, \"total\": 50000, \"currency\": \"752\",\n    \"tax\": [{ \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }] },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": { \"paymentMethod\": \"CARD\" }\n  }\n}\n```\n\n```json\n// Response\n{ \"status\": \"SUCCESS\",\n  \"data\": { \"orderId\": \"83a1ba32774149710b\", \"paymentId\": \"83a1ba3264bd500106\" },\n  \"message\": \"Order created successfully\" }\n```\n\nStore both `orderId` and `paymentId` -- you need them for all subsequent operations.\n\n### Delay Capture\n\nTo 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.\n\n## Capture a Payment\n\nWhen an order uses `delayCapture: true`, explicitly capture to finalize the charge.\n\n```json\nPOST /payments/:paymentId/capture\n{ \"amount\": 50000 }\n```\n\nThe `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 `{}`.\n\n```json\n// Response\n{ \"status\": \"SUCCESS\", \"message\": \"Payment captured successfully\" }\n```\n\nCheck capture status with `GET /payments/:paymentId/capture`. Possible `captureStatus` values: `PENDING`, `SUCCESS`, `ERROR`.\n\n## Void a Payment\n\nVoiding reverses a completed payment **before settlement** -- no money moves.\n\n```json\nPOST /payments/:paymentId/void\n{}\n```\n\n```json\n// Response\n{ \"status\": \"SUCCESS\",\n  \"data\": { \"voidStatus\": \"VOIDED\" },\n  \"message\": \"Payment voided successfully\" }\n```\n\nPossible `voidStatus` values: `VOID_INITIATED`, `CANNOT_VOID`, `VOIDED`.\n\n> **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.\n\n## Cancel a Payment\n\nCancellation stops a payment **before it completes** -- for example, if the customer abandons checkout while payment is processing.\n\n```json\nDELETE /payments/:paymentId\n```\n\n```json\n// Response\n{ \"status\": \"SUCCESS\",\n  \"data\": { \"paymentStatus\": \"PAYMENT_CANCELLED\" },\n  \"message\": \"Payment cancelled successfully\" }\n```\n\n> **Cancel vs. Void:** Cancel applies to in-progress payments (before completion). Void applies to completed payments (before settlement).\n\n## Refund an Order\n\nA full refund is a **new order** with negative quantities and the original `orderId` as `purchaseOrderId` on each line item.\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [{\n    \"id\": \"ITEM-001\",\n    \"purchaseOrderId\": \"ORIGINAL_ORDER_ID\",\n    \"name\": \"Running Shoes\",\n    \"quantity\": -1,\n    \"amount\": { \"regular\": 50000, \"total\": -50000, \"currency\": \"752\",\n      \"tax\": [{ \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }] }\n  }],\n  \"totalOrderAmount\": { \"regular\": 50000, \"total\": -50000, \"currency\": \"752\",\n    \"tax\": [{ \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }] },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": { \"paymentMethod\": \"CARD\" }\n  }\n}\n```\n\nKey details:\n\n- Set `quantity` to a negative value to indicate a return\n- Set `amount.total` to a negative value\n- Include the original `purchaseOrderId` on each line item\n- For card refunds, `CARD_NP` is the recommended payment method\n- Transaction fees are charged again on refunds\n\n## Partial Refund\n\nWorks the same as a full refund, but only include the specific items or reduced quantities you want to return.\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [{\n    \"id\": \"ITEM-002\",\n    \"purchaseOrderId\": \"ORIGINAL_ORDER_ID\",\n    \"name\": \"Water Bottle\",\n    \"quantity\": -1,\n    \"amount\": { \"regular\": 15000, \"total\": -15000, \"currency\": \"752\",\n      \"tax\": [{ \"amount\": 3000, \"percentage\": 25, \"type\": \"VAT\" }] }\n  }],\n  \"totalOrderAmount\": { \"regular\": -15000, \"total\": -15000, \"currency\": \"752\",\n    \"tax\": [{ \"amount\": 3000, \"percentage\": 25, \"type\": \"VAT\" }] },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": { \"paymentMethod\": \"CARD\" }\n  }\n}\n```\n\n> **Note:** All payment methods except NSWISH, SVIPPS, and SMOBILEPAY support partial refunds.\n\n## Checking Order Status\n\nQuery the current state of any order at any point:\n\n```json\nGET /orders/:orderId/status\n```\n\n```json\n// Response\n{ \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderStatus\": \"PAYMENT_COMPLETED\",\n    \"payments\": [{ \"paymentId\": \"83a1ba3264bd500106\",\n      \"paymentStatus\": \"PAYMENT_COMPLETED\", \"paymentMethod\": \"CARD\", \"amount\": 50000 }],\n    \"paymentIds\": [\"83a1ba3264bd500106\"]\n  } }\n```\n\n## Decision Guide\n\n| Situation | Action |\n|-----------|--------|\n| Payment initiated but not completed | **Cancel** -- `DELETE /payments/:paymentId` |\n| Payment completed, not yet settled (before 23:00 UTC) | **Void** -- `POST /payments/:paymentId/void` |\n| Payment settled, need full reversal | **Full Refund** -- create order with negative quantities |\n| Payment settled, need partial reversal | **Partial Refund** -- create order with specific negative items |\n| Delayed-capture order, ready to charge | **Capture** -- `POST /payments/:paymentId/capture` |\n\n## Reference\n\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Payments API](https://developers.surfboardpayments.com/api/payments)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Capture a Payment",
      "slug": "capture-a-payment",
      "url": "/developers/guides/capture-a-payment",
      "description": "Finalize a previously authorized payment by capturing funds. Covers delay capture and pre-authorization flows with step-by-step API examples.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Payments",
        "Capture",
        "In-Store"
      ],
      "content": "## Overview\n\nWhen you create an order with `delayCapture: true` or `authMode: \"PRE-AUTH\"`, funds are authorized but not immediately captured. This lets you verify inventory, confirm fulfillment, or adjust the final amount before charging the customer.\n\nThis guide walks through the full capture flow: create an authorized order, capture the payment, and verify the result.\n\n## When to Use Delay Capture\n\n| Scenario | Description |\n|----------|-------------|\n| **E-commerce fulfillment** | Authorize at checkout, capture at shipment |\n| **Pre-authorization** | Hold a variable amount (e.g., hotel deposit), capture actual charge later |\n| **Service bookings** | Authorize upfront, capture after service delivery |\n| **Digital products** | Authorize, verify access, then capture |\n\n## Step 1: Create an Order with Delay Capture\n\nCreate an order with `delayCapture: true` in `controlFunctions`:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"Nike Shoes\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 50000,\n        \"total\": 50000,\n        \"currency\": \"752\",\n        \"tax\": [\n          { \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }\n        ]\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 50000,\n    \"total\": 50000,\n    \"currency\": \"752\",\n    \"tax\": [\n      { \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }\n    ]\n  },\n  \"controlFunctions\": {\n    \"delayCapture\": true,\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\"\n    }\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderId\": \"83ac302f7c5130810b\",\n    \"paymentId\": \"83ac302f24bfb00b06\"\n  },\n  \"message\": \"Order created successfully\"\n}\n```\n\nStore the `paymentId` -- you need it to capture.\n\n### Pre-Authorization Mode\n\nFor flows where the final capture amount may differ from the authorized amount, use `authMode: \"PRE-AUTH\"`. This automatically enables `delayCapture`:\n\n```json\n{\n  \"controlFunctions\": {\n    \"delayCapture\": true,\n    \"authMode\": \"PRE-AUTH\",\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\"\n    }\n  }\n}\n```\n\n## Step 2: Capture the Payment\n\nOnce ready to finalize, call the capture endpoint with the `paymentId`:\n\n```json\nPOST /payments/:paymentId/capture\n{}\n```\n\nFor `PRE-AUTH` orders, you can specify a different capture amount:\n\n```json\nPOST /payments/:paymentId/capture\n{\n  \"amount\": 45000\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Payment captured successfully\"\n}\n```\n\n> The `amount` field is only valid for `PRE-AUTH` orders. For standard `delayCapture`, send an empty body to capture the full authorized amount.\n\n## Step 3: Check Capture Status\n\nVerify the capture completed successfully:\n\n```json\nGET /payments/:paymentId/capture\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"captureStatus\": \"SUCCESS\"\n  }\n}\n```\n\n**Possible `captureStatus` values:** `PENDING` | `SUCCESS` | `ERROR`\n\n## Reference\n\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Payments API](https://developers.surfboardpayments.com/api/payments)\n- [Create an Order](/developers/guides/create-an-order)\n- [Payment Lifecycle](/developers/guides/payment-lifecycle)"
    },
    {
      "title": "Terminal & Device Management",
      "slug": "terminal-device-management",
      "url": "/developers/guides/terminal-device-management",
      "description": "Manage payment terminals and devices via the Surfboard API. Register in-store and online terminals, configure settings, and handle device operations.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "Terminal",
        "API",
        "Device Management",
        "Configuration"
      ],
      "content": "## Overview\n\nOnce merchants are onboarded and stores are created, the next step is registering and managing terminals. Surfboard supports both physical in-store devices and online payment terminals. This guide covers registration, configuration, and ongoing operations like moving or rebooting terminals.\n\n## Terminal Types\n\n| Type | Category | Description |\n|------|----------|-------------|\n| **EMV** | In-Store | Traditional card-present terminals (countertop, mobile POS, kiosk) |\n| **SoftPOS** | In-Store | Tap-to-pay on Android smartphones or tablets |\n| **PaymentPage** | Online | Surfboard-hosted checkout page with a payment link |\n| **SelfHostedPage** | Online | Surfboard renders card fields on your own web page via the Online SDK |\n| **iFrame** | Online | Embedded payment frame within your site |\n| **MerchantInitiated** | Online | Server-to-server payments using stored card tokens (subscriptions, recurring) |\n\n## Registering In-Store Terminals\n\nRegister physical terminals by providing the device's registration code and the store it belongs to.\n\n```\nPOST /merchants/{merchantId}/stores/{storeId}/devices\n```\n\n```json\n{\n  \"registrationIdentifier\": \"250901\",\n  \"terminalName\": \"Checkout 1\"\n}\n```\n\nThe `registrationIdentifier` is a 6-digit code displayed when you power on the terminal. For SurfPad and Printer devices, use the serial number printed on the back instead.\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"terminalId\": \"trm_abc123\",\n    \"registrationStatus\": \"REGISTERED\"\n  },\n  \"message\": \"Terminal registered successfully\"\n}\n```\n\nThe `registrationStatus` will be either `REGISTERED` (new device) or `ALREADY_REGISTERED` (device was previously linked).\n\n## Registering Online Terminals\n\nTwo online terminals arrive with the store. Creating an online store provisions a `PaymentPage` terminal, for payment links and hosted checkout, and a `MerchantInitiated` terminal, for backend charges against a stored token. Neither takes a registration call — fetch the store's terminals to get their IDs:\n\n```\nGET /merchants/{merchantId}/stores/{storeId}/terminals\n```\n\nThe remaining modes, `SelfHostedPage` and `iFrame`, are registered per store with the mode named in the body:\n\n```\nPOST /merchants/{merchantId}/stores/{storeId}/online-terminals\n```\n\n```json\n{\n  \"onlineTerminalMode\": \"SelfHostedPage\"\n}\n```\n\n> **Note:** The default terminals exist from the moment the online store does, but no online terminal can take a payment until the store's domains are verified and the store is approved.\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"terminalId\": \"trm_xyz789\",\n    \"publicKey\": \"pk_live_...\",\n    \"registrationStatus\": \"REGISTERED\"\n  },\n  \"message\": \"Terminal registered successfully\"\n}\n```\n\nFor `SelfHostedPage` terminals, the response includes a `publicKey` used to initialize the Online SDK on your checkout page.\n\n## Terminal Configuration\n\nTerminal settings follow a hierarchy: merchant-level defaults cascade down to store-level, which cascade down to terminal-level. Terminal-level settings always take precedence.\n\n### Setting Terminal Config\n\n```\nPATCH /merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}\n```\n\n```json\n{\n  \"wifiSsid\": \"StoreNetwork\",\n  \"wifiPassword\": \"securepass\",\n  \"preferredNetwork\": \"WIFI\",\n  \"preferredRestartTime\": \"03:00\",\n  \"language\": \"en\",\n  \"showReceipt\": true,\n  \"alwaysShowMinorUnits\": 1\n}\n```\n\nKey configuration options:\n\n| Parameter | Description |\n|-----------|-------------|\n| `preferredRestartTime` | Scheduled restart in HH:MM format (default `02:00`). Terminals restart within a 1-hour window. |\n| `preferredNetwork` | `WIFI` or `GSM` |\n| `language` | ISO language code: `en`, `fi`, `da`, `se` |\n| `autoSleep` | Sleep timeout in seconds (battery-powered devices only) |\n| `showStatusBar` | Show/hide the status bar (SurfPad only) |\n| `openPosOnReboot` | `enabled` or `disabled` -- auto-launch POS after restart |\n| `enableRefundLock` | Require PIN for refunds (Android terminals only) |\n\n### Fetching Terminal Config\n\n```\nGET /merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}/config\n```\n\nReturns all active configuration values for the terminal, including inherited settings from merchant and store levels.\n\n## Device Operations\n\n### Change Store\n\nMove a terminal between stores under the same merchant. The terminal ID stays the same.\n\n```\nPOST /terminals/change\n```\n\n```json\n{\n  \"terminal$id\": \"trm_abc123\",\n  \"storeId\": \"str_newstore456\"\n}\n```\n\n> Terminals can only be moved between stores belonging to the same merchant. To reassign across merchants, use the Move Terminal endpoint.\n\n### Move Terminal (Cross-Merchant)\n\nPartners who manage terminal inventory in bulk can reassign a hardware terminal to a different merchant entirely.\n\n```\nPUT /partners/{partnerId}/terminals/{terminalSerialNo}/move\n```\n\n```json\n{\n  \"targetMerchantId\": \"mrc_target789\"\n}\n```\n\nThis endpoint uses the terminal's serial number rather than its terminal ID.\n\n### Reboot Terminal\n\nRemotely restart a terminal for troubleshooting or to apply firmware updates.\n\n```\nPOST /terminals/{terminalId}/reboot\n```\n\n```json\n{}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Reboot command published successfully\"\n}\n```\n\nA `SUCCESS` status means the reboot command was sent. It does not guarantee the terminal has rebooted -- the device must be connected to the network and not processing another command.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Register in-store device | POST | `/merchants/{merchantId}/stores/{storeId}/devices` |\n| Register online terminal | POST | `/merchants/{merchantId}/stores/{storeId}/online-terminals` |\n| Set terminal config | PATCH | `/merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}` |\n| Fetch terminal config | GET | `/merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}/config` |\n| Change store | POST | `/terminals/change` |\n| Move terminal | PUT | `/partners/{partnerId}/terminals/{terminalSerialNo}/move` |\n| Reboot terminal | POST | `/terminals/{terminalId}/reboot` |\n\nFor full endpoint details, see the [Terminals API](https://developers.surfboardpayments.com/api/terminals) and [Stores API](https://developers.surfboardpayments.com/api/stores) reference documentation."
    },
    {
      "title": "Cancel a Payment",
      "slug": "cancel-a-payment",
      "url": "/developers/guides/cancel-a-payment",
      "description": "Stop an in-progress payment before it completes. Use cancellation when a customer abandons checkout or a payment needs to be halted mid-process.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Payments",
        "In-Store"
      ],
      "content": "## Overview\n\nCancellation stops a payment **before it completes** -- for example, if the customer abandons checkout while payment is processing, or you need to halt a transaction before funds are transferred.\n\n> **Cancel vs. Void:** Cancel applies to in-progress payments (before completion). If the payment has already completed, use [Void a Payment](/developers/guides/void-a-payment) instead.\n\n## When to Use Cancel\n\n| Scenario | Description |\n|----------|-------------|\n| **Customer abandons checkout** | Payment initiated but customer leaves |\n| **Timeout** | Payment processing takes too long |\n| **Error detected** | Issue found after payment initiation |\n| **Duplicate order** | Accidentally created a second payment |\n\n## Step 1: Cancel the Payment\n\nCall the delete endpoint with the `paymentId` from the original order:\n\n```json\nDELETE /payments/:paymentId\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"paymentStatus\": \"PAYMENT_CANCELLED\"\n  },\n  \"message\": \"Payment cancelled successfully\"\n}\n```\n\n**Possible `paymentStatus` values:** `PAYMENT_COMPLETED` | `PAYMENT_FAILED` | `PAYMENT_CANCELLED`\n\nIf the payment already completed before your cancel request was processed, the status will show `PAYMENT_COMPLETED` and you should use a [void](/developers/guides/void-a-payment) or [refund](/developers/guides/refund-an-order) instead.\n\n## Step 2: Verify Order Status\n\nConfirm the order reflects the cancellation:\n\n```json\nGET /orders/:orderId/status\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderStatus\": \"PAYMENT_CANCELLED\",\n    \"payments\": [\n      {\n        \"paymentId\": \"83a1ba3264bd500106\",\n        \"paymentStatus\": \"PAYMENT_CANCELLED\",\n        \"paymentMethod\": \"CARD\",\n        \"amount\": 50000\n      }\n    ]\n  }\n}\n```\n\n## Decision Guide\n\n| Payment State | Action |\n|---------------|--------|\n| In progress (not completed) | **Cancel** -- `DELETE /payments/:paymentId` |\n| Completed, not settled (before 23:00 UTC) | [Void](/developers/guides/void-a-payment) -- `POST /payments/:paymentId/void` |\n| Settled | [Refund](/developers/guides/refund-an-order) -- create order with negative quantities |\n\n## Reference\n\n- [Payments API](https://developers.surfboardpayments.com/api/payments)\n- [Create an Order](/developers/guides/create-an-order)\n- [Payment Lifecycle](/developers/guides/payment-lifecycle)"
    },
    {
      "title": "Webhooks",
      "slug": "webhooks-notifications",
      "url": "/developers/guides/webhooks-notifications",
      "description": "Receive real-time event notifications via webhooks. Subscribe to order, payment, logistics, and merchant application events with automatic retries and signature verification.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Webhooks",
        "Events"
      ],
      "content": "## Overview\n\nWebhooks 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.\n\nSurfboard supports two webhook mechanisms:\n\n1. **Console webhooks:** Persistent, account-level subscriptions configured in the Surfboard Console. Support retries, failure alerts, and signature verification.\n2. **Callback URL (per-order webhook):** A dynamic webhook URL set per order via `controlFunctions.callBackUrl`. Useful for order-level status updates during checkout.\n\nWebhooks 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.).\n\n## Available Events\n\nYou can subscribe to the following event categories to receive real-time updates within your platform.\n\n### Order and Payment Events\n\nOrder 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.\n\n- **Order Updated** -- The order has been modified (e.g. order lines changed).\n- **Order Payment Initiated** -- A payment attempt has started for the order.\n- **Order Payment Processed** -- The payment is being processed by the payment provider.\n- **Order Payment Completed** -- The payment has been successfully completed.\n- **Order Payment Failed** -- The payment attempt has failed.\n- **Order Payment Cancelled** -- The payment has been cancelled.\n- **Order Cancelled** -- The entire order has been cancelled.\n- **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`.\n- **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`.\n\n### Logistics Events\n\nLogistics events notify you about updates on shipments, including terminals and accessories. These events help track order progress from placement to delivery.\n\n- **Logistics Order Update** -- A logistics shipment status has changed.\n\n### Merchant Application Events\n\nMerchant application events provide updates during the onboarding process, from application creation to approval. These notifications help ensure smooth and timely onboarding for merchants.\n\n- **Application Initiated** -- A new merchant application has been created.\n- **Application Submitted** -- The application has been submitted for review.\n- **Application Signed** -- The application has been signed by the merchant.\n- **Application Started** -- Processing of the application has begun.\n- **Application Pending Merchant Information** -- Additional information is required from the merchant.\n- **Application Completed** -- The application review is complete.\n- **Application Merchant Created** -- The merchant account has been created.\n- **Application Expired** -- The application has expired.\n- **Application Rejected** -- The application has been rejected.\n\n## Event Payload Details\n\n### Order Customer Identity\n\nThis 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.\n\n**Event type:** `order.customer.identify`\n\n**Payload example:**\n\n```json\n{\n  \"eventType\": \"order.customer.identify\",\n  \"metadata\": {\n    \"eventId\": \"832cf9fe1806581dff\",\n    \"created\": 1747553660038,\n    \"retryAttempt\": 0,\n    \"webhookEventId\": \"81a214e74b107801ff\"\n  },\n  \"data\": {\n    \"orderId\": \"832cf9f93d2fd0410b\",\n    \"cardId\": \"c550c29e80908c887a\"\n  }\n}\n```\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `data.orderId` | string | Unique identifier for the order. |\n| `data.cardId` | string | Tokenized identifier for the customer's card, used to recognize or link the customer to the order. |\n\n> **Note:** The `cardId` is a tokenized representation and should be treated as sensitive data.\n\n### Order Terminal Event\n\nThis event is triggered for every state the terminal undergoes during a transaction, including stages like tip selection, card presentation, PIN entry, authorization, and completion.\n\n**Event type:** `order.terminal.event`\n\n**Payload example:**\n\n```json\n{\n  \"eventType\": \"order.terminal.event\",\n  \"metadata\": {\n    \"eventId\": \"81a214e74b107801ff\",\n    \"created\": 1695793998732,\n    \"retryAttempt\": 0,\n    \"webhookEventId\": \"81a214e7455ed01cff\"\n  },\n  \"data\": {\n    \"orderId\": \"81b5f2624b16e0080b\",\n    \"merchantId\": \"8248db4c5c8dd0130e\",\n    \"paymentId\": \"81b5f26215e9583a06\",\n    \"terminalTransactionStatus\": \"STARTED\",\n    \"orderStatus\": \"PAYMENT_INITIATED\"\n  }\n}\n```\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `data.orderId` | string | Unique identifier for the order. |\n| `data.merchantId` | string | Unique identifier of the merchant. |\n| `data.paymentId` | string | Unique identifier for the payment. |\n| `data.terminalTransactionStatus` | string | Current terminal state (see table below). |\n| `data.orderStatus` | string | Current order status. |\n| `data.metadata` | object | Optional metadata passed with the order creation. |\n\n**Terminal transaction statuses:**\n\n| Status | Description |\n|--------|-------------|\n| `STARTED` | Transaction initiated on the terminal. |\n| `SELECT_TIP` | Tip selection screen displayed. |\n| `AWAITING_CARD` | Waiting for card tap/insert. |\n| `CARD_PRESENTED` | Customer has presented card. |\n| `SELECT_APPLICATION` | Card has multiple applications; selection required. |\n| `ENTER_PIN` | Customer needs to enter PIN. |\n| `WRONG_PIN` | Wrong PIN entered. |\n| `AUTHORIZING` | Payment authorization initiated. |\n| `SUBMITTED` | Authorization submitted to the backend. |\n| `AUTHORIZED` | Authorization complete. |\n| `PAGE_LOADED` | Online only -- payment page fully loaded. |\n| `SECURE_CHANNEL_INITIALISED` | Online only -- page ready for card details. |\n| `GOOGLE_PAY_MOUNTED` | Online only -- Google Pay SDK mounted. |\n| `APPLE_PAY_MOUNTED` | Online only -- Apple Pay SDK mounted. |\n| `CUSTOMER_INTERACTION_IN_FORM` | Online only -- customer started entering information. |\n| `CARD_PAYMENT_INITIATED` | Online only -- card payment initiated. |\n| `APPLE_PAY_ATTEMPT_INITIATED` | Online only -- Apple Pay attempt initiated. |\n| `GOOGLE_PAY_ATTEMPT_INITIATED` | Online only -- Google Pay attempt initiated. |\n| `APPLE_PAY_PAYMENT_INITIATED` | Online only -- Apple Pay payment process initiated. |\n| `GOOGLE_PAY_PAYMENT_INITIATED` | Online only -- Google Pay payment initiated. |\n\n## Getting Started\n\nTo set up webhooks via the Surfboard Console:\n\n1. Log in to the [Surfboard Developer Portal](https://developers.surfboardpayments.com).\n2. Click **Add new Webhook**.\n3. Enter a name and the URL of your webhook endpoint.\n4. Enter an email address to receive notifications in case of webhook failures.\n5. Choose which events you would like to receive.\n6. 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.\n7. Click **Test webhooks** to send a test notification to your endpoint and confirm it is working.\n\n> **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.\n\n## Testing Webhooks\n\nWhen 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:\n\n```json\n{\n  \"eventType\": \"test.webhook\",\n  \"metadata\": {\n    \"eventId\": \"string\",\n    \"created\": 1234567890,\n    \"retryAttempt\": 0,\n    \"webhookEventId\": \"string\"\n  }\n}\n```\n\nYour endpoint should return a `200` status code to acknowledge receipt.\n\n## Callback URL (Per-Order Webhook)\n\nIn 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.\n\nSet `controlFunctions.callBackUrl` in the [Create Order API](https://developers.surfboardpayments.com/api/orders) request:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [ ... ],\n  \"controlFunctions\": {\n    \"callBackUrl\": \"https://your-server.com/webhooks/payments\",\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\"\n    }\n  }\n}\n```\n\n> **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).\n\n## Handling Duplicate Deliveries\n\n> **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.\n\nUse 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.\n\n**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.\n\n## Handling Failures and Retries\n\n### Retry Logic\n\nWhen a webhook delivery fails (your endpoint does not return a `200` status code), Surfboard retries automatically:\n\n- **Attempts:** Up to 3 total delivery attempts.\n- **First retry:** 5 minutes after the initial failure.\n- **Second retry:** 10 minutes after the first retry.\n\n### Failure Alerts and Automatic Disabling\n\n- An **alert email** is sent on the first delivery failure.\n- If the endpoint continues to fail, subsequent alerts are sent every 24 hours for up to 7 days.\n- After 7 days of continuous failure with no action taken, the webhook is **automatically disabled**.\n- To re-enable a disabled webhook, fix the underlying issue and re-run **Test Webhook** in the Console.\n\n### Failures on Surfboard's Side\n\nSurfboard 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.\n\n> **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.\n\n## Verifying Webhook Signatures\n\nEvery 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.\n\nThe signature is an HMAC-SHA512 hash of the JSON request body, encoded as Base64. Below are examples in several languages:\n\n### TypeScript\n\n```typescript\nimport { createHmac } from 'node:crypto';\n\nfunction generateHMACSignature(certificate: string, message: string): string {\n  return createHmac('sha512', certificate)\n    .update(message)\n    .digest()\n    .toString('base64');\n}\n\n// Verify incoming webhook\nfunction verifyWebhook(secret: string, body: string, receivedSignature: string): boolean {\n  const expectedSignature = generateHMACSignature(secret, body);\n  return expectedSignature === receivedSignature;\n}\n```\n\n### PHP\n\n```php\n<?php\n\nfunction generateHMACSignature($certificate, $message) {\n    return base64_encode(hash_hmac('sha512', $message, $certificate, true));\n}\n\n// Verify incoming webhook\n$certificate = 'YOUR_WEBHOOK_SECRET';\n$body = file_get_contents('php://input');\n$receivedSignature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];\n\n$expectedSignature = generateHMACSignature($certificate, $body);\n\nif ($expectedSignature === $receivedSignature) {\n    // Signature is valid\n    http_response_code(200);\n} else {\n    // Signature mismatch -- reject the request\n    http_response_code(401);\n}\n```\n\n### Java\n\n```java\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Base64;\n\npublic class WebhookVerifier {\n\n    public static String generateHMACSignature(String certificate, String message) {\n        try {\n            Mac hmac = Mac.getInstance(\"HmacSHA512\");\n            SecretKeySpec secretKey = new SecretKeySpec(\n                certificate.getBytes(StandardCharsets.UTF_8), \"HmacSHA512\"\n            );\n            hmac.init(secretKey);\n            byte[] hash = hmac.doFinal(message.getBytes(StandardCharsets.UTF_8));\n            return Base64.getEncoder().encodeToString(hash);\n        } catch (Exception e) {\n            throw new RuntimeException(\"Failed to generate HMAC signature\", e);\n        }\n    }\n}\n```\n\n### .NET\n\n```csharp\nusing System;\nusing System.Security.Cryptography;\nusing System.Text;\n\npublic static class WebhookVerifier\n{\n    public static string GenerateHMACSignature(string certificate, string message)\n    {\n        using (HMACSHA512 hmac = new HMACSHA512(Encoding.UTF8.GetBytes(certificate)))\n        {\n            byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));\n            return Convert.ToBase64String(hash);\n        }\n    }\n}\n```\n\n### Python\n\n```python\nimport base64\nimport hashlib\nimport hmac\n\ndef generate_hmac_signature(certificate, message):\n    signature = hmac.new(certificate.encode(), message.encode(), hashlib.sha512)\n    return base64.b64encode(signature.digest()).decode()\n```\n\n### Go\n\n```go\npackage main\n\nimport (\n    \"crypto/hmac\"\n    \"crypto/sha512\"\n    \"encoding/base64\"\n)\n\nfunc generateHMACSignature(certificate, message string) string {\n    key := []byte(certificate)\n    h := hmac.New(sha512.New, key)\n    h.Write([]byte(message))\n    return base64.StdEncoding.EncodeToString(h.Sum(nil))\n}\n```\n\n## Best Practices\n\n- **Return 200 quickly.** Process webhook payloads asynchronously. Return a `200` response before performing business logic to avoid timeouts and retries.\n- **Always verify signatures.** Validate the `x-webhook-signature` header on every incoming webhook to confirm it originates from Surfboard.\n- **Handle duplicates idempotently.** Use `orderId` + `paymentId` as your idempotency key and treat terminal payment statuses as final.\n- **Plan for retries.** Your endpoint may receive the same event multiple times. Ensure your processing logic is idempotent.\n- **Query on timeout.** If you have not received a payment event within 60 seconds, query the order status via the API as a fallback.\n- **Use HTTPS.** Always use HTTPS URLs for webhook endpoints to protect data in transit.\n- **Monitor your endpoints.** Watch for failure alert emails and resolve issues promptly to avoid your webhook being disabled after 7 days.\n\n## Related Guides\n\n- [Notification Subscriptions](/developers/guides/notification-subscriptions) -- Set up persistent email, Slack, and SFTP delivery for settlement reports and operational alerts.\n\n## API Reference\n\n| Action | Method | Endpoint |\n|--------|--------|----------|\n| Set per-order webhook | POST | `/orders` (via `controlFunctions.callBackUrl`) |"
    },
    {
      "title": "Notification Subscriptions",
      "slug": "notification-subscriptions",
      "url": "/developers/guides/notification-subscriptions",
      "description": "Subscribe to persistent, account-level event notifications delivered via email, Slack, or SFTP. Receive settlement reports and operational alerts for merchants and partners.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Notifications",
        "Email",
        "Slack",
        "SFTP",
        "Reports"
      ],
      "content": "## Overview\n\nNotification subscriptions let you set up persistent, account-wide delivery of event notifications at the merchant or partner level. Unlike [webhooks](/developers/guides/webhooks-notifications), which deliver HTTP callbacks to your server, notification subscriptions deliver events through **email**, **Slack**, and **SFTP** channels.\n\nUse notification subscriptions for ongoing operational alerts such as daily settlement reports, rather than real-time payment status updates.\n\n## Subscribing to Notifications\n\n### Merchant Report Notifications\n\n```json\nPOST /merchants/:merchantId/notifications/reports\n{\n  \"event\": \"DAILY_FILE_TRANSFER\",\n  \"email\": \"finance@yourcompany.com\",\n  \"slackUrl\": \"https://hooks.slack.com/services/T00/B00/xxx\"\n}\n```\n\nYou must provide at least one delivery channel (`email`, `slackUrl`, or `sftpInfo`). You can include multiple channels in a single request.\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"notificationId\": \"abc123\",\n      \"NotificationChannel\": \"EMAIL\"\n    },\n    {\n      \"notificationId\": \"def456\",\n      \"NotificationChannel\": \"SLACK\"\n    }\n  ],\n  \"message\": \"Notification subscription created successfully\"\n}\n```\n\n### Partner Notifications\n\nPartners use the same structure with the partner endpoint:\n\n```json\nPOST /partners/:partnerId/notifications/reports\n{\n  \"event\": \"DAILY_FILE_TRANSFER\",\n  \"email\": \"ops@partner.com\"\n}\n```\n\n## SFTP Delivery Channel\n\nFor automated file-based delivery (such as settlement CSVs), include `sftpInfo`:\n\n```json\n{\n  \"event\": \"DAILY_FILE_TRANSFER\",\n  \"sftpInfo\": {\n    \"host\": \"sftp.yourserver.com\",\n    \"user\": \"surfboard-reports\",\n    \"port\": 22,\n    \"remoteDirectory\": \"/reports/daily\",\n    \"separator\": \",\"\n  }\n}\n```\n\nThe response includes a `publicKey` for SFTP channels. Add this key to your SFTP server's authorized keys to allow Surfboard to deliver files.\n\n## Fetching Existing Subscriptions\n\nRetrieve all configured notification subscriptions for a merchant or partner:\n\n```\nGET /merchants/:merchantId/notifications\nGET /partners/:partnerId/notifications\n```\n\nBoth endpoints support optional query parameters: `event`, `notificationChannel`, and `notificationId`.\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"notificationId\": \"abc123\",\n      \"event\": \"DAILY_FILE_TRANSFER\",\n      \"notificationChannel\": \"EMAIL\",\n      \"address\": { \"email\": \"finance@yourcompany.com\" },\n      \"status\": \"ACTIVE\"\n    }\n  ]\n}\n```\n\n## Unsubscribing\n\nRemove a subscription by its ID:\n\n```\nDELETE /merchants/:merchantId/notifications/:notificationId\nDELETE /partners/:partnerId/notifications/:notificationId\n```\n\n## Best Practices\n\n- **Store notification IDs.** Save the `notificationId` returned when subscribing -- you need it to unsubscribe or filter notifications later.\n- **Monitor subscriptions.** Periodically fetch your active subscriptions with the GET endpoint to confirm they are still active and correctly configured.\n- **Use multiple channels.** Combine email and Slack for critical alerts to ensure visibility across your team.\n- **Secure SFTP access.** After adding the Surfboard public key to your SFTP server, restrict access to the designated directory and user.\n\n## API Reference\n\n| Action | Method | Endpoint |\n|--------|--------|----------|\n| Subscribe merchant reports | POST | `/merchants/:merchantId/notifications/reports` |\n| Subscribe partner reports | POST | `/partners/:partnerId/notifications/reports` |\n| Fetch merchant notifications | GET | `/merchants/:merchantId/notifications` |\n| Fetch partner notifications | GET | `/partners/:partnerId/notifications` |\n| Unsubscribe merchant | DELETE | `/merchants/:merchantId/notifications/:notificationId` |\n| Unsubscribe partner | DELETE | `/partners/:partnerId/notifications/:notificationId` |"
    },
    {
      "title": "Recurring Payments",
      "slug": "recurring-payments",
      "url": "/developers/guides/recurring-payments",
      "description": "Implement subscription billing and recurring charges using tokenization, recurring payment configuration, and Merchant Initiated Transactions.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Recurring",
        "Subscriptions",
        "Tokenization",
        "MIT"
      ],
      "content": "## Overview\n\nRecurring payments let you charge customers on a schedule without requiring them to re-enter card details each time. This guide covers the full lifecycle: tokenizing a card during the initial payment, configuring subscription parameters, charging with stored tokens, and managing the subscription over time.\n\nThis builds on the concepts introduced in the [Server-to-Server API](/guides/server-to-server-api) guide. If you are new to Merchant Initiated Transactions (MIT) and tokenization, start there first.\n\n## How It Works\n\n1. **Initial payment** -- The customer pays through a PaymentPage or SelfHostedPage terminal. Tokenization stores their card for future use.\n2. **Subscription configuration** -- You define frequency, amount type, and payment count using the `recurring` object.\n3. **Recurring charges** -- Your backend creates orders against a MerchantInitiated terminal and pays using the stored token.\n4. **Lifecycle management** -- You handle renewals, cancellations, amount changes, and failed payment retries.\n\n## Prerequisites\n\n- A **PaymentPage** or **SelfHostedPage** terminal for the initial customer-present payment\n- A **MerchantInitiated** terminal for subsequent server-to-server charges\n- API credentials (API key, API secret, merchant ID)\n\nBoth `PaymentPage` and `MerchantInitiated` terminals are provisioned when the online store is created, so unless you are taking the first payment on your own page with `SelfHostedPage`, there is nothing to register — list the store's terminals and use the IDs it returns.\n\n## Step 1: Create the Initial Tokenized Order\n\nThe first order collects card details and sets up the recurring agreement. Use `enforceTokenization` together with the `recurring` configuration and set `subscription` to `true`:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_PAYMENT_PAGE_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"SUB-001\",\n      \"name\": \"Pro Plan - Monthly\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 9900,\n        \"total\": 9900,\n        \"currency\": \"752\"\n      }\n    }\n  ],\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\",\n      \"amount\": 9900\n    },\n    \"online\": {\n      \"enforceTokenization\": true,\n      \"subscription\": true,\n      \"recurring\": {\n        \"subscriptionAmountType\": \"fixed\",\n        \"frequency\": \"monthly\",\n        \"numberOfPayments\": 12,\n        \"uniqueReference\": \"cust-42-pro-monthly\",\n        \"validation\": \"validated\"\n      }\n    }\n  }\n}\n```\n\nThe customer completes this payment on the payment page. Once the payment succeeds, the card is tokenized.\n\n## Step 2: Retrieve and Store the Token\n\nAfter the initial payment completes, fetch the token:\n\n```\nGET /orders/:orderId/tokens\n```\n\nStore the returned `tokenId` securely on your backend, associated with the customer record. This token is used for all future recurring charges.\n\n> **Warning:** Never expose token IDs in client-side code or logs.\n\n## Recurring Configuration Reference\n\nThe `controlFunctions.online.recurring` object controls how the subscription behaves:\n\n| Field | Required | Type | Description |\n|-------|----------|------|-------------|\n| `subscriptionAmountType` | Yes | string | `\"fixed\"` for same amount each cycle, `\"variable\"` for amounts that change |\n| `maxAmount` | No | number | Maximum charge amount in smallest currency unit. Only used with `\"variable\"` amount type |\n| `frequency` | Yes | string | Billing cycle. See frequency options below |\n| `numberOfPayments` | No | number | Total number of payments for the subscription. Omit for indefinite |\n| `uniqueReference` | No | string | Your unique identifier for this recurring agreement |\n| `validation` | Yes | string | `\"validated\"` if the initial payment is authenticated (3DS), `\"notValidated\"` otherwise |\n\n### Frequency Options\n\n| Value | Cycle |\n|-------|-------|\n| `daily` | Every day |\n| `twiceWeekly` | Twice per week |\n| `weekly` | Every week |\n| `tenDays` | Every 10 days |\n| `fortNightly` | Every 2 weeks |\n| `monthly` | Every month |\n| `everyTwoMonths` | Every 2 months |\n| `trimester` | Every 4 months |\n| `quarterly` | Every 3 months |\n| `twiceYearly` | Every 6 months |\n| `annually` | Every year |\n| `unscheduled` | No fixed schedule (usage-based or on-demand) |\n\n## Step 3: Charge with the Stored Token\n\nWhen a billing cycle is due, create an order on the MerchantInitiated terminal and pay with the token:\n\n### Create the recurring order\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_MIT_TERMINAL_ID\",\n  \"referenceId\": \"sub-cust42-2026-02\",\n  \"orderLines\": [\n    {\n      \"id\": \"SUB-002\",\n      \"name\": \"Pro Plan - February 2026\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 9900,\n        \"total\": 9900,\n        \"currency\": \"752\"\n      }\n    }\n  ]\n}\n```\n\n### Initiate payment with the token\n\n```json\nPOST /payments\n{\n  \"orderId\": \"ORDER_ID_FROM_ABOVE\",\n  \"paymentMethod\": \"CTOKEN\",\n  \"tokenId\": \"STORED_TOKEN_ID\"\n}\n```\n\n### Verify the result\n\n```\nGET /orders/:orderId/status\n```\n\nA successful charge returns `orderStatus: \"PAYMENT_COMPLETED\"`.\n\n## Variable-Amount Subscriptions\n\nFor metered billing or usage-based pricing, set `subscriptionAmountType` to `\"variable\"` and specify a `maxAmount`:\n\n```json\n\"recurring\": {\n  \"subscriptionAmountType\": \"variable\",\n  \"maxAmount\": 50000,\n  \"frequency\": \"monthly\",\n  \"uniqueReference\": \"cust-42-usage\",\n  \"validation\": \"validated\"\n}\n```\n\nEach recurring charge can then use a different amount (up to `maxAmount`) based on the customer's usage for that period.\n\n## Handling Failed Payments\n\nWhen a recurring charge fails, the order status will show `PAYMENT_FAILED` or `PAYMENT_CANCELLED`. Common reasons include expired cards, insufficient funds, or issuer declines.\n\n**Retry strategy:**\n\n1. Check the `failureReason` on the payment status response.\n2. For soft declines (insufficient funds, temporary issuer issues), retry the same order by calling the Initiate Payment API again with the token.\n3. Space retries over increasing intervals (e.g., 1 day, 3 days, 7 days).\n4. After repeated failures, notify the customer to update their card details. Direct them to a new payment page order with `enforceTokenization: true` to capture a fresh token.\n5. Replace the old token with the new one in your system.\n\n## Managing the Subscription Lifecycle\n\n| Action | How to implement |\n|--------|-----------------|\n| **Pause** | Stop creating new orders on your billing schedule. The token remains valid. |\n| **Resume** | Start creating orders again using the same stored token. |\n| **Cancel** | Stop billing. Optionally delete the stored token via your internal records. |\n| **Upgrade / downgrade** | Change the amount on the next order you create. For variable subscriptions this works within `maxAmount`. For fixed subscriptions, create a new initial order with the updated recurring configuration. |\n| **Update payment method** | Direct the customer to a new payment page order with tokenization enabled, then replace the stored token. |\n\n## Reference\n\n- [Server-to-Server API Guide](/guides/server-to-server-api) -- Tokenization and MIT fundamentals\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)\n- [Token Management](https://developers.surfboardpayments.com/api/orders)"
    },
    {
      "title": "Void a Payment",
      "slug": "void-a-payment",
      "url": "/developers/guides/void-a-payment",
      "description": "Reverse a completed payment before settlement. Voiding stops funds from transferring to the merchant's account, avoiding incorrect transactions.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Payments",
        "In-Store"
      ],
      "content": "## Overview\n\nVoiding reverses a **completed** payment before it settles -- no money moves from the customer's account to the merchant's. This is the quickest way to reverse a transaction on the same day, avoiding refund processing fees.\n\n> **Void vs. Cancel vs. Refund:**\n> - **Cancel** -- payment is still in progress (not yet completed)\n> - **Void** -- payment completed but not yet settled (same day, before 23:00 UTC)\n> - **Refund** -- payment has settled (next day or later)\n\n## When to Use Void\n\n| Scenario | Description |\n|----------|-------------|\n| **Wrong amount charged** | Customer was overcharged, caught same day |\n| **Duplicate transaction** | Same payment processed twice |\n| **Customer changed mind** | Immediate post-purchase reversal |\n| **Incorrect product** | Wrong item charged at point of sale |\n\n## Step 1: Void the Payment\n\nCall the void endpoint with the `paymentId`:\n\n```json\nPOST /payments/:paymentId/void\n{}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"voidStatus\": \"VOIDED\"\n  },\n  \"message\": \"Payment voided successfully\"\n}\n```\n\n**Possible `voidStatus` values:** `VOID_INITIATED` | `CANNOT_VOID` | `VOIDED`\n\n> **Important:** Voiding is only possible before 23:00 UTC on the transaction day. After the settlement cutoff, you must process a [refund](/developers/guides/refund-an-order) instead.\n\n## Step 2: Verify Order Status\n\nConfirm the void was applied:\n\n```json\nGET /orders/:orderId/status\n```\n\nThe order's transaction data will show `voided: true` for the affected transaction.\n\n## Handling `CANNOT_VOID`\n\nIf the void returns `CANNOT_VOID`, the payment has either:\n- Already been settled (past 23:00 UTC cutoff)\n- Not yet completed (use [Cancel](/developers/guides/cancel-a-payment) instead)\n\nIn these cases, process a [full refund](/developers/guides/refund-an-order) or [partial refund](/developers/guides/partial-refund) as needed.\n\n## Reference\n\n- [Payments API](https://developers.surfboardpayments.com/api/payments)\n- [Create an Order](/developers/guides/create-an-order)\n- [Payment Lifecycle](/developers/guides/payment-lifecycle)"
    },
    {
      "title": "Receipts",
      "slug": "receipts",
      "url": "/developers/guides/receipts",
      "description": "Generate, email, print, and customise receipts for in-store transactions using the Surfboard Receipts API.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "API",
        "Receipts",
        "Printing",
        "ESC/POS"
      ],
      "content": "## Overview\n\nAfter 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.\n\nAll receipt endpoints accept a Transaction ID, Payment ID, or Order ID as the identifier, so you can work with whichever reference suits your integration.\n\n## Prerequisites\n\nBefore working with receipts, make sure you have:\n\n- A Surfboard developer account with valid API credentials (`API-KEY` and `API-SECRET`)\n- At least one completed transaction, payment, or order\n- For printing: a registered Surfboard terminal with printing capability (SurfTouch with dock or SurfPrint)\n\n## Adding Receipt Information\n\nUse 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.\n\n```\nPUT /receipts/{orderId}\n```\n\n**Request body:**\n\n```json\n{\n  \"sequenceNumber\": \"1234567\",\n  \"cashRegisterName\": \"Kassa 1\",\n  \"controlUnitSerialNumber\": \"9876543\",\n  \"cashierName\": \"Amanda\",\n  \"customerName\": \"Tom\"\n}\n```\n\n**Request parameters:**\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `sequenceNumber` | string | Yes | Receipt sequence number from your cash register. |\n| `cashRegisterName` | string | Yes | Cash register designation or name. |\n| `controlUnitSerialNumber` | string | Yes | Control unit or control system manufacturing number. |\n| `cashierName` | string | No | Name of the cashier handling the transaction. |\n| `customerName` | string | No | Name of the customer. |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Receipt information added successfully\"\n}\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `status` | string | `SUCCESS` or `ERROR`. |\n| `message` | string | Human-readable status message. |\n\n## Emailing Receipts\n\nSend a digital receipt directly to a customer's email address. This is the simplest way to deliver post-payment confirmation without any printing hardware.\n\n```\nPUT /receipts/{id}/email\n```\n\nThe `{id}` path parameter accepts a Transaction ID, Payment ID, or Order ID.\n\n**Request body:**\n\n```json\n{\n  \"email\": \"customer@example.com\"\n}\n```\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `email` | string | Yes | Email address to deliver the receipt to. |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Receipt email sent successfully\"\n}\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `status` | string | Status of the request. |\n| `message` | string | Description of the result. |\n\n> **Tip:** You can call this endpoint multiple times with different email addresses if the customer or merchant both need a copy.\n\n## Fetching a Receipt Link\n\nRetrieve 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.\n\n```\nGET /receipts/{id}/link\n```\n\nThe `{id}` path parameter accepts a Transaction ID, Payment ID, or Order ID.\n\n**Request body:** None (empty `GET` request).\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"receiptURL\": \"https://receipts.surfboardpayments.com/r/abc123xyz\"\n  },\n  \"message\": \"Receipt link fetched successfully\"\n}\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `status` | string | `SUCCESS` or `ERROR`. |\n| `data.receiptURL` | string | URL to access the hosted digital receipt. |\n| `message` | string | Description of the result. |\n\n## Printing Receipts on a Terminal\n\nPrint a receipt directly from a Surfboard terminal that has printing capability. Surfboard supports printing through SurfTouch (which features a dock with a printer for checkout) and SurfPrint (which has a built-in printer for on-floor payments).\n\n```\nPUT /receipts/{id}/print\n```\n\nThe `{id}` path parameter accepts a Transaction ID, Payment ID, or Order ID.\n\n**Request body:**\n\n```json\n{\n  \"templateId\": \"default\",\n  \"terminalId\": \"trm_abc123\",\n  \"language\": \"sv\"\n}\n```\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `templateId` | string | No | Select from Surfboard's default set of receipt templates. |\n| `terminalId` | string | No | Target a specific printing-enabled terminal. If omitted, prints on the terminal that handled the transaction. |\n| `language` | string | No | Receipt language. Available: `sv`, `da`, `fi`, `en`. Defaults to the merchant's configured language. |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Receipt print command sent successfully\"\n}\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `status` | string | Status of the request. |\n| `message` | string | Description of the result. |\n\n> **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.\n\n## Custom ESC/POS Printing\n\nFor 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.\n\n```\nPUT /receipts/{terminalId}/escpos\n```\n\nNote that this endpoint uses the `terminalId` directly in the path, not a transaction or order ID.\n\n**Request body:**\n\n```json\n{\n  \"escposCommands\": \"G0AbYQEbRQFTdXBlciBNYXJ0CjEyMyBNYWluIFN0ChtFABthAERhdGU6IDIwMjQvMTAvMDgKVGltZTogMTI6MDAgUE0KG0UBLS0tLS0tLS0tLQobRQAbYQBJdGVtIEE6IFdhdGVyClByaWNlOiAkMS4wMApJdGVtIEI6IEJyZWFkClByaWNlOiAkMi4wMAobRQEtLS0tLS0tLS0tClRvdGFsOiAkMy4wMAobRQAbYQFUaGFuayB5b3UhCgoKHVYA\",\n  \"codePages\": \"UTF-8\"\n}\n```\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `escposCommands` | string | Yes | A stream of ESC/POS commands encoded as a Base64 string. |\n| `codePages` | string | No | Send `UTF-8` to opt in to the validated ESC/POS contract. Omitting it keeps the deprecated legacy flow. |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"ESC/POS receipt sent to terminal\"\n}\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `status` | string | Status of the request. |\n| `message` | string | Description of the result. |\n\n### Building ESC/POS Commands\n\nESC/POS is a command protocol originally developed by Epson and now supported by most thermal receipt printers. A few common commands:\n\n| Command | Hex | Description |\n|---------|-----|-------------|\n| Initialize printer | `1B 40` | Reset printer to default settings. |\n| Bold on | `1B 45 01` | Enable bold text. |\n| Bold off | `1B 45 00` | Disable bold text. |\n| Center align | `1B 61 01` | Center-align subsequent text. |\n| Left align | `1B 61 00` | Left-align subsequent text. |\n| Cut paper | `1D 56 00` | Full cut of the receipt paper. |\n\n**Workflow:**\n\n1. Compose your ESC/POS byte stream (text interspersed with control commands).\n2. Encode the entire byte stream as a Base64 string.\n3. Send the Base64 string in the `escposCommands` field, with `\"codePages\": \"UTF-8\"`.\n\n> **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.\n\n> **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.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Add receipt information | PUT | `/receipts/{orderId}` |\n| Email a receipt | PUT | `/receipts/{id}/email` |\n| Fetch receipt link | GET | `/receipts/{id}/link` |\n| Print receipt on terminal | PUT | `/receipts/{id}/print` |\n| Print custom ESC/POS receipt | PUT | `/receipts/{terminalId}/escpos` |\n\nFor full endpoint details, see the [Receipts API](https://developers.surfboardpayments.com/api/receipts) reference documentation."
    },
    {
      "title": "Refund an Order",
      "slug": "refund-an-order",
      "url": "/developers/guides/refund-an-order",
      "description": "Process a full refund by creating a return order with negative quantities. Covers the complete refund flow with API examples and payment method requirements.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Payments",
        "Refunds",
        "In-Store"
      ],
      "content": "## Overview\n\nA 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.\n\n## When to Use Full Refund\n\n| Scenario | Description |\n|----------|-------------|\n| **Product return** | Customer returns all items |\n| **Service not delivered** | Full service cancellation |\n| **Order error** | Wrong order fulfilled entirely |\n| **Post-settlement reversal** | Payment already settled, void no longer possible |\n\n> 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.\n\n## Step 1: Create a Refund Order\n\nCreate a new order with negative `quantity` and negative `amount.total` for each line item. Include the original `orderId` as `purchaseOrderId`:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"referenceId\": \"refund-order-001\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"purchaseOrderId\": \"ORIGINAL_ORDER_ID\",\n      \"name\": \"Nike Shoes\",\n      \"quantity\": -2,\n      \"amount\": {\n        \"regular\": 10000,\n        \"total\": -20000,\n        \"currency\": \"752\",\n        \"tax\": [\n          { \"amount\": 4000, \"percentage\": 25, \"type\": \"VAT\" }\n        ]\n      }\n    },\n    {\n      \"id\": \"ITEM-002\",\n      \"purchaseOrderId\": \"ORIGINAL_ORDER_ID\",\n      \"name\": \"Apple Pods\",\n      \"quantity\": -1,\n      \"amount\": {\n        \"regular\": 20000,\n        \"total\": -20000,\n        \"currency\": \"752\",\n        \"tax\": [\n          { \"amount\": 4000, \"percentage\": 25, \"type\": \"VAT\" }\n        ]\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 30000,\n    \"total\": -30000,\n    \"currency\": \"752\",\n    \"tax\": [\n      { \"amount\": 8000, \"percentage\": 25, \"type\": \"VAT\" }\n    ]\n  },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD_NP\",\n      \"refundProcessingParams\": {\n        \"purchasePaymentId\": \"ORIGINAL_PAYMENT_ID\",\n        \"refundReason\": \"CUSTOMER_INITIATED_RETURN\"\n      }\n    }\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderId\": \"83b2ca45889a317b0b\",\n    \"paymentId\": \"83b2ca4564bd500606\"\n  },\n  \"message\": \"Order created successfully\"\n}\n```\n\nThe `terminal$id` only needs to be a **valid** terminal -- it does **not** have to be the same terminal that processed the original purchase.\n\nKey details:\n\n- Set `quantity` to a negative value to indicate a return\n- Set `amount.total` to a negative value\n- Include the original `purchaseOrderId` on each line item\n- `totalOrderAmount.total` must be negative (the refund amount)\n\n## Payment Method for Refunds\n\nSet `paymentMethod` to either the method the customer originally paid with, or `CARD_NP`:\n\n| Original Payment Method | Refund Method |\n|------------------------|---------------|\n| CARD | `CARD_NP` (recommended) or `CARD` |\n| KLARNA | `KLARNA` |\n| SWISH | `SWISH` |\n| Other digital methods | Same as original |\n\nFor card refunds, the two card methods behave differently:\n\n| Method | Behaviour |\n|--------|-----------|\n| `CARD_NP` | **Card not present.** Refunds straight back to the card that paid -- no terminal interaction. This is the recommended default for card refunds. |\n| `CARD` | **Card present.** Triggers a card tap on the terminal, so a card must be physically presented to receive the refund. |\n\n> **Note:** Transaction fees are charged again on refunds.\n\n## Refund Processing Parameters\n\nPass refund metadata through `refundProcessingParams` inside `initiatePaymentsOptions`:\n\n```json\n{\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD_NP\",\n      \"refundProcessingParams\": {\n        \"purchasePaymentId\": \"ORIGINAL_PAYMENT_ID\",\n        \"refundReason\": \"CUSTOMER_INITIATED_RETURN\"\n      }\n    }\n  }\n}\n```\n\n| Parameter | Required | Description |\n|-----------|----------|-------------|\n| `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. |\n| `refundReason` | No | Why the refund is being issued. See the allowed values below. |\n| `otherReason` | Conditional | Free-text explanation. **Required when `refundReason` is `OTHER`.** |\n\n### Refund Reasons\n\n| Value | Meaning |\n|-------|---------|\n| `CUSTOMER_INITIATED_RETURN` | The customer returned the goods or requested the refund. |\n| `SUSPECTED_MALFUNCTION` | The product is suspected to be faulty or not working. |\n| `SUSPECTED_FRAUD` | The transaction is suspected to be fraudulent. |\n| `DUPLICATE_TRANSACTION` | The original charge was a duplicate. |\n| `OTHER` | Any other reason -- requires a message in `otherReason`. |\n\nWhen using `OTHER`, include the explanation:\n\n```json\n{\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD_NP\",\n      \"refundProcessingParams\": {\n        \"purchasePaymentId\": \"ORIGINAL_PAYMENT_ID\",\n        \"refundReason\": \"OTHER\",\n        \"otherReason\": \"Goodwill credit for delayed delivery\"\n      }\n    }\n  }\n}\n```\n\n## Step 2: Check Refund Status\n\nVerify the refund completed:\n\n```json\nGET /orders/:orderId/status\n```\n\nThe order status will show `PAYMENT_COMPLETED` once the refund is processed. You can also track refund status via [webhooks](/developers/guides/webhooks-notifications).\n\n## Adjustments in Refunds\n\nIf the original order included adjustments (tips, discounts), the refund includes them by default. Control this with `includeAdjustmentsForRefund`:\n\n```json\n{\n  \"controlFunctions\": {\n    \"includeAdjustmentsForRefund\": false,\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\"\n    }\n  }\n}\n```\n\nFor partial returns, by default the first refund order includes adjustments (`true`) and subsequent ones do not (`false`).\n\n## Refund via Partner Portal\n\nYou can also process refunds through the UI:\n\n1. Log in to **Partner Portal** > **Merchants** > select merchant > **Transactions**\n2. Select the transaction to refund\n3. Click **Create Refund** > **Full Refund** > **Process Refund**\n\n## Refund FAQ\n\n> **How long after a purchase can I issue a refund?**\n> 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.\n\n> **How long does it take for the customer to receive the refund?**\n> Processing time depends on the payment method:\n>\n> | Payment Method | Refund Timeline |\n> |----------------|-----------------|\n> | **Card** (CARD, CARD_NP) | Up to 7 days. Depends on the issuer and acquirer fraud systems. |\n> | **Swish** (SSWISH, NSWISH) | Instant |\n> | **Vipps** (SVIPPS) | Instant |\n> | **MobilePay** (SMOBILEPAY) | Up to 10 banking days |\n> | **Klarna** (KLARNA) | Up to 10 days |\n\n## Reference\n\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Partial Refund](/developers/guides/partial-refund)\n- [Payment Lifecycle](/developers/guides/payment-lifecycle)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Partial Refund",
      "slug": "partial-refund",
      "url": "/developers/guides/partial-refund",
      "description": "Refund specific items or a reduced amount from a completed order. Process partial returns by creating a return order with only the items to be refunded.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Payments",
        "Refunds",
        "In-Store"
      ],
      "content": "## Overview\n\nA partial refund returns a portion of the order amount to the customer. Like a full refund, it works by creating a **new order** with negative quantities -- but only for the specific items being returned.\n\n## When to Use Partial Refund\n\n| Scenario | Description |\n|----------|-------------|\n| **Single item return** | Customer returns one item from a multi-item order |\n| **Partial quantity** | Customer returns 1 of 3 identical items |\n| **Price adjustment** | Discount applied after purchase |\n| **Damaged goods** | Partial compensation for a defective item |\n\n## Step 1: Create a Partial Refund Order\n\nInclude only the line items being refunded, with negative `quantity` and negative `amount.total`. Reference the original order's `orderId` as `purchaseOrderId`:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"referenceId\": \"partial-refund-001\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-002\",\n      \"purchaseOrderId\": \"ORIGINAL_ORDER_ID\",\n      \"name\": \"Apple Pods\",\n      \"quantity\": -1,\n      \"amount\": {\n        \"regular\": 20000,\n        \"total\": -20000,\n        \"currency\": \"752\",\n        \"tax\": [\n          { \"amount\": 4000, \"percentage\": 25, \"type\": \"VAT\" }\n        ]\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": -20000,\n    \"total\": -20000,\n    \"currency\": \"752\",\n    \"tax\": [\n      { \"amount\": 4000, \"percentage\": 25, \"type\": \"VAT\" }\n    ]\n  },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD_NP\",\n      \"refundProcessingParams\": {\n        \"purchasePaymentId\": \"ORIGINAL_PAYMENT_ID\",\n        \"refundReason\": \"CUSTOMER_INITIATED_RETURN\"\n      }\n    }\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderId\": \"83c4db56990b428c0b\",\n    \"paymentId\": \"83c4db5674ce610706\"\n  },\n  \"message\": \"Order created successfully\"\n}\n```\n\nThe `terminal$id` only needs to be a **valid** terminal -- it does **not** have to be the same terminal that processed the original purchase.\n\n## Payment Method for Refunds\n\nSet `paymentMethod` to either the method the customer originally paid with, or `CARD_NP`:\n\n| Original Payment Method | Refund Method |\n|------------------------|---------------|\n| CARD | `CARD_NP` (recommended) or `CARD` |\n| KLARNA | `KLARNA` |\n| SWISH | `SWISH` |\n| Other digital methods | Same as original |\n\nFor card refunds, the two card methods behave differently:\n\n| Method | Behaviour |\n|--------|-----------|\n| `CARD_NP` | **Card not present.** Refunds straight back to the card that paid -- no terminal interaction. This is the recommended default for card refunds. |\n| `CARD` | **Card present.** Triggers a card tap on the terminal, so a card must be physically presented to receive the refund. |\n\n> **Note:** All payment methods except NSWISH, SVIPPS, and SMOBILEPAY support partial refunds.\n\n## Refund Processing Parameters\n\nPass refund metadata through `refundProcessingParams` inside `initiatePaymentsOptions`:\n\n```json\n{\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD_NP\",\n      \"refundProcessingParams\": {\n        \"purchasePaymentId\": \"ORIGINAL_PAYMENT_ID\",\n        \"refundReason\": \"CUSTOMER_INITIATED_RETURN\"\n      }\n    }\n  }\n}\n```\n\n| Parameter | Required | Description |\n|-----------|----------|-------------|\n| `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. |\n| `refundReason` | No | Why the refund is being issued. See the allowed values below. |\n| `otherReason` | Conditional | Free-text explanation. **Required when `refundReason` is `OTHER`.** |\n\n### Refund Reasons\n\n| Value | Meaning |\n|-------|---------|\n| `CUSTOMER_INITIATED_RETURN` | The customer returned the goods or requested the refund. |\n| `SUSPECTED_MALFUNCTION` | The product is suspected to be faulty or not working. |\n| `SUSPECTED_FRAUD` | The transaction is suspected to be fraudulent. |\n| `DUPLICATE_TRANSACTION` | The original charge was a duplicate. |\n| `OTHER` | Any other reason -- requires a message in `otherReason`. |\n\nWhen using `OTHER`, include the explanation:\n\n```json\n{\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD_NP\",\n      \"refundProcessingParams\": {\n        \"purchasePaymentId\": \"ORIGINAL_PAYMENT_ID\",\n        \"refundReason\": \"OTHER\",\n        \"otherReason\": \"Goodwill credit for delayed delivery\"\n      }\n    }\n  }\n}\n```\n\n## Step 2: Check Refund Status\n\nVerify the partial refund completed:\n\n```json\nGET /orders/:orderId/status\n```\n\nTrack refund status via the API response or through [webhooks](/developers/guides/webhooks-notifications).\n\n## Partial Refund via Partner Portal\n\n1. Log in to **Partner Portal** > **Merchants** > select merchant > **Transactions**\n2. Select the transaction to refund\n3. Click **Create Refund** > **Partial Refund**\n4. Choose **Select Line Items** or **Enter Custom Amount**\n5. **Process Refund** with a refund reason\n\n## Multiple Partial Refunds\n\nYou can issue multiple partial refunds against the same original order. Each refund creates a separate return order referencing the same `purchaseOrderId`.\n\nWhen the original order included adjustments (tips, discounts), the first partial refund includes adjustments by default. Subsequent partial refunds do not. Override this with `includeAdjustmentsForRefund` in `controlFunctions`.\n\n## Reference\n\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Refund an Order](/developers/guides/refund-an-order)\n- [Payment Lifecycle](/developers/guides/payment-lifecycle)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Tips Configuration",
      "slug": "tips-configuration",
      "url": "/developers/guides/tips-configuration",
      "description": "Configure tipping on Surfboard payment terminals at the merchant, store, or terminal level using a hierarchical override model.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "API",
        "Tips",
        "Configuration",
        "Terminal"
      ],
      "content": "## Overview\n\nSurfboard 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.\n\nTip 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.\n\n## Configuration Hierarchy\n\nSettings cascade downward and lower levels always take precedence:\n\n```\nPartner (default)\n  └── Merchant\n        └── Store\n              └── Terminal (highest priority)\n```\n\n**How the hierarchy works:**\n\n- If a terminal has its own tip config, that config is used -- regardless of what is set at the store, merchant, or partner level.\n- 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.\n- Each parameter is resolved independently. You can set `tipLevel1` at the merchant level and override only `tipLevel2` at a specific store.\n\n## Configuration Parameters\n\nAll three levels (merchant, store, terminal) accept the same set of parameters:\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `tipConfig` | string | Enable or disable tips. Values: `ENABLED`, `DISABLED`. |\n| `tipLevel1` | number | First preset tip percentage shown to the customer (e.g., `10` for 10%). |\n| `tipLevel2` | number | Second preset tip percentage (e.g., `20` for 20%). |\n| `tipLevel3` | number | Third preset tip percentage (e.g., `30` for 30%). |\n| `freeAmountEnabled` | boolean | When `true`, customers can enter a custom tip amount. |\n| `defaultCustomAmount` | number | Pre-filled custom amount shown when `freeAmountEnabled` is `true`. |\n| `displayCalculatedAmount` | string | Show the calculated tip in the local currency on screen. Values: `ENABLED`, `DISABLED`. |\n| `tipDisplayFormat` | string | How tip options are presented. Values: `PERCENTAGE`, `AMOUNT`. |\n\n> **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.\n\n## Setting Merchant-Level Tips\n\nApply 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.\n\n```\nPATCH /merchants/{merchantId}/tips\n```\n\n**Request body:**\n\n```json\n{\n  \"tipConfig\": \"ENABLED\",\n  \"tipLevel1\": 10,\n  \"tipLevel2\": 15,\n  \"tipLevel3\": 20,\n  \"freeAmountEnabled\": true,\n  \"defaultCustomAmount\": 50,\n  \"displayCalculatedAmount\": \"ENABLED\",\n  \"tipDisplayFormat\": \"PERCENTAGE\"\n}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Merchant tip configuration updated successfully\"\n}\n```\n\n### Fetching Merchant-Level Tips\n\nRetrieve the current tip configuration for a merchant.\n\n```\nGET /merchants/{merchantId}/tips\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"tipConfig\": \"ENABLED\",\n    \"tipLevel1\": 10,\n    \"tipLevel2\": 15,\n    \"tipLevel3\": 20,\n    \"defaultCustomAmount\": 50,\n    \"displayCalculatedAmount\": \"ENABLED\",\n    \"tipDisplayFormat\": \"PERCENTAGE\"\n  },\n  \"message\": \"Merchant tip configuration fetched successfully\"\n}\n```\n\n## Setting Store-Level Tips\n\nOverride 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.\n\n```\nPATCH /merchants/{merchantId}/stores/{storeId}/tips\n```\n\n**Request body:**\n\n```json\n{\n  \"tipConfig\": \"ENABLED\",\n  \"tipLevel1\": 15,\n  \"tipLevel2\": 20,\n  \"tipLevel3\": 25\n}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Store tip configuration updated successfully\"\n}\n```\n\n### Fetching Store-Level Tips\n\n```\nGET /merchants/{merchantId}/stores/{storeId}/tips\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"tipConfig\": \"ENABLED\",\n    \"tipLevel1\": 15,\n    \"tipLevel2\": 20,\n    \"tipLevel3\": 25,\n    \"defaultCustomAmount\": 50,\n    \"displayCalculatedAmount\": \"ENABLED\",\n    \"tipDisplayFormat\": \"PERCENTAGE\"\n  },\n  \"message\": \"Store tip configuration fetched successfully\"\n}\n```\n\n> **Note:** The response includes all effective values, including those inherited from the merchant level (such as `defaultCustomAmount` and `displayCalculatedAmount` in this example).\n\n## Setting Terminal-Level Tips\n\nApply a tip configuration to a single terminal. Terminal-level settings have the highest priority and override everything above them.\n\n```\nPATCH /merchants/{merchantId}/terminals/{terminalId}/tips\n```\n\n**Request body:**\n\n```json\n{\n  \"tipConfig\": \"ENABLED\",\n  \"tipLevel1\": 5,\n  \"tipLevel2\": 10,\n  \"tipLevel3\": 15,\n  \"freeAmountEnabled\": false,\n  \"tipDisplayFormat\": \"AMOUNT\"\n}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Terminal tip configuration updated successfully\"\n}\n```\n\n### Fetching Terminal-Level Tips\n\n```\nGET /merchants/{merchantId}/terminals/{terminalId}/tips\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"tipConfig\": \"ENABLED\",\n    \"tipLevel1\": 5,\n    \"tipLevel2\": 10,\n    \"tipLevel3\": 15,\n    \"freeAmountEnabled\": false,\n    \"displayCalculatedAmount\": \"ENABLED\",\n    \"tipDisplayFormat\": \"AMOUNT\"\n  },\n  \"message\": \"Terminal tip configuration fetched successfully\"\n}\n```\n\n## Example: Multi-Level Configuration\n\nConsider 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:\n\n```\nMerchant \"Nordic Bistro Group\"  → ENABLED, 10/15/20%, PERCENTAGE\n  └── Store \"Casual Eatery\"     → inherits merchant config\n        └── Terminal \"Checkout 1\"  → 10% / 15% / 20%, PERCENTAGE\n        └── Terminal \"Checkout 2\"  → 10% / 15% / 20%, PERCENTAGE\n  └── Store \"Fine Dining\"       → overrides to 15/20/25%\n        └── Terminal \"Table POS\"   → 15% / 20% / 25%, PERCENTAGE\n        └── Terminal \"Bar POS\"     → 15% / 20% / 25%, AMOUNT, no custom\n```\n\nEach terminal resolves its effective config by merging all levels, with the most specific setting winning.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Set merchant tips | PATCH | `/merchants/{merchantId}/tips` |\n| Fetch merchant tips | GET | `/merchants/{merchantId}/tips` |\n| Set store tips | PATCH | `/merchants/{merchantId}/stores/{storeId}/tips` |\n| Fetch store tips | GET | `/merchants/{merchantId}/stores/{storeId}/tips` |\n| Set terminal tips | PATCH | `/merchants/{merchantId}/terminals/{terminalId}/tips` |\n| Fetch terminal tips | GET | `/merchants/{merchantId}/terminals/{terminalId}/tips` |\n\nFor full endpoint details, see the [Terminals API](https://developers.surfboardpayments.com/api/terminals) and [Merchants API](https://developers.surfboardpayments.com/api/merchants) reference documentation."
    },
    {
      "title": "ESC/POS Printing",
      "slug": "escpos-printing",
      "url": "/developers/guides/escpos-printing",
      "description": "Send custom ESC/POS receipts to any Surfboard terminal using the UTF-8 contract: opt in with one field, get validated payloads and consistent output across current and legacy hardware.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "API",
        "Receipts",
        "Printing",
        "ESC/POS",
        "UTF-8"
      ],
      "content": "## Overview\n\nSurfboard is standardising ESC/POS printing across every payment terminal. You opt in by adding a single field to the print request you already send. In return, your payload is validated before it reaches hardware -- so you get a clear API error instead of a garbled receipt -- and the output is adapted automatically to every terminal model, current and legacy.\n\nThe opt-in is deliberately small. The work is in the rules the contract enforces, which this guide covers: UTF-8 text, a fixed set of supported commands, and line widths that depend on the text size you select.\n\n> **Note:** This guide covers the ESC/POS contract specifically. For the other ways to deliver a receipt -- email, hosted link, or Surfboard's own templates -- see the [Receipts](/developers/guides/receipts) guide.\n\n## Prerequisites\n\n- A Surfboard developer account with valid API credentials (`API-KEY` and `API-SECRET`)\n- A registered terminal with printing capability (SurfTouch with dock, or SurfPrint)\n- The terminal's `terminalId`\n\n## Opting in\n\nAdd `codePages` to your existing ESC/POS request:\n\n```\nPUT /receipts/{terminalId}/escpos\n```\n\n**Request body:**\n\n```json\n{\n  \"escposCommands\": \"<base64 ESC/POS>\",\n  \"codePages\": \"UTF-8\"\n}\n```\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `escposCommands` | string | Yes | Your ESC/POS byte stream, Base64-encoded. |\n| `codePages` | string | No | `UTF-8` is the only accepted value. Omitting it keeps the legacy flow. |\n\nNote that this endpoint takes a `terminalId` in the path, not a transaction or order ID.\n\nThree things follow from opting in:\n\n- **Validation is atomic.** An invalid stream is rejected whole with `PR_0006`, quoting the byte offset and the offending command. Nothing partial ever prints.\n- **Request bodies cap at 75 KB**, which is roughly 55 KB of decoded ESC/POS.\n- **The legacy flow is deprecated** and will be removed once migration completes. Omitting `codePages` keeps you on it for now.\n\n## Character encoding\n\nAll text must be UTF-8. Do not send Latin-1 or CP1252 bytes, and do not send any codepage or charset selection command -- the platform manages character encoding per device, which is precisely what makes one payload work across mixed hardware.\n\nThe quickest way to confirm your encoding is to check a single character. `ä` must be two bytes:\n\n```\nä  =  C3 A4     ✅ UTF-8\nä  =  E4        ❌ Latin-1, will be rejected\n```\n\nFor reference, the Nordic characters most likely to appear on a receipt:\n\n| Lowercase | UTF-8 bytes | Uppercase | UTF-8 bytes |\n|---|---|---|---|\n| `ä` | `C3 A4` | `Ä` | `C3 84` |\n| `å` | `C3 A5` | `Å` | `C3 85` |\n| `ö` | `C3 B6` | `Ö` | `C3 96` |\n\nIf you are migrating from a charset-based integration, translate national characters to their real UTF-8 form rather than relying on the old substitutions -- `{` meaning `ä`, `}` meaning `å`, and so on. Those substitutions depended on a charset command that is no longer accepted.\n\n## Line widths\n\nLine width depends on the terminal's paper and on the text size active at that point in the stream. Count characters, not bytes: `Örsundsbro väg 1` is 16 characters even though it is 18 bytes.\n\n| Context | Payment terminals (58mm) | Printer terminals (80mm) |\n|---|---|---|\n| Normal text (Font A) | **32** | **48** |\n| Fine print (Font B) | **42** | -- |\n| Big text (2×) | **16** | **24** |\n| Biggest text (3×) | **10** | **16** |\n\nLines longer than the budget wrap mid-word, which is almost never what you want on a receipt. Because the fonts are monospace, align columns by padding with spaces rather than tabs.\n\nImages are capped at **384 dots wide on every device**, regardless of paper width.\n\n## Supported commands\n\n| Purpose | Command | Bytes (hex) | Notes |\n|---|---|---|---|\n| Initialise | `ESC @` | `1B 40` | Required as the first bytes of every job |\n| Line feed | `LF` | `0A` | Ends and prints a line |\n| Feed n lines | `ESC d n` | `1B 64 n` | Use `1B 64 04` at the end so the receipt clears the tear bar |\n| Paper cut | `GS V 0` | `1D 56 00` | Cuts on 80mm printer terminals; safely ignored on handheld terminals, which have no cutter. `1D 56 42 n` (feed-then-cut) also accepted |\n| Bold on/off | `ESC ! 08` / `ESC ! 00` | `1B 21 08` / `1B 21 00` | `ESC E 1` / `ESC E 0` (`1B 45 n`) also accepted. One bold level only |\n| Fine print (Font B) | `ESC ! 01` / `ESC ! 00` | `1B 21 01` / `1B 21 00` | `ESC M n` (`1B 4D n`) also accepted |\n| Bold + Font B | `ESC ! 09` | `1B 21 09` | `ESC !` bits: `0x01` Font B, `0x08` bold |\n| Text size | `GS ! n` | `1D 21 00` / `1D 21 11` / `1D 21 22` | Normal / Big (2×) / Biggest (3×) |\n| Underline on/off | `ESC - 1` / `ESC - 0` | `1B 2D n` | Renders on current terminals; legacy prints the text without the underline. Send *after* any `ESC !` on the same line |\n| Alignment | `ESC a n` | `1B 61 00/01/02` | Left / centre / right. Text only -- images always print left-aligned |\n| Line spacing | `ESC 3 24` / `ESC 2` | `1B 33 18` / `1B 32` | For wrapping image stripes only |\n| QR code | `GS ( k` | see below | The only QR method |\n| Image / logo | `ESC *` mode 33 | see below | The only image format |\n\nTwo rules are worth stating separately, because they are the ones existing integrations most often break:\n\n- **Size is set only via `GS !`**, never via the size bits of `ESC !`. Nothing above `GS ! 22` is accepted.\n- **Anything not in the table above is rejected.** That includes `GS v 0` (raster images), `ESC t` / `ESC u` / `ESC R` (charset selection), `GS B` (reverse print), and any unknown command. These can corrupt or damage terminals, so the API blocks them before they reach hardware.\n\n## QR codes\n\nSend the data as a string and let the terminal render it. Five commands, in order:\n\n```\n1D 28 6B 04 00 31 41 32 00              # model 2\n1D 28 6B 03 00 31 43 06                 # module size: 6 dots (1-16)\n1D 28 6B 03 00 31 45 31                 # error correction: 30=L 31=M 32=Q 33=H\n1D 28 6B pL pH 31 50 30 <data bytes>    # store data; pL + pH*256 = len(data) + 3\n1D 28 6B 03 00 31 51 30                 # print\n```\n\nThe only part that varies is the fourth line's length prefix. For 20 bytes of data, `pL + pH*256` is 23, so `pL = 17` (hex) and `pH = 00`.\n\n## Images and logos\n\nMonochrome only, maximum 384 dots wide. Encode the image as 24-dot-tall horizontal stripes in column format:\n\n```\n1B 33 18                                # line spacing = 24 dots, so stripes butt together\nfor each 24-row stripe:\n  1B 2A 21 nL nH <data>                 # nL + nH*256 = width in dots; data = width × 3 bytes\n  0A                                    # LF after each stripe\n1B 32                                   # restore default line spacing\n```\n\nIn column format each of the `width` columns contributes 3 bytes, making 24 vertical dots, with the most significant bit at the top. Pixel `(x, y)` within a stripe lives in byte `x*3 + y/8` at bit `7 - y%8`.\n\nImages always print left-aligned, so `ESC a` will not centre them. To centre one, prepend blank columns: `pad = (384 - width) / 2`.\n\n## A complete receipt\n\nThis builds a 259-byte receipt with real Swedish characters, correct column alignment, and a fine-print tax line. Every line is inside its budget for the size active on it.\n\n```python\nESC, GS = 0x1B, 0x1D\nout = bytearray()\nraw = out.extend\ntxt = lambda s: out.extend(s.encode(\"utf-8\"))\n\nraw(bytes([ESC, 0x40]))              # init\nraw(bytes([ESC, 0x61, 0x01]))        # centre\nraw(bytes([GS, 0x21, 0x11]))         # big (2x)\nraw(bytes([ESC, 0x21, 0x08]))        # bold\ntxt(\"Kaffebaren\\n\")                  # 10 chars, budget 16 at 2x\nraw(bytes([GS, 0x21, 0x00]))         # size back to normal\nraw(bytes([ESC, 0x21, 0x00]))        # style off\ntxt(\"Örsundsbro väg 1\\n\")            # 16 chars, budget 32\nraw(bytes([ESC, 0x61, 0x00]))        # left\ntxt(\"-\" * 32 + \"\\n\")\ntxt(f\"{'Bryggkaffe':<26}{'29,00':>6}\\n\")\ntxt(f\"{'Kanelbulle':<26}{'35,00':>6}\\n\")\ntxt(\"-\" * 32 + \"\\n\")\nraw(bytes([ESC, 0x21, 0x08]))        # bold\ntxt(f\"{'Totalt':<22}{'64,00 SEK':>10}\\n\")\nraw(bytes([ESC, 0x21, 0x00]))\nraw(bytes([ESC, 0x21, 0x01]))        # fine print (Font B, 42 cols)\ntxt(\"Moms 12% ingår med 6,86 SEK\\n\")\nraw(bytes([ESC, 0x21, 0x00]))\nraw(bytes([ESC, 0x64, 0x04]))        # feed out past the tear bar\n\nimport base64\nprint(base64.b64encode(bytes(out)).decode())\n```\n\nWhich prints as (the first line is double-width, so its ten characters occupy twenty of the thirty-two columns):\n\n```\n      Kaffebaren\n        Örsundsbro väg 1\n--------------------------------\nBryggkaffe                 29,00\nKanelbulle                 35,00\n--------------------------------\nTotalt                 64,00 SEK\nMoms 12% ingår med 6,86 SEK\n```\n\nAnd sends as:\n\n```json\n{\n  \"escposCommands\": \"G0AbYQEdIREbIQhLYWZmZWJhcmVuCh0hABshAMOWcnN1bmRzYnJvIHbDpGcgMQobYQAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpCcnlnZ2thZmZlICAgICAgICAgICAgICAgICAyOSwwMApLYW5lbGJ1bGxlICAgICAgICAgICAgICAgICAzNSwwMAotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQobIQhUb3RhbHQgICAgICAgICAgICAgICAgIDY0LDAwIFNFSwobIQAbIQFNb21zIDEyJSBpbmfDpXIgbWVkIDYsODYgU0VLChshABtkBA==\",\n  \"codePages\": \"UTF-8\"\n}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"ESC/POS receipt sent to terminal\"\n}\n```\n\n## Validating before you send\n\n`PR_0006` tells you exactly what is wrong, but catching it in your own test suite is faster than catching it against a terminal. This preflight checks the three things that actually break integrations -- an out-of-contract command, non-UTF-8 text, and an over-long line:\n\n```python\nESC, GS, LF = 0x1B, 0x1D, 0x0A\nWIDTH = {0x00: 32, 0x11: 16, 0x22: 10}   # GS ! n -> chars per line, 58mm Font A\nSKIP  = {0x40: 2, 0x64: 3, 0x61: 3, 0x21: 3, 0x45: 3, 0x4D: 3, 0x2D: 3, 0x33: 3, 0x32: 2}\n\ndef preflight(data):\n    errs, i, size, font_b, line, start = [], 0, 0x00, False, bytearray(), 0\n    if data[:2] != bytes([ESC, 0x40]):\n        errs.append(\"stream must begin with ESC @ (1B 40)\")\n    while i < len(data):\n        b = data[i]\n        if b == LF:\n            if line:\n                try:\n                    s = line.decode(\"utf-8\")\n                    budget = 42 if (font_b and size == 0x00) else WIDTH[size]\n                    if len(s) > budget:\n                        errs.append(f\"byte {start}: {len(s)} chars exceeds {budget}: {s!r}\")\n                except UnicodeDecodeError:\n                    errs.append(f\"byte {start}: line is not valid UTF-8\")\n            line, i = bytearray(), i + 1\n            continue\n        if b == ESC and data[i+1] in SKIP:\n            n = data[i+1]\n            if n == 0x21:\n                if data[i+2] & ~0x09:\n                    errs.append(f\"byte {i}: ESC ! sets size bits; use GS ! for size\")\n                font_b = bool(data[i+2] & 0x01)\n            i += SKIP[n]\n            continue\n        if b == GS and data[i+1] == 0x21:\n            if data[i+2] not in WIDTH:\n                errs.append(f\"byte {i}: GS ! {data[i+2]:02X} must be 00, 11 or 22\")\n            else:\n                size = data[i+2]\n            i += 3\n            continue\n        if b in (ESC, GS):\n            errs.append(f\"byte {i}: unsupported command {b:02X} {data[i+1]:02X}\")\n            i += 2\n            continue\n        if not line:\n            start = i\n        line.append(b)\n        i += 1\n    return errs\n```\n\nHandle `ESC *` images, `GS ( k` QR blocks, and `GS V` cuts before this runs, or extend it to skip over them -- their payloads contain arbitrary bytes that would otherwise be read as text.\n\n## Errors\n\n| Code | Meaning | Action |\n|---|---|---|\n| `PR_0006` | Out-of-contract command. The message names the command, its byte offset, and what to use instead: `unsupported ESC/POS command 1D 76 at byte offset 214` | Replace the command. `1D 76` is raster imaging -- use `ESC *` mode 33 |\n| `codePages must be 'UTF-8' when provided` | Wrong opt-in value | Send exactly `\"UTF-8\"` |\n\n> **Note:** Because validation is atomic, a `PR_0006` means nothing printed at all. There is no half-receipt to clear from the printer.\n\n## Migration checklist\n\n- [ ] Add `\"codePages\": \"UTF-8\"` to your print requests\n- [ ] Encode all text as UTF-8 -- test that `ä` is two bytes (`C3 A4`), not one (`E4`)\n- [ ] Remove charset commands (`ESC t`, `ESC u`, `ESC R`) and translate national characters to real UTF-8\n- [ ] Replace `GS v 0` raster images with `ESC *` mode 33 stripes\n- [ ] Keep `GS V 0` and `GS V 66 n` cuts; remove any other cut variant (`ESC i`, `ESC m`)\n- [ ] Set size only with `GS ! 00`, `11`, or `22`, and respect the width for each\n- [ ] Format normal-text lines to 32 characters\n- [ ] End every job with `ESC d 4` so the receipt clears the tear bar\n\n> **Tip:** Generic ESC/POS libraries are a common source of rejections, because their defaults often emit raster images and charset commands. Check what your library actually produces before assuming it is in contract -- the preflight above will tell you.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Print custom ESC/POS receipt | PUT | `/receipts/{terminalId}/escpos` |\n\nFor the full endpoint reference, see the [Receipts API](https://developers.surfboardpayments.com/api/receipts) documentation. For the other receipt delivery methods, see the [Receipts](/developers/guides/receipts) guide."
    },
    {
      "title": "NFC Tag Reading",
      "slug": "nfc-tag-reading",
      "url": "/developers/guides/nfc-tag-reading",
      "description": "Use the NFC Reading API to create tag-reading sessions on payment terminals, scan NFC/RFID-tagged products, and retrieve scanned tag data.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "API",
        "NFC",
        "RFID",
        "Terminal"
      ],
      "content": "## Overview\n\nThe NFC Reading API enables payment terminals to read NFC (RFID) tags attached to physical products. Instead of manually scanning barcodes or keying in item codes, store staff can place tagged items near the terminal and let the reader capture product identifiers automatically. This is useful for retail scenarios involving apparel, electronics, or high-value goods with embedded NFC tags.\n\n## How It Works\n\n1. **Create a session** -- Start a reading session on a terminal, choosing single-tag or multi-tag mode.\n2. **Read tags** -- The terminal scans NFC tags as products are presented.\n3. **Retrieve results** -- Fetch scanned tags or poll the session status.\n4. **Complete the session** -- Close the session when all items have been scanned.\n\nYou can also tie NFC reading into the order creation flow by including `readTags` in your order request (covered below).\n\n## Creating a Read Session\n\nStart an NFC reading session on a terminal by specifying the scanning mode.\n\n```\nPOST /terminals/{terminalId}/sessions\n```\n\n**Request:**\n\n```json\n{\n  \"mode\": \"single\"\n}\n```\n\n| Parameter | Type   | Required | Description |\n|-----------|--------|----------|-------------|\n| `mode`    | string | Yes      | Number of tags the session can read. Possible values: `single` (one tag only) or `multiple` (continuous reading until completed). |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"sessionId\": \"ses_a1b2c3d4e5\"\n  },\n  \"message\": \"Session created successfully\"\n}\n```\n\n| Parameter        | Type   | Description |\n|------------------|--------|-------------|\n| `status`         | string | `SUCCESS` or `ERROR`. |\n| `data.sessionId` | string | Unique identifier for the NFC reading session. Use this ID in all subsequent session calls. |\n| `message`        | string | Human-readable description of the result. |\n\nUse `single` mode when scanning one product at a time (e.g., verifying a single item). Use `multiple` mode for basket-scanning workflows where several tagged items need to be captured in one session.\n\n## Fetching Session Status\n\nCheck the current state of an NFC reading session to determine whether it is still active, has timed out, or has been completed.\n\n```\nGET /terminals/{terminalId}/sessions/{sessionId}/status\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"sessionStatus\": \"PENDING\",\n    \"nfcTags\": [\"dGFnLWRhdGEtYmFzZTY0\"]\n  },\n  \"message\": \"Session status retrieved successfully\"\n}\n```\n\n| Parameter            | Type   | Description |\n|----------------------|--------|-------------|\n| `status`             | string | `SUCCESS` or `ERROR`. |\n| `data.sessionStatus` | string | Current session state. Possible values: `PENDING`, `COMPLETED`, `CANCELLED`, `TIMED_OUT`, `NOT_FOUND`. |\n| `data.nfcTags`       | array  | List of NFC tag values read so far (Base64-encoded RFID data). |\n| `message`            | string | Human-readable description of the result. |\n\nYou can poll this endpoint to build real-time UI updates showing scan progress. The possible `sessionStatus` values are: `PENDING` (terminal still listening), `COMPLETED` (explicitly closed), `CANCELLED` (cancelled early), `TIMED_OUT` (expired), and `NOT_FOUND` (invalid session ID).\n\n## Listing Sessions for a Terminal\n\nRetrieve all NFC reading sessions that have been created on a specific terminal. This is useful for auditing or reviewing past scanning activity.\n\n```\nGET /terminals/{terminalId}/sessions\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"sessionId\": \"ses_a1b2c3d4e5\",\n      \"action\": \"start\",\n      \"mode\": \"multiple\"\n    },\n    {\n      \"sessionId\": \"ses_f6g7h8i9j0\",\n      \"action\": \"end\",\n      \"mode\": \"single\"\n    }\n  ],\n  \"message\": \"Sessions retrieved successfully\"\n}\n```\n\n| Parameter        | Type   | Description |\n|------------------|--------|-------------|\n| `data.sessionId` | string | Unique identifier for the reading session. |\n| `data.action`    | string | Stage of the session lifecycle. Possible values: `start`, `end`. |\n| `data.mode`      | string | Tag reading mode used. Possible values: `single`, `multiple`. |\n\n## Retrieving Scanned Tags\n\nFetch all NFC tags that were read during a specific session. Each tag entry includes the session it belongs to and a unique tag identifier.\n\n```\nGET /terminals/{terminalId}/sessions/{sessionId}/tags\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"sessionId\": \"ses_a1b2c3d4e5\",\n      \"tagId\": \"tag_nike_shoe_001\"\n    },\n    {\n      \"sessionId\": \"ses_a1b2c3d4e5\",\n      \"tagId\": \"tag_nike_shoe_002\"\n    }\n  ],\n  \"message\": \"Tags retrieved successfully\"\n}\n```\n\n| Parameter        | Type   | Description |\n|------------------|--------|-------------|\n| `data.sessionId` | string | Session the tag was read under. |\n| `data.tagId`     | string | Unique identifier of the scanned NFC tag. |\n\nUse these tag IDs to look up product information in your inventory system and build the order accordingly.\n\n## Completing a Session\n\nWhen all items have been scanned, explicitly complete the session to stop the terminal from listening for additional tags.\n\n```\nPOST /terminals/{terminalId}/sessions/{sessionId}/complete\n```\n\n**Request:**\n\n```json\n{\n  \"result\": \"COMPLETED\"\n}\n```\n\n| Parameter | Type   | Required | Description |\n|-----------|--------|----------|-------------|\n| `result`  | string | No       | Final state of the session. Defaults to `COMPLETED` if omitted. |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Session completed successfully\"\n}\n```\n\nAlways complete sessions when scanning is finished. Uncompleted sessions eventually time out, but explicit completion frees the terminal for new sessions immediately.\n\n## Combining NFC Reading with Orders\n\nYou can integrate NFC tag reading directly into the order creation flow by adding `readTags` to the `controlFunctions` object in your order request. This tells the terminal to perform an NFC read as part of processing the order.\n\n```\nPOST /orders\n```\n\nInclude the `readTags` control function alongside your standard order payload:\n\n```json\n{\n  \"terminal$id\": \"83abab731f6fb00704\",\n  \"orderLines\": [\n    {\n      \"id\": \"0000CHI01\",\n      \"name\": \"Nike Shoes\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 500,\n        \"total\": 500,\n        \"currency\": \"752\"\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 500,\n    \"total\": 500,\n    \"currency\": \"752\"\n  },\n  \"controlFunctions\": {\n    \"readTags\": \"SINGLE\",\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CASH\"\n    }\n  }\n}\n```\n\nThe `readTags` field accepts `SINGLE` or `MULTIPLE`, matching the session mode behaviour. The terminal reads tags before or during payment processing, associating scanned items with the order. The response returns the `orderId`, `paymentId`, and an `interAppJWT` for inter-app communication.\n\n## Typical Integration Flow\n\n1. Call `POST /terminals/{terminalId}/sessions` with `mode: \"multiple\"` to start scanning.\n2. Present tagged products to the terminal.\n3. Poll `GET .../sessions/{sessionId}/status` until `sessionStatus` is no longer `PENDING`.\n4. Call `GET .../sessions/{sessionId}/tags` to get all scanned tag IDs.\n5. Map tag IDs to products in your inventory system.\n6. Call `POST .../sessions/{sessionId}/complete` to close the session.\n7. Create the order using the mapped product data.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Create reading session | POST | `/terminals/{terminalId}/sessions` |\n| Fetch session status | GET | `/terminals/{terminalId}/sessions/{sessionId}/status` |\n| List sessions for terminal | GET | `/terminals/{terminalId}/sessions` |\n| Retrieve scanned tags | GET | `/terminals/{terminalId}/sessions/{sessionId}/tags` |\n| Complete session | POST | `/terminals/{terminalId}/sessions/{sessionId}/complete` |"
    },
    {
      "title": "Partial Payments",
      "slug": "partial-payments",
      "url": "/developers/guides/partial-payments",
      "description": "Split an order across multiple payment methods or transactions. Accept card, cash, and Swish in any combination to settle a single order.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Payments",
        "In-Store"
      ],
      "content": "## Overview\n\nPartial payments let you split a single order across multiple payment transactions. The customer pays a portion with one method (e.g., card), then completes the balance with another (e.g., cash or Swish). The order stays open until the total of all payments equals the `totalOrderAmount`.\n\n## When to Use Partial Payments\n\n| Scenario | Description |\n|----------|-------------|\n| **Mixed payment methods** | Customer pays part by card, part by cash |\n| **Gift card + balance** | Gift card covers partial amount, card covers the rest |\n| **Split between payers** | Two customers splitting a bill |\n| **Installment at POS** | Collecting payment in stages |\n\n## Step 1: Create Order with Partial Amount\n\nCreate an order and specify a partial `amount` in `initiatePaymentsOptions`. This initiates the first payment for less than the total:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"referenceId\": \"split-order-001\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"Nike Shoes\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 50000,\n        \"total\": 50000,\n        \"currency\": \"752\",\n        \"tax\": [\n          { \"amount\": 5000, \"percentage\": 10, \"type\": \"VAT\" }\n        ]\n      }\n    },\n    {\n      \"id\": \"ITEM-002\",\n      \"name\": \"Apple Pods\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 50000,\n        \"total\": 50000,\n        \"currency\": \"752\",\n        \"tax\": [\n          { \"amount\": 5000, \"percentage\": 10, \"type\": \"VAT\" }\n        ]\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 100000,\n    \"total\": 100000,\n    \"currency\": \"752\",\n    \"tax\": [\n      { \"amount\": 10000, \"percentage\": 10, \"type\": \"VAT\" }\n    ]\n  },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\",\n      \"amount\": 50000\n    }\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderId\": \"83d5ec67aab053970b\",\n    \"paymentId\": \"83d5ec6784df720806\"\n  },\n  \"message\": \"Order created successfully\"\n}\n```\n\nThe order total is 100,000 but only 50,000 is charged in this first payment. The order remains in `PENDING` or `PARTIAL_PAYMENT_COMPLETED` status.\n\n## Step 2: Initiate Remaining Payments\n\nUse the Initiate Payment API to pay the remaining balance. Specify the `orderId` from the first step:\n\n```json\nPOST /payments\n{\n  \"orderId\": \"83d5ec67aab053970b\",\n  \"paymentMethod\": \"CARD\",\n  \"amount\": 50000\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"paymentId\": \"83d5ec6794ef830906\"\n  },\n  \"message\": \"Payment initiated successfully\"\n}\n```\n\nYou can split across more than two payments -- keep initiating payments until the total equals `totalOrderAmount`.\n\n> An order is considered **complete** only when the sum of all partial payments equals the order's total amount.\n\n## Step 3: Check Order Status\n\nVerify the order is fully paid:\n\n```json\nGET /orders/:orderId/status\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderStatus\": \"PAYMENT_COMPLETED\",\n    \"payments\": [\n      {\n        \"paymentId\": \"83d5ec6784df720806\",\n        \"paymentStatus\": \"PAYMENT_COMPLETED\",\n        \"paymentMethod\": \"CARD\",\n        \"amount\": 50000\n      },\n      {\n        \"paymentId\": \"83d5ec6794ef830906\",\n        \"paymentStatus\": \"PAYMENT_COMPLETED\",\n        \"paymentMethod\": \"CARD\",\n        \"amount\": 50000\n      }\n    ],\n    \"paymentIds\": [\"83d5ec6784df720806\", \"83d5ec6794ef830906\"]\n  }\n}\n```\n\n## Mixing Payment Methods\n\nEach partial payment can use a different method. For example, first payment by card, second by cash:\n\n```json\n// First payment (at order creation)\n\"initiatePaymentsOptions\": { \"paymentMethod\": \"CARD\", \"amount\": 50000 }\n\n// Second payment\n{ \"orderId\": \"...\", \"paymentMethod\": \"CASH\", \"amount\": 50000 }\n```\n\n## Reference\n\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)\n- [Create an Order](/developers/guides/create-an-order)\n- [Payment Lifecycle](/developers/guides/payment-lifecycle)"
    },
    {
      "title": "Multi-Merchant Terminals",
      "slug": "multi-merchant-terminals",
      "url": "/developers/guides/multi-merchant-terminals",
      "description": "Set up shared payment terminals for multiple merchants using the Multi-Merchant Group API. Ideal for food courts, events, and co-located businesses.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "API",
        "Multi-Merchant",
        "Terminal",
        "Partners"
      ],
      "content": "## Overview\n\nA multi-merchant terminal allows several independent businesses to accept payments through a single physical device. Each merchant retains its own merchant account -- orders, transactions, and payouts are routed individually -- but they share the same hardware.\n\nThis setup is designed for scenarios where multiple vendors operate in close proximity and dedicated terminals per merchant would be impractical or cost-prohibitive. The Multi-Merchant Group API lets partners create a group, onboard merchants into it, and register shared devices.\n\n## Use Cases\n\n| Scenario | Description |\n|----------|-------------|\n| **Food courts** | Multiple food vendors in a shopping centre share terminals at a central checkout or at individual stalls. |\n| **Music festivals and events** | Pop-up vendors at concerts, markets, or festivals share a limited pool of terminals. Each vendor's sales are tracked and settled separately. |\n| **Co-working retail spaces** | Small businesses sharing a physical storefront use the same terminal while keeping financials separate. |\n| **Seasonal markets** | Temporary setups like Christmas markets or farmers' markets where deploying one terminal per vendor is impractical. |\n\n## Setup Flow\n\nSetting up multi-merchant terminals involves three steps:\n\n1. **Create a multi-merchant group** -- Establishes the shared group and generates a group-level merchant and store.\n2. **Add merchants to the group** -- Onboard individual businesses and link them to the group.\n3. **Register a terminal** -- Assign a physical device to the group so all linked merchants can process payments.\n\nAfter setup, each merchant creates orders using their own `merchantId`, but the payment is processed on the shared terminal.\n\n## Step 1: Create a Multi-Merchant Group\n\nCreate the group under your partner account. This generates a group-level `merchantId` and `storeId` that you will use when registering shared terminals.\n\n```\nPOST /partners/{partnerId}/multi-merchant\n```\n\n**Request:**\n\n```json\n{\n  \"country\": \"SE\",\n  \"zipCode\": \"123456\",\n  \"name\": \"Central Food Court\",\n  \"email\": \"foodcourt@example.com\"\n}\n```\n\n| Parameter | Type   | Required | Description |\n|-----------|--------|----------|-------------|\n| `country` | string | Yes      | Two-letter ISO country code in uppercase (e.g., `SE`, `DK`, `NO`). |\n| `zipCode` | string | Yes      | ZIP/postal code of the shared location. |\n| `name`    | string | No       | Human-readable name for the group (e.g., \"Stockholm Food Hall\"). |\n| `email`   | string | No       | Contact email for the multi-merchant group. |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"applicationId\": \"app_mm_a1b2c3\",\n    \"merchantId\": \"8353ffb4664d900d0e\",\n    \"storeId\": \"str_mm_d4e5f6\"\n  },\n  \"message\": \"Multi-merchant group created successfully\"\n}\n```\n\n| Parameter            | Type   | Description |\n|----------------------|--------|-------------|\n| `status`             | string | `SUCCESS` or `ERROR`. |\n| `data.applicationId` | string | Use this ID to track the status of the group creation request. |\n| `data.merchantId`    | string | Group-level merchant ID. Use this when registering shared terminals (Step 3). |\n| `data.storeId`       | string | Group-level store ID. Use this when registering shared terminals (Step 3). |\n| `message`            | string | Human-readable description of the result. |\n\nSave the `merchantId` and `storeId` from this response -- you will need them when registering devices in Step 3.\n\n## Step 2: Add Merchants to the Group\n\nOnboard individual merchants and link them to the multi-merchant group by passing the group's `multiMerchantId` in the standard Create Merchant request.\n\n```\nPOST /partners/{partnerId}/merchants\n```\n\n**Request (minimal):**\n\n```json\n{\n  \"country\": \"SE\",\n  \"organisation\": {\n    \"corporateId\": \"5566692092\"\n  },\n  \"multiMerchantId\": \"8353ffb4664d900d0e\",\n  \"controlFields\": {\n    \"transactionPricingPlan\": \"SP_SE_Fix129\"\n  }\n}\n```\n\n| Parameter                              | Type   | Required | Description |\n|----------------------------------------|--------|----------|-------------|\n| `country`                              | string | Yes      | Two-letter ISO country code in uppercase. |\n| `organisation.corporateId`             | string | Yes      | Corporate/organisation ID of the merchant being added. |\n| `organisation.legalName`               | string | Conditional | Legal name of the organisation. Required for Payment Facilitator (PF) partners. |\n| `organisation.mccCode`                 | string | Conditional | Merchant Category Code. Required for PF partners. |\n| `multiMerchantId`                      | string | Yes      | The `merchantId` returned from Step 1. Links this merchant to the shared group. |\n| `controlFields.transactionPricingPlan` | string | Conditional | Billing plan for transaction costs. Required if more than one plan exists. |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"applicationId\": \"app_merch_g7h8i9\",\n    \"webKybUrl\": \"https://kyb.surfboardpayments.com/app_merch_g7h8i9\",\n    \"merchantId\": \"mrc_vendor_001\"\n  },\n  \"message\": \"Merchant application created successfully\"\n}\n```\n\n| Parameter            | Type   | Description |\n|----------------------|--------|-------------|\n| `data.applicationId` | string | Track the merchant onboarding status with this ID. |\n| `data.webKybUrl`     | string | KYB (Know Your Business) link. Share this with the merchant to complete their verification. |\n| `data.merchantId`    | string | The merchant's individual ID (PF partners only -- returned when the merchant is created immediately). |\n| `data.storeId`       | string | The merchant's store ID (PF partners only). |\n| `data.shortLinkUrl`  | string | Shortened KYB URL, returned when `generateShortLink` is set to `true`. |\n\nRepeat this step for every merchant that should be part of the group. Each merchant completes their own KYB verification independently.\n\n### Adding Merchants via the Partner Portal\n\nYou can also add merchants through the Partner Portal UI:\n\n1. Log in to your **Partner Portal** and go to the **Applications** section.\n2. Click **Create Application**.\n3. Enable the **MultiMerchant** toggle.\n4. Select the group from the **MultiMerchantId or name** dropdown.\n5. Fill in the required merchant details and click **Create Application**.\n6. Share the generated WebKYB link with the merchant.\n\n### Organisation and Address Details\n\nFor PF partners or when full merchant details are required, you can include comprehensive organisation information:\n\n```json\n{\n  \"country\": \"SE\",\n  \"organisation\": {\n    \"corporateId\": \"5566692092\",\n    \"legalName\": \"Vendor AB\",\n    \"mccCode\": \"5812\",\n    \"address\": {\n      \"addressLine1\": \"Storgatan 10\",\n      \"city\": \"Stockholm\",\n      \"countryCode\": \"SE\",\n      \"postalCode\": \"11123\"\n    },\n    \"phoneNumber\": {\n      \"code\": 46,\n      \"number\": \"701234567\"\n    },\n    \"email\": \"vendor@example.com\"\n  },\n  \"multiMerchantId\": \"8353ffb4664d900d0e\",\n  \"controlFields\": {\n    \"transactionPricingPlan\": \"SP_SE_Fix129\"\n  }\n}\n```\n\n## Step 3: Register a Device to the Group\n\nOnce the group is created, register a physical terminal using the group-level `merchantId` and `storeId` from Step 1. This makes the terminal available to all merchants in the group.\n\n```\nPOST /merchants/{merchantId}/stores/{storeId}/devices\n```\n\nUse the **group-level** `merchantId` and `storeId` returned in Step 1, not an individual merchant's IDs.\n\n**Request:**\n\n```json\n{\n  \"registrationIdentifier\": \"250901\",\n  \"terminalName\": \"Kiosk One\"\n}\n```\n\n| Parameter                | Type   | Required | Description |\n|--------------------------|--------|----------|-------------|\n| `registrationIdentifier` | string | Yes      | 6-digit code shown on the terminal at startup. For SurfPad and Printer devices, use the serial number from the back of the device. |\n| `terminalName`           | string | Yes      | A friendly name for the terminal (e.g., \"Food Court Register 1\"). |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"terminalId\": \"trm_shared_001\",\n    \"registrationStatus\": \"REGISTERED\"\n  },\n  \"message\": \"Terminal registered successfully\"\n}\n```\n\n| Parameter                 | Type   | Description |\n|---------------------------|--------|-------------|\n| `data.terminalId`         | string | Terminal ID of the registered device. |\n| `data.registrationStatus` | string | `REGISTERED` for a new device, `ALREADY_REGISTERED` if the device was previously linked. |\n\n## Processing Payments\n\nAfter setup, each merchant processes payments independently using their own `merchantId` -- the shared terminal handles the routing automatically. When creating an order, the merchant specifies the terminal ID of the shared device:\n\n```json\n{\n  \"terminal$id\": \"trm_shared_001\",\n  \"orderLines\": [...],\n  \"totalOrderAmount\": {...}\n}\n```\n\nKey points for payment processing on shared terminals:\n\n- **Orders** are tied to the individual merchant's `merchantId`, not the group.\n- **Payouts** are settled to each merchant's own bank account.\n- **Transaction history** is kept separate per merchant.\n- The terminal displays the correct merchant name and receipt details for each transaction.\n\n## Important Considerations\n\n- **One location per group.** A multi-merchant group represents a single physical location. If you have vendors across multiple sites, create a separate group for each location.\n- **Merchant independence.** Adding a merchant to a group does not affect their ability to have their own dedicated terminals elsewhere. The `multiMerchantId` link only applies to the shared setup.\n- **KYB is still required.** Each merchant must complete their own Know Your Business verification regardless of being part of a group. The group setup does not bypass compliance requirements.\n- **Terminal limits.** You can register multiple terminals to the same group. There is no restriction on the number of devices per group.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Create multi-merchant group | POST | `/partners/{partnerId}/multi-merchant` |\n| Add merchant to group | POST | `/partners/{partnerId}/merchants` |\n| Register shared terminal | POST | `/merchants/{merchantId}/stores/{storeId}/devices` |"
    },
    {
      "title": "Store Management",
      "slug": "store-management",
      "url": "/developers/guides/store-management",
      "description": "Create, update, verify, and manage in-store and online stores using the Surfboard Payments Store APIs.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Stores",
        "Domain Verification",
        "Management"
      ],
      "content": "## Overview\n\nStores 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.\n\nA 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.\n\n## Prerequisites\n\n- A registered **partner** and **merchant** in the Surfboard system\n- Your `partnerId` and `merchantId`\n- API credentials (API key and API secret)\n\n## Create an In-Store (Physical) Store\n\nUse the Create Store endpoint to add a new physical store under a merchant. The store will be assigned a unique `storeId` on creation.\n\n```\nPOST /partners/:partnerId/merchants/:merchantId/stores\n```\n\n### Request\n\n```json\n{\n  \"storeName\": \"Stockholm Flagship\",\n  \"email\": \"flagship@example.com\",\n  \"phoneNumber\": {\n    \"code\": 46,\n    \"number\": \"701234567\"\n  },\n  \"address\": \"Drottninggatan 10\",\n  \"city\": \"Stockholm\",\n  \"zipCode\": \"103 16\",\n  \"country\": \"SE\"\n}\n```\n\n### Key Request Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `storeName` | string | Yes | Name of the store |\n| `email` | string | No | Store email. Mandatory for online payment support |\n| `phoneNumber.code` | number | Yes | International dialing code (e.g., `46` for Sweden) |\n| `phoneNumber.number` | string | Yes | Phone number, 5-15 digits |\n| `address` | string | Yes | Address line 1 |\n| `city` | string | Yes | City name |\n| `zipCode` | string | Yes | Postal code |\n| `country` | string | Yes | Two-letter ISO country code (e.g., `SE`) |\n| `acquirerMID` | string | No | Acquirer Merchant ID, required for PF partners with store-based acquiring |\n\n### Response\n\nThe response includes the new `storeId` along with the full store object:\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"storeId\": \"store-abc-123\",\n    \"merchantId\": \"merchant-xyz-789\",\n    \"name\": \"Stockholm Flagship\",\n    \"address\": {\n      \"addressLine1\": \"Drottninggatan 10\",\n      \"city\": \"Stockholm\",\n      \"countryCode\": \"SE\",\n      \"postalCode\": \"103 16\"\n    },\n    \"phone\": \"+46701234567\",\n    \"email\": \"flagship@example.com\"\n  },\n  \"message\": \"Store created successfully\"\n}\n```\n\n## Create an Online Store\n\nOnline 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.\n\n```\nPOST /partners/:partnerId/merchants/:merchantId/stores\n```\n\n### Request\n\n```json\n{\n  \"storeName\": \"Web Store\",\n  \"email\": \"webstore@example.com\",\n  \"phoneNumber\": {\n    \"code\": 46,\n    \"number\": \"701234567\"\n  },\n  \"address\": \"Drottninggatan 10\",\n  \"city\": \"Stockholm\",\n  \"zipCode\": \"103 16\",\n  \"country\": \"SE\",\n  \"onlineInfo\": {\n    \"merchantWebshopURL\": \"https://shop.example.com\",\n    \"paymentPageHostURL\": \"https://shop.example.com/payment\",\n    \"termsAndConditionsURL\": \"https://shop.example.com/terms\",\n    \"privacyPolicyURL\": \"https://shop.example.com/privacy\"\n  }\n}\n```\n\n### Online Info Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `onlineInfo.merchantWebshopURL` | string | Yes | The merchant's webshop URL |\n| `onlineInfo.paymentPageHostURL` | string | No | Payment page URL. Required for SDK mode integration |\n| `onlineInfo.termsAndConditionsURL` | string | Yes | URL to terms and conditions (must include refund policy) |\n| `onlineInfo.privacyPolicyURL` | string | Yes | URL to the privacy policy |\n\nWhen an online store is created, the response includes two domain verification keys:\n\n- `merchantURLDomainVerficationKey` -- used to verify ownership of the webshop domain\n- `paymentPageURLDomainVerficationKey` -- used to verify the payment page domain (if provided)\n\nYou must complete domain verification before the store is approved for online payments.\n\n## Domain Verification\n\nAfter creating an online store, verify that you own the domains you provided. This is a two-step process.\n\n### Step 1: Set DNS TXT Records\n\nTake 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.\n\n### Step 2: Trigger Verification\n\n```\nPOST /partners/:partnerId/merchants/:merchantId/stores/:storeId/verify\n```\n\n```json\n{\n  \"domainType\": \"MERCHANT_WEBSHOP_URL\"\n}\n```\n\nThe `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.\n\n### Check Verification Status\n\nYou can retrieve the current domain verification status at any time:\n\n```\nGET /partners/:partnerId/merchants/:merchantId/stores/:storeId/online\n```\n\nOnce verification succeeds, the store enters an internal approval process. After approval, the store can take online payments.\n\n### Default Online Terminals\n\nCreating 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.\n\n**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:\n\n```\nPOST /merchants/:merchantId/stores/:storeId/online-terminals\n```\n\n```json\n{\n  \"onlineTerminalMode\": \"SelfHostedPage\"\n}\n```\n\n## Fetch Store Details\n\nRetrieve complete information about a specific store, including its status and online onboarding status.\n\n```\nGET /partners/:partnerId/merchants/:merchantId/stores/:storeId\n```\n\n### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"storeId\": \"store-abc-123\",\n    \"merchantId\": \"merchant-xyz-789\",\n    \"name\": \"Web Store\",\n    \"status\": \"ACTIVE\",\n    \"onlineOnboardingStatus\": \"APPROVED\",\n    \"address\": {\n      \"addressLine1\": \"Drottninggatan 10\",\n      \"city\": \"Stockholm\",\n      \"countryCode\": \"SE\",\n      \"postalCode\": \"103 16\"\n    },\n    \"phone\": \"+46701234567\",\n    \"email\": \"webstore@example.com\",\n    \"onlineInfo\": {\n      \"merchantWebshopURL\": \"https://shop.example.com\",\n      \"paymentPageHostURL\": \"https://shop.example.com/payment\",\n      \"termsAndConditionsURL\": \"https://shop.example.com/terms\",\n      \"privacyPolicyURL\": \"https://shop.example.com/privacy\"\n    }\n  },\n  \"message\": \"Store details fetched successfully\"\n}\n```\n\nStore status values: `ACTIVE`, `DEACTIVATED`, `BLOCKED`, `INACTIVE`.\nOnline onboarding status values: `APPROVED`, `INITIATED`, `FAILED`.\n\n## List All Stores\n\nRetrieve every store registered under a merchant to get a complete overview.\n\n```\nGET /partners/:partnerId/merchants/:merchantId/stores\n```\n\nThe response returns an array of store objects, each with the same structure as the single-store response above.\n\n## Update Store Details\n\nModify an existing store's name, contact information, address, or add online capabilities. Send only the fields you want to change.\n\n```\nPUT /partners/:partnerId/merchants/:merchantId/stores/:storeId\n```\n\n### Request\n\n```json\n{\n  \"storeName\": \"Stockholm Flagship - Updated\",\n  \"email\": \"new-email@example.com\",\n  \"phoneNumber\": {\n    \"code\": 46,\n    \"number\": \"709876543\"\n  }\n}\n```\n\nAll 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.\n\nIf you add `onlineInfo` during an update, the response will include the domain verification keys, and you must complete domain verification as described above.\n\n## Fetch Store Terminals\n\nRetrieve all terminals registered under a specific store. You can optionally filter by terminal type.\n\n```\nGET /partners/:partnerId/merchants/:merchantId/stores/:storeId/terminals\n```\n\nOptional query parameter: `terminalType` (e.g., `surfpad`, `PaymentPage`, `SelfHostedPage`, `MerchantInitiated`).\n\n### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"terminalId\": \"terminal-001\",\n      \"terminalType\": \"PaymentPage\",\n      \"terminalStatus\": \"ACTIVE\",\n      \"storeId\": \"store-abc-123\",\n      \"terminalName\": \"Online Checkout\",\n      \"startDate\": \"2025-06-15T10:00:00Z\"\n    },\n    {\n      \"terminalId\": \"terminal-002\",\n      \"terminalType\": \"MerchantInitiated\",\n      \"terminalStatus\": \"ACTIVE\",\n      \"storeId\": \"store-abc-123\",\n      \"startDate\": \"2025-06-15T10:00:00Z\"\n    }\n  ],\n  \"message\": \"Terminals fetched successfully\"\n}\n```\n\nThis 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.\n\nTerminal types include: `surfpad`, `surftouch`, `surfprint`, `checkoutPro`, `checkoutX`, `PaymentPage`, `SelfHostedPage`, `MerchantInitiated`, `printer`, `surftester`.\n\nTerminal statuses: `REGISTERED`, `ACTIVE`, `IN_ACTIVE`, `DE_REGISTERED`.\n\n## Deactivate a Store\n\nRemove a store that is no longer needed. You can deactivate immediately or schedule deactivation for a future date.\n\n```\nDELETE /partners/:partnerId/merchants/:merchantId/stores/:storeId\n```\n\nOptional query parameter: `deactivationDate` in `yyyy-mm-dd` format. If omitted, the store is deactivated immediately.\n\n> **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.\n\n### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Store deactivated successfully\"\n}\n```\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Create store | POST | `/partners/:partnerId/merchants/:merchantId/stores` |\n| Fetch store details | GET | `/partners/:partnerId/merchants/:merchantId/stores/:storeId` |\n| List all stores | GET | `/partners/:partnerId/merchants/:merchantId/stores` |\n| Update store | PUT | `/partners/:partnerId/merchants/:merchantId/stores/:storeId` |\n| Verify domain | POST | `/partners/:partnerId/merchants/:merchantId/stores/:storeId/verify` |\n| Fetch domain status | GET | `/partners/:partnerId/merchants/:merchantId/stores/:storeId/online` |\n| Fetch store terminals | GET | `/partners/:partnerId/merchants/:merchantId/stores/:storeId/terminals` |\n| Deactivate store | DELETE | `/partners/:partnerId/merchants/:merchantId/stores/:storeId` |"
    },
    {
      "title": "Gift Cards & Promotions",
      "slug": "gift-cards-promotions",
      "url": "/developers/guides/gift-cards-promotions",
      "description": "Issue and manage gift cards, track transactions, and create marketing promotions using the Surfboard Payments APIs.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Gift Cards",
        "Promotions",
        "Commerce"
      ],
      "content": "## Overview\n\nSurfboard Payments provides APIs for two complementary commerce features: **gift cards** for stored-value and entitlement-based programs, and **promotions** for marketing campaigns displayed across merchant channels. This guide covers creating and managing both, with full API details and request/response examples.\n\n## Gift Cards\n\nGift cards in Surfboard come in two types:\n\n- **FUND** -- A stored monetary balance. Customers spend down the balance over one or more transactions.\n- **ENTITLEMENT** -- A usage-limited card. Instead of a cash value, the card grants a fixed number of redemptions (e.g., \"5 free coffees\").\n\n### Create a Gift Card\n\n```\nPOST /gift-cards\n```\n\n#### FUND Type Request\n\n```json\n{\n  \"cardType\": \"FUND\",\n  \"amount\": 500,\n  \"currency\": \"SEK\",\n  \"name\": \"Holiday Gift Card\",\n  \"accessControl\": \"OPEN\",\n  \"expiryDate\": \"12/31/2026\",\n  \"note\": \"Happy Holidays!\"\n}\n```\n\n#### ENTITLEMENT Type Request\n\n```json\n{\n  \"cardType\": \"ENTITLEMENT\",\n  \"redemptionLimit\": 10,\n  \"name\": \"Loyalty Reward Card\",\n  \"accessControl\": \"OPEN\",\n  \"expiryDate\": \"06/30/2027\",\n  \"note\": \"Thank you for being a valued customer\"\n}\n```\n\n#### Request Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `cardType` | string | Yes | `FUND` or `ENTITLEMENT` |\n| `amount` | number | Conditional | Monetary amount in smallest currency unit. Required for `FUND` type |\n| `redemptionLimit` | number | Conditional | Number of allowed uses. Required for `ENTITLEMENT` type |\n| `currency` | string | No | ISO currency code (e.g., `SEK`, `EUR`) |\n| `name` | string | No | Display name for the gift card |\n| `accessControl` | string | No | Access control level (e.g., `OPEN`) |\n| `expiryDate` | string | No | Expiry date in `mm/dd/yyyy` or `mm-dd-yyyy` format |\n| `note` | string | No | Optional note or message |\n\n#### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"giftCardId\": \"gc-abc-123\",\n    \"pan\": \"6789012345678901\",\n    \"name\": \"Holiday Gift Card\",\n    \"cardType\": \"FUND\",\n    \"amount\": 500,\n    \"currency\": \"SEK\",\n    \"accessControl\": \"OPEN\",\n    \"status\": \"ACTIVE\",\n    \"expiryDate\": \"12/31/2026\",\n    \"shareableLink\": \"https://giftcards.surfboardpayments.com/gc-abc-123\",\n    \"formats\": {\n      \"qrCode\": \"data:image/png;base64,...\",\n      \"nfcData\": \"NFC_ENCODED_DATA\",\n      \"barcode\": \"data:image/png;base64,...\"\n    },\n    \"externalId\": \"ext-001\",\n    \"externalIdType\": \"CUSTOM\"\n  },\n  \"message\": \"Gift card created successfully\"\n}\n```\n\nThe response includes multiple format representations (QR code, NFC data, barcode) for flexible distribution. The `shareableLink` provides a URL that can be sent directly to the recipient.\n\n### List All Gift Cards\n\nRetrieve a paginated list of all gift cards for a merchant, with optional filtering.\n\n```\nGET /gift-cards\n```\n\n#### Query Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `type` | string | No | Filter by card type: `FUND` or `ENTITLEMENT` |\n| `status` | string | No | Filter by card status |\n\n#### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"giftCardId\": \"gc-abc-123\",\n      \"pan\": \"6789012345678901\",\n      \"name\": \"Holiday Gift Card\",\n      \"cardType\": \"FUND\",\n      \"amount\": 500,\n      \"currentAmount\": 350,\n      \"usageCount\": 2,\n      \"currency\": \"SEK\",\n      \"accessControl\": \"OPEN\",\n      \"status\": \"ACTIVE\",\n      \"expiryDate\": \"12/31/2026\",\n      \"lastTransactionAt\": \"2026-01-15T14:30:00Z\",\n      \"transactionCount\": 2,\n      \"totalRedeemed\": 150\n    }\n  ],\n  \"message\": \"Gift cards fetched successfully\"\n}\n```\n\nNote the tracking fields: `currentAmount` shows the remaining balance for FUND cards, `usageCount` tracks how many times the card has been used, and `totalRedeemed` shows the cumulative amount spent.\n\n### Get Gift Card Details\n\nRetrieve full details for a single gift card, including customer information and format representations.\n\n```\nGET /gift-cards/:id\n```\n\n#### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"giftCardId\": \"gc-abc-123\",\n    \"pan\": \"6789012345678901\",\n    \"name\": \"Holiday Gift Card\",\n    \"cardType\": \"FUND\",\n    \"amount\": 500,\n    \"currentAmount\": 350,\n    \"usageCount\": 2,\n    \"currency\": \"SEK\",\n    \"status\": \"ACTIVE\",\n    \"expiryDate\": \"12/31/2026\",\n    \"lastTransactionAt\": \"2026-01-15T14:30:00Z\",\n    \"transactionCount\": 2,\n    \"totalRedeemed\": 150,\n    \"customerDetails\": {\n      \"customerId\": \"cust-456\",\n      \"firstName\": \"Anna\",\n      \"surname\": \"Svensson\",\n      \"countryCode\": \"SE\",\n      \"emails\": [{ \"email\": \"anna@example.com\" }],\n      \"phoneNumbers\": [\n        {\n          \"phoneNumber\": {\n            \"countryCode\": \"46\",\n            \"number\": \"701234567\"\n          }\n        }\n      ]\n    },\n    \"shareableLink\": \"https://giftcards.surfboardpayments.com/gc-abc-123\",\n    \"formats\": {\n      \"qrCode\": \"data:image/png;base64,...\",\n      \"nfcData\": \"NFC_ENCODED_DATA\",\n      \"barcode\": \"data:image/png;base64,...\"\n    }\n  },\n  \"message\": \"Gift card details fetched successfully\"\n}\n```\n\n### Get Gift Card Transactions\n\nView the transaction history for a specific gift card. Supports filtering by transaction type and pagination via the `x-page-number` header.\n\n```\nGET /gift-cards/:giftCardId/transactions\n```\n\n#### Query Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `transactionType` | string | No | Filter by type: `ISSUED`, `CREDIT`, or `DEBIT` |\n\n#### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"paymentId\": \"pay-789\",\n      \"transactionType\": \"DEBIT\",\n      \"transactionAmount\": 150,\n      \"currency\": \"SEK\",\n      \"valueBefore\": 500,\n      \"valueAfter\": 350,\n      \"orderId\": \"order-456\",\n      \"merchantId\": \"merchant-xyz-789\",\n      \"storeId\": \"store-abc-123\",\n      \"metadata\": {}\n    },\n    {\n      \"paymentId\": \"pay-001\",\n      \"transactionType\": \"ISSUED\",\n      \"transactionAmount\": 500,\n      \"currency\": \"SEK\",\n      \"valueBefore\": 0,\n      \"valueAfter\": 500,\n      \"merchantId\": \"merchant-xyz-789\",\n      \"metadata\": {}\n    }\n  ],\n  \"message\": \"Transactions fetched successfully\"\n}\n```\n\nEach transaction record shows the `valueBefore` and `valueAfter` fields, giving a clear audit trail of the gift card balance over time.\n\n## Promotions\n\nPromotions let you create and manage marketing campaigns that appear across merchant channels, such as on payment terminals, receipts, and idle screens. Each promotion is scoped to a specific merchant and store.\n\n### Create a Promotion\n\n```\nPOST /merchants/:merchantId/stores/:storeId/promotions\n```\n\n#### Request\n\n```json\n{\n  \"title\": \"Summer Sale\",\n  \"name\": \"summer-sale-2026\",\n  \"description\": \"50% off all summer items\",\n  \"assetUrl\": \"https://cdn.example.com/promo-summer.png\",\n  \"type\": \"RECEIPT_BIG\",\n  \"assetOpacity\": \"0.8\",\n  \"backgroundColor\": \"#1e3a5f\",\n  \"contentTextColor\": \"#ffffff\",\n  \"endProductUrl\": \"https://shop.example.com/summer\",\n  \"endProduct\": \"SUMMER-COLLECTION\",\n  \"buttonLabel\": \"Shop Now\",\n  \"priority\": 1,\n  \"startDate\": \"06-01-2026\",\n  \"endDate\": \"08-31-2026\"\n}\n```\n\n#### Request Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `name` | string | Yes | Unique name for the promotion |\n| `type` | string | Yes | Promotion type (e.g., `RECEIPT_BIG`, `RECEIPT_SMALL`) |\n| `priority` | number | Yes | Display priority. Lower numbers = higher priority |\n| `startDate` | string | Yes | Start date in `MM-DD-YYYY` format |\n| `endDate` | string | Yes | End date in `MM-DD-YYYY` format |\n| `title` | string | No | Display title for the promotion |\n| `description` | string | No | Brief description of the promotion |\n| `assetUrl` | string | No | URL of the promotional image |\n| `assetOpacity` | string | No | Image opacity, `0` (transparent) to `1` (opaque) |\n| `backgroundColor` | string | No | Background color in hex format |\n| `contentTextColor` | string | No | Text color in hex format |\n| `endProductUrl` | string | No | URL of the promoted product |\n| `endProduct` | string | No | Product ID linked to the promotion |\n| `buttonLabel` | string | No | Label for the call-to-action button |\n\n#### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"promotionId\": \"promo-abc-456\"\n  },\n  \"message\": \"Promotion created successfully\"\n}\n```\n\n### List All Promotions\n\nRetrieve all promotions for a merchant's store to view, manage, and track active and past campaigns.\n\n```\nGET /merchants/:merchantId/stores/:storeId/promotions\n```\n\n#### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"promotionId\": \"promo-abc-456\",\n      \"merchantId\": \"merchant-xyz-789\",\n      \"storeId\": \"store-abc-123\",\n      \"name\": \"summer-sale-2026\",\n      \"title\": \"Summer Sale\",\n      \"description\": \"50% off all summer items\",\n      \"assetUrl\": \"https://cdn.example.com/promo-summer.png\",\n      \"endProduct\": \"SUMMER-COLLECTION\",\n      \"buttonLabel\": \"Shop Now\",\n      \"startDate\": \"2026-06-01T00:00:00Z\",\n      \"endDate\": \"2026-08-31T00:00:00Z\",\n      \"priority\": \"1\",\n      \"assetOpacity\": \"0.8\",\n      \"backgroundColor\": \"#1e3a5f\",\n      \"contentTextColor\": \"#ffffff\",\n      \"endProductUrl\": \"https://shop.example.com/summer\"\n    }\n  ],\n  \"message\": \"Promotions fetched successfully\"\n}\n```\n\n### Get Promotion by ID\n\nRetrieve the full configuration and current state of a single promotion.\n\n```\nGET /merchants/:merchantId/stores/:storeId/promotions/:promotionId\n```\n\nThe response structure is identical to a single item in the list response above.\n\n### Update a Promotion\n\nModify any attributes of an existing promotion. Send only the fields you want to change.\n\n```\nPUT /merchants/:merchantId/stores/:storeId/promotions/:promotionId\n```\n\n#### Request\n\n```json\n{\n  \"description\": \"Up to 60% off all summer items - extended!\",\n  \"endDate\": \"09-30-2026\",\n  \"priority\": 1\n}\n```\n\nAll fields are optional. The response confirms the update:\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Promotion updated successfully\"\n}\n```\n\n### Delete a Promotion\n\nPermanently remove a promotion and its associated data.\n\n```\nDELETE /merchants/:merchantId/stores/:storeId/promotions/:promotionId\n```\n\n#### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Promotion deleted successfully\"\n}\n```\n\n> **Warning:** Deletion is permanent. The promotion will no longer be active or visible on any channel.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Create gift card | POST | `/gift-cards` |\n| List all gift cards | GET | `/gift-cards` |\n| Get gift card details | GET | `/gift-cards/:id` |\n| Get gift card transactions | GET | `/gift-cards/:giftCardId/transactions` |\n| Create promotion | POST | `/merchants/:merchantId/stores/:storeId/promotions` |\n| List all promotions | GET | `/merchants/:merchantId/stores/:storeId/promotions` |\n| Get promotion by ID | GET | `/merchants/:merchantId/stores/:storeId/promotions/:promotionId` |\n| Update promotion | PUT | `/merchants/:merchantId/stores/:storeId/promotions/:promotionId` |\n| Delete promotion | DELETE | `/merchants/:merchantId/stores/:storeId/promotions/:promotionId` |"
    },
    {
      "title": "Product Catalog",
      "slug": "product-catalog",
      "url": "/developers/guides/product-catalog",
      "description": "Create and manage product catalogs, products, variants, inventory levels, and analytics through the Catalog API.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Products",
        "Catalog",
        "Inventory"
      ],
      "content": "## Overview\n\nThe Product Catalog API lets you build a structured product hierarchy for your stores. You can create catalogs, add products with pricing and tax configuration, define variants (such as sizes and colours), manage stock levels, and pull sales statistics -- all through a single set of REST endpoints.\n\nThis guide walks through every operation in the catalog lifecycle, from creating an empty catalog to pulling performance analytics.\n\n## Prerequisites\n\n- A configured **store** with a valid `storeId`\n- API credentials (API key, API secret)\n\n## Step 1: Create a Product Catalog\n\nA catalog is the top-level container that groups products for a store. Each store can have one or more catalogs.\n\n### Create catalog\n\n```json\nPOST /catalog\n{\n  \"storeId\": \"8136a645a2c2d1bb0f\"\n}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"productcatalogId\": \"cat_91a3f...\"\n  },\n  \"message\": \"Product catalog created successfully\"\n}\n```\n\n### List catalogs\n\nRetrieve all catalogs that exist under the store:\n\n```\nGET /catalog\n```\n\nThe response returns `data.productcatalogId` as an array of catalog IDs associated with the store.\n\n## Step 2: Add Products\n\nWith a catalog in place, add products to it. Each product requires a name, type, pricing, tax, and descriptive metadata.\n\n```json\nPOST /catalog/:catalogId/products\n{\n  \"storeId\": \"8136a645a2c2d1bb0f\",\n  \"name\": \"SurfPad Purple Logo\",\n  \"type\": \"PRODUCT\",\n  \"unitType\": \"FIXED\",\n  \"costPrice\": 20,\n  \"sellingPrice\": 45,\n  \"currencyCode\": \"752\",\n  \"tax\": [\n    {\n      \"type\": \"VAT\",\n      \"percentage\": \"3\"\n    }\n  ],\n  \"description\": \"SurfPad Payment Terminal in Purple\",\n  \"category\": \"electronics\",\n  \"unit\": \"nos\",\n  \"productImages\": [\n    \"https://example.com/images/surfpad-purple.png\"\n  ],\n  \"hsnCode\": \"723453\",\n  \"barCode\": \"7812123454323\"\n}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"productId\": \"prod_82f4a...\"\n  },\n  \"message\": \"Product created successfully\"\n}\n```\n\n### Product types and unit types\n\n| Field | Values | Description |\n|-------|--------|-------------|\n| `type` | `PRODUCT`, `SERVICE` | Whether the item is a physical product or a service |\n| `unitType` | `FIXED`, `VARIABLE`, `FREE_AMOUNT` | How quantity and pricing are determined |\n\n### Fetch a single product\n\n```\nGET /catalog/:catalogId/products/:productId\n```\n\nPass `storeId` as a query parameter. The response includes the full product object with pricing, tax, attributes, and inventory status.\n\n### List all products in a catalog\n\n```\nGET /catalog/:catalogId/products\n```\n\nReturns an array of products including their variants, inventory, billing plans, campaign info, and tax breakdown.\n\n## Step 3: Add Product Variants\n\nVariants represent different versions of a product, such as colour or size options. Attach them to an existing product.\n\n```json\nPOST /catalog/:catalogId/products/:productId/variants\n{\n  \"storeId\": \"8136a645a2c2d1bb0f\",\n  \"variants\": [\n    {\n      \"name\": \"SurfPad Blue Variant\",\n      \"description\": \"Blue variant of SurfPad\",\n      \"costPrice\": 10,\n      \"sellingPrice\": 12,\n      \"currencyCode\": \"752\",\n      \"productImages\": [\n        \"https://example.com/images/surfpad-blue.png\"\n      ],\n      \"hsnCode\": \"123453\",\n      \"barCode\": \"1212123454323\",\n      \"attributeValues\": [\n        {\n          \"attributeKey\": \"colour\",\n          \"displayName\": \"blue\",\n          \"value\": \"#0000FF\"\n        },\n        {\n          \"attributeKey\": \"size\",\n          \"displayName\": \"medium\",\n          \"value\": \"M\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"variants\": [\"var_73b1c...\"]\n  },\n  \"message\": \"Variants added successfully\"\n}\n```\n\nEach variant in the `attributeValues` array uses an `attributeKey` (e.g. `colour`, `size`) paired with a `displayName` and `value` so the storefront can render selectable options.\n\n## Step 4: Link Related Products\n\nDrive cross-sell and upsell opportunities by associating related products with a primary product.\n\n```json\nPOST /catalog/:catalogId/products/:productId/related-products\n{\n  \"storeId\": \"8136a645a2c2d1bb0f\",\n  \"relatedProducts\": [\n    {\n      \"productId\": \"prod_82f4a...\",\n      \"relatedProductId\": \"prod_55d2b...\"\n    }\n  ]\n}\n```\n\nThe API returns a `SUCCESS` status when the association is saved.\n\n## Step 5: Update Products and Variants\n\n### Update a product\n\nUse `PATCH` to modify any product field. Only the fields you include will be changed.\n\n```json\nPATCH /catalog/:catalogId/products/:productId\n{\n  \"storeId\": \"8136a645a2c2d1bb0f\",\n  \"name\": \"SurfPad Black Logo\",\n  \"sellingPrice\": 15,\n  \"description\": \"SurfPad Payment Terminal in Black\"\n}\n```\n\n### Update a variant\n\nThe same partial-update approach works for variants:\n\n```json\nPATCH /catalog/:catalogId/products/:productId/variants/:variantId\n{\n  \"storeId\": \"8136a645a2c2d1bb0f\",\n  \"name\": \"SurfPad Black Logo - Large\",\n  \"sellingPrice\": 18,\n  \"description\": \"SurfPad Payment Terminal in Black - Large Size\"\n}\n```\n\nBoth endpoints return `{ \"status\": \"SUCCESS\" }` on success.\n\n## Step 6: Manage Inventory\n\nTrack stock at both the product level and the individual variant level.\n\n### Update product inventory\n\n```json\nPATCH /catalog/:catalogId/products/:productId/inventory\n{\n  \"storeId\": \"8136a645a2c2d1bb0f\",\n  \"inventory\": {\n    \"productId\": \"prod_82f4a...\",\n    \"inventory\": {\n      \"quantity\": 10,\n      \"reorderLevel\": 5,\n      \"reorderQuantity\": 10\n    }\n  }\n}\n```\n\n### Update variant inventory\n\n```json\nPATCH /catalog/:catalogId/products/:productId/variants/:variantId/inventory\n{\n  \"storeId\": \"8136a645a2c2d1bb0f\",\n  \"operation\": \"STOCK_UP\",\n  \"quantity\": 15,\n  \"unit\": \"nos\"\n}\n```\n\nThe `operation` field controls how stock is modified (e.g. `STOCK_UP` to add inventory). The `unit` field accepts standard measurement units such as `nos`, `kg`, `l`, `m`, and many others.\n\n## Step 7: View Statistics\n\n### Product statistics\n\nGet sales performance, inventory levels, and VAT breakdowns for a single product:\n\n```\nGET /catalog/:catalogId/products/:productId/statistics\n```\n\nOptionally pass `startDate` and `endDate` query parameters in `YYYY-MM-DD` format to filter by date range. The response includes:\n\n- **Sales by currency** -- units sold, units returned, revenue, VAT, campaign discounts, order count, and average order value\n- **Inventory status** -- current stock, stock in, stock out\n- **VAT breakdown** -- amount and taxable total per VAT percentage\n- **Variant-level stats** -- the same metrics broken down per variant\n\n### Catalog statistics\n\nGet an aggregate view across the entire catalog:\n\n```\nGET /catalog/:catalogId/products/statistics\n```\n\nThis returns:\n\n- **Summary** -- total products, total variants, and aggregated sales metrics by currency\n- **VAT breakdown** -- catalog-wide tax totals\n- **Top-selling products** -- ranked by units sold and revenue, with per-currency breakdowns\n\nBoth endpoints support optional `startDate` and `endDate` query parameters.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Create catalog | POST | `/catalog` |\n| List catalogs | GET | `/catalog` |\n| Create product | POST | `/catalog/:catalogId/products` |\n| Fetch product by ID | GET | `/catalog/:catalogId/products/:productId` |\n| List all products | GET | `/catalog/:catalogId/products` |\n| Update product | PATCH | `/catalog/:catalogId/products/:productId` |\n| Add variants | POST | `/catalog/:catalogId/products/:productId/variants` |\n| Update variant | PATCH | `/catalog/:catalogId/products/:productId/variants/:variantId` |\n| Add related products | POST | `/catalog/:catalogId/products/:productId/related-products` |\n| Update product inventory | PATCH | `/catalog/:catalogId/products/:productId/inventory` |\n| Update variant inventory | PATCH | `/catalog/:catalogId/products/:productId/variants/:variantId/inventory` |\n| Product statistics | GET | `/catalog/:catalogId/products/:productId/statistics` |\n| Catalog statistics | GET | `/catalog/:catalogId/products/statistics` |"
    },
    {
      "title": "Settlements & Reporting",
      "slug": "settlements-reporting",
      "url": "/developers/guides/settlements-reporting",
      "description": "Retrieve settlement reports, view adjustments, manage merchant charges, and register customer profiles for reconciliation and billing.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Settlements",
        "Reporting",
        "Charges",
        "Adjustments"
      ],
      "content": "## Overview\n\nOnce 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.\n\nThis guide covers four related capabilities:\n\n1. **Settlement reports** -- retrieve payout summaries for a merchant over a given period.\n2. **Adjustments** -- view tips, surcharges, insurance, and other amounts applied to orders.\n3. **Merchant charges** -- create, update, and list one-time or recurring charges billed to a merchant.\n4. **Customer details** -- register customer profiles with addresses, contact information, and linked payment cards.\n\nIt 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.\n\n## Prerequisites\n\n- A valid `partnerId` and `merchantId`\n- API credentials (API key, API secret)\n\n## Settlement Reports\n\nSettlement 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.\n\n### Fetch settlement reports\n\n```\nGET /partners/:partnerId/merchants/:merchantId/reports\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"payoutId\": \"po_83a1f...\",\n      \"merchantId\": \"m_91b2c...\",\n      \"transactionStartDate\": \"2026-01-01\",\n      \"transactionEndDate\": \"2026-01-31\",\n      \"settlementDate\": \"2026-02-03\",\n      \"reportType\": \"MONTHLY\",\n      \"url\": \"https://reports.surfboardpayments.com/settlements/po_83a1f...\",\n      \"totalSale\": 1250000,\n      \"totalRefund\": 35000,\n      \"fee\": 18750,\n      \"payout\": 1196250\n    }\n  ],\n  \"message\": \"Settlement reports fetched successfully\"\n}\n```\n\n### Response fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `payoutId` | string | Identifies this specific payout |\n| `transactionStartDate` | string | First transaction date covered (`YYYY-MM-DD`) |\n| `transactionEndDate` | string | Last transaction date covered (`YYYY-MM-DD`) |\n| `settlementDate` | string | Date the payout was issued (`YYYY-MM-DD`) |\n| `reportType` | string | `MONTHLY` or `DAILY` |\n| `url` | string | Direct link to view the full report |\n| `totalSale` | number | Total sales amount in smallest currency unit |\n| `totalRefund` | number | Total refunded amount |\n| `fee` | number | Total fees deducted |\n| `payout` | number | Net payout to the merchant |\n\nUse the `url` field to download or redirect merchants to a detailed breakdown of every transaction in the settlement period.\n\n## Reading a Settlement Report\n\nThis is the part support gets asked about most, so it is worth understanding before a merchant asks you.\n\nA monthly report carries two fee totals, and they are usually different numbers:\n\n- The **header figure** is the fee on transactions that happened in that calendar month. It is on a **transaction-date** basis.\n- The **fee column in the payouts breakdown** sums the fees of the payouts issued during that month. It is on a **payout-date** basis.\n\nBoth are correct. They measure different things, and at a month boundary they cannot agree.\n\n### Why the two totals differ\n\nPayouts 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.\n\nTake a merchant on daily payouts in May:\n\n| Transactions | Paid out | Fee |\n|---|---|---|\n| 29--30 April | 1--2 May | 43.50 |\n| 1--28 May | during May | 1,196.50 |\n| 29--31 May | 1--3 June | 87.20 |\n\nThe 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.\n\nThe 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.\n\n### Mapping a transaction to its report\n\nOne rule covers every case:\n\n> **The monthly report follows the transaction date. The payouts breakdown follows the payout date.**\n\nEvery transaction is therefore counted in two places, and at a month boundary those two places are different months:\n\n| Transaction happened | Paid out | Counted in the monthly report for | Appears in the payouts breakdown for |\n|---|---|---|---|\n| 30 April | 2 May | **April** | **May** |\n| 15 May | 17 May | May | May |\n| 31 May | 2 June | **May** | **June** |\n\nThe middle row is what people expect. The first and last rows are what the questions are about.\n\nDrawn on a calendar, the two views are the same trading, shifted by the settlement lag:\n\n```\ntransactions  │ 29 Apr  30 Apr │ 01 May  ...  30 May  31 May │\npaid out      │ 01 May  02 May │ 03 May  ...  01 Jun  02 Jun │\n                └──────┬───────┘              └──────┬──────┘\n                 April's trading,              May's trading,\n                 inside May's payouts          inside June's payouts\n```\n\nTo 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.\n\n### The fee is not taken out of the payout\n\nA 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.\n\nThat 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.\n\nIf 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.\n\n### Which figure answers which question\n\n| The merchant asks | Use |\n|---|---|\n| \"What were my fees for May?\" | The **monthly report header** fee. Transaction basis, the month they actually traded. |\n| \"What was deducted from my account in June?\" | The **May monthly report** fee total. Fees are collected after the month closes. |\n| \"Why was this payout this amount?\" | The **payout row**, or the daily report for that settlement date. |\n| \"What did I sell in May?\" | The **monthly report header** sales figure, not the sum of May's payouts. |\n\nThe 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.*\n\n### Refunds land in the period they were processed\n\nA 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.\n\n### Before escalating a mismatch\n\nWork through this first -- it resolves most reports of a mismatch:\n\n1. 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.\n2. Is a refund or an adjustment sitting in a different period from its original sale?\n3. Is the merchant comparing a fee deduction against the payouts of the same month rather than the month before?\n\nIf 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.\n\n### Getting the numbers over the API\n\nThe 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:\n\n```\nGET /partners/:partnerId/merchants/:merchantId/reports\n```\n\n- Fees a merchant incurred in May: the `MONTHLY` report whose `transactionStartDate` falls in May.\n- Fees inside payouts issued in May: sum `fee` across the reports whose `settlementDate` falls in May.\n\nReading those two into a support tool, side by side and labelled, answers the question before it gets asked.\n\n## Adjustments\n\nAdjustments 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.\n\n### Fetch adjustments\n\n```\nGET /partners/:partnerId/merchants/:merchantId/adjustments?startDate=2026-01-01&endDate=2026-01-31\n```\n\nBoth `startDate` and `endDate` are required query parameters in `YYYY-MM-DD` format.\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"adjustmentId\": \"adj_44c2e...\",\n      \"adjustmentType\": \"TIP\",\n      \"amount\": \"2500\"\n    },\n    {\n      \"adjustmentId\": \"adj_55d3f...\",\n      \"adjustmentType\": \"SURCHARGE\",\n      \"amount\": \"1500\"\n    }\n  ],\n  \"message\": \"Adjustments fetched successfully\"\n}\n```\n\n### Response fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `adjustmentId` | string | Unique identifier for the adjustment |\n| `adjustmentType` | string | Type of adjustment (e.g. `TIP`, `SURCHARGE`, `INSURANCE`) |\n| `amount` | string | Adjustment amount in smallest currency unit |\n\n## Merchant Charges\n\nMerchant charges let partners bill merchants for services, fees, or subscriptions. A charge can be one-time or recurring, and supports VAT.\n\n### Create a charge\n\n```json\nPOST /partners/:partnerId/merchants/:merchantId/charges\n{\n  \"description\": \"Monthly platform fee\",\n  \"currency\": \"752\",\n  \"amount\": 5000000,\n  \"vat\": 35,\n  \"billingDate\": \"2026-03-01\",\n  \"recurring\": {\n    \"frequency\": \"monthly\",\n    \"billingEndDate\": \"2027-03-01\"\n  }\n}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"chargeId\": \"chg_72a4d...\"\n  },\n  \"message\": \"Charge created successfully\"\n}\n```\n\n### Create charge request fields\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `description` | string | Yes | Brief description of the charge |\n| `currency` | string | Yes | Three-digit ISO currency code |\n| `amount` | number | Yes | Charge amount in smallest currency unit |\n| `vat` | number | No | VAT amount |\n| `billingDate` | string | No | Effective date (`YYYY-MM-DD`) |\n| `recurring.frequency` | string | No | Billing frequency (see table below) |\n| `recurring.billingEndDate` | string | No | When to stop recurring charges (`YYYY-MM-DD`) |\n\n### Frequency options\n\n| Value | Cycle |\n|-------|-------|\n| `daily` | Every day |\n| `twiceWeekly` | Twice per week |\n| `weekly` | Every week |\n| `tenDays` | Every 10 days |\n| `fortNightly` | Every 2 weeks |\n| `monthly` | Every month |\n| `everyTwoMonths` | Every 2 months |\n| `trimester` | Every 4 months |\n| `quarterly` | Every 3 months |\n| `twiceYearly` | Every 6 months |\n| `annually` | Every year |\n| `unscheduled` | No fixed schedule |\n\n### Fetch a charge by ID\n\n```\nGET /partners/:partnerId/merchants/:merchantId/charges/:chargeId\n```\n\nThe response includes subscription details, VAT, frequency, billing dates, and any associated `subCharges`. Sub-charges are individual billing instances generated from a recurring charge.\n\n**Key response fields:**\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `isSubscriptionCharge` | boolean | Whether this is a recurring charge |\n| `description` | string | Charge description |\n| `amount` | number | Charge amount in smallest currency unit |\n| `vat` | number | VAT applied |\n| `frequency` | string | Billing frequency |\n| `billingStartDate` | string | Start date (ISO 8601) |\n| `billingEndDate` | string | End date (ISO 8601) |\n| `subCharges` | array | Individual billing instances with their own `chargeId`, `amount`, `status`, and `billingDate` |\n\n### Update a charge\n\nModify an existing charge's amount, VAT, or recurring configuration:\n\n```json\nPUT /partners/:partnerId/merchants/:merchantId/charges/:chargeId\n{\n  \"amount\": 650000,\n  \"vat\": 15,\n  \"recurring\": {\n    \"updateType\": \"onlyNext\",\n    \"billingEndDate\": \"2027-10-23\"\n  }\n}\n```\n\nThe `recurring.updateType` field controls the scope of the update:\n\n| Value | Behaviour |\n|-------|-----------|\n| `onlyNext` | Apply the change only to the next billing cycle |\n| `allFuture` | Apply the change to all future billing cycles |\n\n### List all merchant charges\n\n```\nGET /partners/:partnerId/merchants/:merchantId/charges\n```\n\nReturns 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.\n\n## Billing Plans\n\nA 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.\n\n### Create billing plans\n\n```json\nPOST /partners/:partnerId/billing-plans\n{\n  \"plans\": [\n    {\n      \"id\": \"SP_STANDARD_CARD\",\n      \"paymentMethod\": \"CARD\",\n      \"cardBrand\": \"VISA\",\n      \"terminalType\": \"STANDARD\",\n      \"planType\": \"FIXED\",\n      \"description\": \"Standard card pricing 2026\",\n      \"domesticDebitNonCommercial\": 0.6,\n      \"domesticCreditNonCommercial\": 0.9,\n      \"eeaDebitNonCommercial\": 0.8,\n      \"eeaCreditNonCommercial\": 1.1,\n      \"internationalDebitNonCommercial\": 1.9,\n      \"internationalCreditNonCommercial\": 2.3,\n      \"fixedCost\": 30,\n      \"vatPercentage\": 25\n    }\n  ]\n}\n```\n\n`plans` is an array, so a full price list goes up in one call.\n\n| Field | Description |\n|-------|-------------|\n| `id` | Your identifier for the plan. |\n| `paymentMethod`, `cardBrand`, `terminalType` | What the plan applies to. One plan per combination. |\n| `planType` | `FIXED` for a flat percentage or amount, `VARIABLE` for pricing that depends on transaction type. |\n| `domestic*`, `eea*`, `international*` | Percentage rates, split by debit or credit and commercial or non-commercial. |\n| `minimumCeiling` | Minimum amount for the rate to apply. |\n| `fixedCost` | Fixed cost per transaction, in minor units. |\n| `fixedPercentage` | Flat percentage across the board. |\n| `vatPercentage` | VAT applied to the plan. |\n\nThe 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.\n\n### Manage plans\n\n```\nGET    /partners/:partnerId/billing-plans\nGET    /partners/:partnerId/billing-plans/:id\nDELETE /partners/:partnerId/billing-plans/:id\nGET    /partners/:partnerId/merchants/:merchantId/plans\n```\n\nThe 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).\n\n## Customer Details\n\nThe 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.\n\n### Create a customer\n\n```json\nPOST /customers\n{\n  \"firstName\": \"John\",\n  \"middleName\": \"Doe\",\n  \"birthDate\": \"1990/03/04\",\n  \"countryCode\": \"SE\",\n  \"address\": [\n    {\n      \"addressLine1\": \"Storgatan 12\",\n      \"city\": \"Stockholm\",\n      \"countryCode\": \"SE\",\n      \"postalCode\": \"111 23\",\n      \"role\": \"shipping\"\n    }\n  ],\n  \"phoneNumbers\": [\n    {\n      \"phoneNumber\": {\n        \"code\": \"46\",\n        \"number\": \"701234567\"\n      },\n      \"role\": \"own\"\n    }\n  ],\n  \"emails\": [\n    {\n      \"email\": \"john.doe@example.com\",\n      \"role\": \"personal\"\n    }\n  ],\n  \"cardIds\": [\n    \"824c514bfe001805f0\"\n  ]\n}\n```\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"customerId\": \"cust_61e3b...\"\n  },\n  \"message\": \"Customer created successfully\"\n}\n```\n\n### Customer fields\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `firstName` | string | No | Customer's first name |\n| `lastName` | string | No | Customer's last name |\n| `birthDate` | string | No | Date of birth (`YYYY/MM/DD`) |\n| `countryCode` | string | No | Two-letter ISO country code |\n| `address` | array | No | Array of address objects (shipping, billing, etc.) |\n| `address.addressLine1` | string | Yes | Primary address line |\n| `address.city` | string | Yes | City |\n| `address.countryCode` | string | Yes | Two-letter ISO country code |\n| `address.postalCode` | string | Yes | Postal code |\n| `address.role` | string | No | Address purpose (`shipping`, `billing`) |\n| `emails` | array | No | Array of email objects with `email` and `role` |\n| `phoneNumbers` | array | No | Array of phone objects with nested `phoneNumber` (`code`, `number`) and `role` |\n| `cardIds` | array | No | Payment card identifiers to associate with the customer |\n\n### Fetch a customer\n\n```\nGET /customers/:customerId\n```\n\nReturns the full customer profile including all addresses, emails, phone numbers, and linked card IDs.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Fetch settlement reports | GET | `/partners/:partnerId/merchants/:merchantId/reports` |\n| Fetch adjustments | GET | `/partners/:partnerId/merchants/:merchantId/adjustments` |\n| Create merchant charge | POST | `/partners/:partnerId/merchants/:merchantId/charges` |\n| Fetch charge by ID | GET | `/partners/:partnerId/merchants/:merchantId/charges/:chargeId` |\n| Update merchant charge | PUT | `/partners/:partnerId/merchants/:merchantId/charges/:chargeId` |\n| List all merchant charges | GET | `/partners/:partnerId/merchants/:merchantId/charges` |\n| Create billing plans | POST | `/partners/:partnerId/billing-plans` |\n| Fetch billing plans | GET | `/partners/:partnerId/billing-plans` |\n| Fetch billing plan by ID | GET | `/partners/:partnerId/billing-plans/:id` |\n| Remove billing plan | DELETE | `/partners/:partnerId/billing-plans/:id` |\n| Fetch a merchant's plans | GET | `/partners/:partnerId/merchants/:merchantId/plans` |\n| Create customer | POST | `/customers` |\n| Fetch customer by ID | GET | `/customers/:customerId` |"
    },
    {
      "title": "Account & Service Provider Management",
      "slug": "account-management",
      "url": "/developers/guides/account-management",
      "description": "Create merchant and partner accounts, manage user roles, register service providers, and configure external notifications via the Surfboard API.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Accounts",
        "Service Providers",
        "Partners"
      ],
      "content": "## Overview\n\nThe Surfboard API provides administrative endpoints for managing accounts, service providers, and notification subscriptions. With these APIs you can:\n\n- **Create user accounts** for merchants and partners with role-based access control\n- **Register service providers** under a partner and track their onboarding lifecycle\n- **Query service providers** linked to a merchant or partner\n- **Subscribe to notifications** for automated report delivery via email, Slack, or SFTP\n\n## User Accounts & Roles\n\nEvery account is assigned a role that controls access. The `role` field is optional when creating accounts.\n\n| Role | Description |\n|------|-------------|\n| `SUPER_ADMIN` | Full access to all features and settings. Typically the account owner. |\n| `ADMIN` | Can manage resources, invite users, and configure integrations. |\n| `USER` | Read access with limited operational permissions. |\n\n### Create a Merchant Account\n\n```\nPOST /merchants/{merchantId}/accounts\n```\n\n```json\n{ \"email\": \"admin@merchant.com\", \"role\": \"ADMIN\" }\n```\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `email` | string | Yes | Email address. An invitation is sent to this address. |\n| `role` | string | No | `SUPER_ADMIN`, `ADMIN`, or `USER`. Defaults if omitted. |\n\nReturns `{ \"status\": \"SUCCESS\", \"message\": \"...\" }` on success.\n\n### Create a Partner Account\n\nUses the same request body and response format, scoped to the partner:\n\n```\nPOST /partners/{partnerId}/accounts\n```\n\n```json\n{ \"email\": \"admin@partner.com\", \"role\": \"ADMIN\" }\n```\n\n## Service Provider Management\n\nService providers are third-party entities that participate in transaction processing, revenue sharing, or value-added services. Partners register them, track onboarding, and query them at both partner and merchant level.\n\nFor the full split-payout flow, including linking a service provider to a merchant and setting the share on an order, see [Service Providers & Split Payouts](/developers/guides/service-providers).\n\n### Register a Service Provider\n\nSubmit a new application. The response includes an `applicationId` and a `webKybUrl` for KYB verification.\n\n```\nPOST /partners/{partnerId}/service-providers\n```\n\n```json\n{\n  \"country\": \"SE\",\n  \"organisation\": { \"corporateId\": \"3532007322\" },\n  \"controlFields\": { \"isServiceProvider\": true }\n}\n```\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `country` | string | Yes | Two-letter ISO country code (e.g., `SE`, `NO`, `DK`). |\n| `organisation.corporateId` | string | Yes | Corporate identification number. |\n| `controlFields.isServiceProvider` | boolean | Yes | Must be `true`. |\n\n**Response:**\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"applicationId\": \"app-abc123\",\n    \"webKybUrl\": \"https://kyb.surfboardpayments.com/application/app-abc123\"\n  },\n  \"message\": \"Service provider application created\"\n}\n```\n\nShare the `webKybUrl` with the service provider so they can complete KYB.\n\n### List All Applications\n\nRetrieve all service provider applications, optionally filtered by lifecycle stage.\n\n```\nGET /partners/{partnerId}/service-providers/applications?applicationType=ONBOARDING\n```\n\n| Query Parameter | Type | Description |\n|-----------------|------|-------------|\n| `applicationType` | string | `ONBOARDING`, `RENEWAL`, or `ONBOARDING,RENEWAL`. Defaults to `ONBOARDING`. |\n\n**Response** returns an array of application objects:\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [{\n    \"applicationId\": \"app-abc123\",\n    \"country\": \"SE\",\n    \"corporateId\": \"3532007322\",\n    \"applicationStatus\": \"APPLICATION_SUBMITTED\",\n    \"createdAt\": \"2025-06-15T10:30:00Z\",\n    \"lastUpdatedAt\": \"2025-06-16T08:00:00Z\",\n    \"legalName\": \"Acme Services AB\"\n  }]\n}\n```\n\n### Check Application Status\n\n```\nGET /partners/{partnerId}/service-providers/applications/{applicationId}\n```\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"onboardingStatus\": \"COMPLETED\",\n    \"applicationStatus\": \"MERCHANT_CREATED\",\n    \"serviceProviderId\": \"sp-789xyz\"\n  }\n}\n```\n\nThe `serviceProviderId` is `null` while pending and populated once approved.\n\n### Fetch Service Providers for a Partner\n\n```\nGET /partners/{partnerId}/service-providers\n```\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"serviceProviders\": [{\n      \"serviceProviderId\": \"sp-789xyz\",\n      \"corporateId\": \"3532007322\",\n      \"name\": \"Acme Services AB\",\n      \"email\": \"contact@acmeservices.com\",\n      \"address\": { \"addressLine1\": \"Birger Jarlsgatan 10\", \"city\": \"Stockholm\", \"countryCode\": \"SE\", \"postalCode\": \"114 34\" },\n      \"phoneNumber\": { \"code\": \"46\", \"number\": \"812345678\" }\n    }]\n  }\n}\n```\n\n### Fetch Service Providers for a Merchant\n\nSame response structure as above, scoped to the merchant: `GET /merchants/{merchantId}/service-providers`\n\n## External Notifications\n\nSubscribe to automated event reports delivered via email, Slack, or SFTP. At least one channel is required per subscription.\n\n### Subscribe to Reports\n\n```\nPOST /merchants/{merchantId}/notifications/reports\nPOST /partners/{partnerId}/notifications/reports\n```\n\n```json\n{\n  \"event\": \"DAILY_FILE_TRANSFER\",\n  \"email\": \"finance@merchant.com\",\n  \"slackUrl\": \"https://hooks.slack.com/services/T00/B00/xxx\",\n  \"sftpInfo\": {\n    \"host\": \"sftp.merchant.com\",\n    \"user\": \"surfboard-reports\",\n    \"port\": 22,\n    \"remoteDirectory\": \"/reports/daily\",\n    \"separator\": \",\"\n  }\n}\n```\n\n**Response** returns a `notificationId` per channel. For SFTP, a `publicKey` is included -- add it to your server's authorized keys.\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    { \"notificationId\": \"notif-001\", \"NotificationChannel\": \"EMAIL\" },\n    { \"notificationId\": \"notif-002\", \"NotificationChannel\": \"SLACK\" },\n    { \"notificationId\": \"notif-003\", \"NotificationChannel\": \"SFTP\", \"publicKey\": \"ssh-rsa AAAA...\" }\n  ]\n}\n```\n\n### Fetch Existing Notifications\n\nRetrieve configured subscriptions with optional filters (`event`, `notificationChannel`, `notificationId`):\n\n```\nGET /merchants/{merchantId}/notifications\nGET /partners/{partnerId}/notifications\n```\n\n### Unsubscribe\n\nRemove a subscription by its ID:\n\n```\nDELETE /merchants/{merchantId}/notifications/{notificationId}\nDELETE /partners/{partnerId}/notifications/{notificationId}\n```\n\nReturns `{ \"status\": \"SUCCESS\", \"message\": \"...\" }` on success.\n\n## API Quick Reference\n\n| Operation | Method | Endpoint |\n|-----------|--------|----------|\n| Create merchant account | POST | `/merchants/{merchantId}/accounts` |\n| Create partner account | POST | `/partners/{partnerId}/accounts` |\n| Register service provider | POST | `/partners/{partnerId}/service-providers` |\n| List SP applications | GET | `/partners/{partnerId}/service-providers/applications` |\n| Check SP application status | GET | `/partners/{partnerId}/service-providers/applications/{applicationId}` |\n| Fetch SPs for partner | GET | `/partners/{partnerId}/service-providers` |\n| Fetch SPs for merchant | GET | `/merchants/{merchantId}/service-providers` |\n| Subscribe merchant reports | POST | `/merchants/{merchantId}/notifications/reports` |\n| Subscribe partner reports | POST | `/partners/{partnerId}/notifications/reports` |\n| Fetch merchant notifications | GET | `/merchants/{merchantId}/notifications` |\n| Fetch partner notifications | GET | `/partners/{partnerId}/notifications` |\n| Unsubscribe merchant | DELETE | `/merchants/{merchantId}/notifications/{notificationId}` |\n| Unsubscribe partner | DELETE | `/partners/{partnerId}/notifications/{notificationId}` |"
    },
    {
      "title": "Service Providers & Split Payouts",
      "slug": "service-providers",
      "url": "/developers/guides/service-providers",
      "description": "Route a share of each payment to a third party with Surfboard Flow. Onboard a service provider, link it to a merchant, and set the split on the order.",
      "category": "online",
      "tags": [
        "Online",
        "In-Store",
        "API",
        "Service Providers",
        "Flow",
        "Payouts",
        "Partners"
      ],
      "content": "## Overview\n\nSurfboard Flow lets a partner route part of a payment to someone other than the merchant: a platform fee, a commission, a franchise royalty, or a tip that belongs to an individual. The recipient is called a **service provider**. Once a service provider is onboarded and linked to a merchant, you name it on the order and Surfboard tracks the share, settles it, and reports it. One payment in, several payouts out, with no separate billing or payout code on your side.\n\nThere are three steps, all at the partner level:\n\n1. **Onboard the service provider** -- submit an application, the recipient completes KYB (Know Your Business) or signs an agreement, and Surfboard issues a `serviceProviderId`\n2. **Link it to a merchant** -- a service provider can only take a share from merchants it is linked to\n3. **Set the split on the order** -- add the service provider and its share under `controlFunctions.serviceProviders`\n\n```\nPartner\n  ├── Service provider  (onboarded once, reused across merchants)\n  └── Merchant\n        ├── link ──────► Service provider\n        └── Order\n              └── controlFunctions.serviceProviders[]\n                    └── { serviceProviderId, amount }\n```\n\nThe same mechanism works for in-store and online orders and for every payment method and acquirer.\n\n## Prerequisites\n\n1. A partner account with API credentials and your `partnerId` from the [Developer Portal](https://developers.surfboardpayments.com/)\n2. At least one onboarded merchant. See [Merchant Onboarding](/developers/guides/merchant-onboarding)\n3. Flow enabled on your partner account. Contact your Surfboard account manager if the service provider endpoints return `403`\n\nService provider endpoints are partner-scoped. Send `API-KEY` and `API-SECRET`. The `MERCHANT-ID` header is optional on these calls and, when present, must match the merchant in the path. See [API Conventions](/developers/guides/api-conventions).\n\n## Step 1: Onboard a Service Provider\n\nEvery recipient goes through an application before it can receive funds. This is how Surfboard, as a licensed payment institution, meets its KYC and AML obligations for the entity being paid. There are two kinds of application.\n\n| Kind | Who | Verification | Endpoint |\n|------|-----|--------------|----------|\n| **Company** | A registered business (a franchisor, a software vendor, a marketplace operator) | Hosted web KYB, same as merchant onboarding | `POST /partners/{partnerId}/service-providers` |\n| **Individual** | A private person tied to one merchant (a waiter who should receive their own tips, a stylist, a driver) | Hosted signing flow | `POST /partners/{partnerId}/service-providers/individual` |\n\n### Company service provider\n\nSubmit the company's country and corporate ID:\n\n```\nPOST /partners/{partnerId}/service-providers\n```\n\n```json\n{\n  \"country\": \"SE\",\n  \"organisation\": {\n    \"corporateId\": \"5560000000\"\n  },\n  \"controlFields\": {\n    \"isServiceProvider\": true\n  }\n}\n```\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `country` | string | Yes | Two-letter ISO country code where the company is registered. |\n| `organisation.corporateId` | string | Yes | Corporate registration number, validated against `country`. |\n| `controlFields.isServiceProvider` | boolean | Yes | Must be `true`. Marks the application as a service provider rather than a merchant. |\n\nThe response returns the application and a hosted KYB link:\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"applicationId\": \"838ca3a7c530200810\",\n    \"webKybUrl\": \"https://kyb.surfboardpayments.com/838ca3a7c530200810?pi=...\"\n  },\n  \"message\": \"Service provider company application created successfully\"\n}\n```\n\nShare `webKybUrl` with the company. A signatory fills in the company details, contact information, and bank account for payouts, then signs. Treat the link as sensitive: it grants access to the application.\n\n### Individual service provider\n\nThe typical individual is an employee who should receive money that belongs to them rather than to the business. Tips are the clearest case. Without Flow, a tip left on the terminal is paid out to the restaurant, taxed as the restaurant's revenue, and only then shared with the waiter through payroll. With the waiter onboarded as an individual service provider, the tip is split off at settlement and paid to the waiter directly.\n\nAn individual is onboarded for one specific merchant, so pass the `merchantId` up front. You can also attach a fee configuration if the person should receive a share of the order amount itself, for example a commission:\n\n```\nPOST /partners/{partnerId}/service-providers/individual\n```\n\n```json\n{\n  \"email\": \"anna@restaurant.example\",\n  \"countryCode\": \"SE\",\n  \"merchantId\": \"8385f437bc6d200b50\",\n  \"spType\": \"INDIVIDUAL\",\n  \"config\": {\n    \"deductApplicableTransactionFee\": false\n  }\n}\n```\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `email` | string | Yes | Where the signing link is sent. |\n| `countryCode` | string | Yes | Two-letter ISO country code of the individual. |\n| `merchantId` | string | Yes | The merchant this individual will take a share from. |\n| `spType` | string | No | Type of service provider, for example `INDIVIDUAL`. |\n| `config.feePercentage` | number | No | Percentage of each applicable transaction that goes to the individual. |\n| `config.feeFixedAmount` | number | No | Fixed amount per applicable transaction, in minor currency units. |\n| `config.deductApplicableTransactionFee` | boolean | No | When `true`, Surfboard's transaction fee is deducted from the individual's share rather than the merchant's. For a waiter receiving tips you normally leave this `false` so the tip arrives in full. |\n\nThe response returns a signing session instead of a KYB link:\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"applicationId\": \"838ca3a7c530200810\",\n    \"signingLink\": \"https://sign.surfboardpayments.com/838ca3a7c530200810\",\n    \"signingSessionId\": \"8391b7c2ad4e100722\",\n    \"status\": \"APPLICATION_INITIATED\"\n  },\n  \"message\": \"Individual SP onboarding initiated successfully\"\n}\n```\n\nThe individual receives the link by email, identifies themselves, and signs the service provider agreement. Once approved, the waiter is associated with the restaurant and can be named on its orders. No separate link step is needed.\n\n### Track the application\n\nThere are no webhooks for service provider applications yet, so poll. Fetch a single application to get its status and, once approved, the `serviceProviderId` you need for the next step:\n\n```\nGET /partners/{partnerId}/service-providers/applications/{applicationId}\n```\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"onboardingStatus\": null,\n    \"applicationStatus\": \"SERVICE_PROVIDER_CREATED\",\n    \"serviceProviderId\": \"839ab2f1c47d300a20\"\n  },\n  \"message\": \"Service provider application status fetched successfully\"\n}\n```\n\n`serviceProviderId` is `null` until the application is approved. The statuses follow the merchant application lifecycle:\n\n| Status | Meaning |\n|--------|---------|\n| `APPLICATION_INITIATED` | Application created, link not yet opened. |\n| `APPLICATION_STARTED` | The recipient has opened the KYB or signing link. |\n| `APPLICATION_SUBMITTED` | Details submitted, awaiting signature or review. |\n| `APPLICATION_PENDING_INFORMATION` | Surfboard needs more information from the recipient. |\n| `APPLICATION_SIGNED` | All required signatures collected. |\n| `APPLICATION_UNDER_REVIEW` | Compliance review in progress. |\n| `APPLICATION_APPROVED` / `APPLICATION_COMPLETED` | Approved, service provider being created. |\n| `SERVICE_PROVIDER_CREATED` | Done. `serviceProviderId` is populated. |\n| `APPLICATION_REJECTED` | Did not pass review. |\n| `APPLICATION_EXPIRED` | Not completed in time. Create a new application. |\n\nTo see every application under the partner, including renewals, list them:\n\n```\nGET /partners/{partnerId}/service-providers/applications?applicationType=ONBOARDING,RENEWAL\n```\n\n`applicationType` defaults to `ONBOARDING`. Each entry carries `applicationStatus`, `legalName`, `webKybUrl` while the link is still valid, and `endDate` for renewals.\n\nOnce created, service providers appear in the partner roster with their contact and address details:\n\n```\nGET /partners/{partnerId}/service-providers\n```\n\n## Step 2: Link the Service Provider to a Merchant\n\nA service provider can only be named on orders from merchants it is linked to. Naming an unlinked one fails order creation with `SP_0001` (see [Create Order Error Codes](/developers/guides/create-order-error-codes)). Company service providers are linked explicitly. Individuals are associated with the merchant given at onboarding.\n\nLink an onboarded service provider:\n\n```\nPOST /partners/{partnerId}/merchants/{merchantId}/service-providers/link\n```\n\n```json\n{\n  \"serviceProviderId\": \"839ab2f1c47d300a20\"\n}\n```\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Service provider linked to merchant successfully\"\n}\n```\n\nOne service provider can be linked to many merchants, and one merchant can have several service providers. A franchisor, for example, is onboarded once and linked to every franchisee.\n\n### Verify the link\n\n```\nGET /partners/{partnerId}/merchants/{merchantId}/service-providers\n```\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"activeServiceProviders\": [\n      {\n        \"merchantId\": \"8385f437bc6d200b50\",\n        \"partnerId\": \"8113d3f8403b380409\",\n        \"isActive\": true,\n        \"serviceProvider\": {\n          \"id\": \"839ab2f1c47d300a20\",\n          \"name\": \"Nordic Franchise AB\"\n        }\n      }\n    ]\n  },\n  \"message\": \"Active service providers fetched successfully\"\n}\n```\n\nThe merchant can see the same list through the merchant-scoped endpoint `GET /merchants/{merchantId}/service-providers`, which returns full contact details.\n\n### Unlink\n\nTo stop a service provider from taking a share of a merchant's payments, remove the link. Orders already created keep their split.\n\n```\nDELETE /partners/{partnerId}/merchants/{merchantId}/service-providers/unlink\n```\n\n```json\n{\n  \"serviceProviderId\": \"839ab2f1c47d300a20\"\n}\n```\n\n### Linking at merchant creation\n\nIf the service provider already exists when you onboard a new merchant, you can link it and set a standing share in the same call. Add `merchantConfig.serviceProvider` to the [Create Merchant](/developers/guides/merchant-onboarding) request:\n\n```json\n{\n  \"country\": \"SE\",\n  \"organisation\": { \"corporateId\": \"5591631360\" },\n  \"controlFields\": {\n    \"merchantConfig\": {\n      \"serviceProvider\": [\n        {\n          \"serviceProviderId\": \"839ab2f1c47d300a20\",\n          \"deductApplicableTransactionFee\": false,\n          \"amount\": {\n            \"percentage\": \"5\",\n            \"fixed\": 200,\n            \"adjustmentTypes\": [\"TIPS\"]\n          }\n        }\n      ]\n    },\n    \"store\": { \"...\": \"...\" }\n  }\n}\n```\n\nThe `amount` object has the same shape as on the order, described below. A share set here becomes the merchant's standing configuration for that service provider.\n\n### Partner Portal\n\nBoth steps can also be done by hand. The Service Providers page in the [Partner Portal](/partner-portal/service-providers) creates applications and shows their status, and each merchant's Service Providers tab links and unlinks providers.\n\n## Step 3: Set the Split on the Order\n\nWith the service provider linked, name it on the order. Nothing else about the order changes: same line items, same totals, same payment initiation.\n\n```\nPOST /orders\n```\n\n```json\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"TABLE-12\",\n      \"name\": \"Dinner for two\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 120000,\n        \"total\": 120000,\n        \"currency\": \"752\",\n        \"tax\": [{ \"amount\": 12857, \"percentage\": 12, \"type\": \"VAT\" }]\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 120000,\n    \"total\": 120000,\n    \"currency\": \"752\",\n    \"tax\": [{ \"amount\": 12857, \"percentage\": 12, \"type\": \"VAT\" }]\n  },\n  \"controlFunctions\": {\n    \"tipsMode\": \"STANDARD\",\n    \"serviceProviders\": [\n      {\n        \"serviceProviderId\": \"839ab2f1c47d300a20\",\n        \"amount\": {\n          \"percentage\": \"100\",\n          \"adjustmentTypes\": [\"TIPS\"]\n        }\n      }\n    ],\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\"\n    }\n  }\n}\n```\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `serviceProviders[]` | array | No | One entry per recipient. Every `serviceProviderId` must be linked to the merchant in the `MERCHANT-ID` header. |\n| `serviceProviderId` | string | Yes | The ID returned when the application was approved. |\n| `amount` | object | No | The share for this order. Omit it to fall back to the share configured on the merchant link. |\n| `amount.percentage` | string | No | Percentage of the order total, for example `\"5\"` for 5%. |\n| `amount.fixed` | string | No | Fixed amount in minor currency units, for example `\"200\"` for 2 SEK. |\n| `amount.adjustmentTypes` | array | No | Adjustment types the share is taken from, for example `[\"TIPS\"]`. A tip added on the terminal is an adjustment on top of the order total, so it is not included unless listed here. |\n\nIn the example above the service provider is the waiter serving table 12. The order total goes to the restaurant as usual. Whatever the guest adds as a tip on the terminal is an adjustment of type `TIPS`, and the entry routes it to the waiter, who is paid at settlement without the amount passing through the restaurant's books. See [Tips Configuration](/developers/guides/tips-configuration) for enabling tips on the terminal.\n\n`percentage` and `fixed` can be combined. A 3% commission plus a 2 SEK fee is `{ \"percentage\": \"3\", \"fixed\": \"200\" }`. The Create Order reference documents both as strings; the Update Order reference accepts numbers.\n\nSeveral service providers on one order each get their own entry:\n\n```json\n\"serviceProviders\": [\n  { \"serviceProviderId\": \"839ab2f1c47d300a20\", \"amount\": { \"percentage\": \"3\" } },\n  { \"serviceProviderId\": \"83a1c4e7f20b100c11\", \"amount\": { \"fixed\": \"500\" } }\n]\n```\n\n### What happens next\n\nSurfboard authorises and captures the payment as usual. When the payment completes, each share is recorded against the transaction. At settlement, the merchant is paid the order amount less the shares, and each service provider is paid its share to the bank account from its application. The split is visible in settlement reports, SFTP exports, and the order itself. Fetching the order returns `controlFunctions.serviceProviders` exactly as sent, so your reconciliation can read the split from the same place it was written. See [Settlements & Reporting](/developers/guides/settlements-reporting).\n\nRefunds follow the money. A refund of a split order reverses the service provider's share in proportion, so you refund the order the same way as any other. See [Refund an Order](/developers/guides/refund-an-order).\n\n### Changing the split\n\nThe split can be changed with [Update Order](https://developers.surfboardpayments.com/api/orders) while the order is still pending. Once the payment is completed, the shares are locked to the transaction.\n\n## Common Patterns\n\n**Platform fee.** The partner onboards itself, or its billing entity, as a company service provider, links it to every merchant, and adds a percentage on each order. The platform's revenue arrives with settlement instead of through monthly invoicing.\n\n**Franchise royalty.** The franchisor is one service provider linked to every franchisee. Each franchisee's orders carry the royalty percentage. Because the split is per order, campaigns or exempt product lines can simply omit the entry.\n\n**Restaurant tips.** Each waiter is onboarded as an individual service provider for the restaurant. The POS puts the waiter serving the table on the order with `adjustmentTypes: [\"TIPS\"]`, and the tip is paid to the waiter at settlement instead of being paid out to the restaurant, taxed, and shared through payroll.\n\n**Staff commissions.** The same setup pays a stylist, trainer, or driver a cut of the order itself. Set `feePercentage` in `config` at onboarding, or a `percentage` on each order for the person who did the work.\n\n**Marketplace seller.** The seller is a company service provider and the marketplace is the merchant. Set the seller's share to the item price less the marketplace's take, using `percentage` for a take rate or `fixed` for a listing fee.\n\n## Error Handling\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| `SP_0001` Service provider IDs `[...]` not associated with this merchant | The ID on the order is not linked to the merchant in `MERCHANT-ID`, or the application is not yet approved. | Check the application status, then link the service provider (Step 2). |\n| `400` Missing required parameter `serviceProviderId` | Link or unlink called without a body. | Send `{ \"serviceProviderId\": \"...\" }`. |\n| `403` on service provider endpoints | Flow is not enabled for the partner. | Contact your account manager. |\n| `404` Resource not found | Wrong `partnerId`, `merchantId`, or `applicationId`. | The merchant must belong to the partner in the path. |\n| `APPLICATION_EXPIRED` | The KYB or signing link was not completed in time. | Create a new application. The old ID cannot be revived. |\n\n## API Quick Reference\n\n| Method | Endpoint | Purpose |\n|--------|----------|---------|\n| `POST` | `/partners/{partnerId}/service-providers` | Create a company service provider application |\n| `POST` | `/partners/{partnerId}/service-providers/individual` | Onboard an individual for a merchant |\n| `GET` | `/partners/{partnerId}/service-providers/applications` | List applications, filter by `applicationType` |\n| `GET` | `/partners/{partnerId}/service-providers/applications/{applicationId}` | Application status and `serviceProviderId` |\n| `GET` | `/partners/{partnerId}/service-providers` | All service providers under the partner |\n| `POST` | `/partners/{partnerId}/merchants/{merchantId}/service-providers/link` | Link a service provider to a merchant |\n| `DELETE` | `/partners/{partnerId}/merchants/{merchantId}/service-providers/unlink` | Remove the link |\n| `GET` | `/partners/{partnerId}/merchants/{merchantId}/service-providers` | Active links on a merchant |\n| `GET` | `/merchants/{merchantId}/service-providers` | Merchant-scoped view with contact details |\n| `POST` | `/orders` | Set the split in `controlFunctions.serviceProviders` |\n\n## Reference\n\n- [Service Providers API](https://developers.surfboardpayments.com/api/service-providers)\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Create Merchant API](https://developers.surfboardpayments.com/api/merchants)\n- [Surfboard Flow](/flow)\n- [Partner Portal: Service Providers](/partner-portal/service-providers)"
    },
    {
      "title": "Payment Methods",
      "slug": "payment-methods",
      "url": "/developers/guides/payment-methods",
      "description": "Activate, deactivate, and list payment methods for a merchant. Manage card, Swish, Klarna, AMEX, Vipps, MobilePay, and more via the API or Partner Portal.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Payment Methods",
        "In-Store"
      ],
      "content": "## Overview\n\nCard payments are enabled by default for all merchants. Use the Payment Methods API to activate or deactivate additional methods like Swish, Klarna, AMEX, Vipps, MobilePay, B2B invoicing, and account-to-account transfers. Payment methods can be set at the merchant or store level.\n\n## Available Payment Methods\n\n| Method | Parameter | Description |\n|--------|-----------|-------------|\n| Card | `card` | Visa, Mastercard (enabled by default) |\n| AMEX | `amex` | American Express |\n| Swish | `swish` | Swedish mobile payments |\n| Klarna | `klarna` | Buy now, pay later |\n| B2B Invoice | `b2binv` | B2B invoice payments |\n| Account-to-Account | `acc2acc` | Bank transfer |\n| Vipps | `svipps` | Norwegian mobile payments |\n| MobilePay | `smobilepay` | Danish mobile payments |\n\n## Activate Payment Methods\n\nEnable one or more payment methods for a merchant in a single request. Set each method to `true` to activate it.\n\n```\nPOST /merchants/:merchantId/payment-methods\n```\n\n### Request\n\n```json\n{\n  \"card\": true,\n  \"swish\": true,\n  \"klarna\": true,\n  \"b2binv\": true\n}\n```\n\n### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"method\": \"card\",\n      \"paymentMethodId\": \"pm_abc123\",\n      \"status\": \"SUCCESS\"\n    },\n    {\n      \"method\": \"swish\",\n      \"paymentMethodId\": \"pm_def456\",\n      \"status\": \"SUCCESS\"\n    }\n  ],\n  \"message\": \"Payment methods activated successfully\"\n}\n```\n\nEach activated method returns a `paymentMethodId` you'll need for fetching details or deactivating later.\n\n### Via Partner Portal\n\n1. Log in to the **Partner Portal** > **Merchants** > select the merchant > **Payment Methods**\n2. Select the payment methods you want to enable\n3. Click **Activate Selected Methods**\n\n## List All Payment Methods\n\nRetrieve all available payment methods and their activation status for a merchant.\n\n```\nGET /merchants/:merchantId/payment-methods\n```\n\n### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"paymentMethodId\": \"pm_abc123\",\n      \"paymentMethod\": \"CARD\"\n    },\n    {\n      \"paymentMethodId\": \"pm_def456\",\n      \"paymentMethod\": \"SWISH\"\n    }\n  ],\n  \"message\": \"Payment methods retrieved successfully\"\n}\n```\n\n## Get Payment Method Details\n\nFetch the configuration and status of a specific payment method.\n\n```\nGET /merchants/:merchantId/payment-methods/:paymentMethodId\n```\n\n### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"paymentMethodId\": \"pm_abc123\",\n    \"paymentMethod\": \"CARD\",\n    \"status\": \"ACTIVATED\",\n    \"acquirerMID\": \"12345678\"\n  },\n  \"message\": \"Payment method retrieved successfully\"\n}\n```\n\nThe `status` field is either `ACTIVATED` or `DEACTIVATED`. For AMEX, the response includes an `amexMID`. For card payments, it includes an `acquirerMID`.\n\n## Deactivate a Payment Method\n\nRemove a payment method from a merchant using its `paymentMethodId`.\n\n```\nDELETE /merchants/:merchantId/payment-methods/:paymentMethodId\n```\n\n### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Payment method deactivated successfully\"\n}\n```\n\n### Via Partner Portal\n\n1. Log in to the **Partner Portal** > **Merchants** > select the merchant > **Payment Methods**\n2. Find the method to remove and click the **Delete** icon\n\n## Store-Level Configuration\n\nYou can scope payment methods to a specific store by including `storeId` in the activation request:\n\n```json\n{\n  \"storeId\": \"YOUR_STORE_ID\",\n  \"swish\": true,\n  \"klarna\": true\n}\n```\n\nThis lets different stores under the same merchant accept different payment methods."
    },
    {
      "title": "Client Auth Tokens",
      "slug": "client-auth-tokens",
      "url": "/developers/guides/client-auth-tokens",
      "description": "Generate client-side authentication tokens for secure API access from browsers and mobile apps without exposing your API key or secret.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Authentication",
        "Security"
      ],
      "content": "## Overview\n\nClient auth tokens let client-side applications (web browsers, mobile apps) authenticate with the Surfboard API without exposing your `API-KEY` or `API-SECRET`. Use them for customer-facing operations where you need to call the API directly from the frontend.\n\n## What You Can Do With Client Tokens\n\nClient tokens support operational API requests:\n\n- Orders\n- Payments\n- Transactions\n- Tips\n- Reporting\n- Branding\n- Receipts\n\n> **Note:** Client tokens cannot perform administrative tasks like creating merchants, managing stores, or other backend operations. Those still require full API credentials.\n\n## Create a Token\n\nGenerate a token by sending a `POST` request with your auth provider credentials.\n\n```\nPOST /partners/:partnerId/token\n```\n\n### Request\n\n```json\n{\n  \"providerId\": \"YOUR_PROVIDER_ID\",\n  \"providerCertificate\": \"YOUR_PROVIDER_CERTIFICATE\",\n  \"externalUserId\": \"user_12345\"\n}\n```\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `providerId` | string | Yes | Provider ID of the auth provider |\n| `providerCertificate` | string | Yes | Certificate of the auth provider |\n| `externalUserId` | string | Yes | Unique identifier for the user (e.g., a customer ID from your system) |\n| `email` | string | No | Email address of the user |\n\n> **Note:** To get your `providerId` and `providerCertificate`, contact [integrations@surfboard.se](mailto:integrations@surfboard.se) or reach out via Slack.\n\n### Response\n\n```json\n{\n  \"token\": \"eyJhbGciOiJIUzI1NiIs...\",\n  \"validUntil\": \"3600\",\n  \"status\": \"SUCCESS\",\n  \"message\": \"Token created successfully\"\n}\n```\n\nThe `validUntil` field indicates the token's validity period in seconds. When a token expires, generate a new one.\n\n## Using Client Tokens\n\nInclude the token in the `Authorization` header as a Bearer token:\n\n```\nAuthorization: Bearer <YOUR_TOKEN>\n```\n\n### Example: Initiate a Payment\n\n```bash\ncurl -X POST YOUR_API_URL/payments \\\n  -H 'Content-Type: application/json' \\\n  -H 'Authorization: Bearer <YOUR_TOKEN>' \\\n  -d '{\n    \"orderId\": \"o_RelSnor1A6gqgKzZxrbM7\",\n    \"paymentMethod\": \"CARD\"\n  }'\n```\n\n## Token Lifecycle\n\n1. **Generate** a token from your backend using full API credentials\n2. **Pass** the token to your frontend application\n3. **Use** the token for client-side API calls\n4. **Refresh** the token when it expires by generating a new one from your backend\n\nKeep your `API-KEY` and `API-SECRET` on the server side. Only the generated token should reach the client."
    },
    {
      "title": "Partner Branding",
      "slug": "partner-branding",
      "url": "/developers/guides/partner-branding",
      "description": "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.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Branding",
        "White-Label"
      ],
      "content": "## Overview\n\nSurfboard 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.\n\n## Set Partner Branding\n\nConfigure the visual appearance for your payment pages and terminals.\n\n```\nPATCH /partners/:partnerId/branding\n```\n\n### Request\n\n```json\n{\n  \"backgroundColor\": \"#071132\",\n  \"brandColor\": \"#0e44e1\",\n  \"accentColor\": \"#00ffa7\",\n  \"footerColor\": \"#071132\",\n  \"rectShape\": \"rounded\",\n  \"fontType\": \"sans-serif\",\n  \"logoUrl\": \"https://your-cdn.com/logo.svg\",\n  \"iconUrl\": \"https://your-cdn.com/icon.png\",\n  \"primaryCoverImage\": \"https://your-cdn.com/cover-primary.jpg\",\n  \"secondaryCoverImage\": \"https://your-cdn.com/cover-secondary.jpg\"\n}\n```\n\nAll fields are optional -- only include the ones you want to update.\n\n### Branding Parameters\n\n| Parameter | Description |\n|-----------|-------------|\n| `backgroundColor` | Background color for pages (hex) |\n| `brandColor` | Primary brand color for buttons and accents (hex) |\n| `accentColor` | Secondary color that complements the brand color (hex) |\n| `footerColor` | Footer background color (hex) |\n| `rectShape` | Button shape: `rounded`, `pill`, or `edgy` |\n| `fontType` | Font family: `sans-serif`, `serif`, or `mono` |\n| `logoUrl` | URL to your logo image |\n| `iconUrl` | URL to your icon/favicon image |\n| `primaryCoverImage` | URL to the primary cover image |\n| `secondaryCoverImage` | URL to the secondary cover image |\n\n### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Branding updated successfully\"\n}\n```\n\n### Via Partner Portal\n\nNavigate to **Settings** > **Set Partner Branding Config**, enter your branding values, and click **Save Changes**.\n\n## Fetch Partner Branding\n\nRetrieve the current branding configuration for your partner account.\n\n```\nGET /partners/:partnerId/branding\n```\n\n### Response\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"backgroundColour\": \"#071132\",\n    \"brandColor\": \"#0e44e1\",\n    \"accentColor\": \"#00ffa7\",\n    \"footerColor\": \"#071132\",\n    \"rectShape\": \"rounded\",\n    \"fontType\": \"sans-serif\",\n    \"logoUrl\": \"https://your-cdn.com/logo.svg\",\n    \"iconUrl\": \"https://your-cdn.com/icon.png\",\n    \"primaryCoverImage\": \"https://your-cdn.com/cover-primary.jpg\",\n    \"secondaryCoverImage\": \"https://your-cdn.com/cover-secondary.jpg\"\n  },\n  \"message\": \"Branding retrieved successfully\"\n}\n```\n\n## How Branding Applies\n\nPartner-level branding is the default for all merchants and stores under your account. It applies to:\n\n- **Payment pages** -- hosted checkout UI\n- **Terminals** -- on-screen branding for smart terminals\n- **Receipts** -- logo and styling on digital receipts\n\nThis means your merchants' customers see your brand, not Surfboard's, across all payment touchpoints."
    },
    {
      "title": "Create Order Error Codes",
      "slug": "create-order-error-codes",
      "url": "/developers/guides/create-order-error-codes",
      "description": "Reference of error codes returned by the create order and initiate payment flows, with notes on when each is thrown and how to handle it.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Orders",
        "Errors",
        "Reference"
      ],
      "content": "## Overview\n\nThis reference lists the error codes returned by the create order and initiate payment flows. Most create order calls also initiate a payment in the same request (via `controlFunctions.initiatePaymentsOptions`), so errors from either step can surface on the same endpoint.\n\nSee the [Create an Order](/developers/guides/create-an-order) guide for request/response shape and the [Payment Lifecycle](/developers/guides/payment-lifecycle) guide for status semantics.\n\n## Error Response Shape\n\nErrors return a `status` of `ERROR` along with an error code and message. Example:\n\n```json\n{\n  \"status\": \"ERROR\",\n  \"errorCode\": \"OR_0042\",\n  \"message\": \"Terminal not found\"\n}\n```\n\nError code prefixes indicate the source:\n\n| Prefix | Source |\n|--------|--------|\n| `OR_` | Orders service (validation, order state, terminal/store checks) |\n| `PS_` | Payment service (payment initiation, refund handling) |\n| `GC_` | Gift card service |\n| `SP_` | Service provider |\n| `P_` | Platform-level validation, ahead of the services above |\n\nMessages may contain placeholders like `<id>`, `<amount>`, `<type>`, or `<status>` -- the API substitutes the real value at runtime.\n\n### `P_0001`, where the message is the code\n\nPayload validation that fails before the order reaches the Orders service comes back as `P_0001`, with the specific reason in free text:\n\n```\nP_0001: Input data validation failed. <reason>\n```\n\n`P_0001` is the only code in this reference where looking the code up tells you nothing. There is one code covering many faults, so **the message is the discriminator**. The three seen most often on a first integration:\n\n| Message fragment | Cause | Fix |\n|---|---|---|\n| `Invalid item price for item id <id>` | `amount.total` sent as the line total | `total` is the **unit** price. See [Create an Order](/developers/guides/create-an-order) |\n| `Invalid total order price` | Tax added on top of the price | Prices are tax-inclusive; `total` must equal `regular`. See [API Conventions](/developers/guides/api-conventions) |\n| `Cannot read properties of undefined (reading 'vatValue')` | A line is missing `amount.tax` | Send a `tax` entry on every line, including zero-rated ones |\n\nThe same underlying faults are also described by `OR_0023` and `OR_0037` below, which is what you get when the order reaches the Orders service before failing. Either code can carry the same message.\n\n## Create Order Errors\n\nErrors thrown while validating and creating the order.\n\n### Validation and Schema\n\n| Code | Message | Notes |\n|------|---------|-------|\n| `OR_0015` | Order validation failed | Wraps the raw schema validator error. Check required fields and types. |\n| `OR_0022` | Invalid order line item. Missing item price | At least one line is missing `amount.regular` or `amount.total`. |\n| `OR_0023` | Invalid item price for item id `[<id>]`, `<errorMsg>` | Price on the named line item failed validation. |\n| `OR_0037` | Invalid total order price | `totalOrderAmount` does not reconcile with the order lines and adjustments. |\n| `OR_0049` | *(empty message)* | Reserved / edge-case validation failure. |\n\n### Merchant, Store, and Terminal\n\n| Code | Message | Notes |\n|------|---------|-------|\n| `PS_0059` | No Merchant found with the given Merchant ID | The `MERCHANT-ID` header does not match a merchant. Order endpoints are not merchant-scoped in the path -- the merchant travels in the header. |\n| `OR_0001` | No Merchant Details found for the Merchant ID | Merchant exists but is missing required configuration. |\n| `OR_0002` | Order creation is not allowed for this merchant type `<type>` | The merchant type does not permit direct order creation. |\n| `OR_0028` | Store is not in an active state | Activate the store before creating orders. |\n| `OR_0042` | Terminal not found | `terminal$id` does not match any terminal. |\n| `OR_0007` | The Terminal ID is not associated with this merchant | Terminal exists but belongs to another merchant. |\n| `OR_0006` | The terminal is in deregistered state | Re-register the terminal before use. |\n| `OR_0050` | Order cannot be created, terminal is in an inactive state | Terminal is registered but not active. |\n\n### Currency\n\n| Code | Message | Notes |\n|------|---------|-------|\n| `OR_0003` | Currency Code is not present for this merchant | Merchant has no configured currency -- contact your onboarding contact. |\n| `OR_0004` | The currency code is not associated with this merchant | Use a currency enabled on the merchant. |\n| `OR_0005` | Currency is mandatory as the merchant has more than one currency | Multi-currency merchants must set `amount.currency` explicitly. |\n| `OR_0048` | Currencies should be same in order lines | All line items must use the same currency. |\n\n### Payment Method Locking\n\n| Code | Message | Notes |\n|------|---------|-------|\n| `OR_0031` | No Payment Method found with the given payment method id | The `lockToPaymentMethods` id is unknown. |\n| `OR_0032` | Lock to payment method is not allowed for this merchant | The merchant does not have the lock-to-method feature enabled. |\n| `SP_0001` | Service provider IDs `[<ids>]` not associated with this merchant | One or more service provider IDs are invalid for this merchant. |\n\n### Refunds (on create order)\n\nRefunds are created as new orders with negative quantities and a `purchaseOrderId` on each line. These errors surface during that validation.\n\n| Code | Message | Notes |\n|------|---------|-------|\n| `OR_0010` | No purchase order found with ID: `<id>` | `purchaseOrderId` does not match any completed order. |\n| `OR_0035` | Cannot refund from purchase order that is not completed. Status: `<status>` | Only completed orders can be refunded. |\n| `OR_0039` | Cannot refund more than the original order amount | Refund amount exceeds the remaining refundable balance. |\n| `PS_0043` | Invalid order line item. Missing purchase order id for refund line | Every refund line must carry `purchaseOrderId`. |\n| `PS_0010` | Unlinked refunds are only supported for CARD payment methods | Unlinked refunds cannot be issued on non-card methods. |\n| `GC_0004` | Gift card refunds are not supported | Gift card payments cannot be refunded via this flow. |\n\n## Initiate Payment Errors\n\nWhen the create order request also initiates a payment, these errors can be returned from the same call. They also surface when you call initiate payment directly.\n\n### Order State\n\n| Code | Message | Notes |\n|------|---------|-------|\n| `PS_0057` | Order not found | The order referenced by the payment request does not exist. |\n| `PS_0062` | Order already paid | The order has already been fully paid. |\n| `OR_0046` | Order cancelled | The order is in a cancelled state and cannot accept payments. |\n| `PS_0019` | Cannot pay `[<amount>]`, amount payable `[<pendingFromOrder>]` | Requested amount exceeds the outstanding balance on the order. |\n\n### Amount and Validation\n\n| Code | Message | Notes |\n|------|---------|-------|\n| `PS_0008` | Payment amount cannot be less than 0.01 | Send a positive amount in the smallest currency unit. |\n| `PS_0033` | `empty payload - payment initiation rejected` or `Initiate payment validation failed with error <err>` | Payload is missing or failed schema validation. |\n| `PS_0025` | Payment initiation failed: `<error.message>` | Wraps unexpected exceptions from downstream services. See the *Handling `PS_0025`* section below. |\n\n### Terminal, Merchant, and Store\n\n| Code | Message | Notes |\n|------|---------|-------|\n| `OR_0042` | Terminal not found | Terminal ID unknown. |\n| `OR_0006` | Terminal `[<terminalId>]` is not active | Re-activate the terminal. |\n| `PS_0013` | No linked terminals found / Terminal not linked to merchant / Terminal not linked to store | Link the terminal to the merchant/store before initiating. |\n| `PS_0059` | Merchant not found | Merchant ID unknown. |\n| `PS_0058` | Store not found | Store ID unknown. |\n\n### Refund-Specific\n\n| Code | Message | Notes |\n|------|---------|-------|\n| `OR_0053` | Mixed linked and unlinked refund lines are not supported | Split mixed refunds into separate requests. |\n| `PS_0104` | Multiple `purchasePaymentId` values found in refund lines / This purchase order has partial payments | Specify which payment to refund using `purchasePaymentId`. |\n| `PS_0017` | No valid purchase payments found for the specified payment IDs / Purchase payment not found | The `purchasePaymentId` does not match a refundable payment. |\n| `PS_0010` | Payment method `<method>` not supported for returns / No payments with matching payment method found / Unlinked refunds are not enabled for this merchant / Unlinked refunds are only supported for `[<methods>]` / Purchase payment does not support CARD\\_NP refund | Refund is not allowed for the given method or configuration. |\n| `PS_0032` | Cannot refund amount `<amount>` (not enough remaining / not enough in card / not enough in other methods / exceeds max for `<method>`) | Refund would exceed the available balance. |\n| `PS_0068` | Cannot refund amount `<amount>` as there are no card payments in the purchase order | No card payments are available to refund against. |\n| `PS_0096` | Cannot initiate refund as there are no payments of type `<method>` | No payments of the requested method exist on the order. |\n| `PS_0034` | Message is missing | Card-not-present refund is missing the `message` field. |\n| `PS_0044` | Cannot refund amount `<amount>` as message `<msg>` is not allowed / Purchase payment id is required, more than one purchase matches the criteria | Card-not-present refund message/ID disambiguation required. |\n\n## Combined Flow Errors\n\nWhen the create order call also initiates a payment, downstream payment failures (processor, network, 3DS) are reported through the payment object rather than as a top-level error:\n\n```json\n{\n  \"status\": \"ERROR\",\n  \"errorCode\": \"<first processor code>\",\n  \"message\": \"Payment failed\",\n  \"payment\": {\n    \"error\": {\n      \"errorMessage\": \"...\",\n      \"errorCode\": [\"...\"],\n      \"psErrorCodes\": [\"...\"]\n    }\n  }\n}\n```\n\nMerge `payment.error.errorCode` and `payment.error.psErrorCodes` to get the full list of processor codes for logging and support.\n\n## Handling `PS_0025` -- Terminal Not Connected\n\nA common `PS_0025` variant is:\n\n```\nPS_0025: Terminal is not connected to server so unable to send transactions\n```\n\nRe-run the terminal configure call and retry the payment. This can be done transparently -- no user interaction is required. It is most common on SoftPOS, where the consumer device can drop its server session between transactions.\n\nSee [Interapp Integration](/developers/guides/interapp-integration) for the full recovery pattern.\n\n## Handling Patterns\n\n- **Retryable vs. terminal.** Treat `PS_0025`, `PS_0013`, and network-class errors as retryable after a configure/refresh. Treat `OR_*` validation errors as terminal -- fix the payload and resubmit.\n- **User-facing vs. internal.** `OR_0022`, `OR_0023`, `OR_0037`, `OR_0048` are actionable by the merchant integration team. `PS_0025`, `PS_0059`, `PS_0058` usually mean misconfiguration -- surface them to an internal log rather than to the cardholder.\n- **Refund edge cases.** When handling `PS_0104`, always re-issue the refund with an explicit `purchasePaymentId` -- there is no sensible default when multiple payments exist on one order.\n- **Log the full response.** Capture `errorCode`, `message`, and `payment.error.*` together. Support tickets without the error code are very hard to trace.\n\n## Reference\n\n- [Create an Order](/developers/guides/create-an-order)\n- [Payment Lifecycle](/developers/guides/payment-lifecycle)\n- [Interapp Integration](/developers/guides/interapp-integration) -- `PS_0025` recovery\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Payments API](https://developers.surfboardpayments.com/api/payments)"
    },
    {
      "title": "Online Payment Link",
      "slug": "online-payment-link",
      "url": "/developers/guides/online-payment-link",
      "description": "Take a payment on the web without a checkout page of your own. Create an online store, verify its domains, create an order against the PaymentPage terminal it comes with, and send the customer the link it returns.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Payment Link",
        "Payment Page",
        "Orders"
      ],
      "content": "## Overview\n\nAn online payment link is an order created against an online terminal. The API returns a URL hosted by Surfboard; you send it to the customer by email, SMS, chat, or a redirect from your own site, and the card details never touch your infrastructure.\n\nThe order call is the same [Create Order](/developers/guides/create-an-order) call you use in store. What changes is everything around it: the terminal must be an **online** terminal, that terminal must sit in an **online store**, and the store's domains must be verified before Surfboard will approve it. This guide walks the whole path once, then covers the `controlFunctions.online` block that shapes the page the customer lands on.\n\nIf you want to render the card fields inside your own page instead of sending the customer away, use the [Self-Hosted Checkout](/developers/guides/self-hosted-checkout) guide. If you want to charge a stored card from your backend with no customer present, see [Server-to-Server API](/developers/guides/server-to-server-api).\n\n## Prerequisites\n\n1. A developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)\n2. A merchant that has completed onboarding and KYB\n3. Control of the DNS for the webshop domain you are going to register\n\n> **Demo environment:** payment page mode is the only online terminal type supported in demo, and only test cards work there. Real cards used in demo are voided automatically after 30 minutes and never settle.\n\n## Step 1: Create an Online Store\n\nTerminals live under stores, and an online terminal needs a store that carries an `onlineInfo` block. Create the store with the webshop details, or add `onlineInfo` to an existing physical store.\n\n```json\nPOST /partners/:partnerId/merchants/:merchantId/stores\n{\n  \"storeName\": \"Web Store\",\n  \"email\": \"webstore@example.com\",\n  \"phoneNumber\": { \"code\": 46, \"number\": \"701234567\" },\n  \"address\": \"Drottninggatan 10\",\n  \"city\": \"Stockholm\",\n  \"zipCode\": \"103 16\",\n  \"country\": \"SE\",\n  \"onlineInfo\": {\n    \"merchantWebshopURL\": \"https://shop.example.com\",\n    \"paymentPageHostURL\": \"https://shop.example.com/payment\",\n    \"termsAndConditionsURL\": \"https://shop.example.com/terms\",\n    \"privacyPolicyURL\": \"https://shop.example.com/privacy\"\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"storeId\": \"81d64e7174dcb00b0f\",\n    \"merchantId\": \"818712cdbcb670070e\",\n    \"name\": \"Web Store\",\n    \"merchantUrlDomainVerificationKey\": \"499470649f03b53fa1175659d4389743974710260b7f410313487e6062b3d559\",\n    \"paymentPageUrlDomainVerificationKey\": \"2179beab4f5e8c3960615205f042939a2ccc6c51a6e5923c9c068b3d9a645590\"\n  },\n  \"message\": \"Store Created Successfully\"\n}\n```\n\n| Field | Required | Notes |\n|-------|----------|-------|\n| `onlineInfo.merchantWebshopURL` | Yes | The shop the customer is buying from. Verified by DNS. |\n| `onlineInfo.termsAndConditionsURL` | Yes | Must include the refund policy. Rendered on the checkout page. |\n| `onlineInfo.privacyPolicyURL` | Yes | Rendered on the checkout page. |\n| `onlineInfo.paymentPageHostURL` | No | Only needed for SDK and iFrame modes. Verified by DNS when present. |\n\n> **Online info can only be set once.** Get these URLs right before you send the call; they cannot be edited afterwards through the same route.\n\nTerms, privacy policy and contact details must also be visible on the webshop itself. This is an acquiring requirement, not a Surfboard preference.\n\n## Step 2: Verify the Domains\n\nThe response carries one verification key per URL. Publish each as a **TXT record** on the matching domain, then ask Surfboard to check it:\n\n```json\nPOST /partners/:partnerId/merchants/:merchantId/stores/:storeId/verify\n{\n  \"domainType\": \"MERCHANT_WEBSHOP_URL\"\n}\n```\n\nRepeat with `\"domainType\": \"PAYMENT_PAGE_HOST_URL\"` if you registered a payment page host. Surfboard also re-checks automatically every six hours, so a record published late is picked up without another call.\n\nVerification is what unlocks the online terminal types: until the webshop domain passes, there is nothing to register a terminal against. Once it passes, the store goes through an internal approval step at Surfboard. Poll the store to see where it stands:\n\n```\nGET /partners/:partnerId/merchants/:merchantId/stores/:storeId/online\n```\n\nSee [Store Management](/developers/guides/store-management) for the full store lifecycle.\n\n## Step 3: Pick Up the Terminal You Already Have\n\nAn online terminal is a mode, not a device — and for payment links you do not have to create one. Creating the online store provisions two terminals by default: a `PaymentPage` terminal, which is the one this guide uses, and a `MerchantInitiated` terminal for backend charges against a stored token. List the store's terminals and take the ID:\n\n```\nGET /partners/:partnerId/merchants/:merchantId/stores/:storeId/terminals\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"terminalId\": \"813ca2cb12ce400405\",\n      \"terminalType\": \"PaymentPage\",\n      \"terminalStatus\": \"ACTIVE\",\n      \"storeId\": \"81d64e7174dcb00b0f\"\n    },\n    {\n      \"terminalId\": \"813ca2cb12ce400406\",\n      \"terminalType\": \"MerchantInitiated\",\n      \"terminalStatus\": \"ACTIVE\",\n      \"storeId\": \"81d64e7174dcb00b0f\"\n    }\n  ],\n  \"message\": \"Terminals fetched successfully\"\n}\n```\n\nStore the `PaymentPage` `terminalId` against something identifiable in your system — it is the `terminal$id` every order in this guide is created against.\n\n| Mode | Use it for | Provisioned with the store |\n|------|------------|----------------------------|\n| `PaymentPage` | Payment links and hosted checkout. This guide. | Yes |\n| `MerchantInitiated` | Backend charges against a stored token, such as subscription renewals. | Yes |\n| `SelfHostedPage` | Card fields rendered on your own page by the Online SDK. Returns a `publicKey` and `sdkUrl`. | No |\n| `iFrame` | An embedded payment frame inside your site. | No |\n\nThe two default terminals exist from the moment the store does, but they cannot take a payment until the domains verify and the store is approved. The other two modes are registered when you need them, and a store can hold as many as you like:\n\n```json\nPOST /merchants/:merchantId/stores/:storeId/online-terminals\n{\n  \"onlineTerminalMode\": \"SelfHostedPage\"\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"terminalId\": \"813ca2cb12ce400407\",\n    \"registrationStatus\": \"REGISTERED\"\n  },\n  \"message\": \"Terminal registered successfully\"\n}\n```\n\n## Step 4: Create the Order\n\nNow the familiar call. The `terminal$id` is the `PaymentPage` terminal from step 3, and the response carries the link.\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"813ca2cb12ce400405\",\n  \"referenceId\": \"order-2026-0418\",\n  \"customer\": {\n    \"person\": {\n      \"name\": { \"firstName\": \"John\", \"lastName\": \"Doe\" },\n      \"email\": \"john@example.com\",\n      \"phoneNumber\": { \"code\": \"46\", \"number\": \"768100190\" }\n    }\n  },\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"Annual Subscription\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 99900,\n        \"total\": 99900,\n        \"currency\": \"752\",\n        \"tax\": [{ \"amount\": 19980, \"percentage\": 25, \"type\": \"VAT\" }]\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 99900,\n    \"total\": 99900,\n    \"currency\": \"752\",\n    \"tax\": [{ \"amount\": 19980, \"percentage\": 25, \"type\": \"VAT\" }]\n  },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"CARD\",\n      \"amount\": 99900\n    },\n    \"online\": {\n      \"paymentPageValidFor\": \"2h\",\n      \"enforce3DSecure\": true,\n      \"generateShortLink\": true,\n      \"payButtonType\": \"PAY\",\n      \"redirectUrl\": \"https://shop.example.com/thanks\",\n      \"failureRedirectUrl\": \"https://shop.example.com/checkout/failed\"\n    }\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderId\": \"8455c12f9fd0620a010b\",\n    \"paymentPageLink\": \"https://pay.withsurfboard.com/8455c12f9fd0620a010b?pi=Dr4GoyMXF0zHvjca_Oa0vHgxcT-OD1qp7KdokyI7dkTwRwYJt8nkXyQm3bT6vqCfgraOw50Bf5uOp__3ckbMWOV6L9QbiTiSEFS3YmF4Eb8lr5pTWP2KFjm9Ukmd0000&add=IzFlNDBhZg==\",\n    \"shortLinkUrl\": \"https://srfb.se/Iq4JfPgHL\"\n  },\n  \"message\": \"Order created successfully\"\n}\n```\n\n`paymentPageLink` is the page to send the customer to. Treat it as opaque and pass it on whole: the query string carries the payment intent, and a link with a trimmed or re-encoded `pi` will not open. `shortLinkUrl` appears only when you asked for it with `generateShortLink`, and is the one to put in an SMS.\n\nKeep the `orderId`. Everything afterwards is keyed on it, and unlike an in-store order there is no `paymentId` yet: nothing has been attempted until the customer opens the page. The `paymentId` arrives with the first attempt, in the status response and in the webhook.\n\nLine items, tax, adjustments, customer objects and the order-level calculation rules all behave exactly as they do in store. [Create an Order](/developers/guides/create-an-order) covers them in full.\n\n## Control Functions for Online Orders\n\nEverything specific to the hosted page lives in `controlFunctions.online`.\n\n| Field | Description |\n|-------|-------------|\n| `paymentPageValidFor` | How long the link works, as `<number><unit>` where the unit is `m`, `h` or `d` — for example `15m`, `2h`, `3d`. Defaults to one day. |\n| `redirectUrl` | Where the customer lands after a successful payment. The `orderId` is appended as a query parameter. |\n| `failureRedirectUrl` | Where the customer lands after a failure. Also carries the `orderId`. |\n| `generateShortLink` | Returns `shortLinkUrl` alongside the full link. Default `false`. |\n| `payButtonType` | The label on the button: `PAY`, `DONATE`, `BOOK`, `ORDER`, `CHECKOUT`, `CONTINUE`, `CONTRIBUTE`, `ADD_MONEY`, `RENT`, `SUPPORT`, `TIP`, `TOP_UP`. |\n| `enforce3DSecure` | Force 3-D Secure where the issuer supports it. |\n| `relaxed3ds` | Allow relaxed 3-D Secure handling. |\n| `addressRequirements` | Ask for an address on the page. |\n| `enforceTokenization` | Save the card for later use, overriding the terminal configuration. |\n| `tokenisationIfPossible` | Tokenize when supported, but do not fail the payment if it is not. |\n| `errorIfTokenizationFails` | Fail the flow when the card cannot be tokenized. |\n| `subscription` | Mark the order as recurring-capable. |\n| `generateOnlineLinkWith` | Generate the link with a different terminal than the one the order was created against. |\n| `selfCardCharging` | Let the customer charge their own card. |\n\nThese sit next to the order-level controls that are not online-specific but matter here:\n\n| Field | Description |\n|-------|-------------|\n| `delayCapture` | Authorize now, capture later. See [Capture a Payment](/developers/guides/capture-a-payment). |\n| `authMode` | `AUTH` or `PRE-AUTH`. Choosing `PRE-AUTH` sets `delayCapture` for you. |\n| `lockToPaymentMethods` | Restrict the page to the methods you list, e.g. `[\"CARD\", \"KLARNA\"]`. |\n| `delayPayout` | Hold the payout for a period, as `<number><unit>`. |\n| `callBackUrl` | Per-order webhook URL for this order and its payments. |\n\n### Recurring Orders\n\nFor a subscription, add the `recurring` object inside `online` and mark the order as one:\n\n```json\n{\n  \"controlFunctions\": {\n    \"online\": {\n      \"subscription\": true,\n      \"enforceTokenization\": true,\n      \"recurring\": {\n        \"subscriptionAmountType\": \"fixed\",\n        \"frequency\": \"monthly\",\n        \"numberOfPayments\": 12,\n        \"uniqueReference\": \"sub-4471\",\n        \"validation\": \"validated\"\n      }\n    }\n  }\n}\n```\n\nThe first payment is a customer-initiated transaction on the page, which is where the card is tokenized and 3-D Secure is satisfied. Every renewal after that is a merchant-initiated transaction against the stored token, charged from your backend through a `MerchantInitiated` terminal. [Recurring Payments](/developers/guides/recurring-payments) has the renewal side.\n\n| Field | Description |\n|-------|-------------|\n| `subscriptionAmountType` | `fixed` or `variable`. |\n| `maxAmount` | Ceiling in minor units, for `variable` subscriptions only. |\n| `frequency` | `daily`, `twiceWeekly`, `weekly`, `tenDays`, `fortNightly`, `monthly`, `everyTwoMonths`, `trimester`, `quarterly`, `twiceYearly`, `annually`, `unscheduled`. Required. |\n| `numberOfPayments` | How many payments the schedule expects. |\n| `uniqueReference` | Your reference for the recurring order. |\n| `validation` | `validated` or `notValidated`. |\n\n## Step 5: Confirm the Payment\n\nThe redirect back to your site tells you the customer finished, not that the money moved. Confirm server-side, either by polling or, better, by subscribing to the webhook.\n\n```\nGET /orders/:orderId/status\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderStatus\": \"PAYMENT_COMPLETED\",\n    \"payments\": [\n      {\n        \"paymentId\": \"83a1ba3264bd500106\",\n        \"paymentStatus\": \"PAYMENT_COMPLETED\",\n        \"paymentMethod\": \"CARD\",\n        \"amount\": 99900\n      }\n    ]\n  }\n}\n```\n\nThe states are the same as in store. An order sits in `PENDING` while the link is unused, and a failed or cancelled payment leaves it there, so the same `orderId` can be paid again without creating a new order.\n\n| Order status | Meaning |\n|--------------|---------|\n| `PENDING` | The link has not been paid yet, or the last attempt failed or was cancelled. |\n| `PAYMENT_COMPLETED` | Paid in full. The order is closed. |\n| `PARTIAL_PAYMENT_COMPLETED` | Part of the total is paid. See [Partial Payments](/developers/guides/partial-payments). |\n| `PAYMENT_CANCELLED` | The order was cancelled. |\n\nSubscribe to `order.paymentcompleted` and `order.paymentfailed` rather than polling on a timer. The payload carries the `orderId`, your `referenceId`, the amount and the transaction details, and each delivery carries a `metadata.eventId` you should use for idempotency — Surfboard retries a failed delivery twice, after 5 and 10 minutes.\n\n```json\n{\n  \"eventType\": \"order.paymentcompleted\",\n  \"metadata\": {\n    \"eventId\": \"831fc2f040bf405fff\",\n    \"created\": 1745821536443,\n    \"retryAttempt\": 0,\n    \"terminalId\": \"813ca2cb12ce400405\"\n  },\n  \"data\": {\n    \"orderId\": \"83a1ba32774149710b\",\n    \"referenceId\": \"order-2026-0418\",\n    \"paymentId\": \"83a1ba3264bd500106\",\n    \"paymentStatus\": \"PAYMENT_COMPLETED\",\n    \"paymentMethod\": \"CARD\",\n    \"amount\": \"99900\",\n    \"type\": \"PURCHASE\"\n  }\n}\n```\n\nSee [Webhooks](/developers/guides/webhooks-notifications) for subscription and signature verification.\n\n## Sending the Link\n\nThe link is a URL, so how it reaches the customer is your call:\n\n- **Redirect** from your own checkout, the closest thing to a hosted checkout flow.\n- **Email or SMS** for invoices, deposits and quotes. Use `shortLinkUrl` in an SMS and keep `paymentPageValidFor` short enough that a stale link cannot be paid by mistake.\n- **QR code** printed or shown on screen, for pay-at-table and self-service.\n\nTwo things to hold on to. Set `paymentPageValidFor` deliberately — a link that lives for three days is a link someone can pay three days late, after you have cancelled the order. And never treat the arrival at `redirectUrl` as proof of payment: a customer can reach that URL by other means. The webhook and the status call are the record.\n\n## Error Handling\n\nCreate-order failures come back as `status: \"ERROR\"` with an `OR_*` or `PS_*` code. The ones you will meet setting this up:\n\n| Code | Cause |\n|------|-------|\n| `OR_0042` | Terminal not found. The `terminal$id` is wrong, or it belongs to another merchant. |\n| `OR_0037` | The total does not reconcile with the line items. |\n| `OR_0048` | Line items mix currencies. |\n\nIf the store's terminal list comes back without a `PaymentPage` entry, the store was created without `onlineInfo` — the two default terminals only come with an online store. A terminal that will not register, or one that is there but refuses a payment, usually means the store has not cleared domain verification or is still in approval. Check the store's online status before you look at the terminal call.\n\nThe [Create Order Error Codes](/developers/guides/create-order-error-codes) reference lists the rest, including errors raised by the payment initiation that happens inside the same call.\n\n## Next Steps\n\n- [Payment Page](/developers/guides/payment-page) — the hosted checkout redirect in more detail\n- [Self-Hosted Checkout](/developers/guides/self-hosted-checkout) — keep the customer on your own page\n- [Server-to-Server API](/developers/guides/server-to-server-api) — charge a stored card with no customer present\n- [Capture a Payment](/developers/guides/capture-a-payment) — finalize a delayed-capture authorization\n- [Refund an Order](/developers/guides/refund-an-order) — return funds after settlement\n\n## Reference\n\n- [Stores API](https://developers.surfboardpayments.com/api/stores)\n- [Terminals API](https://developers.surfboardpayments.com/api/terminals)\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Payments API](https://developers.surfboardpayments.com/api/payments)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "Customer Identification on Card Tap",
      "slug": "customer-identification",
      "url": "/developers/guides/customer-identification",
      "description": "Recognise a returning customer from the card they tap, before the payment runs. Apply their discount, points or price list to the open order, then take the money.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "API",
        "Webhooks",
        "Loyalty",
        "Personalisation"
      ],
      "content": "## Overview\n\nA card is an identity as well as an instrument. When a customer taps at the terminal, Surfboard sends you a webhook carrying a token for that card and the order it belongs to — before the payment is processed. If you recognise the token, you have a short window to change the order: apply a member price, redeem points, add a loyalty discount, attach the customer to the receipt.\n\nThe customer does nothing but pay. No app, no scan, no \"are you a member with us?\" at the till.\n\n> **Android terminals only.** The feature is available for card payments on Surfboard's Android terminals. Support for further payment methods is on the roadmap.\n\n## The Flow\n\n| Step | Who | What happens |\n|------|-----|--------------|\n| 1 | You | Create the order as normal |\n| 2 | Customer | Taps their card at the terminal |\n| 3 | Surfboard | Sends `order.customer.identify` with the order and a card token |\n| 4 | You | Look the token up, and update the order if you recognise it |\n| 5 | You | Initiate the payment against the updated order |\n\nThe window between steps 3 and 5 is where your business logic lives, and it is short — the customer is standing at the terminal. Treat the lookup as a fast path: an indexed read on your side, not a report.\n\n## Prerequisites\n\n1. A registered Android terminal under an onboarded merchant and store\n2. A webhook endpoint subscribed to `order.customer.identify` — see [Webhooks](/developers/guides/webhooks-notifications)\n3. Somewhere to store card tokens against your customers\n\n## Step 1: Create the Order\n\nNothing changes here. Create the order the way you always do, with the line items you have at the point of sale.\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"8386af3b0f71b80b04\",\n  \"referenceId\": \"till-2-0418\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"Nike Shoes\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 50000,\n        \"total\": 50000,\n        \"currency\": \"752\",\n        \"tax\": [{ \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }]\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 50000,\n    \"total\": 50000,\n    \"currency\": \"752\",\n    \"tax\": [{ \"amount\": 10000, \"percentage\": 25, \"type\": \"VAT\" }]\n  }\n}\n```\n\nLeave `initiatePaymentsOptions` out. Payment is initiated as its own call in step 4, once you have had your chance to change the order — an order that starts paying immediately gives you no window to act in.\n\n## Step 2: The Customer Taps\n\nThe terminal reads the card and Surfboard raises the event. The payment has not been processed at this point; the tap is being used for identification.\n\n## Step 3: Receive `order.customer.identify`\n\n```json\n{\n  \"eventType\": \"order.customer.identify\",\n  \"metadata\": {\n    \"eventId\": \"832cf9fe1806581dff\",\n    \"created\": 1747553660038,\n    \"retryAttempt\": 0,\n    \"webhookEventId\": \"81a214e74b107801ff\"\n  },\n  \"data\": {\n    \"orderId\": \"832cf9f93d2fd0410b\",\n    \"cardId\": \"c550c29e80908c887a\"\n  }\n}\n```\n\n`cardId` is a tokenized stand-in for the card, stable for that card, and it is the only identity you get. It is not the card number and cannot be turned back into one, but treat it as personal data: it identifies a person across visits, which is the whole point of it.\n\nAcknowledge with `200 OK` inside 10 seconds. A failed delivery is retried twice — after 5 minutes and then 10 — which is far too late for a customer at a till, so do the work on receipt rather than queueing it for later. Deduplicate on `metadata.eventId`.\n\nYou can also pull the same card data from the order rather than waiting for the webhook:\n\n```\nGET /orders/:orderId/tokens\n```\n\nSee [Tokens](/developers/guides/tokens) for what comes back.\n\n### Matching the Token\n\nThe first time you see a `cardId` you will not recognise it, and that is the normal state of a new customer:\n\n- **Known token** — load the customer, apply what they are entitled to, move to step 4.\n- **Unknown token** — take the payment unchanged. Store the token against the customer if they later identify themselves another way, and the next tap will be recognised.\n\nNever block a payment on your lookup. If your service is slow or down, initiate the payment as it stands; a missed discount is a support ticket, a stalled till is a queue.\n\n## Step 4: Update the Order\n\nApply what you found with the Update Order API. The order keeps its `orderId`.\n\n```json\nPUT /orders/:orderId\n{\n  \"terminal$id\": \"8386af3b0f71b80b04\",\n  \"customer\": {\n    \"customerId\": \"cus_88213\",\n    \"person\": {\n      \"name\": { \"firstName\": \"John\", \"lastName\": \"Doe\" },\n      \"email\": \"john@example.com\"\n    }\n  },\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"Nike Shoes\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 50000,\n        \"campaign\": 5000,\n        \"total\": 45000,\n        \"currency\": \"752\",\n        \"tax\": [{ \"amount\": 9000, \"percentage\": 25, \"type\": \"VAT\" }]\n      }\n    }\n  ],\n  \"totalOrderAmount\": {\n    \"regular\": 50000,\n    \"campaign\": 5000,\n    \"total\": 45000,\n    \"currency\": \"752\",\n    \"tax\": [{ \"amount\": 9000, \"percentage\": 25, \"type\": \"VAT\" }]\n  },\n  \"metadata\": {\n    \"loyaltyTier\": \"gold\",\n    \"memberSince\": \"2023-11-02\"\n  },\n  \"controlFunctions\": {\n    \"orderLineLevelCalculation\": true\n  }\n}\n```\n\nWhat you change depends on what you are giving them:\n\n| Intent | Where it goes |\n|--------|---------------|\n| Member price or loyalty discount | `campaign` on the line, or an order-level `adjustment` |\n| Points redeemed as money off | An `adjustment`, so it is visible as its own line in reporting |\n| Attach the person to the order | `customer`, which also carries the receipt to their email |\n| Anything your own systems need later | `metadata`, on the order or the line |\n\nRecalculate `totalOrderAmount` to match. A total that does not reconcile with its lines is rejected with `OR_0037`.\n\n> **The window closes at payment.** Once a payment has been initiated for an order, it can no longer be updated. Everything you want to change has to be in before step 4.\n\n## Step 5: Initiate the Payment\n\n```json\nPOST /payments\n{\n  \"orderId\": \"832cf9f93d2fd0410b\",\n  \"paymentMethod\": \"CARD\",\n  \"amount\": 45000\n}\n```\n\nThe customer pays the amount you just set. From here it is an ordinary payment: `order.paymentcompleted` fires on success, and the receipt shows the discount as a line the customer can see.\n\n## Storing Tokens Responsibly\n\nThe card token turns anonymous footfall into a recognisable customer, so it deserves the treatment personal data gets:\n\n- Store it against a customer record, not in a log line.\n- Give the customer a way to be forgotten that removes the token as well as the profile.\n- Tell them what you are doing. \"We recognised your card\" is a good experience when the customer knows it can happen, and a bad one when they do not.\n- The token is scoped to your merchant. It is not a national identifier, and it is not portable.\n\n## Error Handling\n\n| Symptom | Likely cause |\n|---------|--------------|\n| No webhook on tap | The endpoint is not subscribed to `order.customer.identify`, or the terminal is not an Android terminal. |\n| `PUT /orders/:orderId` returns 404 | The `orderId` is wrong, or the order belongs to another merchant. |\n| Update rejected after a tap | A payment has already been initiated for the order. The window has closed. |\n| `OR_0037` on update | The new `totalOrderAmount` does not reconcile with the line items. |\n\n## Reference\n\n- [Order Customer Identity webhook](https://developers.surfboardpayments.com/references/webhooks)\n- [Update Order API](https://developers.surfboardpayments.com/api/orders)\n- [Payments API](https://developers.surfboardpayments.com/api/payments)\n- [Tokens](/developers/guides/tokens)\n- [Webhooks](/developers/guides/webhooks-notifications)\n- [Create an Order](/developers/guides/create-an-order)"
    },
    {
      "title": "Tokens",
      "slug": "tokens",
      "url": "/developers/guides/tokens",
      "description": "Turn a card used once into a card you can charge again. Enable tokenization on an order, fetch the token it produced, and store it for subscriptions, repeat purchases and refunds.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Tokenization",
        "Cards",
        "MIT"
      ],
      "content": "## Overview\n\nA token is a reference to a card that Surfboard holds and you do not. The customer enters their details once, on a page or a terminal that is already in scope for PCI DSS, and you get back a `tokenId` you can charge later without the card ever touching your systems.\n\nThat is what makes the rest possible: subscription renewals, one-click repeat purchases, deposits settled after the fact, refunds routed back to the original card. This guide covers producing a token, reading it, and storing it. Charging one is [Server-to-Server API](/developers/guides/server-to-server-api), and scheduling the charges is [Recurring Payments](/developers/guides/recurring-payments).\n\n| Where the card is entered | What tokenization gives you |\n|---------------------------|------------------------------|\n| Payment page or Online SDK | Card data handled inside Surfboard's PCI scope, never yours |\n| MerchantInitiated terminal | A card you can charge with no customer present |\n| Refunds | A reference back to the card that paid, without storing the card |\n\n## Step 1: Ask for a Token\n\nSet `enforceTokenization` when you create the order. On an online order it belongs inside `controlFunctions.online`:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"YOUR_TERMINAL_ID\",\n  \"orderLines\": [\n    {\n      \"id\": \"ITEM-001\",\n      \"name\": \"First month\",\n      \"quantity\": 1,\n      \"amount\": { \"regular\": 19900, \"total\": 19900, \"currency\": \"752\" }\n    }\n  ],\n  \"totalOrderAmount\": { \"regular\": 19900, \"total\": 19900, \"currency\": \"752\" },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": { \"paymentMethod\": \"CARD\", \"amount\": 19900 },\n    \"online\": {\n      \"enforceTokenization\": true\n    }\n  }\n}\n```\n\nThree flags decide how hard you insist, and the difference matters when the card or the issuer will not play along:\n\n| Flag | Behaviour |\n|------|-----------|\n| `enforceTokenization` | Tokenize the card for future use. Overrides the terminal's own configuration. |\n| `tokenisationIfPossible` | Tokenize where it is supported, and carry on quietly where it is not. |\n| `errorIfTokenizationFails` | Fail the whole payment if the card cannot be tokenized. |\n\nPick by what breaks if there is no token. A subscription with no token to renew against is worse than a failed first payment, so `errorIfTokenizationFails` is right there. A shop offering \"save this card for next time\" should not lose the sale over it, so `tokenisationIfPossible` is right there.\n\nFor subscriptions, pair tokenization with the `recurring` block so the first payment is authorised as the start of a series rather than a one-off — see [Online Payment Link](/developers/guides/online-payment-link#recurring-orders).\n\n## Step 2: Fetch the Token\n\nThe token exists once the payment succeeds. Read it from the order:\n\n```\nGET /orders/:orderId/tokens\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [\n    {\n      \"cardBrand\": \"VISA\",\n      \"cardholderName\": \"Tom\",\n      \"tokenId\": \"822d544dc48c200308\",\n      \"createdAt\": \"2024-04-25T11:22:24.845Z\",\n      \"expiryMonth\": 7,\n      \"expiryYear\": 2026,\n      \"truncatedPan\": \"8907\",\n      \"cardArt\": \"iVBORw0KGgoAAAANSUhEUgAAAUQAAA......\"\n    }\n  ],\n  \"message\": \"Fetched the card information.\"\n}\n```\n\n| Field | What it is for |\n|-------|----------------|\n| `tokenId` | The handle you charge against. The only field that does anything. |\n| `cardBrand`, `truncatedPan` | \"Visa ending 8907\" — how you show a saved card back to a customer. |\n| `expiryMonth`, `expiryYear` | When the token stops working. Worth acting on before it does. |\n| `cardholderName` | As given by the card. |\n| `cardArt` | Base64 card image, if you want the saved card to look like the card. |\n\nThe response is an array. An order paid in parts, or retried on a second card, produces more than one token, so do not assume `data[0]`.\n\n## Step 3: Store It\n\nStore the `tokenId` against the customer in your own system, along with enough to describe it back to them — brand, last four, expiry. That pairing is the whole point: the token is meaningless without knowing whose card it is, and useless if the customer cannot tell which of their two Visas it is.\n\nStore it where you would store an account identifier: your primary datastore, encrypted at rest, out of logs and analytics events. A token is not card data, but it is a bearer reference to someone's money.\n\n## Charging a Stored Token\n\nPass it into a payment through `paymentMethodParams`:\n\n```json\nPOST /payments\n{\n  \"orderId\": \"83a1ba32774149710b\",\n  \"paymentMethod\": \"CARD\",\n  \"amount\": 19900,\n  \"paymentMethodParams\": {\n    \"tokenId\": \"822d544dc48c200308\"\n  }\n}\n```\n\nA charge with no customer present is a Merchant Initiated Transaction, and it needs a terminal in `MerchantInitiated` mode — the terminal the customer paid on cannot do it. An online store is provisioned with one, so this is usually a matter of reading the store's terminal list rather than registering anything. [Server-to-Server API](/developers/guides/server-to-server-api) covers the setup and the rules that come with MIT.\n\n## Expiry and Housekeeping\n\nTokens do not live forever, and the failure is silent until you try to charge:\n\n- **The card expires.** You have `expiryMonth` and `expiryYear` at the point of tokenization, so you can warn a subscriber before the renewal that will fail rather than after it.\n- **The card is replaced, lost or cancelled.** The token stops working with no notice to you. Handle the failure on the charge and ask the customer to re-authorise.\n- **The customer asks you to forget them.** Delete your side of the mapping. A token with no customer attached is not usable and should not be kept.\n\nBuild the re-authorisation path before you need it: a link back to a payment page that tokenizes a fresh card and swaps the token on the subscription. It is the difference between a churned subscriber and a two-minute interruption.\n\n## Reference\n\n- [Fetch Tokens from Order](https://developers.surfboardpayments.com/api/orders)\n- [Payments API](https://developers.surfboardpayments.com/api/payments)\n- [Server-to-Server API](/developers/guides/server-to-server-api)\n- [Recurring Payments](/developers/guides/recurring-payments)\n- [Online Payment Link](/developers/guides/online-payment-link)"
    },
    {
      "title": "Order and Return Terminals",
      "slug": "terminal-logistics",
      "url": "/developers/guides/terminal-logistics",
      "description": "Get hardware to your merchants and back again. Order terminals during onboarding or later, track the shipment by webhook, and raise a return when a device is no longer needed.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "API",
        "Logistics",
        "Terminals",
        "Partners"
      ],
      "content": "## Overview\n\nBefore a merchant can take a card payment in a shop, a physical device has to arrive at that shop. The Logistics API is how you place that order, follow it to the door, and send hardware back when a merchant leaves or a device fails.\n\nThere are two moments to order from, and they are different calls:\n\n| When | How |\n|------|-----|\n| During onboarding | Control fields on the Create Merchant call — the merchant picks from a catalogue you curate, or you preselect for them |\n| Any time after | The Create Shipment call, against an existing merchant |\n\nReturns are one call plus a waybill, and everything in flight reports its progress through a single webhook.\n\n## Prerequisites\n\n1. A partner account with API credentials and your `partnerId`\n2. Product IDs and pricing plans for the hardware you resell — Surfboard provides both\n3. A webhook endpoint subscribed to `logistics.orderupdate`\n\n## Ordering During Onboarding\n\nTerminals can be chosen as part of merchant creation, which is the tidiest path: the merchant signs up and orders hardware in the same sitting. It is configured with control fields on [Create Merchant](/developers/guides/merchant-onboarding).\n\n```json\nPOST /partners/:partnerId/merchants\n{\n  \"country\": \"SE\",\n  \"organisation\": { \"corporateId\": \"1234567890\", \"legalName\": \"Example AB\" },\n  \"controlFields\": {\n    \"showProductCatalogue\": true,\n    \"displayProducts\": [\n      { \"productId\": \"815db2c5adc9b00301\", \"pricingPlans\": [\"816192c7efa2b0091a\"] }\n    ],\n    \"preSelectProducts\": [\n      {\n        \"productId\": \"815db2c5adc9b00301\",\n        \"quantity\": 2,\n        \"pricingPlanId\": \"816192c7efa2b0091a\"\n      }\n    ],\n    \"transactionPricingPlan\": \"816192c7efa2b0091a\"\n  }\n}\n```\n\n| Control field | What it does |\n|---------------|--------------|\n| `showProductCatalogue` | Shows the hardware catalogue during onboarding. |\n| `displayProducts` | Restricts the catalogue to the products you list, each with the pricing plan that merchant gets. |\n| `preSelectProducts` | Ships the listed products without asking. Use it when the hardware is part of the package rather than a choice. |\n\nCurate `displayProducts` per segment rather than showing everything. A merchant choosing between two terminals decides; a merchant choosing between nine calls support.\n\n## Ordering After Onboarding\n\nFor additional terminals, replacements, or accessories, create a shipment directly:\n\n```json\nPOST /partners/:partnerId/merchants/:merchantId/shipment\n{\n  \"shippingAddress\": {\n    \"name\": \"John Doe\",\n    \"addressLine1\": \"Main Street 123\",\n    \"addressLine2\": \"Building C\",\n    \"city\": \"Stockholm\",\n    \"countryCode\": \"SE\",\n    \"postalCode\": \"123 45\",\n    \"phoneNumber\": { \"code\": \"46\", \"number\": \"771890089\" },\n    \"email\": \"store@example.com\",\n    \"deliveryInstruction\": \"Reception, ask for the store manager\"\n  },\n  \"lineItems\": [\n    { \"productId\": \"815db2c5adc9b00301\", \"quantity\": 1 }\n  ]\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": { \"orderId\": \"81376ad8ebedf80310\" },\n  \"message\": \"Order for shipping terminal successfully created\"\n}\n```\n\n`shippingAddress` is optional and falls back to the merchant's registered address. Send it anyway when the hardware goes to a shop rather than a head office — the registered address is where the company is incorporated, not where the till is.\n\n| Line item field | Notes |\n|-----------------|-------|\n| `productId` | The Surfboard product ID, unique to you as a partner. |\n| `quantity` | How many of that product. |\n| `billingPlanId` | Optional. Falls back to the default plan for that product. |\n| `replacementFor` | The `terminalId` of a device being replaced. |\n\n### Replacements\n\nSet `replacementFor` to the failing terminal's ID and the shipment is handled as a swap: Surfboard supplies a waybill for the old device, and the merchant can return it in the box the new one arrived in. It saves a separate return request, and it keeps the two halves of the swap linked in reporting.\n\n```json\n{\n  \"lineItems\": [\n    {\n      \"productId\": \"815db2c5adc9b00301\",\n      \"quantity\": 1,\n      \"replacementFor\": \"816a0ff6bc0fb00404\"\n    }\n  ]\n}\n```\n\n## Tracking the Shipment\n\nEvery change of state raises `logistics.orderupdate` against your webhook endpoint:\n\n```json\n{\n  \"eventType\": \"logistics.orderupdate\",\n  \"metadata\": {\n    \"eventId\": \"81a214e74b107801ff\",\n    \"created\": 1695793998732,\n    \"retryAttempt\": 0\n  },\n  \"data\": {\n    \"merchantId\": \"81412e2e4102f80f0e\",\n    \"orderId\": \"81376ad8ebedf80310\",\n    \"orderStatus\": \"ORDER_SHIPPED\",\n    \"trackingUrl\": \"https://www.dhl.com/home/tracking.html\",\n    \"packageDetails\": [\n      { \"productId\": \"817361bb0a23400701\", \"serial\": \"658364\" }\n    ]\n  }\n}\n```\n\n| `orderStatus` | Meaning |\n|---------------|---------|\n| `ORDER_PLACED` | The order is accepted. |\n| `ORDER_PENDING_FOR_STOCK` | Waiting on stock. Worth surfacing to the merchant — this is the status behind \"where is my terminal\". |\n| `ORDER_SHIPPED` | In transit. Carries `trackingUrl` and `packageDetails`. |\n| `ORDER_COMPLETED` | Delivered and fulfilled. |\n\n`trackingUrl` and `packageDetails` appear only on `ORDER_SHIPPED`. Store the serials from `packageDetails` as they arrive: that is the link between a shipment and the physical device a merchant will later register, and the fastest way to answer \"which terminal did we send to which store\". Registration itself is covered in [Device Registration](/developers/guides/device-registration).\n\nAcknowledge with `200 OK` within 10 seconds, and deduplicate on `metadata.eventId`. Failed deliveries are retried twice, after 5 and 10 minutes.\n\n## Returning a Terminal\n\nWhen a merchant churns, downsizes, or has a device that will not come back to life:\n\n```json\nPOST /partners/:partnerId/logistics/return\n{\n  \"terminalId\": \"816a0ff6bc0fb00404\",\n  \"name\": \"John Doe\",\n  \"email\": \"store@example.com\",\n  \"phoneNumber\": { \"code\": \"46\", \"number\": \"771890089\" },\n  \"address\": {\n    \"addressLine1\": \"Main Street 123\",\n    \"addressLine2\": \"Building C\",\n    \"city\": \"Stockholm\",\n    \"countryCode\": \"SE\",\n    \"postalCode\": \"123 45\"\n  },\n  \"deliveryInstruction\": \"Go left after the elevator\",\n  \"comment\": \"Merchant closed the second location\",\n  \"reasonForReturn\": \"NOT_USING_SERVICE\"\n}\n```\n\nThe address here is the pickup address — where the device is now, not where it was originally shipped. A terminal that moved between stores moved with a `changeStore` call, and the return has to follow the device rather than the paperwork.\n\nList what is in flight:\n\n```\nGET /partners/:partnerId/logistics/return\n```\n\n> **Deactivating a store?** A store cannot be deactivated while terminals are registered to it. Move them to another store under the same merchant, or return them first. See [Store Management](/developers/guides/store-management).\n\n## What to Build Around This\n\nThree things repay the effort:\n\n- **Mirror `orderStatus` onto the merchant's own view.** Most support contact about hardware is \"has it shipped\", and the answer is already in your database.\n- **Keep serial-to-store mapping from the shipped event.** It turns a later terminal fault into a lookup rather than an investigation.\n- **Treat `ORDER_PENDING_FOR_STOCK` as an alert, not a status.** It is the one state where the merchant is waiting and nobody is working on it.\n\n## Reference\n\n- [Logistics API](https://developers.surfboardpayments.com/api/logistics)\n- [Logistics Order Update webhook](https://developers.surfboardpayments.com/references/webhooks)\n- [Merchant Onboarding](/developers/guides/merchant-onboarding)\n- [Device Registration](/developers/guides/device-registration)\n- [Terminal & Device Management](/developers/guides/terminal-device-management)"
    },
    {
      "title": "Transactions and Reports",
      "slug": "transactions-and-reports",
      "url": "/developers/guides/transactions-and-reports",
      "description": "Retrieve, filter and search transactions, and read a monthly report the way finance does. Covers why net sales and payout never match and how to reconcile a period end to end.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Transactions",
        "Reporting",
        "Reconciliation",
        "In-Store"
      ],
      "content": "## Overview\n\nA payment leaves two trails. There is the transaction, which is what happened at the terminal or the checkout, and there is the payout, which is money arriving in a bank account days later net of fees. Reconciliation is the work of tying those together, and most of the questions merchants ask about money are really questions about the gap between them.\n\nThis guide covers both halves: the transaction APIs you query directly, and how to read the monthly report that summarises a period.\n\n## Fetching Transactions\n\n### All Transactions\n\n```\nGET /transactions\n```\n\nReturns the merchant's transactions, newest first. Filter to a period with `startDate` and `endDate`:\n\n```\nGET /transactions?startDate=2026-04-01&endDate=2026-04-30\n```\n\nResponses are paged at 100 items. Ask for a page with the `X-PAGE-NUMBER` request header, and read `x-total-items` off the response to know how far to keep going:\n\n```bash\ncurl 'YOUR_API_URL/transactions?startDate=2026-04-01&endDate=2026-04-30' \\\n  -H 'API-KEY: YOUR_API_KEY' \\\n  -H 'API-SECRET: YOUR_API_SECRET' \\\n  -H 'MERCHANT-ID: YOUR_MERCHANT_ID' \\\n  -H 'X-PAGE-NUMBER: 2'\n```\n\n```\n< x-page-number: 2\n< x-total-items: 230\n```\n\nPast the last page you get `SUCCESS` with an empty `data` array rather than an error, so loop until the array comes back empty or you have seen `x-total-items` rows. Pagination works the same way on every list endpoint in the platform.\n\n### One Order, One Payment, One Transaction\n\n```\nGET /transactions/:id/list\n```\n\nAccepts an `orderId`, a `paymentId` or a `transactionId` and returns every transaction attached to it. This is the call for an order settled in parts: one order, several payments, several transactions, all of them here. See [Partial Payments](/developers/guides/partial-payments) for how those orders come about.\n\n### Search\n\n```\nGET /transactions/search?query=8208822\n```\n\nFree-text search across transaction data. It is the endpoint behind a support tool: a merchant reads out a number from a receipt or a bank statement, you paste it in, and you get the transaction without knowing which field it came from.\n\n## The Monthly Report\n\nThe monthly report summarises sales, charges and payouts for a period. It is the document a merchant's bookkeeper opens, and the source of the single most common support question, which is some version of *why is the payout not the same as the sales?*\n\n**Sales and payout do not happen in the same period.** A transaction made at the end of a month is often paid out in the next one. So total net sales is not the amount that landed in the bank that month, and it is not supposed to be.\n\n### 1. Monthly Summary\n\nThe first page carries the totals.\n\n**Total net sales in the period** — everything sold in the report period, after refunds. In plain terms: what was sold this month, net of what was given back.\n\n**Total charges in the period** — everything deducted in the period:\n\n| Line | What it is |\n|------|------------|\n| **Fees** | Charges tied to the payments themselves. |\n| **Adjustments** | Other deductions affecting the payout, such as partner fees, itemised further down the report. |\n| **VAT** | VAT on the fees and services that carry it. |\n\n```\nFees + Adjustments + VAT = Total Charges\n```\n\n### 2. Total Payout in the Period\n\nThe total instructed for payout during the month. This is the money-moved number, and it is why the distinction matters: a payout made on 1 May can contain sales from 30 April. Total payout is not comparable, line for line, with total net sales for the same month.\n\n### 3. Of Which: From Last Period\n\nHow much of this month's payouts came from the previous report period.\n\nSale on 30 April, payout on 1 May: the payout falls in May's *total payout*, but because the sale belongs to April, the same net amount also shows as *of which: from last period*.\n\n### 4. Unsettled Amount From Period\n\nAmounts belonging to this period that did not make it into this period's payouts. It is normal at the end of a month, and more pronounced around weekends and public holidays.\n\nA payment taken on 31 May belongs to May's net sales. If the money is paid out in early June, it is not in May's payout — it sits here instead, and turns up in a later payout.\n\n### 5. Why Net Sales and Payout Differ\n\nNet sales follows the **sale**. Payout follows the **money**. In any given month, the payouts contain:\n\n- sales from the previous month paid out in this one,\n- sales from this month paid out in the next one,\n- and the fees and adjustments attached to each payout.\n\nSo `Total Net Sales − Total Charges` is not `Total Payout` for a single month. Both numbers are right; they answer different questions.\n\n### 6. A Worked Example\n\n| Line | Amount |\n|------|--------|\n| Total net sales | 100 000 kr |\n| Total charges | 5 000 kr |\n| Total payout | 92 000 kr |\n| Of which: from last period | 2 000 kr |\n| Unsettled from period | 5 000 kr |\n\nNothing is missing here, even though 100 000 − 5 000 ≠ 92 000. Part of what was paid out this month came from the previous period, and part of this month's sales has not been paid out yet.\n\n### 7. Behind the Summary\n\nThree detail sections explain any total on the summary page:\n\n| Section | What it shows |\n|---------|---------------|\n| **Monthly Payouts** | The individual payouts, and which sales periods they cover. |\n| **Monthly Adjustments** | The individual adjustments behind the Adjustments total. |\n| **Sales breakdown** | How sales split by store, payment method and so on. |\n\nWhen a summary figure needs explaining, the answer is in one of these three.\n\n### 8. How to Reconcile a Period\n\n1. Check **total net sales** against your own sales report. Manual payment methods are not included in Surfboard's sales figures.\n2. Check **total charges** and its split into fees, adjustments and VAT.\n3. Look at **total payout**, and how much of it is marked *from last period*.\n4. Check **unsettled amount from period** — sales from this period that have not been paid out yet.\n5. For a single figure that still looks wrong, use the detail sections to find the transactions, fees or adjustments behind it.\n\n### 9. Around Month Boundaries\n\nTransactions and payouts near a month boundary can land in different report periods depending on when each was registered. That does not mean anything is missing. Two dates decide where a number appears:\n\n- **Transaction date** — which sales period the payment belongs to.\n- **Payout date** — when the money actually left.\n\n### If It Still Does Not Add Up\n\nCome to support with the specifics, and it is usually resolved in one pass:\n\n- Merchant or company name\n- The report period\n- The two amounts that disagree\n- The transaction ID, if it is about one transaction\n- Whatever you are comparing the report against\n\n## Reconciling With the API\n\nThe report is the summary; the API is the ledger behind it. A reconciliation job that runs monthly usually does this:\n\n1. Pull the period's transactions with `GET /transactions?startDate=&endDate=`, paging to the end.\n2. Pull the settlement reports for the same period — `GET /partners/:partnerId/merchants/:merchantId/reports`. See [Settlements & Reporting](/developers/guides/settlements-reporting).\n3. Group transactions by their own date, and payouts by payout date. Do not expect the two groupings to agree; the difference is exactly *from last period* plus *unsettled from period*.\n4. Investigate the individual items by ID with `GET /transactions/:id/list`.\n\nAutomate the grouping and the two reconciling numbers, and month-end stops being a conversation.\n\n## Reference\n\n- [Reporting API](https://developers.surfboardpayments.com/api/reporting)\n- [Settlements & Reporting](/developers/guides/settlements-reporting)\n- [Partial Payments](/developers/guides/partial-payments)\n- [Payment Lifecycle](/developers/guides/payment-lifecycle)\n- [Notification Subscriptions](/developers/guides/notification-subscriptions) — settlement reports delivered by email or SFTP"
    },
    {
      "title": "AI Product Images and Branding",
      "slug": "ai-merchandising",
      "url": "/developers/guides/ai-merchandising",
      "description": "Turn a phone photo into a usable product image and a website into a branded portal. Two endpoints that remove the design work from onboarding a merchant.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "AI",
        "Catalog",
        "Branding"
      ],
      "content": "## Overview\n\nTwo of the slowest parts of onboarding a merchant have nothing to do with payments. Someone has to photograph the products, and someone has to pick the colours. The AI API does both from what the merchant already has: a snapshot taken on the shop floor, and the URL of their website.\n\n| Endpoint | What it takes | What it returns |\n|----------|---------------|-----------------|\n| `POST /ai/enhance-image` | A product photo and its name | Enhanced images, plain or in a scene |\n| `POST /ai/branding` | A website URL | Colour schemes, shapes and fonts for that brand |\n\nBoth are ordinary authenticated calls — API key, secret and merchant ID — and both are rate limited. Ask support if you need the limit raised.\n\n## Enhancing Product Images\n\nA catalog full of photos taken under shop lighting is the difference between a POS people use and one they abandon. Send the image you have:\n\n```json\nPOST /ai/enhance-image\n{\n  \"productName\": \"Wireless Bluetooth Headphones\",\n  \"url\": \"https://example.com/images/product-12345.jpg\",\n  \"mode\": \"STANDARD\"\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"imageUrls\": [\n      \"https://cdn.example.com/enhanced/image-abc123-v1.jpg\",\n      \"https://cdn.example.com/enhanced/image-abc123-v2.jpg\",\n      \"https://cdn.example.com/enhanced/image-abc123-v3.jpg\"\n    ]\n  },\n  \"message\": \"Image enhanced successfully\"\n}\n```\n\n| Field | Notes |\n|-------|-------|\n| `productName` | Context for the model. \"Wireless Bluetooth Headphones\" produces a better result than \"IMG_4821\". |\n| `url` | Must be publicly reachable, in a normal web format — JPEG or PNG. |\n| `mode` | `STANDARD` cleans up the photo. `SCENE` places the product in a generated setting. |\n\nYou get several variants back, not one. Show them to the merchant and let them choose — this is a suggestion, not a replacement, and the person who sells the product is the one who knows whether it looks right.\n\n`SCENE` is the more computationally expensive mode and takes longer. Use `STANDARD` for the bulk import and offer `SCENE` for the handful of products that carry a storefront.\n\n### Wiring It Into Onboarding\n\nThe natural place for this is the product catalog. A merchant uploads whatever photos they have, you enhance them in the background, and the catalog is presentable before they finish the rest of the form. See [Product Catalog](/developers/guides/product-catalog) for how products and variants are stored, and the POS template in [POS Templates](/developers/guides/pos-templates) for how they are laid out on the till.\n\nTreat the returned URLs as inputs you store, not as a live dependency: download them and put them in your own storage before pointing a catalog entry at them.\n\n## Generating Branding\n\nA merchant's website already says what their brand is. Point the endpoint at it:\n\n```json\nPOST /ai/branding\n{\n  \"url\": \"https://example-company.com\"\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"options\": [\n      {\n        \"brandingOptions\": [\n          {\n            \"backgroundColor\": \"#FDFDFF\",\n            \"brandColor\": \"#38488F\",\n            \"accentColor\": \"#38488F\",\n            \"rectShape\": \"rounded\",\n            \"fontType\": \"mono\",\n            \"logoUrl\": \"\",\n            \"iconUrl\": \"\",\n            \"footerColor\": \"#F0F0F2\"\n          }\n        ],\n        \"metadata\": {\n          \"inputTokens\": 1043,\n          \"outputTokens\": 85,\n          \"outputType\": \"text\"\n        }\n      }\n    ]\n  },\n  \"message\": \"Branding generated successfully\"\n}\n```\n\nEach option is a complete set: background, brand and accent colours, a footer colour, a corner style and a font family, plus logo and icon URLs where they could be derived.\n\nThese map onto the branding you can already configure for terminals, payment pages and portals, so the output of this call is the input to that one. [Partner Branding](/developers/guides/partner-branding) covers where each value lands and at which level it applies.\n\nPresent the options as a choice with a preview. Generated colour is a starting point that saves a merchant an hour, not a decision made on their behalf — and a brand colour applied without asking is the kind of surprise that generates a support ticket rather than delight.\n\n## Practical Notes\n\n- **Both endpoints are rate limited.** Queue bulk work rather than firing a request per row of an import.\n- **Handle the slow path.** Image work, `SCENE` in particular, is not instant. Do it in a background job with a status the merchant can see, not in a request they are waiting on.\n- **A 404 on `/ai/enhance-image` means the image URL was unreachable**, not that the endpoint is wrong. Check that the URL is public before retrying.\n- **Nothing here is authoritative.** The output is a draft for a human to accept. Keep the original photo and the merchant's own colours; a generated asset should always be replaceable by the real one.\n\n## Reference\n\n- [AI API](https://developers.surfboardpayments.com/api/ai)\n- [Product Catalog](/developers/guides/product-catalog)\n- [Partner Branding](/developers/guides/partner-branding)\n- [POS Templates](/developers/guides/pos-templates)"
    },
    {
      "title": "POS Templates",
      "slug": "pos-templates",
      "url": "/developers/guides/pos-templates",
      "description": "Decide what the till shows and in what order. Lay out product categories, payment methods and terminals per store, and switch layouts automatically by time of day.",
      "category": "in-store",
      "tags": [
        "In-Store",
        "API",
        "POS",
        "Templates",
        "Configuration"
      ],
      "content": "## Overview\n\nA POS template is the layout of the till: which product categories appear, in what order, how many products fit on a page, which payment method is offered first, and which terminal the sale goes to. Templates are defined per store, so a chain can run one layout in a flagship and another in a kiosk.\n\nThe `autoSet` field is the one that earns its keep. A café that sells pastries until 11:00 and lunch after it can hold two templates and let the clock switch between them, instead of asking staff to find the right screen during a queue.\n\n## Creating a Template\n\n```json\nPOST /merchants/:merchantId/stores/:storeId/templates\n{\n  \"name\": \"Lunch service\",\n  \"paymentMethodOrder\": [\"SWISH\"],\n  \"productsPerPage\": 10,\n  \"product\": [\n    {\n      \"category\": \"lunch\",\n      \"productOrder\": [\"82674cfdf77f500001\", \"82674cfdf77f500002\"]\n    },\n    {\n      \"category\": \"drinks\",\n      \"productOrder\": [\"82674cfdf77f500003\"]\n    }\n  ],\n  \"autoSet\": [\n    { \"start\": \"11:00\", \"end\": \"14:00\" }\n  ],\n  \"terminal\": {\n    \"primaryTerminal\": \"82674beadf0f700405\",\n    \"terminalOrder\": [\"82674beadf0f700405\"]\n  },\n  \"metaData\": {\n    \"till\": \"counter-2\"\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": { \"templateId\": \"8271dfa5782e380148\" },\n  \"message\": \"Pos-Template Created Successfully\"\n}\n```\n\n| Field | Required | What it does |\n|-------|----------|--------------|\n| `name` | Yes | The label staff see. Name it after the situation — \"Lunch service\", \"Market stall\" — not after the file. |\n| `paymentMethodOrder` | No | Payment methods in the order they are offered. |\n| `productsPerPage` | No | Products per page on the POS. |\n| `product[].category` | Yes | A product category. |\n| `product[].productOrder` | Yes | Product IDs in display order. |\n| `autoSet[]` | No | Time windows, as `HH:MM` in 24-hour format, when this template applies. |\n| `terminal.primaryTerminal` | No | The terminal this template sends payments to first. |\n| `terminal.secondaryTerminal` | No | The fallback terminal. |\n| `terminal.terminalOrder` | No | Terminal IDs in preferred order. |\n| `metaData` | No | Free key-value pairs for your own use. |\n\n**Order is meaningful in every array here.** `paymentMethodOrder`, `productOrder` and `terminalOrder` are display order, not sets, so the first entry is what a member of staff reaches for without thinking. Put the thing they sell most at the front and the layout does the training.\n\nProduct IDs come from the catalog. See [Product Catalog](/developers/guides/product-catalog) for creating products, variants and categories.\n\n## Managing Templates\n\n```\nGET    /merchants/:merchantId/stores/:storeId/templates\nGET    /merchants/:merchantId/stores/:storeId/templates/:templateId\nPUT    /merchants/:merchantId/stores/:storeId/templates/:templateId\nDELETE /merchants/:merchantId/stores/:storeId/templates/:templateId\n```\n\nUpdate carries the same body as create. Keep the template's `name` stable when you change its contents — staff learn the name, and renaming a layout they know costs more than the change was worth.\n\n## Designing Templates That Work\n\nA till is used by someone who is being watched by a customer, so the rules are unforgiving:\n\n- **Fewer products per page beats more.** `productsPerPage` is a temptation to fit everything; a page of ten items that are found instantly beats a page of thirty that must be read.\n- **One template per situation, not per person.** Lunch, evening, and the summer terrace are situations. Individual staff preferences are not, and they multiply.\n- **Let `autoSet` do the switching.** A template that has to be chosen manually will be the wrong one at the busiest moment of the day.\n- **Mind the gaps between windows.** `autoSet` windows that do not cover opening hours leave the till on whatever was last used. Cover the full day, or keep one template as the default that others interrupt.\n\n## Reference\n\n- [Templates API](https://developers.surfboardpayments.com/api/templates)\n- [Product Catalog](/developers/guides/product-catalog)\n- [Payment Methods](/developers/guides/payment-methods)\n- [Terminal & Device Management](/developers/guides/terminal-device-management)"
    },
    {
      "title": "API Conventions",
      "slug": "api-conventions",
      "url": "/developers/guides/api-conventions",
      "description": "The rules that hold across every Surfboard endpoint: authentication, the response envelope, amounts and currencies, identifiers, dates, pagination and environments.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Pagination",
        "Conventions",
        "Authentication"
      ],
      "content": "## Overview\n\nEvery endpoint in the platform shares the same shape. Learn it once and the rest of the reference reads faster: you will know where the data is, how amounts are expressed, and how to page through a list before you have opened the page for a particular call.\n\n## Authentication\n\nMost calls take a key and secret pair, plus the merchant they act on behalf of:\n\n```\nAPI-KEY:      YOUR_API_KEY\nAPI-SECRET:   YOUR_API_SECRET\nMERCHANT-ID:  YOUR_MERCHANT_ID\n```\n\n**The merchant is a header, not a path segment.** Order, payment, and receipt endpoints are `/orders`, `/payments`, and `/receipts` — there is no `/merchants/{merchantId}/orders`. That path returns a bare `404 Not Found` with no hint about its shape, so it reads as a bad merchant ID rather than a bad URL. If a create-order call 404s, check the path before you check the ID.\n\nConfiguration endpoints — stores, terminals, tips, notifications, payment methods — *are* merchant-scoped and do carry `:merchantId` in the path.\n\nPartner-level endpoints — onboarding a merchant, logistics, billing plans — carry the `partnerId` in the path and often do not need `MERCHANT-ID` at all. Where it is optional and you send it anyway, it must match the `:merchantId` in the path.\n\nThree other schemes exist for cases where a long-lived secret cannot travel:\n\n| Scheme | Header | Used by |\n|--------|--------|---------|\n| Bearer JWT | `Authorization` | Server-side integrations that already hold a session |\n| API token | `X-Surfboard-Api-Token` | Scoped machine access |\n| Nonce | `X-Surfboard-Nonce` | Self-hosted checkout pages, issued per order |\n\nAnything running in a browser or on a customer's phone uses a short-lived token instead of your key and secret. That is what [Client Auth Tokens](/developers/guides/client-auth-tokens) is for, and it is not optional: a key in client code is a key in public.\n\n## The Response Envelope\n\nEvery response is the same three fields.\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": { },\n  \"message\": \"Order created successfully\"\n}\n```\n\n| Field | Notes |\n|-------|-------|\n| `status` | `SUCCESS` or `ERROR`. Check this, not just the HTTP code. |\n| `data` | The payload. An object for a single resource, an array for a list. Absent or `null` on errors. |\n| `message` | Human-readable. Log it; do not branch on it — the wording is not a contract. |\n\nErrors keep the envelope and add a code where one applies:\n\n```json\n{\n  \"status\": \"ERROR\",\n  \"message\": \"Invalid request body.\"\n}\n```\n\n| HTTP | Meaning |\n|------|---------|\n| 400 | The body is malformed or a required field is missing. |\n| 401 | Credentials are wrong, missing, or not valid for this account. |\n| 403 | Authenticated, but not permitted to do this. |\n| 404 | An identifier in the path does not resolve. |\n| 500 | Server-side. Retry with backoff; if it persists, contact support. |\n\nOrder creation adds its own codes in the `OR_*`, `PS_*`, `GC_*` and `SP_*` families — see [Create Order Error Codes](/developers/guides/create-order-error-codes).\n\n## Amounts and Currencies\n\n**Amounts are integers in the smallest currency unit.** 10.00 SEK is `1000`. 5.00 EUR is `500`. There are no decimal amounts anywhere in the API, and passing one is a class of bug that survives testing and surfaces in production at a hundredth of the intended price.\n\n**Currencies are numeric ISO 4217 codes, as strings.** SEK is `\"752\"`, EUR is `\"978\"`, NOK is `\"578\"`, DKK is `\"208\"`. Not `\"SEK\"`.\n\n```json\n\"amount\": {\n  \"regular\": 50000,\n  \"total\": 50000,\n  \"currency\": \"752\"\n}\n```\n\n**Prices are tax-inclusive.** Every amount you send is gross: the tax is already inside it. The `tax` array reports how much VAT is *contained within* the price, and never an amount to add on top. That is why `regular` and `total` match in the example above -- it is the rule, not a coincidence of round numbers.\n\n```json\n\"totalOrderAmount\": {\n  \"regular\": 10999,\n  \"total\": 10999,\n  \"currency\": \"752\",\n  \"tax\": [{ \"amount\": 2200, \"percentage\": 25, \"type\": \"VAT\" }]\n}\n```\n\n`totalOrderAmount.total` must equal `regular` plus shipping, minus campaign discounts and adjustments. Adding tax on top so that `total` exceeds `regular` returns `P_0001: Invalid total order price`.\n\nIf you are coming from a sales-tax market, this is a real transformation rather than a field rename. A system that stores net prices and computes tax at checkout has to gross each unit up before building the order, and round per unit rather than on the order total. It is worth checking early: net prices pass every local test and fail on the first call to the API.\n\nCountries, by contrast, are two-letter ISO 3166-1 alpha-2 codes in uppercase — `\"SE\"`, `\"NO\"` — and phone numbers split into a dialling code without the plus and a national number:\n\n```json\n\"phoneNumber\": { \"code\": \"46\", \"number\": \"701234567\" }\n```\n\n## Identifiers and Dates\n\nIdentifiers are opaque hex strings — `\"83a1ba32774149710b\"`. Do not parse them, infer type from them, or assume a length; store them as strings and hand them back unchanged. A `terminal$id` carries a `$` in its field name, which trips up some ORMs and query builders — quote it.\n\nDates and timestamps are ISO 8601 (`2026-04-04T10:20:30+02:00`). Durations, where a field takes one, are `<number><unit>` with the unit as `m`, `h` or `d`: `15m`, `2h`, `3d`.\n\n## Pagination\n\nList endpoints are page-based, and the rules are the same everywhere:\n\n- Sorted newest to oldest by creation time.\n- **Page size is fixed at 100.** There is no page-size parameter.\n- The total is returned in a header, so the body keeps its shape.\n\nAsk for a page with the `X-PAGE-NUMBER` request header. Without it you get the first page.\n\n```bash\ncurl 'YOUR_API_URL/transactions' \\\n  -H 'Content-Type: application/json' \\\n  -H 'API-KEY: YOUR_API_KEY' \\\n  -H 'API-SECRET: YOUR_API_SECRET' \\\n  -H 'MERCHANT-ID: YOUR_MERCHANT_ID' \\\n  -H 'X-PAGE-NUMBER: 2'\n```\n\nThe response reports where you are and how much there is:\n\n```\n< x-page-number: 2\n< x-total-items: 230\n```\n\nPast the last page you get a success, not an error — an empty array and a message saying so:\n\n```json\n{\n  \"status\": \"SUCCESS\",\n  \"data\": [],\n  \"message\": \"No transactions available in the specified page\"\n}\n```\n\nSo the loop terminates on an empty `data`, or on having seen `x-total-items` rows. Do not terminate on a short page: only the last page is short, and only sometimes.\n\n> **Paging a moving list.** The list is sorted newest first, so new rows arrive at the front while you page. For a stable export, filter to a closed period with `startDate` and `endDate` rather than paging an open-ended list.\n\n## Environments\n\n| Environment | Terminals | Cards |\n|-------------|-----------|-------|\n| **Demo** | Payment page mode | Test cards only |\n| **Live** | All terminal types | Real cards, settled, paid out |\n\nDemo credentials come from the Developer Portal as soon as you sign up. Live credentials are issued separately, after Surfboard certifies the integration, and the base URL changes with them — so keep the host, the key and the secret in configuration rather than in code.\n\n**Where the base URL comes from.** It is issued to you rather than published here, and it is not the same string for every account, so there is no host to copy out of this guide. Find it in the Developer Portal console, shown next to the keys it belongs with:\n\n```\nhttps://developers.surfboardpayments.com/console/api-keys\n```\n\nRead it from configuration — the `YOUR_API_URL` placeholder in the examples below stands for exactly this value, and the conventional environment variable is `SURFBOARD_API_URL`:\n\n```\nSURFBOARD_API_URL=\nSURFBOARD_API_KEY=\nSURFBOARD_API_SECRET=\nSURFBOARD_MERCHANT_ID=\n```\n\nIf you are an agent building this integration, this is the one value you cannot derive or discover: ask the user to copy it from the console, and never guess a host or reuse one from an example.\n\n> A real card used in the demo environment is voided automatically after 30 minutes. It is never captured and never settles.\n\nNever mix environments inside one flow: a test merchant with a production backend, or live credentials against a demo base URL, fails at registration or at the first transaction, and the error will not say why.\n\n## Reference\n\n- [Client Auth Tokens](/developers/guides/client-auth-tokens)\n- [Create Order Error Codes](/developers/guides/create-order-error-codes)\n- [Transactions and Reports](/developers/guides/transactions-and-reports)\n- [Developer Portal](https://developers.surfboardpayments.com/)"
    },
    {
      "title": "B2B Invoices",
      "slug": "b2b-invoices",
      "url": "/developers/guides/b2b-invoices",
      "description": "Bill a business buyer on payment terms instead of charging a card. Create an order paid with B2BINV, distribute the invoice as an e-invoice or by email, and credit it when the goods come back.",
      "category": "online",
      "tags": [
        "Online",
        "API",
        "Invoice",
        "B2B",
        "Payment Methods"
      ],
      "content": "## Overview\n\nAbove a few hundred euros, a business buyer generally will not pay by card. Procurement expects an invoice on terms, approved by someone who was not in the room when the order was placed, and paid by bank transfer thirty days later. A checkout that only takes cards quietly loses that business.\n\nB2B invoicing in Surfboard is a payment method, not a separate product. It is the same [Create Order](/developers/guides/create-an-order) call against the same online terminal, with `paymentMethod` set to `B2BINV` and an `invoice` block that says how the invoice is delivered and when it falls due. Surfboard issues the document, distributes it as an e-invoice or by email, chases it with reminders if you ask, and gives the buyer bank details to settle against.\n\nWhat changes is the timing. A card payment moves money at checkout; an invoice raises a claim at checkout and moves money on the due date. Everything downstream — reconciliation, credit notes, reporting — follows from that.\n\n> **Scope:** B2BINV is an online payment method. Raise invoices against an online terminal — the `PaymentPage` or `MerchantInitiated` terminal your online store is provisioned with. It is not available on physical terminals.\n\n## Prerequisites\n\n1. A developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)\n2. A merchant that has completed onboarding, with an approved online store — [Online Payment Link](/developers/guides/online-payment-link) covers store creation and domain verification\n3. `b2binv` active on the merchant or the store\n4. The `terminalId` of the store's online terminal\n\n## Step 1: Activate B2B Invoicing\n\nCard is on by default; `b2binv` is not. Activate it through the Payment Methods API:\n\n```json\nPOST /merchants/:merchantId/payment-methods\n{\n  \"b2binv\": true\n}\n```\n\nYou can also activate it from the Partner Portal or the Merchant Portal — the three paths write to the same configuration, so pick whichever suits how the merchant is managed. To scope invoicing to one store rather than the whole merchant, use the store-level endpoint:\n\n```\nPOST /merchants/:merchantId/stores/:storeId/payment-methods\n```\n\nSee [Payment Methods](/developers/guides/payment-methods) for the full activation and deactivation flow. Until the method is active, the payment initiation inside Create Order will fail, so do this before you send the first invoice.\n\n## Step 2: Identify the Buyer\n\nThis is the part that has no equivalent in a card payment. You are not charging a person, you are billing a legal entity, and the invoice has to name it correctly and say where to send it. Two blocks do that work: `customer`, which identifies who owes the money, and `billing`, which is the address the invoice is issued to.\n\n### The Customer\n\nSend both `person` and `company`:\n\n```json\n\"customer\": {\n  \"person\": {\n    \"name\": { \"firstName\": \"Elin\", \"lastName\": \"Berg\" },\n    \"email\": \"ap@radio-ocean.example\",\n    \"phoneNumber\": { \"code\": \"46\", \"number\": \"701234567\" }\n  },\n  \"company\": {\n    \"companyName\": \"Radio Ocean AB\",\n    \"vatId\": \"SE556000000001\",\n    \"registrationNumber\": \"5560000000\"\n  }\n}\n```\n\n| Field | Notes |\n|-------|-------|\n| `company.companyName` | The legal name of the entity being billed, as it should appear on the invoice. |\n| `company.vatId` | VAT registration number, including the country prefix. |\n| `company.registrationNumber` | Company registration number. |\n| `person.email` | Where an `EMAIL` invoice is delivered. Use the buyer's accounts-payable address, not the salesperson's. |\n| `person.name`, `person.phoneNumber` | The contact on the buying side. |\n\nThe company details identify who owes the money and are what the invoice is issued against. Get them from the buyer at checkout rather than inferring them from an email domain — a wrong registration number is an invoice the buyer's finance team can reject.\n\n### The Billing Address\n\n`billing` is optional on a Create Order call in general. **For a B2B invoice it is mandatory** — an invoice is a document addressed to somewhere, and there is no sensible default.\n\n```json\n\"billing\": {\n  \"address\": {\n    \"addressLine1\": \"Surfgatan 1\",\n    \"city\": \"Stockholm\",\n    \"postalCode\": \"11122\",\n    \"countryCode\": \"SE\"\n  }\n}\n```\n\n| Field | Required | Notes |\n|-------|----------|-------|\n| `billing.address.addressLine1` | Yes | Street address of the entity being billed. |\n| `billing.address.city` | Yes | City. |\n| `billing.address.postalCode` | Yes | Postal code. |\n| `billing.address.countryCode` | Yes | ISO 3166-1 alpha-2, uppercase. |\n| `billing.address.careOf` | No | Attention line — useful when invoices go to a named accounts-payable desk. |\n| `billing.address.addressLine2`, `addressLine3` | No | Further address lines. |\n| `billing.name`, `billing.email`, `billing.phoneNumber` | No | A billing contact distinct from `customer.person`. |\n\nThis is the buyer's registered billing address, which is not necessarily where the goods go. If you are shipping somewhere else, put that in `shipping` and leave `billing` as the address finance works from.\n\n## Step 3: Create the Invoice Order\n\nThe call is Create Order with the invoice configuration carried in `controlFunctions.initiatePaymentsOptions.paymentMethodParams.invoice`:\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"813ca2cb12ce400405\",\n  \"referenceId\": \"order-2026-0418\",\n  \"billing\": {\n    \"address\": {\n      \"addressLine1\": \"Surfgatan 1\",\n      \"city\": \"Stockholm\",\n      \"postalCode\": \"11122\",\n      \"countryCode\": \"SE\"\n    }\n  },\n  \"orderLines\": [\n    {\n      \"id\": \"83dddf1596c8d03937\",\n      \"name\": \"7'8 Radio Ocean Liner\",\n      \"description\": \"7'8 Radio Ocean Liner surfboard\",\n      \"quantity\": 1,\n      \"amount\": {\n        \"regular\": 179200,\n        \"total\": 179200,\n        \"currency\": \"752\",\n        \"tax\": [{ \"type\": \"VAT\", \"percentage\": 25, \"amount\": 35840 }]\n      }\n    }\n  ],\n  \"customer\": {\n    \"person\": {\n      \"name\": { \"firstName\": \"Elin\", \"lastName\": \"Berg\" },\n      \"email\": \"ap@radio-ocean.example\",\n      \"phoneNumber\": { \"code\": \"46\", \"number\": \"701234567\" }\n    },\n    \"company\": {\n      \"companyName\": \"Radio Ocean AB\",\n      \"vatId\": \"SE556000000001\",\n      \"registrationNumber\": \"5560000000\"\n    }\n  },\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"B2BINV\",\n      \"paymentMethodParams\": {\n        \"invoice\": {\n          \"invoiceDistribution\": \"EINVOICE\",\n          \"dueDate\": \"30d\",\n          \"reminder\": false,\n          \"invoicePaymentMethods\": [\"BANK\", \"DIRECT_BANK\"]\n        }\n      }\n    }\n  }\n}\n```\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"data\": {\n    \"orderId\": \"845712d3b9674383020b\",\n    \"paymentId\": \"845712d3b9675f900206\",\n    \"interAppJWT\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n    \"paymentPageLink\": \"https://pay.withsurfboard.com/845712d3b9674383020b?pi=ocaY_Xgzq9SqhTPR-ry9d8Ne2s3Cl9rB...\",\n    \"invoiceDetails\": {\n      \"invoiceId\": 5100232680,\n      \"invoicePdfUrl\": \"https://b2b.payer.se/api/v1/receiptViewer/invoice/pdf/1946a1ab-141f-441c-938d-8044278922ed\",\n      \"iban\": \"SE0000000000000000000000\",\n      \"accountHolderName\": \"Surfboard Payments AB\",\n      \"bic\": \"DNBASESX\",\n      \"bankgiro\": \"0000000\",\n      \"ocr\": \"00845712396759002065\"\n    }\n  },\n  \"message\": \"Order created successfully\"\n}\n```\n\nAmounts follow the same rules as every other order: minor units, a numeric ISO 4217 `currency` (`752` is SEK), and tax stated per line. `totalOrderAmount` is optional, but when you send it, it has to reconcile with the lines.\n\nGive every line a `description` as well as a `name`. On a card payment nobody reads the line items; on an invoice they are the document, and the person approving it may never have seen the order. A line that says only \"Liner\" is a line someone has to email you about.\n\n### The Invoice Block\n\n| Field | Required | Notes |\n|-------|----------|-------|\n| `invoiceDistribution` | Yes | `EINVOICE` or `EMAIL`. E-invoice is routed to the company; email goes to `customer.person.email`. |\n| `dueDate` | Yes | A relative duration such as `30d` for Net 30, or an absolute date. |\n| `invoicePaymentMethods` | Yes | How the buyer may settle: `BANK`, `DIRECT_BANK`, `CARD`. Send the ones you will accept. |\n| `reminder` | Yes | Whether Surfboard sends reminders as the due date passes. |\n| `debtCollectionEnabled` | No | Hands a past-due invoice to the debt-collection flow. |\n| `contractReference` | No | Your reference for the underlying contract, carried on the invoice. |\n\n`dueDate` is a commercial decision, not a technical one. Net 30 is the common default for procurement; longer terms are a financing choice the merchant is making on the buyer's behalf. Set `reminder` and `debtCollectionEnabled` deliberately too — both change what the buyer receives after the due date, and both should match what the merchant agreed with them.\n\n### What Comes Back\n\nBecause the payment is initiated inside the same call, the response carries the invoice itself in `invoiceDetails`:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `invoiceId` | number | The invoice identifier. Note it is a **number**, not a string — store it as one. |\n| `invoicePdfUrl` | string | The invoice document. This is the thing the buyer's finance team will actually open. |\n| `iban` | string | The account to transfer to, for international settlement. |\n| `bic` | string | Bank identifier code for that account. |\n| `accountHolderName` | string | The account holder shown on the invoice. |\n| `bankgiro` | string | Swedish bankgiro number, for domestic transfers. |\n| `ocr` | string | The OCR reference the buyer quotes on the payment so it reconciles automatically. |\n\n`bankgiro` and `ocr` are how a Swedish buyer settles a `BANK` transfer, and the OCR is what matches their payment back to this invoice without anyone reading a bank statement. `iban` and `bic` cover payment from outside the country. All of them appear on the PDF, so you do not have to surface them yourself — but store `invoiceId` and `ocr` against your own accounts-receivable record, because those are what reconciliation is keyed on later.\n\nThe response also carries the usual `orderId` and `paymentId`, plus a `paymentPageLink` — the same hosted page a card order returns, where a buyer can settle through whichever of `DIRECT_BANK` or `CARD` you allowed in `invoicePaymentMethods`.\n\nIf you initiate the payment separately rather than inside Create Order, the same `invoiceDetails` block comes back from the [Initiate Payment](https://developers.surfboardpayments.com/api/payments) call instead.\n\n## Step 4: Confirm and Reconcile\n\nOrder status works the same as for any other order:\n\n```\nGET /orders/:orderId/status\n```\n\nSubscribe to `order.paymentcompleted` and `order.paymentfailed` rather than polling on a timer — see [Webhooks](/developers/guides/webhooks-notifications) for subscriptions, retries and signature verification.\n\nThe thing to hold on to is that an invoice settles on its own timetable. The order tells you the invoice was raised and where it stands; the money arriving is a separate event on the buyer's terms. Reconcile invoice revenue against [Settlements & Reporting](/developers/guides/settlements-reporting) rather than treating order creation as cash in the bank.\n\n## Crediting an Invoice\n\nWhen an invoice was wrong or the goods come back, the buyer gets a credit invoice. **How you raise it depends on whether the original invoice has been paid**, and this is the one thing to get right before you write any code:\n\n| The original invoice | What you do | One call or two |\n|----------------------|-------------|-----------------|\n| Raised, not yet paid | Cancel the order | One call, no body |\n| Paid | Create a return order | A full order payload |\n\nBecause an invoice sits unpaid for the whole of its term by design, the unpaid case is the one you will hit most.\n\n### Unpaid: Cancel the Order\n\nThere is nothing to give back yet, so this is a cancellation rather than a refund. Cancelling the order raises a credit invoice by default:\n\n```\nDELETE /orders/:orderId\n```\n\nThe endpoint takes no request body.\n\n```json\n// Response\n{\n  \"status\": \"SUCCESS\",\n  \"message\": \"Order cancelled successfully\"\n}\n```\n\nThat is the whole operation. Use it for the ordinary cases — wrong amount, wrong entity, the buyer walked away after the invoice went out.\n\n### Paid: Create a Return Order\n\nOnce the buyer has settled, crediting is a refund and takes the standard return-order shape: a new order with negative quantities, each line pointing back at the order and the payment it credits.\n\n```json\nPOST /orders\n{\n  \"terminal$id\": \"813ca2cb12ce400405\",\n  \"referenceId\": \"credit-2026-0418\",\n  \"billing\": {\n    \"address\": {\n      \"addressLine1\": \"Surfgatan 1\",\n      \"city\": \"Stockholm\",\n      \"postalCode\": \"11122\",\n      \"countryCode\": \"SE\"\n    }\n  },\n  \"customer\": {\n    \"person\": {\n      \"name\": { \"firstName\": \"Elin\", \"lastName\": \"Berg\" },\n      \"email\": \"ap@radio-ocean.example\",\n      \"phoneNumber\": { \"code\": \"46\", \"number\": \"701234567\" }\n    },\n    \"company\": {\n      \"companyName\": \"Radio Ocean AB\",\n      \"vatId\": \"SE556000000001\",\n      \"registrationNumber\": \"5560000000\"\n    }\n  },\n  \"orderLines\": [\n    {\n      \"id\": \"83dddf1596c8d03937\",\n      \"name\": \"7'8 Radio Ocean Liner\",\n      \"description\": \"7'8 Radio Ocean Liner surfboard\",\n      \"quantity\": -1,\n      \"purchaseOrderId\": \"845712d3b9674383020b\",\n      \"purchasePaymentId\": \"845712d3b9675f900206\",\n      \"amount\": {\n        \"regular\": 179200,\n        \"total\": 179200,\n        \"currency\": \"752\",\n        \"tax\": [{ \"type\": \"VAT\", \"percentage\": 25, \"amount\": 35840 }]\n      }\n    }\n  ],\n  \"controlFunctions\": {\n    \"initiatePaymentsOptions\": {\n      \"paymentMethod\": \"B2BINV\",\n      \"paymentMethodParams\": {\n        \"invoice\": {\n          \"invoiceDistribution\": \"EMAIL\",\n          \"dueDate\": \"30d\",\n          \"reminder\": false,\n          \"invoicePaymentMethods\": [\"BANK\", \"DIRECT_BANK\"]\n        }\n      }\n    }\n  }\n}\n```\n\nKey details:\n\n- `quantity` goes negative on every credited line. `amount.total` stays positive.\n- `purchaseOrderId` and `purchasePaymentId` both belong **on the line item**: the `orderId` and `paymentId` returned when the original invoice was created.\n- Repeat the `billing` address and the `invoice` block. The credit is its own addressed document and can be distributed differently from the original — `EMAIL` here, where the original went out as `EINVOICE`.\n- Credit only some of the lines and you have a partial credit. The mechanics are the same as [Partial Refund](/developers/guides/partial-refund).\n\n> **`OR_0035: Cannot refund from purchase order that is not completed. Status: PENDING`** means exactly what the two paths above describe — the invoice has not been paid, so there is nothing to refund. Cancel the order instead.\n\nThe buyer keeps the original invoice and receives a credit against it. Both documents stand; the credit does not erase the original.\n\n## Error Handling\n\nFailures come back as `status: \"ERROR\"` with an `OR_*` or `PS_*` code, and the ones you will meet setting this up are mostly configuration rather than payload:\n\n- **The method is not active.** `b2binv` has to be activated on the merchant or store before an invoice payment can initiate. This is the most common first failure.\n- **The company block is missing.** A B2B invoice needs an entity to bill; a `customer` with only a `person` is not enough.\n- **The billing address is missing.** `billing.address` is optional on Create Order generally and mandatory here. Easy to miss if you are adapting a working card payload.\n- **The invoice block is incomplete.** `invoiceDistribution`, `dueDate`, `invoicePaymentMethods` and `reminder` are all required by the payment initiation, even though Create Order will accept the order without them.\n- **You refunded an invoice nobody paid.** `OR_0035` on a return order means the original is still `PENDING`. Cancel it instead.\n- **A credit line has no `purchaseOrderId`.** Every negative line must reference the order it credits, and carry the `purchasePaymentId` alongside it.\n\n[Create Order Error Codes](/developers/guides/create-order-error-codes) lists the full set, including the errors raised by the payment initiation that happens inside the same call.\n\n## Next Steps\n\n- [Payment Methods](/developers/guides/payment-methods) — activating and deactivating `b2binv`\n- [Create an Order](/developers/guides/create-an-order) — line items, tax and control functions in full\n- [Refund an Order](/developers/guides/refund-an-order) — the card-side equivalent of a credit invoice\n- [Settlements & Reporting](/developers/guides/settlements-reporting) — reconciling what has actually been paid\n- [Online Payment Link](/developers/guides/online-payment-link) — the store and terminal setup this guide assumes\n\n## Reference\n\n- [Create Order API](https://developers.surfboardpayments.com/api/orders)\n- [Cancel an Order API](https://developers.surfboardpayments.com/api/orders) — the one-call credit for an unpaid invoice\n- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)\n- [Payment Methods API](https://developers.surfboardpayments.com/api/payment-methods)"
    }
  ]
}