# Surfboard Payments, Full Content Reference
> This document provides a comprehensive content dump of the Surfboard Payments website for AI systems and language models. For a concise overview, see /llms.txt
---
## When to Use Surfboard Payments
- **Embedding payments in software you sell to merchants.** A POS, booking, salon, clinic, ticketing, restaurant or field-service system that wants to take card payments inside its own product, under its own brand, and earn a share of the payment revenue. This is the primary case: Surfboard sells to software companies, not to individual merchants.
- **Card payments in a physical location.** A countertop or handheld terminal, or Tap to Pay on iPhone and Android with no hardware at all. Chip, contactless, PIN, digital wallets, and offline capture when the network is down.
- **One integration for in-store, online and unattended.** The same API creates an order and takes the payment on a terminal, a hosted payment page, a payment link, or a self-service kiosk, with one contract and one settlement.
- **Multi-merchant setups.** Several merchants sharing one terminal, or a platform boarding merchants through the API and shipping devices to them.
- **White-label payments.** Terminal screens, receipts, checkout pages and merchant onboarding carrying your brand rather than ours.
- **Europe.** Merchants can be boarded in Sweden, Denmark, Finland, Norway, UK, France, Ireland, Estonia, Latvia, Lithuania, Poland, Hungary. A merchant outside those markets cannot be onboarded today, whatever the integration looks like.
- **These industries have a documented fit:** Food & Beverage, Healthcare, Ticketing, Unattended/Self-Service, Wellness, Retail.
### When not to use it
- **Online-only payments outside the supported markets.** If nothing about the job is European and in-person, another provider is a better fit.
- **A single merchant with no software of its own.** Surfboard reaches merchants through software partners. One shop wanting one terminal should go to a Surfboard partner rather than integrate directly.
- **Anything that puts card data on your servers.** Card capture always happens on a Surfboard terminal, hosted page or SDK. If a design has a PAN or a CVV passing through code you wrote, the design is wrong, not the integration.
- **Consumer-to-consumer transfers, crypto, and lending.** Not what this platform does.
Full agent instructions, including how to call the API and the rules that apply to payments work: /agent-instructions.md
---
## Company Overview
Surfboard Payments is a Swedish fintech company that provides a unified, API-first payment platform for independent software vendors (ISVs) and software companies. The platform enables in-store, online, and mobile payments through a single modern API.
### Mission
To change the payment landscape, one tap at a time. Empowering software providers to embed seamless, secure payment capabilities into their products.
### Key Facts
- Founded: 2019
- Headquarters: Stockholm, Sweden (Barnhusgatan 4)
- Second Office: Chennai, India
- Employees: ~50
- CEO: Christopher Lindfeldt
- Regulation: Licensed payment institution under Finansinspektionen (Swedish Financial Supervisory Authority)
- Infrastructure: Multi-cloud (Google Cloud, AWS, Azure) with PCI DSS certification on each
- Uptime: 99.99% guarantee
- Setup Record: 5 hours from signup to first live transaction
- Revenue Growth: 122% recurring revenue growth year-over-year (Q4 2025)
### Core Value Proposition
- One unified API for in-store, online, and mobile payments
- White-label platform, ISVs maintain their own brand identity
- Acquiring-agnostic with smart routing
- Cloud-native, built from scratch with no legacy code
- Zero hidden fees, transparent pricing
- Complete developer portal with sandbox access
- Active in 12 European markets: Sweden, Denmark, Finland, Norway, UK, France, Ireland, Estonia, Latvia, Lithuania, Poland, Hungary
### Pricing
- Model: Custom pricing for software companies and ISVs
- Approach: Transparent pricing with zero hidden fees
- Based on: Transaction volume and selected products/services
- Contact sales for a tailored quote at https://www.surfboardpayments.com/contact
---
## Products
### Payment Terminals
#### SurfPrint Pro
All-in-one handheld terminal with full touchscreen interface and built-in thermal printer. Perfect for restaurants, deliveries, and service businesses on the move.
- Type: Handheld Terminal
- Built-in thermal printer
- Full touchscreen
- Handheld and mountable
- All-day battery
- 4G/WiFi
- Connectivity: 4G, 3G, WiFi
- Certifications: PCI PTS 6.x, EMV L1/L2
Hardware Specifications:
- Processor: Cortex Quad-core A53, 2.0GHz
- Security: ARMv7-M security core, 192MHz
- Memory: 2GB RAM, 16GB Flash (options: 3GB/32GB, 4GB/64GB, MicroSD up to 256GB)
- OS: Android 12 SurfOS
- Display: 5.99-inch TFT LCD, 1440x720, capacitive multi-touch
- Printer: High-speed thermal printer, 80mm/s, paper width: 58mm, roll diameter: 40mm
- Card Reader: Magnetic, Smart Card (chip), Contactless (NFC)
- Camera: 0.3MP/2MP front, 5MP/8MP rear autofocus with flashlight
- Battery: Li-ion 7.2V/2600mAh (optional 3300mAh)
- Connectivity: 4G/3G/2G, WiFi 2.4G+5G
- Ports: 1x USB Type-C, optional extension hub (RS232, USB-A, Ethernet)
- Card Slots: 2x SIM, 2x SAM
- Dimensions: 201mm x 79.8mm x 62mm
- Weight: 370g without battery
- Certifications: PCI PTS 6.x, EMV L1/L2, CE, FCC, RoHS, CB
- Environment: Operating: -10°C to 50°C, Storage: -20°C to 60°C
- URL: /products/surfprint/pro
#### SurfPrint Pro K
Handheld terminal with physical keypad, 4.5-inch capacitive display, and built-in printer. Designed for reliability, inclusivity, and control.
- Type: Handheld Terminal
- Physical keypad
- Built-in thermal printer
- 4.5-inch display
- Handheld and mountable
- Accessible design
- Connectivity: 4G, 3G, WiFi
- Certifications: PCI PTS 6.x, EMV L1/L2
Hardware Specifications:
- Processor: Cortex Quad-core A53, 2.0GHz
- Security: ARMv7-M security core, 192MHz
- Memory: 2GB RAM, 8GB Flash (options: 2GB/16GB, MicroSD up to 256GB)
- OS: Android 12 SurfOS
- Display: 4.5-inch TFT LCD, 854x480, capacitive multi-touch
- Keypad: 10 numeric keys, 5 function keys, backlit
- Printer: High-speed thermal printer, 80mm/s, paper width: 58mm, roll diameter: 40mm
- Card Reader: Magnetic, Smart Card (chip), Contactless (NFC)
- Camera: 0.3MP front, optional 2MP/5MP rear
- Battery: Li-ion 7.2V/2600mAh
- Connectivity: 4G/3G/2G, WiFi 2.4G+5G
- Ports: 1x USB Type-C, 1x DC Jack, optional extension hub
- Card Slots: 2x SIM, 2x SAM, 1x SD (optional eSIM)
- Dimensions: 187mm x 80.2mm x 63mm
- Weight: 433g including battery
- Certifications: PCI PTS 6.x, EMV L1/L2, PayPass, PayWave, RoHS, CE
- Environment: Operating: -10°C to 50°C, Storage: -20°C to 60°C
- URL: /products/surfprint/pro-k
#### SurfTouch Pro
High-performance mobile payment terminal with 5-inch display, next-gen processor, and all-day battery. Built for merchants on the move.
- Type: Mobile Terminal
- 5-inch TFT display (1280x720)
- Quad-core 2.0GHz processor
- Up to 4GB RAM / 64GB storage
- 5300mAh battery
- Optional 2D barcode scanner
- Dual SIM support
- GPS
- Connectivity: 4G, 3G, 2G, WiFi 2.4G/5G
- Dimensions: 150mm x 74mm x 18.8mm
- Weight: 245g-270g
- Certifications: PCI PTS 6.x, EMV L1/L2, CE, FCC, RoHS
Hardware Specifications:
- Processor: Cortex Quad-core A53, 2.0GHz
- Security: Cortex-M3 security core
- Memory: 2GB RAM, 16GB Flash (optional 4GB/64GB, MicroSD up to 128GB)
- OS: Android 12 SurfOS
- Display: 5-inch TFT LCD, 1280x720, capacitive multi-touch
- Card Reader: Magnetic, Smart Card (chip), Contactless (NFC)
- Camera: 0.3MP/2MP front, 5MP/8MP rear autofocus
- Battery: Li-ion 3.8V/5300mAh (or 3600mAh option)
- Connectivity: 4G/3G/2G, WiFi 2.4G+5G
- Positioning: GPS/BEIDOU/GLONASS/GALILEO
- Ports: 1x USB Type-C (OTG)
- Card Slots: 1x SIM, 1x SAM (optional 2x SIM)
- Dimensions: 150mm x 74mm x 18.8mm
- Weight: 245g (3600mAh) / 270g (5300mAh)
- Certifications: PCI PTS 6.x, EMV L1/L2, CE, FCC, RoHS, CB
- Environment: Operating: -10°C to 50°C, Storage: -20°C to 60°C
- URL: /products/surftouch/pro
#### SurfTouch Pro Retail
Countertop-first terminal for fixed retail environments. Same high-performance hardware as SurfTouch Pro with Ethernet connectivity and optional barcode scanner.
- Type: Countertop Terminal
- 5-inch touchscreen
- Built-in Ethernet
- Optional barcode scanner
- Countertop design
- Connectivity: Ethernet, WiFi, 4G
- Certifications: PCI PTS 6.x, EMV L1/L2
Hardware Specifications:
- Processor: Cortex Quad-core A53, 2.0GHz
- Security: Cortex-M3 security core
- Memory: 2GB RAM, 16GB Flash (optional 4GB/64GB, MicroSD up to 256GB)
- OS: Android 12 SurfOS
- Display: 5-inch TFT LCD, 1280x720, capacitive multi-touch
- Card Reader: Magnetic, Smart Card (chip), Contactless (NFC)
- Camera: 0.3MP front, optional 2MP
- Battery: N/A (mains powered)
- Connectivity: Ethernet 10/100, WiFi 2.4G+5G, optional 4G
- Ports: 1x USB Type-C, multi-function composite (RS232/RJ45/USB-A)
- Card Slots: 1x SAM (optional SIM)
- Dimensions: 150mm x 74mm x 22.65mm
- Weight: 251g
- Certifications: PCI PTS 6.x, EMV L1/L2, CE, FCC, RoHS, CB
- Environment: Operating: -10°C to 50°C, Storage: -20°C to 60°C
- URL: /products/surftouch/retail
#### SurfPad Pro
High-performance countertop terminal with dedicated physical PIN pad, bright 5-inch display, and enterprise-grade security for large-scale retail and hospitality.
- Type: Countertop Terminal
- Physical PIN pad
- 5-inch display
- Enterprise-grade security
- Countertop and mountable
- Connectivity: Ethernet, WiFi, 4G
- Certifications: PCI PTS 6.x, EMV L1/L2
Hardware Specifications:
- Processor: 32-bit high performance secure processor
- Memory: 2GB RAM, 16GB eMMC (optional 4GB/32GB, MicroSD up to 128GB)
- OS: Android 11 SurfOS
- Display: 5-inch IPS LCD, 1280x720, capacitive multi-touch
- Keypad: 10 alphanumeric keys, 5 function keys, backlit
- Card Reader: Magnetic, Smart Card (chip), Contactless (NFC)
- Camera: 2MP front camera
- Connectivity: Ethernet 10/100, WiFi 2.4G+5G, optional 4G
- Positioning: GPS/BEIDOU/GLONASS/GALILEO
- Ports: 1x MCI (RS232, RJ45, USB-A, power input)
- Dimensions: 166.7mm x 138mm x 42mm
- Weight: 390g
- Certifications: PCI PTS 6.x, EMV L1/L2, CE, FCC, RoHS, JCB
- Environment: Operating: -10°C to 50°C, Storage: -20°C to 60°C
- URL: /products/surfpad/pro
#### SurfAlone
Compact, secure, API-ready payment terminal for self-service and unattended environments. Integrates into existing systems for vending, parking, EV charging, and transport.
- Type: Unattended Terminal
- Compact form factor
- Unattended operation
- API-ready integration
- Secure and certified
- Connectivity: Ethernet, 4G, WiFi
- Certifications: PCI PTS, EMV L1/L2
Hardware Specifications:
- Processor: Cortex Quad-core A53, 2.0GHz
- Security: Cortex-M4F security core
- Memory: 2GB RAM, 16GB Flash, MicroSD up to 256GB
- OS: Android 12 SurfOS
- Display: 5-inch, 720x1280, capacitive multi-touch, anti-fingerprint, anti-glare, glove-compatible
- Card Reader: Magnetic, Smart Card (chip), Contactless (NFC)
- Camera: 2MP QR camera, 2MP front camera
- IP Rating: IK10, IP65 (water & dust proof)
- Connectivity: Ethernet 10/100, WiFi 2.4G+5G, 4G/3G/2G
- Positioning: GPS/GLONASS/BEIDOU/GALILEO
- Ports: 1x USB-A, 1x USB-C, 2x RS232 (RJ45), 1x LAN, MDB Master/Slave, 3.5mm audio, optional HDMI
- Card Slots: 2x SAM, 2x SIM
- Power: 12V-48V DC, MDB/RS232, 24V AC via Executive
- Dimensions: 151.5mm x 94mm x 51.4mm
- Weight: 520.7g
- Certifications: PCI PTS 6.x, EMV L1/L2, CE, RoHS, ATEX
- Environment: Operating: -20°C to 70°C, Storage: -30°C to 70°C
- URL: /products/surfalone
#### SurfXpress
22-inch Android tablet running SoftPOS for the ultimate self-service experience with no extra hardware needed.
- Type: Self-Service Kiosk
- 22-inch display
- SoftPOS-based (no extra payment hardware)
- Android-powered
- Self-service optimized
Hardware Specifications:
- Platform: Qualcomm Snapdragon 660
- Memory: 4GB LPDDR4, 64GB eMMC
- OS: Android 12 with GMS
- Display: 22-inch FHD, 1080x1920, multi-touch
- Payment NFC: Front-facing antenna behind display, EMV, Felica, MiFare
- Camera: 8MP front autofocus
- Connectivity: WiFi 802.11ac, GNSS
- Ports: 2x USB-C (data), 2x USB-A, RJ-45 LAN, DC barrel input
- Dimensions: 528.3mm x 323.5mm x 42.2mm
- Weight: 5.2kg including power supply
- Environment: Operating: 0°C to 50°C, Humidity: 10-90%
- URL: /products/surfxpress
#### SurfPrint Go
SoftPOS-based handheld terminal that is compact and durable with a built-in printer. Perfect for merchants who need to print on the spot.
- Type: Handheld Terminal
- Built-in printer
- SoftPOS-based
- Compact and durable
- Handheld
Hardware Specifications:
- Processor: Octa-core (Quad A73 2.0GHz + Quad A53 2.0GHz)
- Memory: 4GB RAM, 32GB ROM
- OS: Android 13, 64-bit
- Display: 6.5-inch, 720x1600, multi-point capacitive touch
- Printer: High-speed thermal printer, up to 100mm/s, paper width: 58mm, roll diameter: 50mm
- Camera: 5MP rear, supports 1D barcode / 2D QR scanning
- Battery: 7.7V / 3350mAh / 25.8Wh
- Connectivity: WiFi 5 (2.4GHz+5GHz), 2G/3G/4G LTE, optional NFC/GPS
- Ports: 1x USB-C (OTG), 1x Nano SIM, 1x TF card, optional eSIM/PSAM
- Dimensions: 84.3mm x 240.6mm x 59.6mm
- Weight: 450g
- Certifications: Google GMS, FCC, CE, IMDA, WEEE
- Environment: Operating: -10°C to 50°C, Storage: -20°C to 60°C
- URL: /products/surfprint/go
#### SurfPrint
First generation handheld smart terminal device with a built-in printer for countertop and the floor.
- Type: Handheld Terminal
- Built-in printer
- Handheld and mountable
- Touchscreen
Hardware Specifications:
- Processor: Quad-core ARM Cortex-A53, 2.0GHz
- Memory: 2GB RAM, 16GB Flash, MicroSD up to 32GB
- OS: Android 10 SurfOS
- Display: 5.4-inch capacitive, 720x1440 + customer display (128x32 OLED)
- Printer: Direct line thermal, 203x203 dpi, print width: 48mm (384 dots), paper width: 58mm, roll diameter: 40mm, speed: 12 lines/sec
- Card Reader: Magnetic (triple track), Smart Card (friction), Contactless (NFC)
- Camera: 5MP
- Battery: Li-Ion 7.4V, 2600mAh
- Connectivity: WiFi 2.4G+5G, 2G/3G/4G
- Dimensions: 82mm x 206mm x 61mm
- Weight: 506g
- Drop Tested: 1.2m, faces and edges
- Certifications: PCI PTS 5.x SRED, EMV L1/L2, CE, FCC, RoHS, CB
- Environment: Operating: -10°C to 50°C
- URL: /products/surfprint
#### SurfTouch
First generation smart terminal device with or without a scanner for the countertop and the floor.
- Type: Smart Terminal
- Touchscreen
- Optional scanner
- Handheld and mountable
Hardware Specifications:
- Processor: Quad-core ARM Cortex-A53, 1.8GHz
- Memory: 1GB RAM, 8GB Flash (optional 2GB/16GB)
- OS: Android 10 SurfOS
- Display: 5-inch capacitive, 720x1280
- Card Reader: Smart Card (friction), Contactless (NFC)
- Camera: 5MP fixed focus with flashlight
- Battery: Li-Ion 4.35V, 2500mAh
- Connectivity: WiFi 2.4G+5G, 2G/3G/4G
- Dimensions: 144mm x 77mm x 16mm
- Weight: 208g
- Drop Tested: 1.2m, faces and edges
- Certifications: PCI PTS 5.x SRED, EMV L1/L2, CE, FCC, RoHS, CB
- Environment: Operating: -10°C to 50°C
- URL: /products/surftouch
#### SurfPad
First generation traditional card terminal built on the Surfboard API.
- Type: Card Terminal
- Traditional form factor
- Keypad
- Handheld and mountable
- API-powered
Hardware Specifications:
- Processor: Secure ARM Cortex M3
- Display: 320x240 touchscreen
- Keypad: 10 numeric keys, 5 function keys
- Card Reader: Smart Card (landing), Contactless (NFC)
- Battery: Li-Ion 3.7V, 1180mAh
- Connectivity: WiFi 802.11 b/g/n, optional 2G/3G/4G
- Ports: RS-232, USB Type-C, optional SAM/SIM slots
- Dimensions: 71mm x 133mm x 19mm
- Weight: 180g
- Drop Tested: 1.2m, faces and edges
- Certifications: PCI PTS 4.x, EMV L1/L2, CE, FCC, TQM
- Environment: Operating: -10°C to 40°C
- URL: /products/surfpad
#### SurfMini
The ultra-slim miniPOS for effortless payments. A pocket-size powerhouse just 16.5mm thin, with a 4-inch full-screen contactless reader, Android 14, and all-day battery. Fits anywhere, works everywhere.
- Type: Compact Terminal
- Ultra-slim 16.5mm pocket design
- 4-inch full-screen contactless reader
- Android 14
- Contactless, chip, and magstripe
- G-sensor motion awareness
- 500+ transactions per charge
- 4G/WiFi/Bluetooth connectivity
- Connectivity: 4G, 3G, 2G, WiFi, Bluetooth
- Dimensions: 94.4mm x 83mm x 16.5mm
- Weight: 143g including battery
- Certifications: PCI PTS 7.x, EMV L1/L2, EMV Contactless L1
Hardware Specifications:
- Processor: Cortex Quad-core A53, 2.0GHz + Cortex-M3 security core
- Memory: 2GB RAM, 8GB Flash (optional 16GB)
- OS: Android 14 SurfOS
- Display: 4-inch IPS LCD, 480x480, capacitive multi-touch, electronic signature
- Card Reader: Magnetic (triple track), Smart Card (chip), Contactless (NFC)
- Camera: 2MP fixed-focus front, support 1D/2D code payment
- Sensor: G-sensor (auto rotate, wake and rest)
- Battery: Polymer 3.8V/1800mAh/6.84Wh, 500+ transactions per charge
- Connectivity: 4G/3G/2G, WiFi 2.4G+5G, Bluetooth 5.0, GPS
- Positioning: GPS, GLONASS, BEIDOU, GALILEO
- Ports: 1x USB Type-C (OTG), 5x Pogo Pins
- Card Slots: 2x SIM (optional SAM / eSIM)
- Docking: Optional magnetic smart docking station with Pogo pins
- Dimensions: 94.4mm x 83mm x 16.5mm
- Weight: 143g including battery
- Certifications: PCI PTS 7.x, EMV L1/L2, EMV Contactless L1, CE, CB, RoHS
- Environment: Operating: -10°C to 50°C, Storage: -20°C to 60°C
In the box:
- 1x SurfMini terminal (blue)
- 1x USB-A to USB-C cable (white)
- URL: /products/surfmini
### Tap to Pay / SoftPOS
#### Tap to Pay on iPhone
Accept all types of in-person contactless payments on iPhone, from physical debit and credit cards to Apple Pay and other digital wallets. No extra terminals or hardware needed.
- Type: SoftPOS
- No additional hardware
- Contactless payments
- Apple Pay support
- Digital wallet support
- Chip card support
- Available in: Sweden, Denmark, Finland, Norway, UK, France
- URL: /products/tap-to-pay-on-iphone
#### CheckoutX (Tap to Pay on Android)
Accept all types of in-person contactless payments on Android devices, from physical debit and credit cards to digital wallets. Described as the world's fastest SoftPOS.
- Type: SoftPOS
- No additional hardware
- Contactless payments
- Digital wallet support
- World's fastest SoftPOS
- URL: /products/checkoutx
### Online Payments
#### Online Checkout
Embed seamless, secure, multi-method payments into your product. Available as hosted checkout page or custom checkout via SDK/API. Unified with in-store payments on the same platform.
Integration options:
1. Hosted Checkout Page, Fastest way to accept payments, fully responsive, auto 3DS, customizable to merchant brand including custom domains
2. Online SDK, Full control with tokenized cards, recurring payments, on/off-session transactions with dynamic acquirer routing
3. REST API, Direct API integration for complete customization
Supported payment methods:
- Visa
- Mastercard
- American Express
- Apple Pay
- Google Pay
- Pay by Bank
- BNPL
- Dankort
- Vipps
URL: /products/online
### Accessories (Fins)
- FinPrinter: External receipt printer, /products/fins/finprinter
- Hub: Connectivity hub for terminals, /products/fins/hub
- SurfPrint Pro Multicomm Base: Combined charging base and network hub for SurfPrint Pro, /products/fins/surfprint-base
In the box: 1x SurfPrint Pro Multicomm Base (Base only). No cables ship with the base. Power it with the USB-C cable and power adapter that came with your SurfPrint Pro terminal. It shares the same adapter, so there is nothing extra to buy.
- SurfPrint Pro Charging Base: Charging base for SurfPrint Pro, /products/fins/surfprint-charger
In the box: 1x SurfPrint Pro Charging Base (Base only), 1x Double-sided tape (White foam pad, to fix the base to the counter). No cable or power adapter ships with the base. Power it with the USB-C cable and adapter that came with your SurfPrint Pro terminal. It shares the same adapter, so there is nothing extra to buy.
- SurfPrint Pro K Multicomm Base: Combined charging base and network hub for SurfPrint Pro K, /products/fins/surfprint-pro-k-base
In the box: 1x SurfPrint Pro K Multicomm Base (Base only). No cables ship with the base. Power it with the USB-C cable and power adapter that came with your SurfPrint Pro K terminal. It shares the same adapter, so there is nothing extra to buy.
- SurfTouch Pro Charging Base: Charging base for SurfTouch Pro, /products/fins/surftouch-charger
In the box: 1x SurfTouch Pro Charging Base (Base only). No cable or power adapter ships with the base. Power it with the USB-C cable and adapter that came with your SurfTouch Pro terminal. To link up to four bases and run them off one outlet you also need the DC Box, which is sold separately.
- SpacePole MultiGrip: Universal payment terminal holder with individual grip plates for SurfPrint Pro, SurfPrint Pro K, SurfTouch Pro, SurfTouch Retail, SurfPad Pro and SurfMini, /products/fins/spacepole-multigrip
- SpacePole Mounting Pole: Steel mounting pole for secure payment terminal placement, /products/fins/spacepole-mounting-pole
- SpacePole DuraTilt: Tilt-and-swivel counter mount for payment terminals, /products/fins/spacepole-duratilt
- Halfmoon Floor Stand: Freestanding VESA 75/100 floor stand for self-checkout tablets and kiosk screens, 120 cm standard (SURF002) or 100 cm (SURF015), /products/fins/spacepole-halfmoon-floor-stand
---
## Platform
### Overview
The Surfboard platform is modular, AI-powered, and built for scale. It serves financial institutions, ISVs, and enterprises to power every payment flow, unify acceptance, and unlock value-added services.
Key differentiators vs legacy providers:
- Setup Time: 5 hours (vs 2-6 months)
- Hidden Fees: None (transparent pricing)
- Agility: Real-time deployment
- Redundancy: Multi-cloud (GCP, AWS, Azure)
- White Label: Fully customizable
- Legacy Code: None (built from scratch)
### Unified Commerce
Cross-channel payment unification, share data between online and in-store channels, single view of customer behavior and transaction history, use tokens from in-store for online or vice versa.
URL: /platform/unified-commerce
### Adjustments
Refunds, voids, partial captures, and amount adjustments through the unified API.
URL: /platform/adjustments
### AI
AI-powered features across the platform, branding, smart routing, automated support, AI-assisted developer tooling.
URL: /platform/ai
### Billing
Recurring billing and subscription management capabilities.
URL: /platform/billing
### Customer Management
Customer profiles, tokenization, and lifecycle management.
URL: /platform/customers
### Gift Cards
Issue, redeem, and reconcile branded gift cards across in-store and online channels.
URL: /platform/gift-cards
### Identification
KYC (Know Your Customer) and merchant onboarding workflows.
URL: /platform/identification
### Invoice
B2C and B2B invoice payments via the unified Surfboard API. Email/e-invoice/print distribution, configurable due dates, automated reminders, and debt collection.
URL: /platform/invoice
### Logistics
Terminal shipping, returns, replacements, and lifecycle management.
URL: /platform/logistics
### Multi-merchant
One device, multiple merchants, shared terminals across tenants in spaces like wellness clinics or retail co-working.
URL: /platform/multi-merchant
### NFC Reading
Tap-to-read use cases beyond payments, loyalty cards, ID, ticketing.
URL: /platform/nfc-reading
### Notifications
Transactional and operational alerts to merchants and partners.
URL: /platform/notifications
### Offline Payments
Store-and-forward capability for scenarios without connectivity.
URL: /platform/offline-payments
### Receipts
Digital and printed receipts with self-hosted digital receipts and merchant-branded layouts.
URL: /platform/receipts
### Reporting
One settlement and one reporting pipeline across every payment method and channel.
URL: /platform/reporting
### Tips
Tipping configurations across F&B and hospitality, per merchant, store, or terminal, configurable presets.
URL: /platform/tips
### Tokenization
Network tokens for cards across the unified Surfboard API.
URL: /platform/tokenization
### Webhooks
Real-time event notifications for payment events, status changes, and more.
URL: /platform/webhooks
### White Label
Fully ISV-branded payments end-to-end, terminals, checkout, portal, receipts.
URL: /platform/white-label
---
## Solutions by Industry
### Food & Beverage
Payment solutions for restaurants, cafes, and food service. Includes tableside payments, tipping, split bills.
URL: /solutions/food-and-beverage
### Healthcare
Payment solutions for healthcare providers. Includes patient check-in, billing integration.
URL: /solutions/healthcare
### Hospitality
Payment solutions for hotels, lodging, and hospitality groups, front-of-house, in-room, F&B outlets, and event spaces.
URL: /solutions/hospitality
### Retail
Payment solutions for boutique through enterprise retail, countertop, mobile, and customer-facing terminals with unified online/in-store reporting.
URL: /solutions/retail
### Ticketing
Payment solutions for ticketing and events.
URL: /solutions/ticketing
### Unattended / Self-Service
Payment solutions for vending, parking, EV charging, and self-service kiosks.
URL: /solutions/unattended
### Wellness
Payment solutions for wellness, fitness, and spa businesses.
URL: /solutions/wellness
## Solutions by Audience
### For Acquirers and Banks
Modern payments infrastructure for traditional acquirers and banks, accelerate time to market for in-person and online acquiring without rebuilding the platform.
URL: /solutions/for-acquirers-and-banks
### For Enterprise Merchants
Unified commerce platform for enterprise merchants, multi-region acquiring, smart routing, in-store + online + unattended on one stack.
URL: /solutions/for-enterprise-merchants
### For ISVs
Embedded payments for independent software vendors, white-label terminals, online checkout, SoftPOS, and revenue share, all under the ISV brand.
URL: /solutions/for-isvs
### For Marketplaces
Split payments, escrow, and seller onboarding for marketplaces operating in Europe.
URL: /solutions/for-marketplaces
### For PayFacs and PSPs
Payment facilitator and PSP infrastructure, sub-merchant onboarding, KYC, scheme registration, and operational tooling.
URL: /solutions/for-payfacs-and-psps
### For Small Businesses
Plug-and-play payments for small merchants, Tap to Pay on iPhone, CheckoutX, terminals, and self-service onboarding.
URL: /solutions/for-small-businesses
---
## Developer Resources
Surfboard Payments is built API-first for developers. The platform provides:
- RESTful JSON APIs over HTTPS
- Client Auth Tokens for security
- Demo and Live environments (sandbox access)
- Comprehensive documentation at https://developers.surfboardpayments.com
- AI-powered developer tools (MCP server, llms.txt)
- Online SDK for custom checkout flows
- Webhook integration for real-time events
### API Overview
The Surfboard APIs provide a software interface to simplify and manage payment operations. They enable handling orders, processing payments, and various payment-related tasks. The APIs are compatible with all operating systems and can be used with most programming languages.
### Key Developer Pages
- Developer Portal: https://developers.surfboardpayments.com
- Developers Hub: /developers
- Migration Guide: /developers/migration-guide
---
## Developer Guides
Complete integration guides for building with Surfboard APIs and SDKs.
### Tap to Pay on iPhone SDK
Category: in-store | Tags: iOS, Swift, SoftPOS, In-Store, XCFramework
URL: /developers/guides/tap-to-pay-iphone
## Overview
The 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.
- **SDK binary:** `tap_to_pay_apple.xcframework` (static framework, module name `tap_to_pay_apple`).
- **Entry point:** The `Gap` class is the single facade your app interacts with.
- **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.
- **Backend responsibilities:** Your server handles auth token issuance, order creation, and credential storage. The app never holds long-lived secrets.
- **App responsibilities:** Initialize the SDK, register the terminal, start transactions, and handle events.
> **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.
## Availability
Tap to Pay on iPhone with Surfboard is available in the following countries:
🇸🇪 Sweden
🇫🇮 Finland
🇩🇰 Denmark
🇳🇴 Norway
🇬🇧 United Kingdom
🇫🇷 France
🇮🇪 Ireland
🇪🇪 Estonia
🇱🇻 Latvia
🇱🇹 Lithuania
🇵🇱 Poland
🇭🇺 Hungary
> **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.
## Supported Devices
Tap to Pay on iPhone requires specific hardware and software:
- **Device:** iPhone XS or later (models with NFC capability).
- **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.
- **Beta iOS:** Beta versions of iOS are **not supported** for Tap to Pay on iPhone. Always test on stable iOS releases.
- **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.
## Prerequisites
Before you start, confirm you have the following:
| Category | Requirement |
|----------|-------------|
| Apple Developer | Active account with Tap to Pay on iPhone permission |
| Surfboard credentials | `CONNECTION_BLOB` -- opaque config string provided by Surfboard |
| Surfboard credentials | `TMS_PUBLIC_KEY` -- retrieved from Developer Portal after registering your SDK app under Console → SDK Apps |
| Merchant identifiers | `MERCHANT_ID` and `STORE_ID` from your Surfboard merchant onboarding |
| Auth provider | `providerId` and `providerCertificate` issued by Surfboard for your backend |
| Backend | A server that calls Surfboard's Client Auth Tokens API (`/api/auth`) and creates orders via the Orders API |
| iOS device | iPhone running iOS 17.4+ with NFC (required for real payments) |
| Tooling | Xcode 15+ |
| Simulator note | Simulator supports SDK init and registration flows, but not real card reads |
## Apple Developer Setup
Warning
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.
Four steps to configure your Apple Developer account and Xcode project for Tap to Pay on iPhone.
### Step 1: Register your App ID
In 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.
### Step 2: Enable the capability
Edit the App ID, find **Tap to Pay on iPhone** under Capabilities, and enable it. Apple may require additional contracts before this is available.
### Step 3: Update provisioning profiles
Regenerate your development and distribution profiles for the updated App ID and install them in Xcode.
### Step 4: Add the entitlement in Xcode
In 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.
The entitlement key that must be present in your `.entitlements` file:
| Entitlement Key | Type | Value |
|-----------------|------|-------|
| `com.apple.developer.proximity-reader.payment.acceptance` | Boolean | `true` |
### Further reading
- [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)
- [Apple: Tap to Pay on iPhone for Developers](https://developer.apple.com/tap-to-pay/)
> **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.
## SDK Installation
The SDK is delivered as a prebuilt `tap_to_pay_apple.xcframework`. Surfboard provides separate staging and production builds.
1. Copy `tap_to_pay_apple.xcframework` into your repo (e.g., `YourApp/SDKs/tap_to_pay_apple/`).
2. In Xcode, go to your app target > General > Frameworks, Libraries, and Embedded Content.
3. Click + > Add Other > Add Files, then select the xcframework.
4. Set embedding to **"Do Not Embed"** (the SDK is a static framework).
5. Build to verify linking.
Import in Swift:
```swift
import tap_to_pay_apple
```
> **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.
## Credentials & Initialization
Three objects to set up before processing payments: a logger, credentials, and the SDK instance.
### GapLogger
Implement the `GapLogger` protocol to route SDK logs into your logging stack. The protocol requires three methods: `addLog`, `addErrorLog`, and `addDebugLog`.
```swift
import os
import tap_to_pay_apple
final class AppGapLogger: GapLogger {
private let logger = Logger(subsystem: "YourApp", category: "TapToPay")
func addLog(logText: String) {
logger.log("\(logText, privacy: .public)")
}
func addErrorLog(logText: String) {
logger.error("\(logText, privacy: .public)")
}
func addDebugLog(logText: String) {
logger.debug("\(logText, privacy: .public)")
}
}
```
### GapCredentials
Build `GapCredentials` once, typically at app startup, using values provided by Surfboard:
```swift
let credentials = GapCredentials(
connectionBlob: connectionBlob, // from Surfboard
versionNumber: appVersion,
tmsPublicKey: tmsPublicKey, // from Surfboard Developer Portal
merchantId: merchantId, // from merchant onboarding
storeId: storeId, // from merchant onboarding
applicationBundleId: Bundle.main.bundleIdentifier ?? ""
)
```
### Gap instance
Create a single, long-lived `Gap` instance. Subscribe to events, initialize the SDK, and set the auth token:
```swift
let gap = try Gap(logger: logger, credentials: credentials)
gap.subscribeToPublicEvents { wrapper in
// Handle wrapper.event and wrapper.data
}
try await gap.initializeSdk()
let authToken = try await fetchAuthTokenFromBackend()
gap.setAuthToken(authToken: authToken)
```
> **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.
## Terminal Lifecycle
The terminal represents a logical payment endpoint on the Surfboard side. You register it once per device, then open it for each session.
### Register (first run)
```swift
let result = await gap.registerTerminal()
switch result {
case .success:
print("Terminal ID: \(gap.terminalId)")
case .failure(let error):
print("Registration failed: \(error.code.details.message)")
}
```
Registration is a one-time operation per device or installation. After success, `gap.terminalId` contains the terminal identifier.
### Open and initialize (each session)
On subsequent app launches, skip registration and go straight to opening:
```swift
let openResult = await gap.openTerminal()
let readerResult = await gap.initializeReader()
try await gap.getReadyForTransaction()
```
Call these three methods in sequence before starting any payment.
### Cleanup
For 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.
## Payment Flow
Every payment follows three steps: create an order on your backend, start a transaction through the SDK, and handle the result.
### Step 1: Create an order
From 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`.
### Step 2: Start the transaction
```swift
let parameters = GapPaymentParameters(
amount: NSNumber(value: amountInMinorUnits),
type: "PURCHASE",
currency: "SEK",
orderId: orderId,
aidPreference: nil
)
let paymentId = try await gap.startTransaction(parameters: parameters)
```
The 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").
### Step 3: Complete or cancel
After the transaction resolves:
```swift
// Confirm completion
gap.sendCompletedEvent(paymentId: paymentId, approved: true)
// Or cancel an in-progress transaction
let cancelResult = await gap.cancelTransaction(paymentId: paymentId)
```
## Cardholder Verification & PIN
Tap 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.
### When is PIN required?
- **NFC wallet payments** (Apple Pay, Google Pay) typically do not require PIN -- the cardholder authenticates via Face ID or passcode on their own device.
- **Physical contactless cards** may require PIN depending on the transaction amount, card issuer policy, and regional regulations.
### Regional considerations
| Region | Consideration |
|--------|--------------|
| 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. |
| Canada & Finland | Cards that only support offline PIN are not compatible with Tap to Pay on iPhone. These transactions will be declined. |
Recommendation
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
Payment Link or a traditional card reader.
For 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).
## Events & Monitoring
Subscribe to the SDK event stream to drive your UI and logging:
```swift
gap.subscribeToPublicEvents { wrapper in
switch wrapper.event {
case .INITIALIZED: // SDK ready
case .TERMINAL_CREATED: // Registration succeeded
case .TERMINAL_CREATION_ERROR: // Registration failed
case .TRANSACTION_COMPLETED: // Payment finished
case .TRANSACTION_FAILED: // Payment failed
default: break
}
}
```
### Key status properties
| Property | Purpose |
|----------|---------|
| `gap.isInitialized` | SDK has completed initialization |
| `gap.terminalId` | Terminal ID after successful registration |
| `gap.isReadyForTransactions` | Terminal and reader are ready for payments |
| `gap.canMakePayments()` | Device supports Tap to Pay and is in a usable state |
Use these for health checks and to enable or disable payment UI elements.
## Best Practices
Follow these recommendations to deliver a reliable and polished Tap to Pay experience.
### Reader connection
- **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.
- **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.
### User experience
- 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.
- On **iOS 18+**, use Apple's `ProximityReaderDiscovery` API to display localized educational content that helps merchants and customers understand how Tap to Pay works.
### Marketing & branding
- 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.
## Testing
**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.
**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.
### Common issues and checks
- **SDK not initializing:** Verify credentials (connection blob, TMS public key, merchant/store IDs). Check network connectivity. Inspect `GapLogger` output.
- **Registration fails:** Confirm `initializeSdk()` completed. Verify a valid `authToken` is set. Check that merchant/store IDs match the target environment.
- **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.
## Release Checklist
Verify these items before shipping your app with Tap to Pay on iPhone.
### Apple configuration
- App ID has Tap to Pay on iPhone capability enabled
- Provisioning profiles are up to date and installed
- Required `Info.plist` usage descriptions are present
### Surfboard configuration
- Using the correct `connectionBlob` and `tmsPublicKey` for the target environment
- Backend calls the correct Surfboard endpoints (staging vs. production) for auth tokens and orders
- `merchantId` and `storeId` match the target environment
### SDK wiring
- `tap_to_pay_apple.xcframework` is linked to the correct app target
- Only one xcframework variant (staging or production) is linked at a time
- `Gap` is instantiated once and reused across the app lifecycle
- Auth tokens are refreshed before expiry via `setAuthToken(authToken:)`
### Functional validation
- `gap.isInitialized` returns `true` after startup
- `gap.terminalId` is populated after registration
- At least one full transaction completes on a real device
- Event stream fires expected events: `INITIALIZED`, `TERMINAL_CREATED`, `TRANSACTION_COMPLETED`
## Reference
- [iOS Example App on GitHub](https://github.com/surfboardpayments/ios-gap-example)
- [Client Auth Token API](https://developers.surfboardpayments.com/references/api/client-auth-token/create-token)
- [Orders API](https://developers.surfboardpayments.com/api/orders)
## Disclaimer
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.
### Android SoftPOS SDK
Category: in-store | Tags: Android, Kotlin, SoftPOS, In-Store, NFC
URL: /developers/guides/android-softpos-sdk
## Overview
The 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.
The SDK is distributed via Surfboard-hosted Maven repositories and ships as separate debug and release variants.
> **Note:** The canonical reference implementation is the [Android GAP Example App](https://github.com/surfboardpayments/android-gap-example), which demonstrates the full integration flow.
## Prerequisites
Before starting integration, confirm you have the following:
### Development Environment
- **Android Studio** 4.1 or higher
- **Minimum SDK**: API level 29 (Android 10.0)
- **Java**: 11 or higher
- **Kotlin**: 2.0.21 or higher
### Surfboard Account & Credentials
- Registered Surfboard Partner account
- Active merchant setup with at least one store
- Surfboard-issued SoftPOS configuration values:
- `connectionBlob` -- provided by Surfboard during onboarding
- `tmsPublicKey` -- retrieved from the Developer Portal after registering your SDK app under **Console > SDK Apps**
- Merchant and store identifiers:
- `merchantId`
- `storeId`
- Access to the [Client Auth Token API](https://developers.surfboardpayments.com/references/api/client-auth-token/create-token) for obtaining bearer tokens
> **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.
## SDK Setup & Installation
### 1. Add softpos.properties
Create a `softpos.properties` file in your Android project root (same level as `settings.gradle.kts`):
```properties
softpos1.mavenurl = MAVEN_URL1
softpos1.mavenusername = MAVEN_USERNAME1
softpos1.mavenpassword = MAVEN_PASSWORD1
softpos2.mavenurl = MAVEN_URL2
softpos2.mavenusername = MAVEN_USERNAME2
softpos2.mavenpassword = MAVEN_PASSWORD2
```
Add it to `.gitignore`:
```text
softpos.properties
```
### 2. Configure Gradle Repositories
In your root `build.gradle.kts`, load the properties and configure repositories:
```kotlin
import java.util.Properties
import java.io.FileInputStream
val localProperties = Properties()
val localPropertiesFile = rootProject.file("softpos.properties")
if (localPropertiesFile.exists()) {
localProperties.load(FileInputStream(localPropertiesFile))
}
allprojects {
repositories {
google()
mavenCentral()
mavenLocal()
maven {
url = uri(localProperties.getProperty("softpos1.mavenurl"))
credentials {
username = localProperties.getProperty("softpos1.mavenusername")
password = localProperties.getProperty("softpos1.mavenpassword")
}
}
maven {
url = uri(localProperties.getProperty("softpos2.mavenurl"))
credentials {
username = localProperties.getProperty("softpos2.mavenusername")
password = localProperties.getProperty("softpos2.mavenpassword")
}
authentication {
create("basic")
}
}
}
}
```
### 3. Add Dependencies
In `gradle/libs.versions.toml`:
```toml
[versions]
softposSDK = "1.1.4"
[libraries]
softposSDKDebug = { module = "com.surfboardpayments:gapsdk-debug", version.ref = "softposSDK" }
softposSDKRelease = { module = "com.surfboardpayments:gapsdk", version.ref = "softposSDK" }
```
In `app/build.gradle.kts`:
```kotlin
dependencies {
debugImplementation(libs.softposSDKDebug)
releaseImplementation(libs.softposSDKRelease)
}
```
### 4. App Module Configuration
```kotlin
android {
compileSdk = 35
defaultConfig {
applicationId = "com.your.app.id"
minSdk = 29
targetSdk = 35
multiDexEnabled = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
```
### 5. Android Manifest
Add the required permissions and features to `AndroidManifest.xml`:
```xml
```
### 6. Verify Installation
```kotlin
import com.surfboardpayments.gapsdk.Gap
// If this import compiles without errors, the SDK is properly installed
```
## Credentials & SDK Initialization
### 1. Create Logger
The SDK requires a logger implementation:
```kotlin
class AppLogger : GapLogger() {
override fun addDebugLog(log: String) {
Log.d("SoftPOS", log)
}
override fun addErrorLog(log: String) {
Log.e("SoftPOS", log)
}
override fun addLog(log: String) {
Log.i("SoftPOS", log)
}
}
```
### 2. Create SDK Instance
```kotlin
val softposSDK = Gap(
logger = AppLogger(),
gapCredentials = GapCredentials(
connectionBlob = BuildConfig.connectionBlob,
versionNumber = "YOUR_APP_VERSION",
tmsPublicKey = "YOUR_TMS_PUBLIC_KEY",
merchantId = BuildConfig.merchantId,
storeId = BuildConfig.storeId,
applicationBundleId = "YOUR_BUNDLE_ID"
),
context = applicationContext
)
```
### 3. Set Authentication Token
Set 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):
```kotlin
val bearerToken = fetchTokenFromBackend()
softposSDK.setAuthToken(bearerToken)
```
> **Warning:** Bearer tokens expire every 60 minutes. Implement automatic refresh. Never generate tokens on the device -- always fetch them from your backend.
### 4. Subscribe to Events
```kotlin
softposSDK.subscribeToPublicEvents { wrapper ->
when (wrapper.event) {
GapPublicEvent.INITIALIZED -> { /* SDK ready */ }
GapPublicEvent.TRANSACTION_APPROVED -> { /* Payment approved */ }
GapPublicEvent.TRANSACTION_DECLINED -> { /* Payment declined */ }
// Handle other events
}
}
```
## Terminal Lifecycle & Payment Flow
### Step 1: Register Terminal
One-time operation per device. Returns a `terminalId` used for creating orders:
```kotlin
val result = softposSDK.registerTerminal().await()
result.fold(
{ error -> Log.e("SoftPOS", "Registration failed: ${error.explainError()}") },
{ terminalId -> Log.i("SoftPOS", "Terminal registered: $terminalId") }
)
```
### Step 2: Initialize SDK
```kotlin
softposSDK.initializeGapSDK()
// Wait for INITIALIZED event
```
### Step 3: Fetch Prerequisites (First Run Only)
Downloads merchant branding, EMV configs, and currency data:
```kotlin
softposSDK.fetchPrerequisites()
```
> **Note:** Only required on first run. Skip on subsequent launches unless you change environment or credentials.
### Step 4: Open Terminal Session
Call every time the app comes to the foreground:
```kotlin
softposSDK.openTerminal().onRight { _ ->
Log.i("SoftPOS", "Terminal session opened")
}
```
### Step 5: Prepare for Transaction
Prepares transaction keys. Valid for 120 seconds -- call right before starting a payment:
```kotlin
val result = softposSDK.getReadyForTransaction().await()
```
### Step 6: Create Order
Create 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`.
### Step 7: Start Transaction
```kotlin
val result = softposSDK.startTransaction(
GapInitiatePayment(
amount = amountInMinorUnits,
type = "PURCHASE",
orderId = orderId,
currency = Currency.getInstance("SEK")
)
).await()
result.fold(
{ error -> Log.e("SoftPOS", "Transaction failed: ${error.explainError()}") },
{ paymentId -> Log.i("SoftPOS", "Transaction started: $paymentId") }
)
```
## Transaction Events
During a transaction, handle these events to drive your UI:
```kotlin
when (event) {
GapPublicEvent.TRANSACTION_STARTED -> { /* Show payment UI */ }
GapPublicEvent.PRESENT_CARD -> { /* "Tap your card" */ }
GapPublicEvent.HOLD_CARD -> { /* "Hold card still" */ }
GapPublicEvent.CARD_READ -> { /* Card read successfully */ }
GapPublicEvent.TRANSACTION_ENTER_PIN -> { /* PIN screen appears */ }
GapPublicEvent.TRANSACTION_AUTHORIZING -> { /* Processing */ }
GapPublicEvent.TRANSACTION_APPROVED -> { /* Show success */ }
GapPublicEvent.TRANSACTION_DECLINED -> { /* Show declined */ }
GapPublicEvent.TRANSACTION_COMPLETED -> { /* Show receipt */ }
GapPublicEvent.TRANSACTION_CANCELLED -> { /* Clean up */ }
}
```
After displaying the receipt, notify the SDK:
```kotlin
softposSDK.sendCompletedEvent(paymentId, true)
```
## Production Requirements
Before releasing your app:
1. **Google Play Services** must be enabled
2. **Play Integrity** must be enabled
3. **Application signing** must match the registered SHA-256 hash
4. **Debug mode** must be disabled
5. **Card scheme logos** must be displayed during transactions (Visa, Mastercard)
6. **Battery level** recommended above 10%
7. **Screen recording** not allowed during transactions
## Reference
- [Android GAP Example App](https://github.com/surfboardpayments/android-gap-example)
- [Client Auth Token API](https://developers.surfboardpayments.com/references/api/client-auth-token/create-token)
- [Orders API](https://developers.surfboardpayments.com/api/orders)
### CheckoutX SoftPOS
Category: in-store | Tags: In-Store, Android, iOS, CheckoutX, SoftPOS, App Switch
URL: /developers/guides/checkoutx-softpos
## Overview
CheckoutX SoftPOS is the fastest way to accept in-person payments on a smartphone or tablet without integrating an SDK. You install the CheckoutX app alongside your own POS app on the same device, and your app hands off transactions to CheckoutX through a native app switch.
Use this setup when you want contactless acceptance on consumer hardware but don't want to embed and maintain a SoftPOS SDK inside your own app.
## Two Ways to Accept Payments on Phones
Surfboard gives you two routes for in-person payments on iOS and Android. Pick the one that fits your product:
| Option | What you do | When to pick it |
|--------|-------------|-----------------|
| **[Tap to Pay on iPhone SDK](/developers/guides/tap-to-pay-iphone)** / **[Android SoftPOS SDK](/developers/guides/android-softpos-sdk)** | Embed the Surfboard SoftPOS SDK directly inside your own app | You want a single, branded app with full control over the checkout UX |
| **CheckoutX SoftPOS (this guide)** | Install the CheckoutX app next to your POS app and use [Inter-App Integration](/developers/guides/interapp-integration) to hand off transactions | You want to ship faster, avoid SDK maintenance, or already have a working POS app |
Both approaches run on the same Surfboard platform, the difference is only where the payment UI lives.
## How It Works
1. **Install CheckoutX** from the App Store (iOS) or Google Play (Android) on the device running your POS app.
2. **Register CheckoutX** as a terminal once per device using the Inter-App flow.
3. **Initiate a payment** from your POS app, CheckoutX opens, accepts the tap, and returns the result to your app.
The underlying registration, payment, and tag-scanning flows are all documented in the [Inter-App Integration guide](/developers/guides/interapp-integration). CheckoutX SoftPOS is simply that flow running on a consumer phone or tablet instead of a dedicated terminal.
## Setup
1. **Get a Surfboard account** and register a store under your merchant.
2. **Download CheckoutX** on the target device.
3. **Follow [Inter-App Integration](/developers/guides/interapp-integration)** for terminal registration, payment, and tag-scanning deep link flows. The same API contract applies whether CheckoutX runs on a Surfboard terminal or on a phone in SoftPOS mode.
## The Configure Call
Before the first payment, and whenever the device has been idle, rebooted, or has lost its server session, call CheckoutX's configure route to prepare the terminal:
```
checkoutx://com.surfboard.checkoutx/configure?redirectUrl=REDIRECT_URL
```
Replace `REDIRECT_URL` with your base64-encoded app URL. CheckoutX opens, establishes its connection to the Surfboard server, and returns to your app with `isConfigured: true` when ready.
Running configure before the first transaction of a session gives the smoothest first-payment experience. See [Configure Terminal Before Payment](/developers/guides/interapp-integration#configure-terminal-before-payment) in the Inter-App guide for full details.
## Handling `PS_0025`, Terminal Not Connected
When you initiate a payment on SoftPOS, you may occasionally see:
```
PS_0025: Terminal is not connected to server so unable to send transactions
```
In these cases, run the configure call again and then re-initiate the payment. You can do this seamlessly on your end, the transaction may be slightly slower, but this is the easiest way to recover. No user action is needed.
This is specific to SoftPOS because consumer devices can go idle or lose their session to the server between transactions; a re-configure re-establishes the connection before the next payment.
## Reference
- [Inter-App Integration](/developers/guides/interapp-integration), full deep link flow
- [Tap to Pay on iPhone SDK](/developers/guides/tap-to-pay-iphone), iOS SDK alternative
- [Android SoftPOS SDK](/developers/guides/android-softpos-sdk), Android SDK alternative
### EMV Terminal Integration
Category: in-store | Tags: EMV, Terminal, In-Store, API, Hardware
URL: /developers/guides/emv-terminal-integration
## Overview
Surfboard Payments lets you integrate traditional EMV card-present terminals through a single, unified API. Whether you are deploying countertop terminals, mobile POS devices, or kiosk setups, the integration follows the same workflow: create an account, get API credentials, build and test in the sandbox, then go live.
This guide walks you through the complete process from zero to accepting live in-store payments.
## Step 1: Create a Developer Account
Sign up at the [Surfboard Developer Portal](https://developers.surfboardpayments.com/sign-up) to get started. A developer account gives you:
- Access to the Console for managing your integration
- A sandbox environment for building and testing
- The path to certification and live payments
No approval process required -- you get instant sandbox access.
## Step 2: Generate API Credentials
After creating your account, open the **Console** in the Developer Portal. From there you can:
- Generate your **API-KEY** and **API-SECRET**
- Configure webhooks
- Access logs and monitoring
- Manage terminals and merchants
> **Tip:** You can also request test credentials through the Surfboard support team on Slack during onboarding.
## Step 3: Understand Environments
Surfboard provides different environments for building and testing your integration:
| Environment | Supported Terminals | Cards Supported |
|-------------|-------------------|-----------------|
| **Demo** | All hardware terminals, Terminal Tester App, Mobile Checkout | Live cards can be used. Transactions are voided immediately after payment. |
| **Live** | All hardware terminals, Mobile Checkout | Live cards. Transactions are settled and you receive payouts. |
By default, you gain access to the demo environment when you create a developer account. Use it to build and test your integration with the [Surfboard APIs](https://developers.surfboardpayments.com/) and SDKs.
> **Note:** For in-store payments, use the Terminal Tester App (available on Android) for payment simulations. It includes built-in success and failure test cards.
## Step 4: Build Your Integration
Complete these steps in the demo environment before going live:
### 4.1 Merchant Onboarding
Set up your merchant hierarchy using the [Merchants API](https://developers.surfboardpayments.com/api/merchants) and [Stores API](https://developers.surfboardpayments.com/api/stores). Each merchant can have multiple stores, and each store can have multiple terminals.
### 4.2 Terminal Registration
Register your hardware terminals through the [Terminals API](https://developers.surfboardpayments.com/api/terminals). When registering a terminal, you provide:
- The `registrationIdentifier` (printed on the terminal or provided during provisioning)
- The `storeId` for the store the terminal belongs to
- A human-readable `terminalName`
```json
POST /merchants/:merchantId/stores/:storeId/devices
{
"registrationIdentifier": "250901",
"terminalName": "Checkout 1"
}
```
### 4.3 Accept Payments
Create orders and initiate payments using the [Orders API](https://developers.surfboardpayments.com/api/orders). The Carbon API uses an orders-first workflow -- create an order and initiate payment in a single call:
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [
{
"id": "ITEM-001",
"name": "Coffee",
"quantity": 1,
"amount": {
"total": 4500,
"currency": "752"
}
}
],
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD",
"amount": 4500
}
}
}
```
The terminal displays the payment UI automatically. The customer taps, inserts, or swipes their card. You receive the result via the API response or webhooks.
### 4.4 Post-Payment Operations
After payments are accepted, integrate post-payment functionality:
- **Refunds** -- Use negative quantities in order lines
- **Receipts** -- Send digital receipts via the [Receipts API](https://developers.surfboardpayments.com/api/receipts)
- **Reporting** -- Query order and payment history
## Step 5: Certification & Go Live
Once your integration is built and tested in the demo environment:
1. Sign the contract and receive approval
2. Complete an onboarding call to test and certify your integration
3. Receive production credentials
4. Update your base URL from demo to production
5. Start accepting live payments
## Webhooks
Configure webhooks to receive real-time notifications about order and payment events. Key events include:
- `order.paymentcompleted` -- Payment was successful
- `order.paymentcancelled` -- Payment was cancelled
- `order.paymentfailed` -- Payment failed
- `order.terminal.event` -- Every terminal state during the transaction
Set up webhook endpoints in the Console under your developer account settings.
## API Quick Reference
| API | Purpose |
|-----|---------|
| [Merchants API](https://developers.surfboardpayments.com/api/merchants) | Create and manage merchants |
| [Stores API](https://developers.surfboardpayments.com/api/stores) | Create and manage stores |
| [Terminals API](https://developers.surfboardpayments.com/api/terminals) | Register and manage terminals |
| [Orders API](https://developers.surfboardpayments.com/api/orders) | Create orders and initiate payments |
| [Receipts API](https://developers.surfboardpayments.com/api/receipts) | Send digital receipts |
| [Branding API](https://developers.surfboardpayments.com/api/branding) | Customise terminal branding |
## Reference
- [Developer Portal](https://developers.surfboardpayments.com/)
- [Carbon API Documentation](https://developers.surfboardpayments.com/references/api/orders/create-order)
- [Webhook Reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)
### Payment Page
Category: online | Tags: Online, Payment Page, Hosted Checkout, API
URL: /developers/guides/payment-page
## Overview
The Payment Page is the simplest way to accept online payments with Surfboard. Instead of building your own checkout form, you redirect customers to a Surfboard-hosted payment page. The customer completes payment there and is redirected back to your site.
This approach requires minimal frontend work -- you only need to create an order via the API and redirect the customer to the returned payment link.
## Prerequisites
Before accepting payments with the Payment Page:
1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. Complete onboarding (merchant and store setup)
3. The `terminalId` of the store's **PaymentPage** terminal — creating an online store provisions one for you, so fetch the store's terminals rather than registering a new one
## Payment Types
There are two primary types of payments:
1. **Customer Initiated Transaction (CIT):** Transactions initiated by customers on your webshop, such as e-commerce purchases.
2. **Merchant Initiated Transaction (MIT):** Transactions initiated by the merchant, such as subscription charges.
> **Note:** MIT payments can only be processed by terminals set to `MerchantInitiated`. See the [Server-to-Server API guide](/developers/guides/server-to-server-api) for details on MIT.
## Payment Process
### Step 1: Create an Order
Create an order using the [Create Order API](https://developers.surfboardpayments.com/api/orders). On success, you receive a **payment link** to share with the customer.
For the store, domain and terminal setup this call assumes, see [Online Payment Link](/developers/guides/online-payment-link).
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [
{
"id": "ITEM-001",
"name": "Annual Subscription",
"quantity": 1,
"amount": {
"total": 99900,
"currency": "752"
}
}
],
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD",
"amount": 99900
}
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderId": "8455c12f9fd0620a010b",
"paymentPageLink": "https://pay.withsurfboard.com/8455c12f9fd0620a010b?pi=Dr4GoyMXF0zHvjca_Oa0vHgxcT-OD1qp7KdokyI7dkTwRwYJt8nkXyQm3bT6vqCfgraOw50Bf5uOp__3ckbMWOV6L9QbiTiSEFS3YmF4Eb8lr5pTWP2KFjm9Ukmd0000&add=IzFlNDBhZg=="
},
"message": "Order created successfully"
}
```
Pass `paymentPageLink` on whole -- the query string carries the payment intent, and a trimmed or re-encoded link will not open.
### Control Fields
The Payment Page supports additional control fields for fine-grained payment control:
| Field | Description |
|-------|-------------|
| `delayCapture` | Set to `true` to capture payment later after authorisation. Default: `false`. |
| `enforceTokenization` | Override tokenisation config -- control whether the card is saved for future use. |
| `enforce3DSecure` | Whether the customer goes through 3D Secure verification. |
| `paymentPageValidFor` | How long the payment link is valid. Default: one day. |
| `lockToPaymentMethod` | Force the customer to use a specific payment method. |
| `authMode` | `PREAUTH` or `AUTH`. Default: `AUTH`. If `PREAUTH`, `delayCapture` is set to `true` automatically. |
| `redirectUrl` | URL to redirect to after successful payment. Includes `orderId` as a query param. |
| `failureRedirectUrl` | URL to redirect to after failed payment. Includes `orderId` as a query param. |
| `generateShortLink` | Set to `true` to get a shortened payment URL. Default: `false`. |
### Recurring Payment Fields
For subscription-based payments, include these additional fields:
| Field | Description |
|-------|-------------|
| `subscriptionAmountType` | `FIXED` or `VARIABLE` |
| `maxAmount` | Maximum amount in minor units (for variable subscriptions) |
| `frequency` | `daily`, `weekly`, `monthly`, `quarterly`, `annually`, `unscheduled`, etc. |
| `numberOfPayments` | Total expected payments for this subscription |
| `uniqueReference` | Unique reference for the recurring order |
### Step 2: Check Order Status
Monitor the order status using the [Fetch Order Status API](https://developers.surfboardpayments.com/api/orders). When the status changes to `PAYMENT_COMPLETED` or `PAYMENT_CANCELLED`, you can view the transaction details.
You can also receive real-time updates via webhook notifications -- configure them in the Developer Portal Console.
## Integration Flow
Here is the typical integration flow:
1. Customer clicks "Pay" on your website
2. Your backend calls the Create Order API
3. You redirect the customer to the `paymentPageLink` from the response, or to `shortLinkUrl` if you asked for a short link
4. Customer completes payment on the Surfboard-hosted page
5. Customer is redirected to your `redirectUrl` (or `failureRedirectUrl`)
6. Your backend verifies the order status via the API or webhook
> **Tip:** Always verify the order status server-side after redirect. Do not rely solely on the redirect URL to confirm payment success.
## Reference
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Fetch Order Status API](https://developers.surfboardpayments.com/api/orders)
- [Webhook Reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Inter-App Integration
Category: in-store | Tags: In-Store, Android, iOS, Web, CheckoutX, App Switch
URL: /developers/guides/interapp-integration
## Overview
Surfboard's CheckoutX app handles payment acceptance on Android payment terminals and as a SoftPOS solution. If you have your own POS or business app, you can integrate with CheckoutX through **native app switch** -- your app opens CheckoutX to process a payment, and CheckoutX returns control to your app when done.
This guide covers terminal registration, the payment flow, and NFC tag scanning -- all through deep links.
> **Important:** Surfboard terminals operate in full online mode. All data exchange happens through APIs and deep link parameters -- no offline data passing is supported.
## How It Works
The inter-app flow is a bi-directional app switch:
1. **Your app -> CheckoutX** -- initiate a task (registration, payment, or tag scan)
2. **CheckoutX -> Your app** -- return the result via your redirect URL
There are three flows:
| Flow | Purpose | Frequency |
|------|---------|-----------|
| **Terminal Registration** | Link CheckoutX to a terminal | Once per device |
| **Payment** | Process a payment via CheckoutX | Every transaction |
| **Tag Scanning** | Read NFC product tags | As needed |
## Setting Up Your App for App Switch
Configure your app to receive the callback from CheckoutX after a task completes.
### Android
Register a deep link intent filter in your `AndroidManifest.xml`:
```xml
```
### iOS
Register a custom URL scheme in your `Info.plist` or Xcode project settings. Add your scheme (e.g., `posapp`) under **URL Types**.
### Browser-based POS
A web app has no scheme of its own to register. The switch out to CheckoutX works the same way, but the return needs a redirect URL that names the operator's browser -- see [Browser-Based POS (Web Apps)](#browser-based-pos-web-apps).
## Configure Terminal Before Payment
Before the first payment (especially after a device reboot), call the configuration route to prepare CheckoutX:
```
checkoutx://com.surfboard.checkoutx/configure?redirectUrl=REDIRECT_URL
```
Replace `REDIRECT_URL` with your base64-encoded app URL. CheckoutX will open, configure itself, and return with `isConfigured: true` when ready.
Use this step before starting the payment flow for optimal performance on the first transaction.
### Handling `PS_0025`, Terminal Not Connected
When initiating a payment you may occasionally see:
```
PS_0025: Terminal is not connected to server so unable to send transactions
```
Run the configure call again and then re-initiate the payment. You can do this seamlessly on your end, the transaction may be slightly slower, but this is the easiest way to recover, and no user action is needed. This is most common in SoftPOS setups where consumer devices can go idle or lose their server session between transactions; re-configuring re-establishes the connection before the next payment.
## Terminal Registration (One-Time Setup)
Register a terminal with CheckoutX once per device. This links your Surfboard terminal to the CheckoutX app.
### Step 1: Get an Interapp Code
Call the API to generate a registration code:
```json
GET /merchants/:merchantId/stores/:storeId/terminals/interapp
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"registrationCode": "abc123..."
},
"message": "Interapp code generated successfully"
}
```
> The registration code is valid for **120 seconds**. Complete the app switch before it expires.
### Step 2: App Switch to Register
Build the registration deep link with the code:
```
checkoutx://com.surfboard.checkoutx/register?redirectUrl=REDIRECT_URL&data=REGISTRATION_CODE
```
- `REDIRECT_URL` -- your base64-encoded app callback URL
- `REGISTRATION_CODE` -- base64-encoded JSON: `{"registrationCode": "GENERATED_CODE"}`
### Step 3: Handle the Callback
After registration, CheckoutX calls your redirect URL with a `data` query parameter containing the `terminalId`:
```
posapp://hello/order?orderRef=...&data=
```
Decode the base64 `data` parameter to get the terminal ID:
```kotlin
// Kotlin
val data = String(Base64.getUrlDecoder().decode(uri.getQueryParameter("data")))
val jsonObject = serializer.fromJson(data, JsonObject::class.java)
val terminalId = jsonObject["terminalId"].asString
```
```swift
// Swift
guard let base64String = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?.first(where: { $0.name == "data" })?.value,
let jsonData = Data(base64Encoded: base64String),
let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let terminalId = json["terminalId"] as? String
else { return }
```
Store the `terminalId` -- you need it for all future payments on this device.
### Step 4: Verify Registration
Confirm the registration status via API:
```json
GET /merchants/:merchantId/stores/:storeId/terminals/interapp/:interappCode
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"registrationStatus": "REGISTERED",
"terminalId": "83abab731f6fb00704"
}
}
```
**Possible `registrationStatus` values:** `REGISTERED` | `NOT_REGISTERED`
## Payment Flow
Once the terminal is registered, process payments through app switch.
### Step 1: Create an Order via API
Create an order using the [Create Order API](/developers/guides/create-an-order) with the `terminalId` from registration. The response includes a `paymentId` and an `interAppJWT`:
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [
{
"id": "ITEM-001",
"name": "Running Shoes",
"quantity": 1,
"amount": { "regular": 50000, "total": 50000, "currency": "752" }
}
],
"totalOrderAmount": { "regular": 50000, "total": 50000, "currency": "752" },
"controlFunctions": {
"initiatePaymentsOptions": { "paymentMethod": "CARD" }
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderId": "83a1ba32774149710b",
"paymentId": "83a1ba3264bd500106",
"interAppJWT": "eyJhbGciOiJIUzI1NiIs..."
}
}
```
### Step 2: App Switch to CheckoutX
Build the transaction deep link:
```
checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=REDIRECT_URL&data=REQUIRED_DATA
```
- `REDIRECT_URL` -- your base64-encoded callback URL
- `REQUIRED_DATA` -- base64-encoded JSON containing the terminal ID and the `interAppJWToken`:
```json
{
"terminalId": "YOUR_TERMINAL_ID",
"interAppJWToken": "eyJhbGciOiJIUzI1NiIs..."
}
```
> **Required on both Android and iOS:** Include the `interAppJWToken` -- the `interAppJWT` value returned in the order response -- in the data parameter on every app switch transaction. This is required for the app switch flow on both platforms, not an iOS-only step.
### Step 3: Perform the App Switch
```kotlin
// Kotlin
val url = "checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=$encodedRedirectUrl&data=$encodedData"
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
startActivity(intent)
```
```swift
// Swift
let url = "checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=\(encodedRedirectUrl)&data=\(encodedData)"
if let deepLink = URL(string: url) {
UIApplication.shared.open(deepLink)
}
```
```dart
// Flutter
String url = "checkoutx://com.surfboard.checkoutx/transaction?redirectUrl=$encodedRedirectUrl&data=$encodedData";
Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
```
### Step 4: Handle the Result
CheckoutX calls your redirect URL with the result. Check the order status via API to confirm payment completion:
```json
GET /orders/:orderId/status
```
## Framing the Redirect URL
The redirect URL follows the format:
```
:///?
```
For example, if your scheme is `posapp` and host is `hello`:
```
posapp://hello/order?orderRef=6ba7b7db-519f-4ed9-9f6b-a834140466f7
```
This URL must be **base64-encoded** before passing it as the `redirectUrl` parameter:
```kotlin
// Kotlin
val url = "posapp://hello/order?orderRef=6ba7b7db-519f-4ed9-9f6b-a834140466f7"
val encoded = Base64.getUrlEncoder().encodeToString(url.toByteArray())
```
```swift
// Swift
let url = "posapp://hello/order?orderRef=6ba7b7db-519f-4ed9-9f6b-a834140466f7"
let encoded = Data(url.utf8).base64EncodedString()
```
```js
// JavaScript -- URL-safe base64, no padding
const encoded = btoa(url).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
```
For a browser-based POS the redirect URL names the operator's browser instead of your app -- see [Browser-Based POS (Web Apps)](#browser-based-pos-web-apps).
## Browser-Based POS (Web Apps)
If your POS runs in a browser rather than as an installed app -- a web POS on an Android tablet, for instance -- the switch **out** to CheckoutX works exactly as described above. A `checkoutx://` deep link is just a link, and the browser hands it to CheckoutX.
The difference is the way **back**. A web app has no custom scheme to register, and an `https://` redirect URL does not return the operator to their browser: CheckoutX opens it in its own in-app browser, leaving the POS session behind in a tab nobody is looking at.
### Launching CheckoutX from a page
Build and encode the deep link exactly as elsewhere in this guide, then follow it:
```js
const deepLink =
`checkoutx://com.surfboard.checkoutx/transaction` +
`?redirectUrl=${encodedRedirectUrl}&data=${encodedData}`;
// A temporary anchor click is more reliable than assigning window.location,
// which some in-app browsers and webviews intercept
const a = document.createElement("a");
a.href = deepLink;
a.style.display = "none";
document.body.appendChild(a);
a.click();
setTimeout(() => a.remove(), 100);
```
### Returning to the browser
Point the redirect URL at the **browser**, not at a page. On Android Chrome:
```
googlechrome://com.android.chrome
```
Base64-encode it like any other redirect URL. Chrome comes to the front on the tab the flow started in -- nothing is navigated and nothing reloads, so the POS keeps its state.
If the return has to land on a specific page instead:
```
googlechrome://navigate?url=
```
This works too, but opens a **new tab** on every return and leaves the original behind. Prefer the first form unless a specific landing URL is essential.
> A `?url=` parameter on the first form is silently dropped -- `com.android.chrome` is a host Chrome ignores rather than a navigate endpoint. There is no same-tab-with-landing-URL variant.
### The return carries no data
Because the redirect names a browser rather than a URL, nothing comes back in it -- no `data` parameter to decode. That is not a limitation to work around: the API is the source of truth for the result in every flow, and a browser POS simply leans on it entirely.
- **Payment:** poll `GET /orders/:orderId/status` until it reaches a terminal state.
- **Registration:** poll `GET /merchants/:merchantId/stores/:storeId/terminals/interapp/:interappCode` until `registrationStatus` is `REGISTERED`, then store the returned `terminalId`.
Since the tab is never reloaded, becoming visible again is the signal that the operator is back:
```js
document.addEventListener("visibilitychange", () => {
if (!document.hidden && pendingOrderId) {
checkOrderStatus(pendingOrderId); // re-check immediately, then keep polling
}
});
```
Persist the pending `orderId` (and the registration code) in `localStorage` as well. The tab is not reloaded on the way back with the redirect above, but it can still be evicted while backgrounded, and the `navigate` form reloads by design.
### Other browsers
The mechanism is not Chrome-specific: **any browser that registers a launch scheme can be named in the redirect URL the same way.** Chrome on Android is simply the combination we verified end to end.
| Redirect URL | Behaviour |
|---|---|
| `googlechrome://com.android.chrome` | Chrome to the front, original tab, no reload -- verified on an Android tablet |
| `googlechrome://navigate?url=` | Chrome opens the given URL in a new tab -- verified |
| Another browser's scheme | Same shape, verify per browser |
To check what a given browser answers to on your target device:
```
adb shell am start -a android.intent.action.VIEW -d "://"
```
If the browser comes to the front, that scheme works as a redirect URL. Confirm on the device and browser your merchants actually use -- schemes differ between browsers and vendors, and some register none at all. Where a browser registers nothing, the flows still complete: polling reports the result, and the operator returns to the browser manually.
### What does not work from a browser
Measured against CheckoutX on Android, so you do not have to retry them:
| Redirect URL | Result |
|---|---|
| `intent://…#Intent;package=com.android.chrome;end` | No return at all, with or without `action=` and extras -- the redirect is not parsed as an intent URI |
| `https://your-pos.example.com/...` | Opens in CheckoutX's in-app browser rather than the operator's browser |
| An `https://` page that re-launches an `intent://` URI | Ignored as well -- the in-app browser does not follow it |
## NFC Tag Scanning
Scan product NFC tags through CheckoutX before or during a sale:
```
checkoutx://com.surfboard.checkoutx/scanProducts?redirectUrl=REDIRECT_URL&data=REQUIRED_DATA
```
The `REQUIRED_DATA` is a base64-encoded JSON specifying the read mode:
```json
{ "readMode": "SINGLE" }
```
| Read Mode | Description |
|-----------|-------------|
| `SINGLE` | Scan one product tag |
| `MULTIPLE_EDITABLE` | Scan multiple tags, allow editing scanned data |
| `MULTIPLE_NONEDITABLE` | Scan multiple tags, no editing allowed |
The redirect URL and app switch mechanics are identical to the payment flow.
## Example Repositories
- [Android Example App (Kotlin)](https://github.com/surfboardpayments/surfboard-interapp-kotlin-simple)
## Reference
- [Terminals API](https://developers.surfboardpayments.com/api/terminals)
- [Create an Order](/developers/guides/create-an-order)
- [Tap to Pay on iPhone](/developers/guides/tap-to-pay-iphone)
- [NFC Tag Reading](/developers/guides/nfc-tag-reading)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Self-Hosted Checkout
Category: online | Tags: Online, SDK, JavaScript, Self-Hosted, Checkout
URL: /developers/guides/self-hosted-checkout
## Overview
The Self-Hosted Checkout (Online SDK) lets you embed payment fields directly in your website while Surfboard handles PCI compliance and payment processing. You get full control over the look and feel of your checkout page.
Surfboard renders secure input fields inside your page using the Online SDK. You control the layout, branding, and customer experience.
## Prerequisites
1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. Complete onboarding (merchant and store setup)
3. Register a terminal with the type set to **SelfHostedPage**
4. Note the terminal's `publicKey` from the registration response
> **Tip:** You can retrieve the terminal's public key later using the [Fetch Terminal by ID API](https://developers.surfboardpayments.com/api/terminals). It is returned as `terminalPublicKey` in the response.
## Initializing the SDK
To initialize the SDK, you need three parameters:
1. **`publicKey`** -- From terminal registration
2. **`orderId`** -- From the [Create Order API](https://developers.surfboardpayments.com/api/orders) response
3. **`nonce`** -- From the Create Order response (serves as access control)
```javascript
// Set up error handling
SurfboardOnlineSDK.errorCallback((code, message) => {
console.error(`Error [${code}]: ${message}`);
});
// Listen for payment status changes
SurfboardOnlineSDK.paymentStatusCallback = function (data) {
// data.paymentStatus: 'PAYMENT_INITIATED' | 'PAYMENT_COMPLETED' |
// 'PAYMENT_CANCELLED' | 'PAYMENT_FAILED' | 'PAYMENT_PROCESSING'
console.log("Payment status:", data.paymentStatus);
};
// Initialize
SurfboardOnlineSDK.initialiseOnlineSDK({
publicKey: "YOUR_PUBLIC_KEY",
orderId: "YOUR_ORDER_ID",
nonce: "YOUR_NONCE",
});
```
To re-initialize for a different order without a full page reload:
```javascript
SurfboardOnlineSDK.remountOnlineSDK({
publicKey: "YOUR_PUBLIC_KEY",
orderId: "NEW_ORDER_ID",
nonce: "NEW_NONCE",
});
```
### Error Codes
| Error Code | Message | Category |
|------------|---------|----------|
| -- | Surfboard SDK cannot function in the given environment | FATAL |
| -- | Surfboard SDK initialisation failed | FATAL |
| -- | Public key validation failed | FATAL |
| -- | Invalid Order ID | FATAL |
| -- | Invalid Nonce | FATAL |
| 401 | Invalid or Expired Link | FATAL |
## Available Data Objects
After successful initialization, the SDK exposes data objects on `SurfboardOnlineSDK`:
- **`order`** -- Order details, line items, and payment methods
- **`merchant`** -- Merchant name and organization number
- **`branding`** -- Colors, fonts, logos for your checkout styling
- **`store`** -- Store contact info, privacy policy, and terms URLs
- **`paymentMethods`** -- Supported payment methods for this terminal
- **`customer`** -- Customer details, saved cards, and addresses
> **Warning:** You are required to display the store contact information, privacy policy, and terms and conditions on your payment page.
## Payment Flow
### Updating Customer Information
Most payment methods require customer information. Provide it via the SDK or include it when creating the order.
| Payment Method | Required Fields |
|----------------|----------------|
| Card | Email, Phone, Address |
| Klarna | Email, Phone, Address, Shipping Address (physical goods) |
| Apple Pay | Email, Name, Phone, Postal Address |
```javascript
await SurfboardOnlineSDK.order.addCustomerInformation({
name: "Jane Doe",
email: "jane@example.com",
phone: { countryCode: "+46", number: "701234567" },
billingAddress: {
addressLine1: "Main Street 1",
city: "Stockholm",
postalCode: "11122",
countryCode: "SE",
},
});
```
> **Tip:** Include the customer address in the Create Order API request when you have it. This pre-fills the address so the customer does not need to enter it manually.
### Card Payments
Mount the card input fields in your page:
```html
```
```javascript
SurfboardOnlineSDK.mount({
mountCardWidget: "card-details",
});
// When the customer clicks "Pay":
await SurfboardOnlineSDK.order.initiatePayments("CARD");
```
### Swish Payments
```javascript
const paymentAttempt = await SurfboardOnlineSDK.order.initiatePayments("NSWISH");
// For mobile: redirect to Swish app
const redirectUrl = paymentAttempt.getSwishAppRedirectUrl("https://your-site.com/callback");
// For web: display QR code
const qrData = paymentAttempt.getSwishQRData;
```
### Apple Pay
```html
```
```javascript
SurfboardOnlineSDK.mount({
mountApplePayWidget: "apple-pay",
});
// Payment is initiated automatically when the customer clicks the Apple Pay button
```
For Apple Pay, you must host the domain association file at `/.well-known/apple-developer-merchantid-domain-association` on your domain.
### Klarna
```javascript
await SurfboardOnlineSDK.order.addCustomerInformation({
phone: { countryCode: "+46", number: "701234567" },
name: "Jane Doe",
email: "jane@example.com",
billingAddress: {
city: "Stockholm",
postalCode: "11122",
countryCode: "SE",
addressLine1: "Main Street 1",
},
});
await SurfboardOnlineSDK.order.initiatePayments("KLARNA");
```
## Payment Error Codes
| Code | Message | Category |
|------|---------|----------|
| ON_009 | Phone number required for Swish payment | Non Fatal |
| ON_010 | Payment method not supported for this order | Non Fatal |
| ON_011 | Payment already completed | Non Fatal |
| ON_012 | Error initiating payment -- retry | Non Fatal |
| ON_013 | Unknown error -- page reload may help | Non Fatal |
| ON_016 | Invalid card details | Non Fatal |
| ON_017 | Email required for this payment | Non Fatal |
| ON_018 | Billing address required for this payment | Non Fatal |
## Reference
- [React Sample App](https://github.com/surfboardpayments/react-next-online-sdk) for a complete working example
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Fetch Terminal API](https://developers.surfboardpayments.com/api/terminals)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Server-to-Server API
Category: online | Tags: Online, API, Server-to-Server, MIT, Tokenization
URL: /developers/guides/server-to-server-api
## Overview
Merchant 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.
The flow has two stages: the customer completes an initial payment (which tokenizes their card), then you use the stored token for subsequent charges.
## Prerequisites
1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. Complete onboarding (merchant and store setup)
3. Two terminal IDs:
- A **PaymentPage** or **SelfHostedPage** terminal for the initial customer payment
- A **MerchantInitiated** terminal for subsequent server-to-server payments
An 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.
## Stage 1: Initial Customer Payment
The first payment must be initiated by the customer. This step collects and tokenizes the card details.
### Step 1: Create Order with Tokenization
Use the [Create Order API](https://developers.surfboardpayments.com/api/orders) with `enforceTokenization` set to `true`:
```json
POST /orders
{
"terminal$id": "YOUR_PAYMENT_PAGE_TERMINAL_ID",
"orderLines": [
{
"id": "SUB-001",
"name": "Monthly Subscription",
"quantity": 1,
"amount": {
"total": 29900,
"currency": "752"
}
}
],
"controlFunctions": {
"enforceTokenization": true,
"initiatePaymentsOptions": {
"paymentMethod": "CARD",
"amount": 29900
}
}
}
```
### Step 2: Customer Completes Payment
The 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.
### Step 3: Retrieve the Token
After the payment completes, call the [Fetch Tokens from Orders API](https://developers.surfboardpayments.com/api/orders) to retrieve the `tokenId` and card information:
```json
GET /orders/:orderId/tokens
```
Store the `tokenId` securely against the customer in your system. You will use it for all future charges.
> **Warning:** Store tokens securely on your backend. Never expose token IDs to the client or include them in frontend code.
## Stage 2: Merchant Initiated Payments
With the `tokenId` stored, you can now charge the customer from your backend at any time.
### Step 1: Create an Order
Create a new order using the **MerchantInitiated** terminal:
```json
POST /orders
{
"terminal$id": "YOUR_MIT_TERMINAL_ID",
"orderLines": [
{
"id": "SUB-002",
"name": "Monthly Subscription - February",
"quantity": 1,
"amount": {
"total": 29900,
"currency": "752"
}
}
]
}
```
### Step 2: Initiate Payment with Token
Use the [Initiate Payment API](https://developers.surfboardpayments.com/api/payments) with the stored `tokenId`:
```bash
curl -X POST YOUR_API_URL/payments \
-H 'Content-Type: application/json' \
-H 'API-KEY: YOUR_API_KEY' \
-H 'API-SECRET: YOUR_API_SECRET' \
-H 'MERCHANT-ID: YOUR_MERCHANT_ID' \
-d '{
"orderId": "YOUR_ORDER_ID",
"paymentMethod": "CTOKEN",
"tokenId": "YOUR_TOKEN_ID"
}'
```
### Step 3: Check Order Status
Verify the payment result using the [Fetch Order Status API](https://developers.surfboardpayments.com/api/orders):
```json
GET /orders/:orderId/status
```
The status will be `PAYMENT_COMPLETED` on success or `PAYMENT_CANCELLED` / `PAYMENT_FAILED` otherwise. You can also receive real-time updates via webhooks.
## Common Use Cases
| Use Case | Description |
|----------|-------------|
| **Subscriptions** | Charge customers monthly/yearly on a schedule |
| **Metered billing** | Charge variable amounts based on usage |
| **Retry failed payments** | Re-attempt a charge after a soft decline |
| **Installments** | Split a large payment into scheduled charges |
## Post-Payment Operations
Post-payment operations (refunds, receipts, reporting) work the same way as other online payment modes. Use the standard APIs:
- [Receipts API](https://developers.surfboardpayments.com/api/receipts) for sending digital receipts
- [Orders API](https://developers.surfboardpayments.com/api/orders) for refunds and order management
## Reference
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)
- [Token Management](https://developers.surfboardpayments.com/api/orders)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Create an Order
Category: online | Tags: Online, API, Orders, In-Store
URL: /developers/guides/create-an-order
## Overview
An order is the starting point for every payment in Surfboard. You create an order against a `terminal$id`, include line items with pricing, and optionally initiate payment in the same call. The API returns an `orderId` and `paymentId` that you use for all subsequent operations.
This guide covers basic order creation, line items, customer details, tax handling, and common control functions.
## Prerequisites
1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. Complete onboarding (merchant and store setup)
3. A terminal to create the order against (any type -- in-store, PaymentPage, SelfHostedPage, or MerchantInitiated). In-store devices and SelfHostedPage are registered; an online store already carries a PaymentPage and a MerchantInitiated terminal, so fetch the store's terminals to find them.
## Basic Order
Order, payment, and receipt endpoints are **not** merchant-scoped in the path. The merchant travels in the `MERCHANT-ID` header alongside `API-KEY` and `API-SECRET`, so the path is `/orders`, not `/merchants/{merchantId}/orders`. See [API Conventions](/developers/guides/api-conventions) for the full header set.
Create an order with a single line item and initiate payment:
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [
{
"id": "ITEM-001",
"name": "Nike Shoes",
"quantity": 1,
"amount": {
"regular": 50000,
"total": 50000,
"currency": "752",
"tax": [
{ "amount": 10000, "percentage": 25, "type": "VAT" }
]
}
}
],
"totalOrderAmount": {
"regular": 50000,
"total": 50000,
"currency": "752",
"tax": [
{ "amount": 10000, "percentage": 25, "type": "VAT" }
]
},
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD"
}
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderId": "83a1ba32774149710b",
"paymentId": "83a1ba3264bd500106"
},
"message": "Order created successfully"
}
```
Store both `orderId` and `paymentId` -- you need them for status checks, captures, voids, and refunds.
## Line Items
Every order requires at least one line item in the `orderLines` array. Each line item must include:
| Field | Required | Description |
|-------|----------|-------------|
| `id` | Yes | Unique line item identifier |
| `name` | Yes | Product name |
| `quantity` | Yes | Quantity (negative for refunds) |
| `amount.regular` | Yes | Unit price in smallest currency unit |
| `amount.total` | Yes | **Unit** price after shipping and campaign (`regular + shipping - campaign`). Not the line total |
| `amount.currency` | Yes | Numeric ISO 4217 code (e.g., `"752"` for SEK) |
| `amount.tax` | Yes | Tax array for the line. Required even at zero rate -- send a `0` entry rather than omitting it |
Optional fields include `description`, `brand`, `imageUrl`, `gtin`, `categoryId`, `unit`, and `metadata`.
> **`amount.total` is per unit, not per line.** This is the single most common first-integration error, and it only shows up once a cart has a quantity above one. `total` must equal `regular + shipping - campaign` for **one** unit; the order total is `sum(total * quantity)`. Sending `unitPrice × quantity` returns `P_0001: Invalid item price for item id `.
Two lines, one of them with a quantity above one:
```json
"orderLines": [
{
"id": "ITEM-001",
"name": "Flat white",
"quantity": 2,
"amount": {
"regular": 4500,
"total": 4500,
"currency": "752",
"tax": [{ "amount": 900, "percentage": 25, "type": "VAT" }]
}
},
{
"id": "ITEM-002",
"name": "Gift card",
"quantity": 1,
"amount": {
"regular": 10000,
"total": 10000,
"currency": "752",
"tax": [{ "amount": 0, "percentage": 0, "type": "VAT" }]
}
}
]
```
The first line contributes `4500 * 2 = 9000`, not `4500`. The order total is `19000`. The gift card is zero-rated and still carries a `tax` entry: omitting it returns `P_0001: Input data validation failed. Cannot read properties of undefined (reading 'vatValue')`.
> **Currency format:** All amounts use the smallest currency unit. For example, 10.00 SEK = `1000`, 5.00 EUR = `500`.
> **Prices include tax.** `amount.regular` and `amount.total` are gross. The `tax` array reports the VAT *contained within* that price, not an amount to add on top. See [API Conventions](/developers/guides/api-conventions) if you are coming from a sales-tax market.
## Customer, Billing, and Shipping
Include customer, billing, and shipping details when available:
```json
{
"terminal$id": "YOUR_TERMINAL_ID",
"customer": {
"person": {
"name": { "firstName": "John", "lastName": "Doe" },
"email": "john@example.com",
"phoneNumber": { "code": "46", "number": "768100190" }
},
"company": {
"vatId": "SE556026998601"
}
},
"billing": {
"name": { "firstName": "John", "lastName": "Doe" },
"phoneNumber": { "code": "46", "number": "768100190" },
"address": {
"addressLine1": "Storgatan 1",
"city": "Stockholm",
"postalCode": "11122",
"countryCode": "SE"
}
},
"shipping": {
"name": { "firstName": "John", "lastName": "Doe" },
"phoneNumber": { "code": "46", "number": "768100190" },
"address": {
"addressLine1": "Storgatan 1",
"city": "Stockholm",
"postalCode": "11122",
"countryCode": "SE"
}
},
"orderLines": [...]
}
```
All customer fields are optional but recommended for invoice payments, fraud prevention, and receipt delivery.
## Order Line Level Calculation
The `orderLineLevelCalculation` control function changes how `totalOrderAmount` is computed from line items.
| Setting | Formula | Example |
|---------|---------|---------|
| `false` (default) | Sum of `(total * quantity)` per line | `(50 * 2) + (150 * 1) = 250` |
| `true` (recommended) | Sum of `((regular * quantity) - campaign + shipping)` per line | `((200 * 2) - 100 + 50) = 350` |
Enable it when your line items have campaigns or shipping costs:
```json
{
"controlFunctions": {
"orderLineLevelCalculation": true,
"initiatePaymentsOptions": { "paymentMethod": "CARD" }
}
}
```
## Adjustments
Adjustments modify the total order value for tips, donations, gift cards, or discounts:
```json
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [...],
"adjustments": [
{ "type": "TIP", "value": 1000 }
],
"totalOrderAmount": {
"regular": 50000,
"total": 51000,
"currency": "752"
},
"controlFunctions": {
"initiatePaymentsOptions": { "paymentMethod": "CARD" }
}
}
```
The `totalOrderAmount.total` should reflect the adjusted amount (regular + adjustments).
## Delay Capture
To authorize payment now but capture funds later (e.g., at shipment), set `delayCapture: true`:
```json
{
"controlFunctions": {
"delayCapture": true,
"initiatePaymentsOptions": { "paymentMethod": "CARD" }
}
}
```
You can also use `authMode: "PRE-AUTH"` for pre-authorization flows, which automatically enables delayed capture and lets you capture a different amount than originally authorized.
See the [Capture a Payment](/developers/guides/capture-a-payment) guide for the full flow.
## Check Order Status
After creating an order, check its status at any time:
```json
GET /orders/:orderId/status
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderStatus": "PAYMENT_COMPLETED",
"payments": [
{
"paymentId": "83a1ba3264bd500106",
"paymentStatus": "PAYMENT_COMPLETED",
"paymentMethod": "CARD",
"amount": 50000
}
],
"paymentIds": ["83a1ba3264bd500106"]
}
}
```
**Order statuses:** `PENDING` | `PAYMENT_COMPLETED` | `PAYMENT_CANCELLED` | `PARTIAL_PAYMENT_COMPLETED` | `PAYMENT_PROCESSED`
**Payment statuses:** `PAYMENT_INITIATED` | `PAYMENT_PROCESSING` | `PAYMENT_PROCESSED` | `PAYMENT_COMPLETED` | `PAYMENT_FAILED` | `PAYMENT_CANCELLED`
Every payment ends in one of three terminal states:
| Payment Status | Order Status | Description |
|----------------|--------------|-------------|
| `PAYMENT_COMPLETED` | `PAYMENT_COMPLETED` | Payment succeeded -- the order is closed. |
| `PAYMENT_CANCELLED` | `PENDING` | Payment was cancelled -- the order remains open and a new payment can be initiated using the existing `orderId`. |
| `PAYMENT_FAILED` | `PENDING` | Payment failed -- the order remains open and a new payment can be initiated using the existing `orderId`. |
## Error Handling
Create order responses return `status: "ERROR"` with a code in the `OR_*`, `PS_*`, `GC_*`, or `SP_*` prefix when validation or initiation fails. The most common ones are `OR_0042` (terminal not found), `OR_0037` (invalid total), `OR_0048` (mixed currencies), and `PS_0025` (terminal not connected -- retry after configure).
See the [Create Order Error Codes](/developers/guides/create-order-error-codes) reference for the full list, including errors thrown by the initiate payment step when both happen in the same call.
## Next Steps
Once you have an order created, you can:
- [Capture a Payment](/developers/guides/capture-a-payment) -- finalize a delayed-capture authorization
- [Cancel a Payment](/developers/guides/cancel-a-payment) -- stop an in-progress payment
- [Void a Payment](/developers/guides/void-a-payment) -- reverse a completed payment before settlement
- [Refund an Order](/developers/guides/refund-an-order) -- return funds after settlement
- [Partial Payments](/developers/guides/partial-payments) -- split an order across multiple payments
## Reference
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Create Order Error Codes](/developers/guides/create-order-error-codes)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Merchant Onboarding
Category: online | Tags: Onboarding, Merchant, KYB, Prefill, MCC, Store, Partners, API
URL: /developers/guides/merchant-onboarding
## Overview
Merchant onboarding is the first step before accepting payments on Surfboard. A partner creates a merchant application through the API and receives a ready-to-use web onboarding link (**web KYB**, Know Your Business) that is handed to the merchant to finish. You can onboard merchants for both in-store and online payments using the same endpoint.
When you pre-enter the merchant's details, Surfboard does two things before returning the link:
1. **Registry prefill** -- the company's registry data (legal name, address, directors and beneficial owners where available) is resolved from the national business registry for the merchant's country.
2. **Business classification (MCC)** -- the free-text `businessDescription` is classified into a merchant category, which in turn determines the exact documents the merchant must supply (a taxi licence, association statutes, and so on) and any category-specific questions.
The link that comes back is therefore already populated. The merchant only has to add what a partner cannot know for them: their **bank account**, any **required documents** for their business category, and the **signing** (identity verification and e-signature) of the signatories and beneficial owners.
If any part of the prefill cannot be resolved, the call still succeeds and returns a working link. The merchant simply fills those sections in the web KYB flow as normal. Prefill is an accelerator, never a blocker.
The typical flow is:
1. **Create Merchant** -- submit the merchant's details and receive the web KYB link
2. **Merchant completes the web KYB** -- confirms the prefilled data, adds bank account and documents, signs
3. **Check Application Status** -- poll for the result or listen for webhooks
4. **Store Setup** -- optionally create additional stores after onboarding completes
## Prerequisites
Before onboarding merchants:
1. Create a developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. Obtain your `partnerId` from the Developer Portal Console
3. Generate API credentials (API key and secret)
Merchant applications in test and demo environments are approved automatically.
## Step 1: Create a Merchant Application
Send a `POST` request to the Create Merchant endpoint. The same endpoint handles in-store and online merchants; the difference is whether you include `onlineInfo` in the store configuration.
```
POST /partners/{partnerId}/merchants
```
The body has three parts:
- `country` and `organisation` -- who the merchant is. Required.
- `controlFields` -- how the onboarding should behave (store, acquirer, flags). Optional.
- `controlFields.preEnteredInformation` -- the data you prefill on the merchant's behalf. Optional, but this is what unlocks the accelerated flow.
### The minimum request
Country and corporate ID are enough to create an application. Surfboard resolves the legal name and registered address from the business registry, and the merchant fills in everything else in the web KYB:
```json
{
"country": "SE",
"organisation": {
"corporateId": "5591631360"
}
}
```
`country` is one of `SE`, `NO`, `DK`, `FI`, `IE`, and the format of `corporateId` is validated per country. Add `localeSelected` (`sv`, `da`, `fi`, `en`) to set the language of the web KYB; it defaults to the country's language.
### Create the first store in the same call
Include `controlFields.store` to create the merchant's first store during onboarding. This is recommended, since the merchant needs a store before it can take payments. `paymentChannels` tells Surfboard where the merchant takes payments; at least one channel must be `true`, and `physicalSharePercent` (1-99) only matters when both are.
```json
{
"country": "SE",
"organisation": {
"corporateId": "5591631360"
},
"controlFields": {
"store": {
"name": "Main Street Store",
"email": "store@example.com",
"phoneNumber": {
"code": "46",
"number": "701234567"
},
"address": {
"addressLine1": "Main Street 123",
"city": "Stockholm",
"countryCode": "SE",
"postalCode": "123 45"
},
"paymentChannels": { "physical": true, "online": false }
}
}
}
```
For online payments, add the `onlineInfo` object to the store with the webshop URL, terms and conditions, and privacy policy. To stop the merchant from changing those URLs in the web KYB, set `controlFields.disableFields.onlineInfo` to `true`; that then requires `merchantWebshopURL`, `termsAndConditionsURL` and `privacyPolicyURL` in the same request.
```json
{
"country": "SE",
"organisation": {
"corporateId": "5591631360"
},
"controlFields": {
"disableFields": { "onlineInfo": true },
"store": {
"name": "My Webshop",
"email": "shop@example.com",
"address": {
"addressLine1": "Main Street 123",
"city": "Stockholm",
"countryCode": "SE",
"postalCode": "123 45"
},
"paymentChannels": { "physical": false, "online": true },
"onlineInfo": {
"merchantWebshopURL": "https://shop.example.com",
"paymentPageHostURL": "https://pay.example.com",
"termsAndConditionsURL": "https://shop.example.com/terms",
"privacyPolicyURL": "https://shop.example.com/privacy"
}
}
}
}
```
### Prefill the application
Everything under `controlFields.preEnteredInformation` is optional. Supply what you know; anything you omit is collected from the merchant in the flow, and nothing you prefill is discarded.
**Describe the business.** `businessDescription` is what the merchant will *primarily use the payment solution for*: the specific activity that generates card transactions, not the general company purpose. "Selling coffee and pastries at our café" is right; "Food and beverage services" is not. Supplying it triggers automatic category (MCC) classification, which sets the required documents and category questions. If you already know the MCC, pass `organisation.mccCode` instead.
**Name the people.** You can prefill the `applicant` (the main contact), plus `signatories`, `ubos` (beneficial owners) and `chairpersons`. When you supply a person, give at least their `name` and `email`. A person can hold more than one role: the applicant is often both a signatory and a beneficial owner, which you express with `isSignatory` and `isUbo`.
> **Who receives a signing link:** signing invitations go only to the people who must sign, i.e. the signatories and the beneficial owners. Being the applicant or a chairperson alone does not trigger a signing link; that person signs only if they are also a signatory or UBO.
The ownership fields (`ownershipPercent`, `ownershipType`, `entityName`) describe beneficial ownership and only apply when a person is a UBO. `ownershipType` is `direct` or `indirect`; an indirect owner holds the shares through another company, and then `entityName` (the intermediary company) is required.
**Add trading details.** `openingInfo`, `giftcards`, `prePayments` and `fundsInfo` answer the questions the merchant would otherwise be asked in the flow. A few rules apply on the backend: `isOpenAllYear` and `isSeasonalOpen` must be opposites, `monthsOpen` is required when not open all year, and `reasonForOpeningAtNight` is required when `isStoreOpenAtNight` is `true`. Include `giftcards` and `prePayments` only if the merchant actually sells gift cards or takes prepayments.
### Full example
A Danish café, prefilled by the partner. The registry resolves the legal name and address; the business description classifies the merchant; the applicant is both signatory and sole direct owner, with a second, indirect owner listed under `ubos`.
```json
{
"country": "DK",
"localeSelected": "da",
"organisation": {
"corporateId": "12345678"
},
"controlFields": {
"generateShortLink": true,
"store": {
"name": "Havnens Café",
"email": "hello@havnenscafe.dk",
"phoneNumber": { "code": "45", "number": "31234567" },
"address": {
"addressLine1": "Havnegade 12",
"city": "København",
"countryCode": "DK",
"postalCode": "1058"
},
"paymentChannels": { "physical": true, "online": true, "physicalSharePercent": 80 }
},
"preEnteredInformation": {
"businessDescription": "Selling coffee, pastries and light lunches at our harbourside café.",
"applicant": {
"email": "owner@havnenscafe.dk",
"name": "Mette Jensen",
"isSignatory": true,
"isUbo": true,
"ownershipPercent": 100,
"ownershipType": "direct"
},
"ubos": [
{
"name": "Lars Holm",
"email": "lars@example.dk",
"ownershipPercent": 0,
"ownershipType": "indirect",
"entityName": "Holm Holding ApS"
}
],
"openingInfo": {
"isOpenAllYear": true,
"isSeasonalOpen": false,
"monthsOpen": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
"isStoreOpenAtNight": false,
"reasonForOpeningAtNight": ""
},
"giftcards": { "revenueSharePercent": 15, "averageValidDays": 365 },
"fundsInfo": {
"averageTransactionValuePerDay": 4000,
"estimatedAmountPerYear": 1200000,
"priceOfMostExpensiveItemSold": 250,
"estimatedAmountPerTransaction": 95,
"estimatedFrequencyOfTransactions": "DAILY"
}
}
}
}
```
### Response
A successful request returns the application ID and the web KYB link. `shortLinkUrl` is present when you asked for it with `generateShortLink`, and `storeId` when you supplied a store.
```json
{
"status": "SUCCESS",
"message": "Merchant application created successfully.",
"data": {
"applicationId": "845adba035abb00310",
"webKybUrl": "https://onboarding.surfboard.se/845adba035abb00310?pi=…",
"shortLinkUrl": "https://sb.fyi/abcd12",
"validUntil": "2026-12-01T00:00:00.000Z",
"merchantId": "83af75d53169b0070e",
"storeId": "845adbc0a3f2b00711"
}
}
```
Share `webKybUrl` (or `shortLinkUrl`) with the merchant. Treat it as sensitive: it grants access to the application. The link is valid until `validUntil`. Each call creates a new application, so do not call again for the same merchant to recover a link; the current link is always available from the status endpoint in Step 3.
### Key fields
| Field | Description |
|-------|-------------|
| `country` | Two-letter ISO country code: `SE`, `NO`, `DK`, `FI` or `IE`. Required. |
| `localeSelected` | Language of the web KYB (`sv`, `da`, `fi`, `en`). Defaults to the country's language. |
| `organisation.corporateId` | The merchant's corporate or organisation number. Required. |
| `organisation.legalName`, `organisation.address` | Resolved from the registry if omitted. Mandatory for Payment Facilitator (PF) partners. |
| `organisation.mccCode` | Merchant Category Code, if you already know it. Otherwise derived from `businessDescription`. |
| `controlFields.store` | Create a store during onboarding (recommended). |
| `controlFields.store.paymentChannels` | Whether the merchant takes payments in person, online, or both. |
| `controlFields.preEnteredInformation` | Business description, people and trading details to prefill. |
| `controlFields.disableFields.onlineInfo` | Lock the webshop URLs against merchant edits. Requires `store.onlineInfo`. |
| `controlFields.showProductCatalogue` | Show the terminal catalogue step. Requires the catalogue to be enabled for your programme. |
| `controlFields.preSelectProducts` | Pre-select terminals to ship automatically. |
| `controlFields.linkUsers` | Existing user IDs to link to the new merchant. |
| `controlFields.redirectUrl` | Where to send the merchant after they finish the web KYB. |
| `controlFields.generateShortLink` | Set `true` to also receive a shortened link. |
| `controlFields.merchantConfig.settlementFrequency` | Payout cadence: `daily`, `weekly`, `monthly` and more. |
| `controlFields.acquirerConfig`, `controlFields.directMerchantCreation` | PF programmes and direct acquirer agreements only. Leave unset otherwise. |
### Pre-selecting terminals
You can pre-select devices for automatic shipment using `preSelectProducts`, or let the merchant choose from a catalogue by setting `showProductCatalogue` to `true` and optionally filtering with `displayProducts`:
```json
{
"controlFields": {
"showProductCatalogue": true,
"preSelectProducts": [
{
"productId": "PRODUCT_ID",
"quantity": "2",
"pricingPlanId": "PLAN_ID"
}
]
}
}
```
### Linking a service provider
If a service provider already exists when you onboard the merchant, you can link it and set its standing share in the same call under `controlFields.merchantConfig.serviceProvider`. See [Service Providers & Split Payouts](/developers/guides/service-providers).
## Step 2: The Merchant Completes the Web KYB
The merchant opens `webKybUrl` and, because you prefilled the rest, only needs to:
1. **Confirm the prefilled company and people**, already populated from the registry and your data
2. **Add their bank account** for settlement
3. **Upload any required documents** for their business category, determined automatically from `businessDescription`
4. **Complete signing**: each signatory and beneficial owner verifies their identity and e-signs
Signing invitations are sent by email to the signatories and beneficial owners you named, or that the registry returned. Once everyone has signed, the compliance team reviews the application, typically within 3-4 business days. Applications in test and demo environments are approved automatically.
## Step 3: Check Application Status
Poll the application status to track progress. The response also carries the current `webKybUrl` while the application is open, so you never need to store the link from the create call.
```
GET /partners/{partnerId}/merchants/{applicationId}/status
```
```json
{
"status": "SUCCESS",
"data": {
"applicationId": "845adba035abb00310",
"webKybUrl": "https://onboarding.surfboard.se/845adba035abb00310?pi=…",
"applicationStatus": "APPLICATION_SUBMITTED",
"merchantId": "83af75d53169b0070e",
"storeId": "845adbc0a3f2b00711",
"onlineOnboardingStatus": "PENDING",
"billingPlans": [],
"paymentMethods": [
{ "paymentMethod": "card", "enabledSchemes": ["VISA", "MASTERCARD"], "status": "ACTIVE" }
],
"domainVerification": []
},
"message": "Application status fetched successfully"
}
```
### Application statuses
| Status | Description |
|--------|-------------|
| `APPLICATION_INITIATED` | Application created; the merchant has not started. |
| `APPLICATION_STARTED` | The merchant has opened the link and begun. |
| `APPLICATION_SUBMITTED` | The merchant has submitted all information. |
| `APPLICATION_PENDING_INFORMATION` | Awaiting additional information or documents from the merchant. |
| `APPLICATION_SIGNED` | All required signatories and beneficial owners have signed. |
| `APPLICATION_REJECTED` | Application rejected. |
| `APPLICATION_EXPIRED` | The link expired before the application was completed. Create a new application. |
| `APPLICATION_COMPLETED` | Compliance review passed; the merchant is being created. |
| `MERCHANT_CREATED` | The merchant is live and can transact. `merchantId` and `storeId` are returned. |
> **Tip:** You can also receive status updates via webhooks instead of polling. Configure webhooks in the Developer Portal Console. See [Webhooks & Notifications](/developers/guides/webhooks-notifications).
## Step 4: Create Additional Stores
A default store is typically created during onboarding. If the merchant needs additional stores, use the Create Store API:
```
POST /partners/{partnerId}/merchants/{merchantId}/stores
```
```json
{
"storeName": "Second Location",
"email": "store2@example.com",
"phoneNumber": {
"code": 46,
"number": "709876543"
},
"address": "Second Street 456",
"city": "Gothenburg",
"zipCode": "411 01",
"country": "SE"
}
```
For an online store, add `onlineInfo` with your webshop URLs:
```json
{
"storeName": "Online Store",
"email": "online@example.com",
"phoneNumber": {
"code": 46,
"number": "709876543"
},
"address": "Main Street 123",
"city": "Stockholm",
"zipCode": "103 16",
"country": "SE",
"onlineInfo": {
"merchantWebshopURL": "https://shop.example.com",
"termsAndConditionsURL": "https://shop.example.com/terms",
"privacyPolicyURL": "https://shop.example.com/privacy"
}
}
```
### Domain Verification (Online Stores)
Online stores in production require domain verification before they can process payments:
1. **Get verification keys** -- returned in the Create Store response (`merchantURLDomainVerificationKey` and `paymentPageURLDomainVerificationKey`)
2. **Add DNS TXT record** -- add the verification key as a TXT record on your domain
3. **Trigger verification** -- Surfboard checks automatically every 6 hours, or use the Verify Domain API to trigger it manually
4. **Monitor status** -- use the Fetch Store Details API to check the `onlineOnboardingStatus` field
> **Note:** Domain verification is only required in production (not in demo/sandbox). A verified domain applies to all merchants under the same partner account.
## Notes and Behaviours
- **Prefill.** Registry data and category classification are resolved as part of the create call, so the returned link is already populated. If either cannot be resolved, the call still returns a valid link and the merchant completes those sections manually.
- **Documents are category-driven.** The `businessDescription` sets the merchant category, which sets exactly which documents are mandatory and any category-specific questions. You do not specify documents in the request.
- **Prefill is additive.** Anything you omit is collected from the merchant in the flow; nothing you prefill is discarded.
- **One application per call.** Each call creates a new application. Avoid duplicate calls for the same merchant. The current link for an application is always available from the status endpoint.
## Reference
- [Create Merchant API](https://developers.surfboardpayments.com/api/merchants)
- [Check Application Status API](https://developers.surfboardpayments.com/api/merchants)
- [Create Store API](https://developers.surfboardpayments.com/api/stores)
- [Verify Domain API](https://developers.surfboardpayments.com/api/stores)
- [Webhook Reference](https://developers.surfboardpayments.com/references/webhooks/merchants/application-completed)
- [Service Providers & Split Payouts](/developers/guides/service-providers)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Device Registration
Category: in-store | Tags: In-Store, Terminal, API, Device Management, Onboarding
URL: /developers/guides/device-registration
## Overview
Before a terminal can accept payments -- or receive a partner POS app -- it must be **registered to a store** under a merchant. Registration links the physical (or software) device to your merchant hierarchy. Once a terminal is registered to a store, it cannot be repurposed by another merchant. You can still move it between stores under the same merchant using the [Change Store](/developers/guides/terminal-device-management) endpoint.
Surfboard supports several registration methods so you can pick the lowest-friction option for your setup. They are **not mutually exclusive** -- the same terminal can be registered through whichever path the merchant has available.
> If you integrate through one of our SDKs (Android SoftPOS, Tap to Pay on iPhone), the SDK handles registration for you and you do not need to call the registration APIs directly. See [Android SoftPOS SDK](/developers/guides/android-softpos-sdk) and [Tap to Pay on iPhone](/developers/guides/tap-to-pay-iphone).
## When Is Registration Needed?
| Terminal type | Registration required? | Typical method |
|---------------|------------------------|----------------|
| **EMV** (countertop, mobile POS, kiosk) | Yes -- the terminal must be registered before it can receive a partner POS app or take payments | Rotating code, QR/registration link, or pre-shipped code |
| **SoftPOS** (CheckoutX on Android) | Yes | In-app (interapp) registration, or any of the code-based methods |
| **SDK-based** (Android SoftPOS SDK, Tap to Pay on iPhone) | Handled by the SDK | N/A -- SDK methods cover registration |
## Registration Methods at a Glance
| Method | How the merchant registers | Code validity | Best for |
|--------|----------------------------|---------------|----------|
| **Rotating 6-digit code** | Reads a code from the terminal screen and enters it into the Surfboard merchant portal or the partner's portal/app | ~90 seconds (rotates) | Attended setup where someone is in front of both the terminal and a portal |
| **QR / registration link** | Taps the QR icon on the terminal's registration screen and scans a QR generated by the partner or Surfboard merchant portal | Short-lived | Fast, near zero-touch scan-to-register |
| **Pre-shipped code** | Enters a code provided ahead of time (e.g. a welcome email or SMS) directly on the terminal registration screen | Longer-lived | Unattended setup, or when the 90-second rotation is impractical |
| **In-app (interapp)** | Switches to CheckoutX, which registers the device automatically -- no code entry | n/a | SoftPOS / CheckoutX only |
---
## Method 1 -- Rotating 6-Digit Code
When a terminal starts up on its registration screen, it displays a **6-digit code that rotates roughly every 90 seconds**. The merchant reads this code and enters it into a registration screen -- either the **Surfboard merchant portal** or the **partner's own portal or app**.
Behind the scenes, the portal calls the **Register Device** API with the code as the `registrationIdentifier`:
```
POST /merchants/:merchantId/stores/:storeId/devices
```
```json
{
"registrationIdentifier": "250901",
"terminalName": "Kiosk One"
}
```
| Parameter | Required | Description |
|-----------|----------|-------------|
| `registrationIdentifier` | Yes | The 6-digit code shown on the terminal. For SurfPad and Printer devices, use the serial number printed on the back instead. |
| `terminalName` | No | A human-readable label to identify the terminal. |
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"terminalId": "813ca2cb12ce400405",
"registrationStatus": "REGISTERED"
},
"message": "Terminal registered successfully"
}
```
`registrationStatus` is either `REGISTERED` (new device) or `ALREADY_REGISTERED` (device was previously linked). Store the returned `terminalId` -- you need it for all subsequent calls on this device.
> Because the code rotates every ~90 seconds, complete the entry promptly. If it expires, read the new code from the screen and try again.
See the [Register Device API reference](https://developers.surfboardpayments.com/references/api/terminals/register-device) for the full request/response and error codes.
---
## Method 2 -- QR Code / Registration Link
The terminal's registration screen also shows a **QR icon**. Tapping it opens the camera so the merchant can scan a QR code provided by the **partner** or by the **Surfboard merchant portal**. The QR encodes a `registrationLink` -- a deep link that registers the device automatically, with no manual code entry.
Generate the link with the **Get Device Registration Code** API:
```
GET /merchants/:merchantId/stores/:storeId/device-registration
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"registrationCode": "905788",
"registrationLink": "checkoutx://com.surfboard.checkoutx/register?data=eyJyZWdpc3RyYXRpb25Db2RlIjoiOTA1Nzg4In0="
},
"message": "Registration Code Generated Successfully"
}
```
| Field | Description |
|-------|-------------|
| `registrationCode` | A 6-digit code the merchant can enter manually (see Method 3). |
| `registrationLink` | A deep link that, encoded as a QR code, registers the device when scanned. |
Render `registrationLink` as a QR code in your partner portal or app, or let the Surfboard merchant portal generate it for you. The merchant scans it from the terminal's registration screen and the device registers itself.
See the [Get Device Registration Code API reference](https://developers.surfboardpayments.com/references/api/terminals/get-device-registration-code).
---
## Method 3 -- Pre-Shipped Code
The same **Get Device Registration Code** API also returns a `registrationCode`. Unlike the rotating on-screen code, this code has **longer validity**, which makes it a good fit for setups where the ~90-second rotation is impractical.
A common pattern: a partner generates the code ahead of time and delivers it to the merchant out of band -- for example in a **welcome email or SMS** sent before the terminal ships. When the device arrives, the merchant simply enters the code on the terminal's registration screen and the device registers.
This supports a near zero-touch onboarding experience: the merchant never has to coordinate a live, time-limited code between the terminal and a portal.
---
## Method 4 -- In-App (Interapp) Registration for SoftPOS
For **SoftPOS** running CheckoutX, you can register a device by switching into the CheckoutX app -- **no code entry needed**. Your app opens CheckoutX via a deep link, CheckoutX registers the terminal, and control returns to your app with the resulting `terminalId`.
This is the smoothest option for SoftPOS and partner POS apps that already integrate with CheckoutX. It is **not available for EMV terminals**, which must use one of the code- or QR-based methods above.
For the full app-switch flow -- generating the interapp code, building the deep link, and handling the callback -- see the [Inter-App Integration](/developers/guides/interapp-integration) guide.
---
## After Registration
Once a terminal is registered:
- Use the returned `terminalId` to create orders and initiate payments. See [Create an Order](/developers/guides/create-an-order).
- Apply configuration (network, language, restart schedule, and more) -- see [Terminal & Device Management](/developers/guides/terminal-device-management).
- Move the terminal between stores under the same merchant with the Change Store endpoint, or reassign across merchants (partner-managed inventory) with Move Terminal -- both covered in [Terminal & Device Management](/developers/guides/terminal-device-management).
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Register a device | POST | `/merchants/:merchantId/stores/:storeId/devices` |
| Get registration code & link | GET | `/merchants/:merchantId/stores/:storeId/device-registration` |
| Get interapp code (SoftPOS) | GET | `/merchants/:merchantId/stores/:storeId/terminals/interapp` |
## Reference
- [Register Device API](https://developers.surfboardpayments.com/references/api/terminals/register-device)
- [Get Device Registration Code API](https://developers.surfboardpayments.com/references/api/terminals/get-device-registration-code)
- [Inter-App Integration](/developers/guides/interapp-integration)
- [EMV Terminal Integration](/developers/guides/emv-terminal-integration)
- [Terminal & Device Management](/developers/guides/terminal-device-management)
### Payment Lifecycle
Category: online | Tags: Online, API, Payments, Refunds, Capture
URL: /developers/guides/payment-lifecycle
## Overview
Every payment follows a lifecycle: create an order, authorize payment, capture funds, and settle. At each stage you can intervene -- void before settlement, cancel before completion, or refund after. This guide covers each operation with the API calls you need.
## Lifecycle at a Glance
| Operation | When to Use | Endpoint | Method |
|-----------|-------------|----------|--------|
| **Create Order** | Start a new payment | `/orders` | POST |
| **Capture** | Finalize a delayed-capture auth | `/payments/:paymentId/capture` | POST |
| **Void** | Reverse before settlement | `/payments/:paymentId/void` | POST |
| **Cancel** | Stop before completion | `/payments/:paymentId` | DELETE |
| **Refund** | Full return after settlement | `/orders` | POST |
| **Partial Refund** | Partial return after settlement | `/orders` | POST |
## Order and Payment Statuses
**Order statuses:** `PENDING` | `PAYMENT_COMPLETED` | `PAYMENT_CANCELLED` | `PARTIAL_PAYMENT_COMPLETED` | `PAYMENT_PROCESSED`
**Payment statuses:** `PAYMENT_INITIATED` | `PAYMENT_PROCESSING` | `PAYMENT_PROCESSED` | `PAYMENT_COMPLETED` | `PAYMENT_FAILED` | `PAYMENT_CANCELLED`
### Status Flow
A payment moves through progressive statuses before settling into one of three final (terminal) states. The happy path is:
```
PAYMENT_INITIATED → PAYMENT_PROCESSING → PAYMENT_PROCESSED → PAYMENT_COMPLETED
→ PAYMENT_FAILED
→ PAYMENT_CANCELLED
```
`PAYMENT_CANCELLED` and `PAYMENT_FAILED` can also occur **directly after** `PAYMENT_INITIATED` -- for example, if the customer abandons checkout or the payment is rejected before processing begins.
#### Progressive statuses
| Payment Status | Description |
|----------------|-------------|
| `PAYMENT_INITIATED` | Payment has been created on the order and is awaiting processing. Can transition to `PAYMENT_PROCESSING`, `PAYMENT_CANCELLED`, or `PAYMENT_FAILED`. |
| `PAYMENT_PROCESSING` | Payment is actively being processed by the network. |
| `PAYMENT_PROCESSED` | Payment has been authorised and processed, but is not yet in its final state (for example, awaiting capture or confirmation). |
#### Order-level intermediate status
| Order Status | Description |
|--------------|-------------|
| `PARTIAL_PAYMENT_COMPLETED` | Only set on the **order**, not on an individual payment. Indicates that one or more payments against the order have completed, but the full order amount has not yet been paid. |
### Terminal Payment States
Every payment ends in one of three terminal states. Once a payment reaches a terminal state, it is final and cannot change.
| Payment Status | Order Status | Description |
|----------------|--------------|-------------|
| `PAYMENT_COMPLETED` | `PAYMENT_COMPLETED` | Payment succeeded -- funds are captured and the order is closed. |
| `PAYMENT_CANCELLED` | `PENDING` | Payment was cancelled -- the order remains open and a new payment can be initiated using the existing `orderId`. |
| `PAYMENT_FAILED` | `PENDING` | Payment failed -- the order remains open and a new payment can be initiated using the existing `orderId`. |
> **Tip:** When a payment is cancelled or fails, you do not need to create a new order. Simply initiate a new payment against the same `orderId` to retry.
## Create an Order
Every payment starts with an order containing line items and a terminal ID.
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [{
"id": "ITEM-001",
"name": "Running Shoes",
"quantity": 1,
"amount": { "regular": 50000, "total": 50000, "currency": "752",
"tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }] }
}],
"totalOrderAmount": { "regular": 50000, "total": 50000, "currency": "752",
"tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }] },
"controlFunctions": {
"initiatePaymentsOptions": { "paymentMethod": "CARD" }
}
}
```
```json
// Response
{ "status": "SUCCESS",
"data": { "orderId": "83a1ba32774149710b", "paymentId": "83a1ba3264bd500106" },
"message": "Order created successfully" }
```
Store both `orderId` and `paymentId` -- you need them for all subsequent operations.
### Delay Capture
To authorize now but capture later (e.g., charge at shipment), set `delayCapture: true` in `controlFunctions`. You can also use `authMode: "PRE-AUTH"` for pre-authorization flows, which automatically enables delayed capture.
## Capture a Payment
When an order uses `delayCapture: true`, explicitly capture to finalize the charge.
```json
POST /payments/:paymentId/capture
{ "amount": 50000 }
```
The `amount` field is only required for `PRE-AUTH` orders where you capture a different amount than authorized. For standard delayed capture, send an empty body `{}`.
```json
// Response
{ "status": "SUCCESS", "message": "Payment captured successfully" }
```
Check capture status with `GET /payments/:paymentId/capture`. Possible `captureStatus` values: `PENDING`, `SUCCESS`, `ERROR`.
## Void a Payment
Voiding reverses a completed payment **before settlement** -- no money moves.
```json
POST /payments/:paymentId/void
{}
```
```json
// Response
{ "status": "SUCCESS",
"data": { "voidStatus": "VOIDED" },
"message": "Payment voided successfully" }
```
Possible `voidStatus` values: `VOID_INITIATED`, `CANNOT_VOID`, `VOIDED`.
> **Important:** Voiding is only possible before 23:00 UTC on the transaction day, and only for completed payments. After settlement cutoff, use a refund instead.
## Cancel a Payment
Cancellation stops a payment **before it completes** -- for example, if the customer abandons checkout while payment is processing.
```json
DELETE /payments/:paymentId
```
```json
// Response
{ "status": "SUCCESS",
"data": { "paymentStatus": "PAYMENT_CANCELLED" },
"message": "Payment cancelled successfully" }
```
> **Cancel vs. Void:** Cancel applies to in-progress payments (before completion). Void applies to completed payments (before settlement).
## Refund an Order
A full refund is a **new order** with negative quantities and the original `orderId` as `purchaseOrderId` on each line item.
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [{
"id": "ITEM-001",
"purchaseOrderId": "ORIGINAL_ORDER_ID",
"name": "Running Shoes",
"quantity": -1,
"amount": { "regular": 50000, "total": -50000, "currency": "752",
"tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }] }
}],
"totalOrderAmount": { "regular": 50000, "total": -50000, "currency": "752",
"tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }] },
"controlFunctions": {
"initiatePaymentsOptions": { "paymentMethod": "CARD" }
}
}
```
Key details:
- Set `quantity` to a negative value to indicate a return
- Set `amount.total` to a negative value
- Include the original `purchaseOrderId` on each line item
- For card refunds, `CARD_NP` is the recommended payment method
- Transaction fees are charged again on refunds
## Partial Refund
Works the same as a full refund, but only include the specific items or reduced quantities you want to return.
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [{
"id": "ITEM-002",
"purchaseOrderId": "ORIGINAL_ORDER_ID",
"name": "Water Bottle",
"quantity": -1,
"amount": { "regular": 15000, "total": -15000, "currency": "752",
"tax": [{ "amount": 3000, "percentage": 25, "type": "VAT" }] }
}],
"totalOrderAmount": { "regular": -15000, "total": -15000, "currency": "752",
"tax": [{ "amount": 3000, "percentage": 25, "type": "VAT" }] },
"controlFunctions": {
"initiatePaymentsOptions": { "paymentMethod": "CARD" }
}
}
```
> **Note:** All payment methods except NSWISH, SVIPPS, and SMOBILEPAY support partial refunds.
## Checking Order Status
Query the current state of any order at any point:
```json
GET /orders/:orderId/status
```
```json
// Response
{ "status": "SUCCESS",
"data": {
"orderStatus": "PAYMENT_COMPLETED",
"payments": [{ "paymentId": "83a1ba3264bd500106",
"paymentStatus": "PAYMENT_COMPLETED", "paymentMethod": "CARD", "amount": 50000 }],
"paymentIds": ["83a1ba3264bd500106"]
} }
```
## Decision Guide
| Situation | Action |
|-----------|--------|
| Payment initiated but not completed | **Cancel** -- `DELETE /payments/:paymentId` |
| Payment completed, not yet settled (before 23:00 UTC) | **Void** -- `POST /payments/:paymentId/void` |
| Payment settled, need full reversal | **Full Refund** -- create order with negative quantities |
| Payment settled, need partial reversal | **Partial Refund** -- create order with specific negative items |
| Delayed-capture order, ready to charge | **Capture** -- `POST /payments/:paymentId/capture` |
## Reference
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Capture a Payment
Category: online | Tags: Online, API, Payments, Capture, In-Store
URL: /developers/guides/capture-a-payment
## Overview
When you create an order with `delayCapture: true` or `authMode: "PRE-AUTH"`, funds are authorized but not immediately captured. This lets you verify inventory, confirm fulfillment, or adjust the final amount before charging the customer.
This guide walks through the full capture flow: create an authorized order, capture the payment, and verify the result.
## When to Use Delay Capture
| Scenario | Description |
|----------|-------------|
| **E-commerce fulfillment** | Authorize at checkout, capture at shipment |
| **Pre-authorization** | Hold a variable amount (e.g., hotel deposit), capture actual charge later |
| **Service bookings** | Authorize upfront, capture after service delivery |
| **Digital products** | Authorize, verify access, then capture |
## Step 1: Create an Order with Delay Capture
Create an order with `delayCapture: true` in `controlFunctions`:
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [
{
"id": "ITEM-001",
"name": "Nike Shoes",
"quantity": 1,
"amount": {
"regular": 50000,
"total": 50000,
"currency": "752",
"tax": [
{ "amount": 10000, "percentage": 25, "type": "VAT" }
]
}
}
],
"totalOrderAmount": {
"regular": 50000,
"total": 50000,
"currency": "752",
"tax": [
{ "amount": 10000, "percentage": 25, "type": "VAT" }
]
},
"controlFunctions": {
"delayCapture": true,
"initiatePaymentsOptions": {
"paymentMethod": "CARD"
}
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderId": "83ac302f7c5130810b",
"paymentId": "83ac302f24bfb00b06"
},
"message": "Order created successfully"
}
```
Store the `paymentId` -- you need it to capture.
### Pre-Authorization Mode
For flows where the final capture amount may differ from the authorized amount, use `authMode: "PRE-AUTH"`. This automatically enables `delayCapture`:
```json
{
"controlFunctions": {
"delayCapture": true,
"authMode": "PRE-AUTH",
"initiatePaymentsOptions": {
"paymentMethod": "CARD"
}
}
}
```
## Step 2: Capture the Payment
Once ready to finalize, call the capture endpoint with the `paymentId`:
```json
POST /payments/:paymentId/capture
{}
```
For `PRE-AUTH` orders, you can specify a different capture amount:
```json
POST /payments/:paymentId/capture
{
"amount": 45000
}
```
```json
// Response
{
"status": "SUCCESS",
"message": "Payment captured successfully"
}
```
> The `amount` field is only valid for `PRE-AUTH` orders. For standard `delayCapture`, send an empty body to capture the full authorized amount.
## Step 3: Check Capture Status
Verify the capture completed successfully:
```json
GET /payments/:paymentId/capture
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"captureStatus": "SUCCESS"
}
}
```
**Possible `captureStatus` values:** `PENDING` | `SUCCESS` | `ERROR`
## Reference
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Create an Order](/developers/guides/create-an-order)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
### Terminal & Device Management
Category: in-store | Tags: In-Store, Terminal, API, Device Management, Configuration
URL: /developers/guides/terminal-device-management
## Overview
Once merchants are onboarded and stores are created, the next step is registering and managing terminals. Surfboard supports both physical in-store devices and online payment terminals. This guide covers registration, configuration, and ongoing operations like moving or rebooting terminals.
## Terminal Types
| Type | Category | Description |
|------|----------|-------------|
| **EMV** | In-Store | Traditional card-present terminals (countertop, mobile POS, kiosk) |
| **SoftPOS** | In-Store | Tap-to-pay on Android smartphones or tablets |
| **PaymentPage** | Online | Surfboard-hosted checkout page with a payment link |
| **SelfHostedPage** | Online | Surfboard renders card fields on your own web page via the Online SDK |
| **iFrame** | Online | Embedded payment frame within your site |
| **MerchantInitiated** | Online | Server-to-server payments using stored card tokens (subscriptions, recurring) |
## Registering In-Store Terminals
Register physical terminals by providing the device's registration code and the store it belongs to.
```
POST /merchants/{merchantId}/stores/{storeId}/devices
```
```json
{
"registrationIdentifier": "250901",
"terminalName": "Checkout 1"
}
```
The `registrationIdentifier` is a 6-digit code displayed when you power on the terminal. For SurfPad and Printer devices, use the serial number printed on the back instead.
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"terminalId": "trm_abc123",
"registrationStatus": "REGISTERED"
},
"message": "Terminal registered successfully"
}
```
The `registrationStatus` will be either `REGISTERED` (new device) or `ALREADY_REGISTERED` (device was previously linked).
## Registering Online Terminals
Two online terminals arrive with the store. Creating an online store provisions a `PaymentPage` terminal, for payment links and hosted checkout, and a `MerchantInitiated` terminal, for backend charges against a stored token. Neither takes a registration call — fetch the store's terminals to get their IDs:
```
GET /merchants/{merchantId}/stores/{storeId}/terminals
```
The remaining modes, `SelfHostedPage` and `iFrame`, are registered per store with the mode named in the body:
```
POST /merchants/{merchantId}/stores/{storeId}/online-terminals
```
```json
{
"onlineTerminalMode": "SelfHostedPage"
}
```
> **Note:** The default terminals exist from the moment the online store does, but no online terminal can take a payment until the store's domains are verified and the store is approved.
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"terminalId": "trm_xyz789",
"publicKey": "pk_live_...",
"registrationStatus": "REGISTERED"
},
"message": "Terminal registered successfully"
}
```
For `SelfHostedPage` terminals, the response includes a `publicKey` used to initialize the Online SDK on your checkout page.
## Terminal Configuration
Terminal settings follow a hierarchy: merchant-level defaults cascade down to store-level, which cascade down to terminal-level. Terminal-level settings always take precedence.
### Setting Terminal Config
```
PATCH /merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}
```
```json
{
"wifiSsid": "StoreNetwork",
"wifiPassword": "securepass",
"preferredNetwork": "WIFI",
"preferredRestartTime": "03:00",
"language": "en",
"showReceipt": true,
"alwaysShowMinorUnits": 1
}
```
Key configuration options:
| Parameter | Description |
|-----------|-------------|
| `preferredRestartTime` | Scheduled restart in HH:MM format (default `02:00`). Terminals restart within a 1-hour window. |
| `preferredNetwork` | `WIFI` or `GSM` |
| `language` | ISO language code: `en`, `fi`, `da`, `se` |
| `autoSleep` | Sleep timeout in seconds (battery-powered devices only) |
| `showStatusBar` | Show/hide the status bar (SurfPad only) |
| `openPosOnReboot` | `enabled` or `disabled` -- auto-launch POS after restart |
| `enableRefundLock` | Require PIN for refunds (Android terminals only) |
### Fetching Terminal Config
```
GET /merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}/config
```
Returns all active configuration values for the terminal, including inherited settings from merchant and store levels.
## Device Operations
### Change Store
Move a terminal between stores under the same merchant. The terminal ID stays the same.
```
POST /terminals/change
```
```json
{
"terminal$id": "trm_abc123",
"storeId": "str_newstore456"
}
```
> Terminals can only be moved between stores belonging to the same merchant. To reassign across merchants, use the Move Terminal endpoint.
### Move Terminal (Cross-Merchant)
Partners who manage terminal inventory in bulk can reassign a hardware terminal to a different merchant entirely.
```
PUT /partners/{partnerId}/terminals/{terminalSerialNo}/move
```
```json
{
"targetMerchantId": "mrc_target789"
}
```
This endpoint uses the terminal's serial number rather than its terminal ID.
### Reboot Terminal
Remotely restart a terminal for troubleshooting or to apply firmware updates.
```
POST /terminals/{terminalId}/reboot
```
```json
{}
```
**Response:**
```json
{
"status": "SUCCESS",
"message": "Reboot command published successfully"
}
```
A `SUCCESS` status means the reboot command was sent. It does not guarantee the terminal has rebooted -- the device must be connected to the network and not processing another command.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Register in-store device | POST | `/merchants/{merchantId}/stores/{storeId}/devices` |
| Register online terminal | POST | `/merchants/{merchantId}/stores/{storeId}/online-terminals` |
| Set terminal config | PATCH | `/merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}` |
| Fetch terminal config | GET | `/merchants/{merchantId}/stores/{storeId}/terminals/{terminalId}/config` |
| Change store | POST | `/terminals/change` |
| Move terminal | PUT | `/partners/{partnerId}/terminals/{terminalSerialNo}/move` |
| Reboot terminal | POST | `/terminals/{terminalId}/reboot` |
For full endpoint details, see the [Terminals API](https://developers.surfboardpayments.com/api/terminals) and [Stores API](https://developers.surfboardpayments.com/api/stores) reference documentation.
### Cancel a Payment
Category: online | Tags: Online, API, Payments, In-Store
URL: /developers/guides/cancel-a-payment
## Overview
Cancellation stops a payment **before it completes** -- for example, if the customer abandons checkout while payment is processing, or you need to halt a transaction before funds are transferred.
> **Cancel vs. Void:** Cancel applies to in-progress payments (before completion). If the payment has already completed, use [Void a Payment](/developers/guides/void-a-payment) instead.
## When to Use Cancel
| Scenario | Description |
|----------|-------------|
| **Customer abandons checkout** | Payment initiated but customer leaves |
| **Timeout** | Payment processing takes too long |
| **Error detected** | Issue found after payment initiation |
| **Duplicate order** | Accidentally created a second payment |
## Step 1: Cancel the Payment
Call the delete endpoint with the `paymentId` from the original order:
```json
DELETE /payments/:paymentId
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"paymentStatus": "PAYMENT_CANCELLED"
},
"message": "Payment cancelled successfully"
}
```
**Possible `paymentStatus` values:** `PAYMENT_COMPLETED` | `PAYMENT_FAILED` | `PAYMENT_CANCELLED`
If the payment already completed before your cancel request was processed, the status will show `PAYMENT_COMPLETED` and you should use a [void](/developers/guides/void-a-payment) or [refund](/developers/guides/refund-an-order) instead.
## Step 2: Verify Order Status
Confirm the order reflects the cancellation:
```json
GET /orders/:orderId/status
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderStatus": "PAYMENT_CANCELLED",
"payments": [
{
"paymentId": "83a1ba3264bd500106",
"paymentStatus": "PAYMENT_CANCELLED",
"paymentMethod": "CARD",
"amount": 50000
}
]
}
}
```
## Decision Guide
| Payment State | Action |
|---------------|--------|
| In progress (not completed) | **Cancel** -- `DELETE /payments/:paymentId` |
| Completed, not settled (before 23:00 UTC) | [Void](/developers/guides/void-a-payment) -- `POST /payments/:paymentId/void` |
| Settled | [Refund](/developers/guides/refund-an-order) -- create order with negative quantities |
## Reference
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Create an Order](/developers/guides/create-an-order)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
### Webhooks
Category: online | Tags: Online, API, Webhooks, Events
URL: /developers/guides/webhooks-notifications
## Overview
Webhooks enable you to receive real-time notifications for payment-related events in Surfboard, eliminating the need for repeated polling of the Surfboard APIs. When an event occurs, Surfboard sends an HTTP `POST` request to a URL on your server with the event details in the request body. All webhook messages include a signature for authenticity verification.
Surfboard supports two webhook mechanisms:
1. **Console webhooks:** Persistent, account-level subscriptions configured in the Surfboard Console. Support retries, failure alerts, and signature verification.
2. **Callback URL (per-order webhook):** A dynamic webhook URL set per order via `controlFunctions.callBackUrl`. Useful for order-level status updates during checkout.
Webhooks are also offered alongside other integration methods such as SSE (Server Sent Events) and event bus-based solutions (Kafka, Azure Event Stream, Google Pub/Sub, etc.).
## Available Events
You can subscribe to the following event categories to receive real-time updates within your platform.
### Order and Payment Events
Order and payment events provide real-time updates on order status and payment flow. These notifications help track orders, detect issues, and improve the checkout experience.
- **Order Updated** -- The order has been modified (e.g. order lines changed).
- **Order Payment Initiated** -- A payment attempt has started for the order.
- **Order Payment Processed** -- The payment is being processed by the payment provider.
- **Order Payment Completed** -- The payment has been successfully completed.
- **Order Payment Failed** -- The payment attempt has failed.
- **Order Payment Cancelled** -- The payment has been cancelled.
- **Order Cancelled** -- The entire order has been cancelled.
- **Order Customer Identity** -- A customer taps their card on the terminal, enabling you to identify the customer during a transaction and personalize the experience. Event type: `order.customer.identify`.
- **Order Terminal Event** -- Triggered for every state the terminal undergoes during a transaction (e.g. tip selection, card presented, PIN entry, authorizing). Also covers online terminal states such as page loaded, wallet SDK mounted, and payment initiated. Event type: `order.terminal.event`.
### Logistics Events
Logistics events notify you about updates on shipments, including terminals and accessories. These events help track order progress from placement to delivery.
- **Logistics Order Update** -- A logistics shipment status has changed.
### Merchant Application Events
Merchant application events provide updates during the onboarding process, from application creation to approval. These notifications help ensure smooth and timely onboarding for merchants.
- **Application Initiated** -- A new merchant application has been created.
- **Application Submitted** -- The application has been submitted for review.
- **Application Signed** -- The application has been signed by the merchant.
- **Application Started** -- Processing of the application has begun.
- **Application Pending Merchant Information** -- Additional information is required from the merchant.
- **Application Completed** -- The application review is complete.
- **Application Merchant Created** -- The merchant account has been created.
- **Application Expired** -- The application has expired.
- **Application Rejected** -- The application has been rejected.
## Event Payload Details
### Order Customer Identity
This event is triggered when a customer taps their card on the terminal, before the order is finalized or payment is processed. It enables customer identification early in the transaction flow.
**Event type:** `order.customer.identify`
**Payload example:**
```json
{
"eventType": "order.customer.identify",
"metadata": {
"eventId": "832cf9fe1806581dff",
"created": 1747553660038,
"retryAttempt": 0,
"webhookEventId": "81a214e74b107801ff"
},
"data": {
"orderId": "832cf9f93d2fd0410b",
"cardId": "c550c29e80908c887a"
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `data.orderId` | string | Unique identifier for the order. |
| `data.cardId` | string | Tokenized identifier for the customer's card, used to recognize or link the customer to the order. |
> **Note:** The `cardId` is a tokenized representation and should be treated as sensitive data.
### Order Terminal Event
This event is triggered for every state the terminal undergoes during a transaction, including stages like tip selection, card presentation, PIN entry, authorization, and completion.
**Event type:** `order.terminal.event`
**Payload example:**
```json
{
"eventType": "order.terminal.event",
"metadata": {
"eventId": "81a214e74b107801ff",
"created": 1695793998732,
"retryAttempt": 0,
"webhookEventId": "81a214e7455ed01cff"
},
"data": {
"orderId": "81b5f2624b16e0080b",
"merchantId": "8248db4c5c8dd0130e",
"paymentId": "81b5f26215e9583a06",
"terminalTransactionStatus": "STARTED",
"orderStatus": "PAYMENT_INITIATED"
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `data.orderId` | string | Unique identifier for the order. |
| `data.merchantId` | string | Unique identifier of the merchant. |
| `data.paymentId` | string | Unique identifier for the payment. |
| `data.terminalTransactionStatus` | string | Current terminal state (see table below). |
| `data.orderStatus` | string | Current order status. |
| `data.metadata` | object | Optional metadata passed with the order creation. |
**Terminal transaction statuses:**
| Status | Description |
|--------|-------------|
| `STARTED` | Transaction initiated on the terminal. |
| `SELECT_TIP` | Tip selection screen displayed. |
| `AWAITING_CARD` | Waiting for card tap/insert. |
| `CARD_PRESENTED` | Customer has presented card. |
| `SELECT_APPLICATION` | Card has multiple applications; selection required. |
| `ENTER_PIN` | Customer needs to enter PIN. |
| `WRONG_PIN` | Wrong PIN entered. |
| `AUTHORIZING` | Payment authorization initiated. |
| `SUBMITTED` | Authorization submitted to the backend. |
| `AUTHORIZED` | Authorization complete. |
| `PAGE_LOADED` | Online only -- payment page fully loaded. |
| `SECURE_CHANNEL_INITIALISED` | Online only -- page ready for card details. |
| `GOOGLE_PAY_MOUNTED` | Online only -- Google Pay SDK mounted. |
| `APPLE_PAY_MOUNTED` | Online only -- Apple Pay SDK mounted. |
| `CUSTOMER_INTERACTION_IN_FORM` | Online only -- customer started entering information. |
| `CARD_PAYMENT_INITIATED` | Online only -- card payment initiated. |
| `APPLE_PAY_ATTEMPT_INITIATED` | Online only -- Apple Pay attempt initiated. |
| `GOOGLE_PAY_ATTEMPT_INITIATED` | Online only -- Google Pay attempt initiated. |
| `APPLE_PAY_PAYMENT_INITIATED` | Online only -- Apple Pay payment process initiated. |
| `GOOGLE_PAY_PAYMENT_INITIATED` | Online only -- Google Pay payment initiated. |
## Getting Started
To set up webhooks via the Surfboard Console:
1. Log in to the [Surfboard Developer Portal](https://developers.surfboardpayments.com).
2. Click **Add new Webhook**.
3. Enter a name and the URL of your webhook endpoint.
4. Enter an email address to receive notifications in case of webhook failures.
5. Choose which events you would like to receive.
6. Save the **webhook secret** that is displayed. This secret is used to verify that messages originate from Surfboard. It is only shown once -- store it securely.
7. Click **Test webhooks** to send a test notification to your endpoint and confirm it is working.
> **Note:** You can add multiple webhooks to listen to different events. You can also customise your URLs so that each endpoint receives only specific events -- useful for microservice or service-oriented architectures.
## Testing Webhooks
When you create or test a webhook in the Console, Surfboard sends a test message to verify your endpoint is reachable. The test message has the following structure:
```json
{
"eventType": "test.webhook",
"metadata": {
"eventId": "string",
"created": 1234567890,
"retryAttempt": 0,
"webhookEventId": "string"
}
}
```
Your endpoint should return a `200` status code to acknowledge receipt.
## Callback URL (Per-Order Webhook)
In addition to Console webhooks, you can set a per-order callback URL when creating an order. This is useful for receiving status updates for a specific order during checkout.
Set `controlFunctions.callBackUrl` in the [Create Order API](https://developers.surfboardpayments.com/api/orders) request:
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [ ... ],
"controlFunctions": {
"callBackUrl": "https://your-server.com/webhooks/payments",
"initiatePaymentsOptions": {
"paymentMethod": "CARD"
}
}
}
```
> **Note:** Retries and alert emails are not supported for callback URL webhooks. The validation process is the same as regular webhooks -- you can obtain the webhook certificate for signature validation from the [Surfboard Developer Portal](https://developers.surfboardpayments.com).
## Handling Duplicate Deliveries
> **Info:** Surfboard guarantees **at-least-once delivery** for webhook callbacks. Because the system operates in a distributed multi-cloud environment, your endpoint may receive duplicate notifications for the same event. Surfboard performs deduplication on its side, but you must also handle duplicates on yours.
Use the combination of `orderId` and `paymentId` as your idempotency key. When you receive a callback, update the payment status to the value in the payload rather than applying it as an incremental state change.
**Important:** Due to network conditions, callbacks may arrive out of order. Once a payment reaches a terminal state -- `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, or `PAYMENT_CANCELLED` -- do not overwrite it with an earlier status update. Your implementation should treat these three statuses as final and ignore any subsequent callbacks that would move the payment to a non-terminal state.
## Handling Failures and Retries
### Retry Logic
When a webhook delivery fails (your endpoint does not return a `200` status code), Surfboard retries automatically:
- **Attempts:** Up to 3 total delivery attempts.
- **First retry:** 5 minutes after the initial failure.
- **Second retry:** 10 minutes after the first retry.
### Failure Alerts and Automatic Disabling
- An **alert email** is sent on the first delivery failure.
- If the endpoint continues to fail, subsequent alerts are sent every 24 hours for up to 7 days.
- After 7 days of continuous failure with no action taken, the webhook is **automatically disabled**.
- To re-enable a disabled webhook, fix the underlying issue and re-run **Test Webhook** in the Console.
### Failures on Surfboard's Side
Surfboard guarantees to deliver events at least once. If Surfboard experiences an outage, all queued events are republished once the servers recover. Ensure your system can handle a burst of incoming events in this scenario.
> **Tip:** As a safety net for payment events, perform a status query via the API if you have not received a webhook within 60 seconds of initiating a payment. Do not rely solely on webhooks for critical payment status confirmation.
## Verifying Webhook Signatures
Every webhook event is signed using the secret key provided when you created the webhook. The signature is included in the `x-webhook-signature` header of the `POST` request. Always validate this signature to confirm that the message originates from Surfboard.
The signature is an HMAC-SHA512 hash of the JSON request body, encoded as Base64. Below are examples in several languages:
### TypeScript
```typescript
import { createHmac } from 'node:crypto';
function generateHMACSignature(certificate: string, message: string): string {
return createHmac('sha512', certificate)
.update(message)
.digest()
.toString('base64');
}
// Verify incoming webhook
function verifyWebhook(secret: string, body: string, receivedSignature: string): boolean {
const expectedSignature = generateHMACSignature(secret, body);
return expectedSignature === receivedSignature;
}
```
### PHP
```php
**Warning:** Never expose token IDs in client-side code or logs.
## Recurring Configuration Reference
The `controlFunctions.online.recurring` object controls how the subscription behaves:
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `subscriptionAmountType` | Yes | string | `"fixed"` for same amount each cycle, `"variable"` for amounts that change |
| `maxAmount` | No | number | Maximum charge amount in smallest currency unit. Only used with `"variable"` amount type |
| `frequency` | Yes | string | Billing cycle. See frequency options below |
| `numberOfPayments` | No | number | Total number of payments for the subscription. Omit for indefinite |
| `uniqueReference` | No | string | Your unique identifier for this recurring agreement |
| `validation` | Yes | string | `"validated"` if the initial payment is authenticated (3DS), `"notValidated"` otherwise |
### Frequency Options
| Value | Cycle |
|-------|-------|
| `daily` | Every day |
| `twiceWeekly` | Twice per week |
| `weekly` | Every week |
| `tenDays` | Every 10 days |
| `fortNightly` | Every 2 weeks |
| `monthly` | Every month |
| `everyTwoMonths` | Every 2 months |
| `trimester` | Every 4 months |
| `quarterly` | Every 3 months |
| `twiceYearly` | Every 6 months |
| `annually` | Every year |
| `unscheduled` | No fixed schedule (usage-based or on-demand) |
## Step 3: Charge with the Stored Token
When a billing cycle is due, create an order on the MerchantInitiated terminal and pay with the token:
### Create the recurring order
```json
POST /orders
{
"terminal$id": "YOUR_MIT_TERMINAL_ID",
"referenceId": "sub-cust42-2026-02",
"orderLines": [
{
"id": "SUB-002",
"name": "Pro Plan - February 2026",
"quantity": 1,
"amount": {
"regular": 9900,
"total": 9900,
"currency": "752"
}
}
]
}
```
### Initiate payment with the token
```json
POST /payments
{
"orderId": "ORDER_ID_FROM_ABOVE",
"paymentMethod": "CTOKEN",
"tokenId": "STORED_TOKEN_ID"
}
```
### Verify the result
```
GET /orders/:orderId/status
```
A successful charge returns `orderStatus: "PAYMENT_COMPLETED"`.
## Variable-Amount Subscriptions
For metered billing or usage-based pricing, set `subscriptionAmountType` to `"variable"` and specify a `maxAmount`:
```json
"recurring": {
"subscriptionAmountType": "variable",
"maxAmount": 50000,
"frequency": "monthly",
"uniqueReference": "cust-42-usage",
"validation": "validated"
}
```
Each recurring charge can then use a different amount (up to `maxAmount`) based on the customer's usage for that period.
## Handling Failed Payments
When a recurring charge fails, the order status will show `PAYMENT_FAILED` or `PAYMENT_CANCELLED`. Common reasons include expired cards, insufficient funds, or issuer declines.
**Retry strategy:**
1. Check the `failureReason` on the payment status response.
2. For soft declines (insufficient funds, temporary issuer issues), retry the same order by calling the Initiate Payment API again with the token.
3. Space retries over increasing intervals (e.g., 1 day, 3 days, 7 days).
4. After repeated failures, notify the customer to update their card details. Direct them to a new payment page order with `enforceTokenization: true` to capture a fresh token.
5. Replace the old token with the new one in your system.
## Managing the Subscription Lifecycle
| Action | How to implement |
|--------|-----------------|
| **Pause** | Stop creating new orders on your billing schedule. The token remains valid. |
| **Resume** | Start creating orders again using the same stored token. |
| **Cancel** | Stop billing. Optionally delete the stored token via your internal records. |
| **Upgrade / downgrade** | Change the amount on the next order you create. For variable subscriptions this works within `maxAmount`. For fixed subscriptions, create a new initial order with the updated recurring configuration. |
| **Update payment method** | Direct the customer to a new payment page order with tokenization enabled, then replace the stored token. |
## Reference
- [Server-to-Server API Guide](/guides/server-to-server-api) -- Tokenization and MIT fundamentals
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)
- [Token Management](https://developers.surfboardpayments.com/api/orders)
### Void a Payment
Category: online | Tags: Online, API, Payments, In-Store
URL: /developers/guides/void-a-payment
## Overview
Voiding reverses a **completed** payment before it settles -- no money moves from the customer's account to the merchant's. This is the quickest way to reverse a transaction on the same day, avoiding refund processing fees.
> **Void vs. Cancel vs. Refund:**
> - **Cancel** -- payment is still in progress (not yet completed)
> - **Void** -- payment completed but not yet settled (same day, before 23:00 UTC)
> - **Refund** -- payment has settled (next day or later)
## When to Use Void
| Scenario | Description |
|----------|-------------|
| **Wrong amount charged** | Customer was overcharged, caught same day |
| **Duplicate transaction** | Same payment processed twice |
| **Customer changed mind** | Immediate post-purchase reversal |
| **Incorrect product** | Wrong item charged at point of sale |
## Step 1: Void the Payment
Call the void endpoint with the `paymentId`:
```json
POST /payments/:paymentId/void
{}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"voidStatus": "VOIDED"
},
"message": "Payment voided successfully"
}
```
**Possible `voidStatus` values:** `VOID_INITIATED` | `CANNOT_VOID` | `VOIDED`
> **Important:** Voiding is only possible before 23:00 UTC on the transaction day. After the settlement cutoff, you must process a [refund](/developers/guides/refund-an-order) instead.
## Step 2: Verify Order Status
Confirm the void was applied:
```json
GET /orders/:orderId/status
```
The order's transaction data will show `voided: true` for the affected transaction.
## Handling `CANNOT_VOID`
If the void returns `CANNOT_VOID`, the payment has either:
- Already been settled (past 23:00 UTC cutoff)
- Not yet completed (use [Cancel](/developers/guides/cancel-a-payment) instead)
In these cases, process a [full refund](/developers/guides/refund-an-order) or [partial refund](/developers/guides/partial-refund) as needed.
## Reference
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Create an Order](/developers/guides/create-an-order)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
### Receipts
Category: in-store | Tags: In-Store, API, Receipts, Printing, ESC/POS
URL: /developers/guides/receipts
## Overview
After a payment is completed, Surfboard gives you several ways to deliver receipts to customers. You can attach cash register details for regulatory compliance, email a digital copy, retrieve a shareable link, print directly on a Surfboard terminal, or send fully custom ESC/POS commands for branded receipt output.
All receipt endpoints accept a Transaction ID, Payment ID, or Order ID as the identifier, so you can work with whichever reference suits your integration.
## Prerequisites
Before working with receipts, make sure you have:
- A Surfboard developer account with valid API credentials (`API-KEY` and `API-SECRET`)
- At least one completed transaction, payment, or order
- For printing: a registered Surfboard terminal with printing capability (SurfTouch with dock or SurfPrint)
## Adding Receipt Information
Use this endpoint to store cash register-specific details against an order. This data is used when generating receipt output and is often required for fiscal compliance in Nordic markets.
```
PUT /receipts/{orderId}
```
**Request body:**
```json
{
"sequenceNumber": "1234567",
"cashRegisterName": "Kassa 1",
"controlUnitSerialNumber": "9876543",
"cashierName": "Amanda",
"customerName": "Tom"
}
```
**Request parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `sequenceNumber` | string | Yes | Receipt sequence number from your cash register. |
| `cashRegisterName` | string | Yes | Cash register designation or name. |
| `controlUnitSerialNumber` | string | Yes | Control unit or control system manufacturing number. |
| `cashierName` | string | No | Name of the cashier handling the transaction. |
| `customerName` | string | No | Name of the customer. |
**Response:**
```json
{
"status": "SUCCESS",
"message": "Receipt information added successfully"
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | `SUCCESS` or `ERROR`. |
| `message` | string | Human-readable status message. |
## Emailing Receipts
Send a digital receipt directly to a customer's email address. This is the simplest way to deliver post-payment confirmation without any printing hardware.
```
PUT /receipts/{id}/email
```
The `{id}` path parameter accepts a Transaction ID, Payment ID, or Order ID.
**Request body:**
```json
{
"email": "customer@example.com"
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `email` | string | Yes | Email address to deliver the receipt to. |
**Response:**
```json
{
"status": "SUCCESS",
"message": "Receipt email sent successfully"
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | Status of the request. |
| `message` | string | Description of the result. |
> **Tip:** You can call this endpoint multiple times with different email addresses if the customer or merchant both need a copy.
## Fetching a Receipt Link
Retrieve a URL that points to a hosted digital receipt. This is useful when you want to display a QR code on the terminal screen, include a link in an SMS, or embed it in your own notification flow.
```
GET /receipts/{id}/link
```
The `{id}` path parameter accepts a Transaction ID, Payment ID, or Order ID.
**Request body:** None (empty `GET` request).
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"receiptURL": "https://receipts.surfboardpayments.com/r/abc123xyz"
},
"message": "Receipt link fetched successfully"
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | `SUCCESS` or `ERROR`. |
| `data.receiptURL` | string | URL to access the hosted digital receipt. |
| `message` | string | Description of the result. |
## Printing Receipts on a Terminal
Print a receipt directly 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).
```
PUT /receipts/{id}/print
```
The `{id}` path parameter accepts a Transaction ID, Payment ID, or Order ID.
**Request body:**
```json
{
"templateId": "default",
"terminalId": "trm_abc123",
"language": "sv"
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `templateId` | string | No | Select from Surfboard's default set of receipt templates. |
| `terminalId` | string | No | Target a specific printing-enabled terminal. If omitted, prints on the terminal that handled the transaction. |
| `language` | string | No | Receipt language. Available: `sv`, `da`, `fi`, `en`. Defaults to the merchant's configured language. |
**Response:**
```json
{
"status": "SUCCESS",
"message": "Receipt print command sent successfully"
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | Status of the request. |
| `message` | string | Description of the result. |
> **Note:** A `SUCCESS` response means the print command was dispatched to the terminal. The terminal must be online and not processing another command for the receipt to print.
## Custom ESC/POS Printing
For full control over receipt layout and branding, send raw ESC/POS commands to a terminal's built-in printer. This lets you design completely custom receipts -- including logos, formatted tables, QR codes, and styled text -- using the industry-standard ESC/POS command set.
```
PUT /receipts/{terminalId}/escpos
```
Note that this endpoint uses the `terminalId` directly in the path, not a transaction or order ID.
**Request body:**
```json
{
"escposCommands": "G0AbYQEbRQFTdXBlciBNYXJ0CjEyMyBNYWluIFN0ChtFABthAERhdGU6IDIwMjQvMTAvMDgKVGltZTogMTI6MDAgUE0KG0UBLS0tLS0tLS0tLQobRQAbYQBJdGVtIEE6IFdhdGVyClByaWNlOiAkMS4wMApJdGVtIEI6IEJyZWFkClByaWNlOiAkMi4wMAobRQEtLS0tLS0tLS0tClRvdGFsOiAkMy4wMAobRQAbYQFUaGFuayB5b3UhCgoKHVYA",
"codePages": "UTF-8"
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `escposCommands` | string | Yes | A stream of ESC/POS commands encoded as a Base64 string. |
| `codePages` | string | No | Send `UTF-8` to opt in to the validated ESC/POS contract. Omitting it keeps the deprecated legacy flow. |
**Response:**
```json
{
"status": "SUCCESS",
"message": "ESC/POS receipt sent to terminal"
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | Status of the request. |
| `message` | string | Description of the result. |
### Building ESC/POS Commands
ESC/POS is a command protocol originally developed by Epson and now supported by most thermal receipt printers. A few common commands:
| Command | Hex | Description |
|---------|-----|-------------|
| Initialize printer | `1B 40` | Reset printer to default settings. |
| Bold on | `1B 45 01` | Enable bold text. |
| Bold off | `1B 45 00` | Disable bold text. |
| Center align | `1B 61 01` | Center-align subsequent text. |
| Left align | `1B 61 00` | Left-align subsequent text. |
| Cut paper | `1D 56 00` | Full cut of the receipt paper. |
**Workflow:**
1. Compose your ESC/POS byte stream (text interspersed with control commands).
2. Encode the entire byte stream as a Base64 string.
3. Send the Base64 string in the `escposCommands` field, with `"codePages": "UTF-8"`.
> **Note:** The table above is a starting point, not the full picture. Once you send `"codePages": "UTF-8"`, payloads are validated against a defined contract: UTF-8 text, a fixed command set, and line widths that vary by text size. Commands outside that set -- including `GS v 0` raster images and `ESC t` charset selection -- are rejected before they reach the terminal. See the [ESC/POS Printing](/developers/guides/escpos-printing) guide for the complete contract, a worked receipt, and a preflight validator.
> **Tip:** Generic ESC/POS libraries (`escpos` for Python, `node-escpos` for Node.js) can generate the byte stream for you, but their defaults often emit raster images and charset commands that the contract rejects. Check what your library actually produces before sending it.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Add receipt information | PUT | `/receipts/{orderId}` |
| Email a receipt | PUT | `/receipts/{id}/email` |
| Fetch receipt link | GET | `/receipts/{id}/link` |
| Print receipt on terminal | PUT | `/receipts/{id}/print` |
| Print custom ESC/POS receipt | PUT | `/receipts/{terminalId}/escpos` |
For full endpoint details, see the [Receipts API](https://developers.surfboardpayments.com/api/receipts) reference documentation.
### Refund an Order
Category: online | Tags: Online, API, Payments, Refunds, In-Store
URL: /developers/guides/refund-an-order
## Overview
A full refund in Surfboard is processed by creating a **new order** with negative quantities and negative amounts, referencing the original order's `orderId` as the `purchaseOrderId` on each line item. When the payment completes, the full amount is returned to the customer.
## When to Use Full Refund
| Scenario | Description |
|----------|-------------|
| **Product return** | Customer returns all items |
| **Service not delivered** | Full service cancellation |
| **Order error** | Wrong order fulfilled entirely |
| **Post-settlement reversal** | Payment already settled, void no longer possible |
> If the payment hasn't settled yet (same day, before 23:00 UTC), consider using [Void a Payment](/developers/guides/void-a-payment) instead -- it's faster and avoids refund processing fees.
## Step 1: Create a Refund Order
Create a new order with negative `quantity` and negative `amount.total` for each line item. Include the original `orderId` as `purchaseOrderId`:
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"referenceId": "refund-order-001",
"orderLines": [
{
"id": "ITEM-001",
"purchaseOrderId": "ORIGINAL_ORDER_ID",
"name": "Nike Shoes",
"quantity": -2,
"amount": {
"regular": 10000,
"total": -20000,
"currency": "752",
"tax": [
{ "amount": 4000, "percentage": 25, "type": "VAT" }
]
}
},
{
"id": "ITEM-002",
"purchaseOrderId": "ORIGINAL_ORDER_ID",
"name": "Apple Pods",
"quantity": -1,
"amount": {
"regular": 20000,
"total": -20000,
"currency": "752",
"tax": [
{ "amount": 4000, "percentage": 25, "type": "VAT" }
]
}
}
],
"totalOrderAmount": {
"regular": 30000,
"total": -30000,
"currency": "752",
"tax": [
{ "amount": 8000, "percentage": 25, "type": "VAT" }
]
},
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD_NP",
"refundProcessingParams": {
"purchasePaymentId": "ORIGINAL_PAYMENT_ID",
"refundReason": "CUSTOMER_INITIATED_RETURN"
}
}
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderId": "83b2ca45889a317b0b",
"paymentId": "83b2ca4564bd500606"
},
"message": "Order created successfully"
}
```
The `terminal$id` only needs to be a **valid** terminal -- it does **not** have to be the same terminal that processed the original purchase.
Key details:
- Set `quantity` to a negative value to indicate a return
- Set `amount.total` to a negative value
- Include the original `purchaseOrderId` on each line item
- `totalOrderAmount.total` must be negative (the refund amount)
## Payment Method for Refunds
Set `paymentMethod` to either the method the customer originally paid with, or `CARD_NP`:
| Original Payment Method | Refund Method |
|------------------------|---------------|
| CARD | `CARD_NP` (recommended) or `CARD` |
| KLARNA | `KLARNA` |
| SWISH | `SWISH` |
| Other digital methods | Same as original |
For card refunds, the two card methods behave differently:
| Method | Behaviour |
|--------|-----------|
| `CARD_NP` | **Card not present.** Refunds straight back to the card that paid -- no terminal interaction. This is the recommended default for card refunds. |
| `CARD` | **Card present.** Triggers a card tap on the terminal, so a card must be physically presented to receive the refund. |
> **Note:** Transaction fees are charged again on refunds.
## Refund Processing Parameters
Pass refund metadata through `refundProcessingParams` inside `initiatePaymentsOptions`:
```json
{
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD_NP",
"refundProcessingParams": {
"purchasePaymentId": "ORIGINAL_PAYMENT_ID",
"refundReason": "CUSTOMER_INITIATED_RETURN"
}
}
}
}
```
| Parameter | Required | Description |
|-----------|----------|-------------|
| `purchasePaymentId` | No | The `paymentId` of the **original purchase** (returned when the original order was created). This is the payment-level reference, distinct from the `purchaseOrderId` you set on each line item. |
| `refundReason` | No | Why the refund is being issued. See the allowed values below. |
| `otherReason` | Conditional | Free-text explanation. **Required when `refundReason` is `OTHER`.** |
### Refund Reasons
| Value | Meaning |
|-------|---------|
| `CUSTOMER_INITIATED_RETURN` | The customer returned the goods or requested the refund. |
| `SUSPECTED_MALFUNCTION` | The product is suspected to be faulty or not working. |
| `SUSPECTED_FRAUD` | The transaction is suspected to be fraudulent. |
| `DUPLICATE_TRANSACTION` | The original charge was a duplicate. |
| `OTHER` | Any other reason -- requires a message in `otherReason`. |
When using `OTHER`, include the explanation:
```json
{
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD_NP",
"refundProcessingParams": {
"purchasePaymentId": "ORIGINAL_PAYMENT_ID",
"refundReason": "OTHER",
"otherReason": "Goodwill credit for delayed delivery"
}
}
}
}
```
## Step 2: Check Refund Status
Verify the refund completed:
```json
GET /orders/:orderId/status
```
The order status will show `PAYMENT_COMPLETED` once the refund is processed. You can also track refund status via [webhooks](/developers/guides/webhooks-notifications).
## Adjustments in Refunds
If the original order included adjustments (tips, discounts), the refund includes them by default. Control this with `includeAdjustmentsForRefund`:
```json
{
"controlFunctions": {
"includeAdjustmentsForRefund": false,
"initiatePaymentsOptions": {
"paymentMethod": "CARD"
}
}
}
```
For partial returns, by default the first refund order includes adjustments (`true`) and subsequent ones do not (`false`).
## Refund via Partner Portal
You can also process refunds through the UI:
1. Log in to **Partner Portal** > **Merchants** > select merchant > **Transactions**
2. Select the transaction to refund
3. Click **Create Refund** > **Full Refund** > **Process Refund**
## Refund FAQ
> **How long after a purchase can I issue a refund?**
> Refunds can be issued up to **90 days** after the original purchase. This limit is enforced by Surfboard across all payment methods -- there is no difference between card, Swish, Klarna, or other methods. If you need to reverse a transaction older than 90 days (e.g., an event ticket refund a year later), it cannot be processed through the API.
> **How long does it take for the customer to receive the refund?**
> Processing time depends on the payment method:
>
> | Payment Method | Refund Timeline |
> |----------------|-----------------|
> | **Card** (CARD, CARD_NP) | Up to 7 days. Depends on the issuer and acquirer fraud systems. |
> | **Swish** (SSWISH, NSWISH) | Instant |
> | **Vipps** (SVIPPS) | Instant |
> | **MobilePay** (SMOBILEPAY) | Up to 10 banking days |
> | **Klarna** (KLARNA) | Up to 10 days |
## Reference
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Partial Refund](/developers/guides/partial-refund)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Partial Refund
Category: online | Tags: Online, API, Payments, Refunds, In-Store
URL: /developers/guides/partial-refund
## Overview
A partial refund returns a portion of the order amount to the customer. Like a full refund, it works by creating a **new order** with negative quantities -- but only for the specific items being returned.
## When to Use Partial Refund
| Scenario | Description |
|----------|-------------|
| **Single item return** | Customer returns one item from a multi-item order |
| **Partial quantity** | Customer returns 1 of 3 identical items |
| **Price adjustment** | Discount applied after purchase |
| **Damaged goods** | Partial compensation for a defective item |
## Step 1: Create a Partial Refund Order
Include only the line items being refunded, with negative `quantity` and negative `amount.total`. Reference the original order's `orderId` as `purchaseOrderId`:
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"referenceId": "partial-refund-001",
"orderLines": [
{
"id": "ITEM-002",
"purchaseOrderId": "ORIGINAL_ORDER_ID",
"name": "Apple Pods",
"quantity": -1,
"amount": {
"regular": 20000,
"total": -20000,
"currency": "752",
"tax": [
{ "amount": 4000, "percentage": 25, "type": "VAT" }
]
}
}
],
"totalOrderAmount": {
"regular": -20000,
"total": -20000,
"currency": "752",
"tax": [
{ "amount": 4000, "percentage": 25, "type": "VAT" }
]
},
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD_NP",
"refundProcessingParams": {
"purchasePaymentId": "ORIGINAL_PAYMENT_ID",
"refundReason": "CUSTOMER_INITIATED_RETURN"
}
}
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderId": "83c4db56990b428c0b",
"paymentId": "83c4db5674ce610706"
},
"message": "Order created successfully"
}
```
The `terminal$id` only needs to be a **valid** terminal -- it does **not** have to be the same terminal that processed the original purchase.
## Payment Method for Refunds
Set `paymentMethod` to either the method the customer originally paid with, or `CARD_NP`:
| Original Payment Method | Refund Method |
|------------------------|---------------|
| CARD | `CARD_NP` (recommended) or `CARD` |
| KLARNA | `KLARNA` |
| SWISH | `SWISH` |
| Other digital methods | Same as original |
For card refunds, the two card methods behave differently:
| Method | Behaviour |
|--------|-----------|
| `CARD_NP` | **Card not present.** Refunds straight back to the card that paid -- no terminal interaction. This is the recommended default for card refunds. |
| `CARD` | **Card present.** Triggers a card tap on the terminal, so a card must be physically presented to receive the refund. |
> **Note:** All payment methods except NSWISH, SVIPPS, and SMOBILEPAY support partial refunds.
## Refund Processing Parameters
Pass refund metadata through `refundProcessingParams` inside `initiatePaymentsOptions`:
```json
{
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD_NP",
"refundProcessingParams": {
"purchasePaymentId": "ORIGINAL_PAYMENT_ID",
"refundReason": "CUSTOMER_INITIATED_RETURN"
}
}
}
}
```
| Parameter | Required | Description |
|-----------|----------|-------------|
| `purchasePaymentId` | No | The `paymentId` of the **original purchase** (returned when the original order was created). This is the payment-level reference, distinct from the `purchaseOrderId` you set on each line item. |
| `refundReason` | No | Why the refund is being issued. See the allowed values below. |
| `otherReason` | Conditional | Free-text explanation. **Required when `refundReason` is `OTHER`.** |
### Refund Reasons
| Value | Meaning |
|-------|---------|
| `CUSTOMER_INITIATED_RETURN` | The customer returned the goods or requested the refund. |
| `SUSPECTED_MALFUNCTION` | The product is suspected to be faulty or not working. |
| `SUSPECTED_FRAUD` | The transaction is suspected to be fraudulent. |
| `DUPLICATE_TRANSACTION` | The original charge was a duplicate. |
| `OTHER` | Any other reason -- requires a message in `otherReason`. |
When using `OTHER`, include the explanation:
```json
{
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD_NP",
"refundProcessingParams": {
"purchasePaymentId": "ORIGINAL_PAYMENT_ID",
"refundReason": "OTHER",
"otherReason": "Goodwill credit for delayed delivery"
}
}
}
}
```
## Step 2: Check Refund Status
Verify the partial refund completed:
```json
GET /orders/:orderId/status
```
Track refund status via the API response or through [webhooks](/developers/guides/webhooks-notifications).
## Partial Refund via Partner Portal
1. Log in to **Partner Portal** > **Merchants** > select merchant > **Transactions**
2. Select the transaction to refund
3. Click **Create Refund** > **Partial Refund**
4. Choose **Select Line Items** or **Enter Custom Amount**
5. **Process Refund** with a refund reason
## Multiple Partial Refunds
You can issue multiple partial refunds against the same original order. Each refund creates a separate return order referencing the same `purchaseOrderId`.
When the original order included adjustments (tips, discounts), the first partial refund includes adjustments by default. Subsequent partial refunds do not. Override this with `includeAdjustmentsForRefund` in `controlFunctions`.
## Reference
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Refund an Order](/developers/guides/refund-an-order)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Tips Configuration
Category: in-store | Tags: In-Store, API, Tips, Configuration, Terminal
URL: /developers/guides/tips-configuration
## Overview
Surfboard Payments provides flexible tipping capabilities across all native Android payment terminals, including SurfTouch, SurfPad, SurfPrint, and SoftPOS. You can enable tips, define preset percentage options, allow custom amounts, and control how tip values are displayed to customers -- all through the API.
Tip settings follow a hierarchical model. Configuration set at a higher level acts as the default for everything below it, while lower-level settings override higher-level ones. This lets you define a baseline across your entire merchant account and then fine-tune individual stores or terminals as needed.
## Configuration Hierarchy
Settings cascade downward and lower levels always take precedence:
```
Partner (default)
└── Merchant
└── Store
└── Terminal (highest priority)
```
**How the hierarchy works:**
- If a terminal has its own tip config, that config is used -- regardless of what is set at the store, merchant, or partner level.
- If a terminal has no config, the system checks the store level, then the merchant level, and finally falls back to the partner-level default.
- Each parameter is resolved independently. You can set `tipLevel1` at the merchant level and override only `tipLevel2` at a specific store.
## Configuration Parameters
All three levels (merchant, store, terminal) accept the same set of parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| `tipConfig` | string | Enable or disable tips. Values: `ENABLED`, `DISABLED`. |
| `tipLevel1` | number | First preset tip percentage shown to the customer (e.g., `10` for 10%). |
| `tipLevel2` | number | Second preset tip percentage (e.g., `20` for 20%). |
| `tipLevel3` | number | Third preset tip percentage (e.g., `30` for 30%). |
| `freeAmountEnabled` | boolean | When `true`, customers can enter a custom tip amount. |
| `defaultCustomAmount` | number | Pre-filled custom amount shown when `freeAmountEnabled` is `true`. |
| `displayCalculatedAmount` | string | Show the calculated tip in the local currency on screen. Values: `ENABLED`, `DISABLED`. |
| `tipDisplayFormat` | string | How tip options are presented. Values: `PERCENTAGE`, `AMOUNT`. |
> **Note:** All parameters are optional on every request. You can update a single field without resending the entire configuration. The system merges your changes with the existing config.
## Setting Merchant-Level Tips
Apply a tip configuration to all terminals registered under a merchant. This is the best starting point when you want a consistent tipping experience across every location.
```
PATCH /merchants/{merchantId}/tips
```
**Request body:**
```json
{
"tipConfig": "ENABLED",
"tipLevel1": 10,
"tipLevel2": 15,
"tipLevel3": 20,
"freeAmountEnabled": true,
"defaultCustomAmount": 50,
"displayCalculatedAmount": "ENABLED",
"tipDisplayFormat": "PERCENTAGE"
}
```
**Response:**
```json
{
"status": "SUCCESS",
"message": "Merchant tip configuration updated successfully"
}
```
### Fetching Merchant-Level Tips
Retrieve the current tip configuration for a merchant.
```
GET /merchants/{merchantId}/tips
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"tipConfig": "ENABLED",
"tipLevel1": 10,
"tipLevel2": 15,
"tipLevel3": 20,
"defaultCustomAmount": 50,
"displayCalculatedAmount": "ENABLED",
"tipDisplayFormat": "PERCENTAGE"
},
"message": "Merchant tip configuration fetched successfully"
}
```
## Setting Store-Level Tips
Override the merchant defaults for a specific store. Useful when different locations have different tipping norms -- for example, a restaurant store might offer higher preset percentages than a retail store under the same merchant.
```
PATCH /merchants/{merchantId}/stores/{storeId}/tips
```
**Request body:**
```json
{
"tipConfig": "ENABLED",
"tipLevel1": 15,
"tipLevel2": 20,
"tipLevel3": 25
}
```
**Response:**
```json
{
"status": "SUCCESS",
"message": "Store tip configuration updated successfully"
}
```
### Fetching Store-Level Tips
```
GET /merchants/{merchantId}/stores/{storeId}/tips
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"tipConfig": "ENABLED",
"tipLevel1": 15,
"tipLevel2": 20,
"tipLevel3": 25,
"defaultCustomAmount": 50,
"displayCalculatedAmount": "ENABLED",
"tipDisplayFormat": "PERCENTAGE"
},
"message": "Store tip configuration fetched successfully"
}
```
> **Note:** The response includes all effective values, including those inherited from the merchant level (such as `defaultCustomAmount` and `displayCalculatedAmount` in this example).
## Setting Terminal-Level Tips
Apply a tip configuration to a single terminal. Terminal-level settings have the highest priority and override everything above them.
```
PATCH /merchants/{merchantId}/terminals/{terminalId}/tips
```
**Request body:**
```json
{
"tipConfig": "ENABLED",
"tipLevel1": 5,
"tipLevel2": 10,
"tipLevel3": 15,
"freeAmountEnabled": false,
"tipDisplayFormat": "AMOUNT"
}
```
**Response:**
```json
{
"status": "SUCCESS",
"message": "Terminal tip configuration updated successfully"
}
```
### Fetching Terminal-Level Tips
```
GET /merchants/{merchantId}/terminals/{terminalId}/tips
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"tipConfig": "ENABLED",
"tipLevel1": 5,
"tipLevel2": 10,
"tipLevel3": 15,
"freeAmountEnabled": false,
"displayCalculatedAmount": "ENABLED",
"tipDisplayFormat": "AMOUNT"
},
"message": "Terminal tip configuration fetched successfully"
}
```
## Example: Multi-Level Configuration
Consider a restaurant chain with one merchant account, two stores, and several terminals. The merchant enables tips at 10/15/20%, the fine dining store overrides to 15/20/25%, and the bar terminal at that store switches to amount display with no custom entry:
```
Merchant "Nordic Bistro Group" → ENABLED, 10/15/20%, PERCENTAGE
└── Store "Casual Eatery" → inherits merchant config
└── Terminal "Checkout 1" → 10% / 15% / 20%, PERCENTAGE
└── Terminal "Checkout 2" → 10% / 15% / 20%, PERCENTAGE
└── Store "Fine Dining" → overrides to 15/20/25%
└── Terminal "Table POS" → 15% / 20% / 25%, PERCENTAGE
└── Terminal "Bar POS" → 15% / 20% / 25%, AMOUNT, no custom
```
Each terminal resolves its effective config by merging all levels, with the most specific setting winning.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Set merchant tips | PATCH | `/merchants/{merchantId}/tips` |
| Fetch merchant tips | GET | `/merchants/{merchantId}/tips` |
| Set store tips | PATCH | `/merchants/{merchantId}/stores/{storeId}/tips` |
| Fetch store tips | GET | `/merchants/{merchantId}/stores/{storeId}/tips` |
| Set terminal tips | PATCH | `/merchants/{merchantId}/terminals/{terminalId}/tips` |
| Fetch terminal tips | GET | `/merchants/{merchantId}/terminals/{terminalId}/tips` |
For full endpoint details, see the [Terminals API](https://developers.surfboardpayments.com/api/terminals) and [Merchants API](https://developers.surfboardpayments.com/api/merchants) reference documentation.
### ESC/POS Printing
Category: in-store | Tags: In-Store, API, Receipts, Printing, ESC/POS, UTF-8
URL: /developers/guides/escpos-printing
## Overview
Surfboard is standardising ESC/POS printing across every payment terminal. You opt in by adding a single field to the print request you already send. In return, your payload is validated before it reaches hardware -- so you get a clear API error instead of a garbled receipt -- and the output is adapted automatically to every terminal model, current and legacy.
The opt-in is deliberately small. The work is in the rules the contract enforces, which this guide covers: UTF-8 text, a fixed set of supported commands, and line widths that depend on the text size you select.
> **Note:** This guide covers the ESC/POS contract specifically. For the other ways to deliver a receipt -- email, hosted link, or Surfboard's own templates -- see the [Receipts](/developers/guides/receipts) guide.
## Prerequisites
- A Surfboard developer account with valid API credentials (`API-KEY` and `API-SECRET`)
- A registered terminal with printing capability (SurfTouch with dock, or SurfPrint)
- The terminal's `terminalId`
## Opting in
Add `codePages` to your existing ESC/POS request:
```
PUT /receipts/{terminalId}/escpos
```
**Request body:**
```json
{
"escposCommands": "",
"codePages": "UTF-8"
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `escposCommands` | string | Yes | Your ESC/POS byte stream, Base64-encoded. |
| `codePages` | string | No | `UTF-8` is the only accepted value. Omitting it keeps the legacy flow. |
Note that this endpoint takes a `terminalId` in the path, not a transaction or order ID.
Three things follow from opting in:
- **Validation is atomic.** An invalid stream is rejected whole with `PR_0006`, quoting the byte offset and the offending command. Nothing partial ever prints.
- **Request bodies cap at 75 KB**, which is roughly 55 KB of decoded ESC/POS.
- **The legacy flow is deprecated** and will be removed once migration completes. Omitting `codePages` keeps you on it for now.
## Character encoding
All text must be UTF-8. Do not send Latin-1 or CP1252 bytes, and do not send any codepage or charset selection command -- the platform manages character encoding per device, which is precisely what makes one payload work across mixed hardware.
The quickest way to confirm your encoding is to check a single character. `ä` must be two bytes:
```
ä = C3 A4 ✅ UTF-8
ä = E4 ❌ Latin-1, will be rejected
```
For reference, the Nordic characters most likely to appear on a receipt:
| Lowercase | UTF-8 bytes | Uppercase | UTF-8 bytes |
|---|---|---|---|
| `ä` | `C3 A4` | `Ä` | `C3 84` |
| `å` | `C3 A5` | `Å` | `C3 85` |
| `ö` | `C3 B6` | `Ö` | `C3 96` |
If you are migrating from a charset-based integration, translate national characters to their real UTF-8 form rather than relying on the old substitutions -- `{` meaning `ä`, `}` meaning `å`, and so on. Those substitutions depended on a charset command that is no longer accepted.
## Line widths
Line width depends on the terminal's paper and on the text size active at that point in the stream. Count characters, not bytes: `Örsundsbro väg 1` is 16 characters even though it is 18 bytes.
| Context | Payment terminals (58mm) | Printer terminals (80mm) |
|---|---|---|
| Normal text (Font A) | **32** | **48** |
| Fine print (Font B) | **42** | -- |
| Big text (2×) | **16** | **24** |
| Biggest text (3×) | **10** | **16** |
Lines longer than the budget wrap mid-word, which is almost never what you want on a receipt. Because the fonts are monospace, align columns by padding with spaces rather than tabs.
Images are capped at **384 dots wide on every device**, regardless of paper width.
## Supported commands
| Purpose | Command | Bytes (hex) | Notes |
|---|---|---|---|
| Initialise | `ESC @` | `1B 40` | Required as the first bytes of every job |
| Line feed | `LF` | `0A` | Ends and prints a line |
| Feed n lines | `ESC d n` | `1B 64 n` | Use `1B 64 04` at the end so the receipt clears the tear bar |
| Paper cut | `GS V 0` | `1D 56 00` | Cuts on 80mm printer terminals; safely ignored on handheld terminals, which have no cutter. `1D 56 42 n` (feed-then-cut) also accepted |
| Bold on/off | `ESC ! 08` / `ESC ! 00` | `1B 21 08` / `1B 21 00` | `ESC E 1` / `ESC E 0` (`1B 45 n`) also accepted. One bold level only |
| Fine print (Font B) | `ESC ! 01` / `ESC ! 00` | `1B 21 01` / `1B 21 00` | `ESC M n` (`1B 4D n`) also accepted |
| Bold + Font B | `ESC ! 09` | `1B 21 09` | `ESC !` bits: `0x01` Font B, `0x08` bold |
| Text size | `GS ! n` | `1D 21 00` / `1D 21 11` / `1D 21 22` | Normal / Big (2×) / Biggest (3×) |
| Underline on/off | `ESC - 1` / `ESC - 0` | `1B 2D n` | Renders on current terminals; legacy prints the text without the underline. Send *after* any `ESC !` on the same line |
| Alignment | `ESC a n` | `1B 61 00/01/02` | Left / centre / right. Text only -- images always print left-aligned |
| Line spacing | `ESC 3 24` / `ESC 2` | `1B 33 18` / `1B 32` | For wrapping image stripes only |
| QR code | `GS ( k` | see below | The only QR method |
| Image / logo | `ESC *` mode 33 | see below | The only image format |
Two rules are worth stating separately, because they are the ones existing integrations most often break:
- **Size is set only via `GS !`**, never via the size bits of `ESC !`. Nothing above `GS ! 22` is accepted.
- **Anything not in the table above is rejected.** That includes `GS v 0` (raster images), `ESC t` / `ESC u` / `ESC R` (charset selection), `GS B` (reverse print), and any unknown command. These can corrupt or damage terminals, so the API blocks them before they reach hardware.
## QR codes
Send the data as a string and let the terminal render it. Five commands, in order:
```
1D 28 6B 04 00 31 41 32 00 # model 2
1D 28 6B 03 00 31 43 06 # module size: 6 dots (1-16)
1D 28 6B 03 00 31 45 31 # error correction: 30=L 31=M 32=Q 33=H
1D 28 6B pL pH 31 50 30 # store data; pL + pH*256 = len(data) + 3
1D 28 6B 03 00 31 51 30 # print
```
The only part that varies is the fourth line's length prefix. For 20 bytes of data, `pL + pH*256` is 23, so `pL = 17` (hex) and `pH = 00`.
## Images and logos
Monochrome only, maximum 384 dots wide. Encode the image as 24-dot-tall horizontal stripes in column format:
```
1B 33 18 # line spacing = 24 dots, so stripes butt together
for each 24-row stripe:
1B 2A 21 nL nH # nL + nH*256 = width in dots; data = width × 3 bytes
0A # LF after each stripe
1B 32 # restore default line spacing
```
In column format each of the `width` columns contributes 3 bytes, making 24 vertical dots, with the most significant bit at the top. Pixel `(x, y)` within a stripe lives in byte `x*3 + y/8` at bit `7 - y%8`.
Images always print left-aligned, so `ESC a` will not centre them. To centre one, prepend blank columns: `pad = (384 - width) / 2`.
## A complete receipt
This builds a 259-byte receipt with real Swedish characters, correct column alignment, and a fine-print tax line. Every line is inside its budget for the size active on it.
```python
ESC, GS = 0x1B, 0x1D
out = bytearray()
raw = out.extend
txt = lambda s: out.extend(s.encode("utf-8"))
raw(bytes([ESC, 0x40])) # init
raw(bytes([ESC, 0x61, 0x01])) # centre
raw(bytes([GS, 0x21, 0x11])) # big (2x)
raw(bytes([ESC, 0x21, 0x08])) # bold
txt("Kaffebaren\n") # 10 chars, budget 16 at 2x
raw(bytes([GS, 0x21, 0x00])) # size back to normal
raw(bytes([ESC, 0x21, 0x00])) # style off
txt("Örsundsbro väg 1\n") # 16 chars, budget 32
raw(bytes([ESC, 0x61, 0x00])) # left
txt("-" * 32 + "\n")
txt(f"{'Bryggkaffe':<26}{'29,00':>6}\n")
txt(f"{'Kanelbulle':<26}{'35,00':>6}\n")
txt("-" * 32 + "\n")
raw(bytes([ESC, 0x21, 0x08])) # bold
txt(f"{'Totalt':<22}{'64,00 SEK':>10}\n")
raw(bytes([ESC, 0x21, 0x00]))
raw(bytes([ESC, 0x21, 0x01])) # fine print (Font B, 42 cols)
txt("Moms 12% ingår med 6,86 SEK\n")
raw(bytes([ESC, 0x21, 0x00]))
raw(bytes([ESC, 0x64, 0x04])) # feed out past the tear bar
import base64
print(base64.b64encode(bytes(out)).decode())
```
Which prints as (the first line is double-width, so its ten characters occupy twenty of the thirty-two columns):
```
Kaffebaren
Örsundsbro väg 1
--------------------------------
Bryggkaffe 29,00
Kanelbulle 35,00
--------------------------------
Totalt 64,00 SEK
Moms 12% ingår med 6,86 SEK
```
And sends as:
```json
{
"escposCommands": "G0AbYQEdIREbIQhLYWZmZWJhcmVuCh0hABshAMOWcnN1bmRzYnJvIHbDpGcgMQobYQAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpCcnlnZ2thZmZlICAgICAgICAgICAgICAgICAyOSwwMApLYW5lbGJ1bGxlICAgICAgICAgICAgICAgICAzNSwwMAotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQobIQhUb3RhbHQgICAgICAgICAgICAgICAgIDY0LDAwIFNFSwobIQAbIQFNb21zIDEyJSBpbmfDpXIgbWVkIDYsODYgU0VLChshABtkBA==",
"codePages": "UTF-8"
}
```
**Response:**
```json
{
"status": "SUCCESS",
"message": "ESC/POS receipt sent to terminal"
}
```
## Validating before you send
`PR_0006` tells you exactly what is wrong, but catching it in your own test suite is faster than catching it against a terminal. This preflight checks the three things that actually break integrations -- an out-of-contract command, non-UTF-8 text, and an over-long line:
```python
ESC, GS, LF = 0x1B, 0x1D, 0x0A
WIDTH = {0x00: 32, 0x11: 16, 0x22: 10} # GS ! n -> chars per line, 58mm Font A
SKIP = {0x40: 2, 0x64: 3, 0x61: 3, 0x21: 3, 0x45: 3, 0x4D: 3, 0x2D: 3, 0x33: 3, 0x32: 2}
def preflight(data):
errs, i, size, font_b, line, start = [], 0, 0x00, False, bytearray(), 0
if data[:2] != bytes([ESC, 0x40]):
errs.append("stream must begin with ESC @ (1B 40)")
while i < len(data):
b = data[i]
if b == LF:
if line:
try:
s = line.decode("utf-8")
budget = 42 if (font_b and size == 0x00) else WIDTH[size]
if len(s) > budget:
errs.append(f"byte {start}: {len(s)} chars exceeds {budget}: {s!r}")
except UnicodeDecodeError:
errs.append(f"byte {start}: line is not valid UTF-8")
line, i = bytearray(), i + 1
continue
if b == ESC and data[i+1] in SKIP:
n = data[i+1]
if n == 0x21:
if data[i+2] & ~0x09:
errs.append(f"byte {i}: ESC ! sets size bits; use GS ! for size")
font_b = bool(data[i+2] & 0x01)
i += SKIP[n]
continue
if b == GS and data[i+1] == 0x21:
if data[i+2] not in WIDTH:
errs.append(f"byte {i}: GS ! {data[i+2]:02X} must be 00, 11 or 22")
else:
size = data[i+2]
i += 3
continue
if b in (ESC, GS):
errs.append(f"byte {i}: unsupported command {b:02X} {data[i+1]:02X}")
i += 2
continue
if not line:
start = i
line.append(b)
i += 1
return errs
```
Handle `ESC *` images, `GS ( k` QR blocks, and `GS V` cuts before this runs, or extend it to skip over them -- their payloads contain arbitrary bytes that would otherwise be read as text.
## Errors
| Code | Meaning | Action |
|---|---|---|
| `PR_0006` | Out-of-contract command. The message names the command, its byte offset, and what to use instead: `unsupported ESC/POS command 1D 76 at byte offset 214` | Replace the command. `1D 76` is raster imaging -- use `ESC *` mode 33 |
| `codePages must be 'UTF-8' when provided` | Wrong opt-in value | Send exactly `"UTF-8"` |
> **Note:** Because validation is atomic, a `PR_0006` means nothing printed at all. There is no half-receipt to clear from the printer.
## Migration checklist
- [ ] Add `"codePages": "UTF-8"` to your print requests
- [ ] Encode all text as UTF-8 -- test that `ä` is two bytes (`C3 A4`), not one (`E4`)
- [ ] Remove charset commands (`ESC t`, `ESC u`, `ESC R`) and translate national characters to real UTF-8
- [ ] Replace `GS v 0` raster images with `ESC *` mode 33 stripes
- [ ] Keep `GS V 0` and `GS V 66 n` cuts; remove any other cut variant (`ESC i`, `ESC m`)
- [ ] Set size only with `GS ! 00`, `11`, or `22`, and respect the width for each
- [ ] Format normal-text lines to 32 characters
- [ ] End every job with `ESC d 4` so the receipt clears the tear bar
> **Tip:** Generic ESC/POS libraries are a common source of rejections, because their defaults often emit raster images and charset commands. Check what your library actually produces before assuming it is in contract -- the preflight above will tell you.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Print custom ESC/POS receipt | PUT | `/receipts/{terminalId}/escpos` |
For the full endpoint reference, see the [Receipts API](https://developers.surfboardpayments.com/api/receipts) documentation. For the other receipt delivery methods, see the [Receipts](/developers/guides/receipts) guide.
### NFC Tag Reading
Category: in-store | Tags: In-Store, API, NFC, RFID, Terminal
URL: /developers/guides/nfc-tag-reading
## Overview
The 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.
## How It Works
1. **Create a session** -- Start a reading session on a terminal, choosing single-tag or multi-tag mode.
2. **Read tags** -- The terminal scans NFC tags as products are presented.
3. **Retrieve results** -- Fetch scanned tags or poll the session status.
4. **Complete the session** -- Close the session when all items have been scanned.
You can also tie NFC reading into the order creation flow by including `readTags` in your order request (covered below).
## Creating a Read Session
Start an NFC reading session on a terminal by specifying the scanning mode.
```
POST /terminals/{terminalId}/sessions
```
**Request:**
```json
{
"mode": "single"
}
```
| Parameter | Type | Required | Description |
|-----------|--------|----------|-------------|
| `mode` | string | Yes | Number of tags the session can read. Possible values: `single` (one tag only) or `multiple` (continuous reading until completed). |
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"sessionId": "ses_a1b2c3d4e5"
},
"message": "Session created successfully"
}
```
| Parameter | Type | Description |
|------------------|--------|-------------|
| `status` | string | `SUCCESS` or `ERROR`. |
| `data.sessionId` | string | Unique identifier for the NFC reading session. Use this ID in all subsequent session calls. |
| `message` | string | Human-readable description of the result. |
Use `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.
## Fetching Session Status
Check the current state of an NFC reading session to determine whether it is still active, has timed out, or has been completed.
```
GET /terminals/{terminalId}/sessions/{sessionId}/status
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"sessionStatus": "PENDING",
"nfcTags": ["dGFnLWRhdGEtYmFzZTY0"]
},
"message": "Session status retrieved successfully"
}
```
| Parameter | Type | Description |
|----------------------|--------|-------------|
| `status` | string | `SUCCESS` or `ERROR`. |
| `data.sessionStatus` | string | Current session state. Possible values: `PENDING`, `COMPLETED`, `CANCELLED`, `TIMED_OUT`, `NOT_FOUND`. |
| `data.nfcTags` | array | List of NFC tag values read so far (Base64-encoded RFID data). |
| `message` | string | Human-readable description of the result. |
You 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).
## Listing Sessions for a Terminal
Retrieve all NFC reading sessions that have been created on a specific terminal. This is useful for auditing or reviewing past scanning activity.
```
GET /terminals/{terminalId}/sessions
```
**Response:**
```json
{
"status": "SUCCESS",
"data": [
{
"sessionId": "ses_a1b2c3d4e5",
"action": "start",
"mode": "multiple"
},
{
"sessionId": "ses_f6g7h8i9j0",
"action": "end",
"mode": "single"
}
],
"message": "Sessions retrieved successfully"
}
```
| Parameter | Type | Description |
|------------------|--------|-------------|
| `data.sessionId` | string | Unique identifier for the reading session. |
| `data.action` | string | Stage of the session lifecycle. Possible values: `start`, `end`. |
| `data.mode` | string | Tag reading mode used. Possible values: `single`, `multiple`. |
## Retrieving Scanned Tags
Fetch all NFC tags that were read during a specific session. Each tag entry includes the session it belongs to and a unique tag identifier.
```
GET /terminals/{terminalId}/sessions/{sessionId}/tags
```
**Response:**
```json
{
"status": "SUCCESS",
"data": [
{
"sessionId": "ses_a1b2c3d4e5",
"tagId": "tag_nike_shoe_001"
},
{
"sessionId": "ses_a1b2c3d4e5",
"tagId": "tag_nike_shoe_002"
}
],
"message": "Tags retrieved successfully"
}
```
| Parameter | Type | Description |
|------------------|--------|-------------|
| `data.sessionId` | string | Session the tag was read under. |
| `data.tagId` | string | Unique identifier of the scanned NFC tag. |
Use these tag IDs to look up product information in your inventory system and build the order accordingly.
## Completing a Session
When all items have been scanned, explicitly complete the session to stop the terminal from listening for additional tags.
```
POST /terminals/{terminalId}/sessions/{sessionId}/complete
```
**Request:**
```json
{
"result": "COMPLETED"
}
```
| Parameter | Type | Required | Description |
|-----------|--------|----------|-------------|
| `result` | string | No | Final state of the session. Defaults to `COMPLETED` if omitted. |
**Response:**
```json
{
"status": "SUCCESS",
"message": "Session completed successfully"
}
```
Always complete sessions when scanning is finished. Uncompleted sessions eventually time out, but explicit completion frees the terminal for new sessions immediately.
## Combining NFC Reading with Orders
You 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.
```
POST /orders
```
Include the `readTags` control function alongside your standard order payload:
```json
{
"terminal$id": "83abab731f6fb00704",
"orderLines": [
{
"id": "0000CHI01",
"name": "Nike Shoes",
"quantity": 1,
"amount": {
"regular": 500,
"total": 500,
"currency": "752"
}
}
],
"totalOrderAmount": {
"regular": 500,
"total": 500,
"currency": "752"
},
"controlFunctions": {
"readTags": "SINGLE",
"initiatePaymentsOptions": {
"paymentMethod": "CASH"
}
}
}
```
The `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.
## Typical Integration Flow
1. Call `POST /terminals/{terminalId}/sessions` with `mode: "multiple"` to start scanning.
2. Present tagged products to the terminal.
3. Poll `GET .../sessions/{sessionId}/status` until `sessionStatus` is no longer `PENDING`.
4. Call `GET .../sessions/{sessionId}/tags` to get all scanned tag IDs.
5. Map tag IDs to products in your inventory system.
6. Call `POST .../sessions/{sessionId}/complete` to close the session.
7. Create the order using the mapped product data.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create reading session | POST | `/terminals/{terminalId}/sessions` |
| Fetch session status | GET | `/terminals/{terminalId}/sessions/{sessionId}/status` |
| List sessions for terminal | GET | `/terminals/{terminalId}/sessions` |
| Retrieve scanned tags | GET | `/terminals/{terminalId}/sessions/{sessionId}/tags` |
| Complete session | POST | `/terminals/{terminalId}/sessions/{sessionId}/complete` |
### Partial Payments
Category: online | Tags: Online, API, Payments, In-Store
URL: /developers/guides/partial-payments
## Overview
Partial 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`.
## When to Use Partial Payments
| Scenario | Description |
|----------|-------------|
| **Mixed payment methods** | Customer pays part by card, part by cash |
| **Gift card + balance** | Gift card covers partial amount, card covers the rest |
| **Split between payers** | Two customers splitting a bill |
| **Installment at POS** | Collecting payment in stages |
## Step 1: Create Order with Partial Amount
Create an order and specify a partial `amount` in `initiatePaymentsOptions`. This initiates the first payment for less than the total:
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"referenceId": "split-order-001",
"orderLines": [
{
"id": "ITEM-001",
"name": "Nike Shoes",
"quantity": 1,
"amount": {
"regular": 50000,
"total": 50000,
"currency": "752",
"tax": [
{ "amount": 5000, "percentage": 10, "type": "VAT" }
]
}
},
{
"id": "ITEM-002",
"name": "Apple Pods",
"quantity": 1,
"amount": {
"regular": 50000,
"total": 50000,
"currency": "752",
"tax": [
{ "amount": 5000, "percentage": 10, "type": "VAT" }
]
}
}
],
"totalOrderAmount": {
"regular": 100000,
"total": 100000,
"currency": "752",
"tax": [
{ "amount": 10000, "percentage": 10, "type": "VAT" }
]
},
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD",
"amount": 50000
}
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderId": "83d5ec67aab053970b",
"paymentId": "83d5ec6784df720806"
},
"message": "Order created successfully"
}
```
The 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.
## Step 2: Initiate Remaining Payments
Use the Initiate Payment API to pay the remaining balance. Specify the `orderId` from the first step:
```json
POST /payments
{
"orderId": "83d5ec67aab053970b",
"paymentMethod": "CARD",
"amount": 50000
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"paymentId": "83d5ec6794ef830906"
},
"message": "Payment initiated successfully"
}
```
You can split across more than two payments -- keep initiating payments until the total equals `totalOrderAmount`.
> An order is considered **complete** only when the sum of all partial payments equals the order's total amount.
## Step 3: Check Order Status
Verify the order is fully paid:
```json
GET /orders/:orderId/status
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderStatus": "PAYMENT_COMPLETED",
"payments": [
{
"paymentId": "83d5ec6784df720806",
"paymentStatus": "PAYMENT_COMPLETED",
"paymentMethod": "CARD",
"amount": 50000
},
{
"paymentId": "83d5ec6794ef830906",
"paymentStatus": "PAYMENT_COMPLETED",
"paymentMethod": "CARD",
"amount": 50000
}
],
"paymentIds": ["83d5ec6784df720806", "83d5ec6794ef830906"]
}
}
```
## Mixing Payment Methods
Each partial payment can use a different method. For example, first payment by card, second by cash:
```json
// First payment (at order creation)
"initiatePaymentsOptions": { "paymentMethod": "CARD", "amount": 50000 }
// Second payment
{ "orderId": "...", "paymentMethod": "CASH", "amount": 50000 }
```
## Reference
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)
- [Create an Order](/developers/guides/create-an-order)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
### Multi-Merchant Terminals
Category: in-store | Tags: In-Store, API, Multi-Merchant, Terminal, Partners
URL: /developers/guides/multi-merchant-terminals
## Overview
A 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.
This 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.
## Use Cases
| Scenario | Description |
|----------|-------------|
| **Food courts** | Multiple food vendors in a shopping centre share terminals at a central checkout or at individual stalls. |
| **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. |
| **Co-working retail spaces** | Small businesses sharing a physical storefront use the same terminal while keeping financials separate. |
| **Seasonal markets** | Temporary setups like Christmas markets or farmers' markets where deploying one terminal per vendor is impractical. |
## Setup Flow
Setting up multi-merchant terminals involves three steps:
1. **Create a multi-merchant group** -- Establishes the shared group and generates a group-level merchant and store.
2. **Add merchants to the group** -- Onboard individual businesses and link them to the group.
3. **Register a terminal** -- Assign a physical device to the group so all linked merchants can process payments.
After setup, each merchant creates orders using their own `merchantId`, but the payment is processed on the shared terminal.
## Step 1: Create a Multi-Merchant Group
Create the group under your partner account. This generates a group-level `merchantId` and `storeId` that you will use when registering shared terminals.
```
POST /partners/{partnerId}/multi-merchant
```
**Request:**
```json
{
"country": "SE",
"zipCode": "123456",
"name": "Central Food Court",
"email": "foodcourt@example.com"
}
```
| Parameter | Type | Required | Description |
|-----------|--------|----------|-------------|
| `country` | string | Yes | Two-letter ISO country code in uppercase (e.g., `SE`, `DK`, `NO`). |
| `zipCode` | string | Yes | ZIP/postal code of the shared location. |
| `name` | string | No | Human-readable name for the group (e.g., "Stockholm Food Hall"). |
| `email` | string | No | Contact email for the multi-merchant group. |
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"applicationId": "app_mm_a1b2c3",
"merchantId": "8353ffb4664d900d0e",
"storeId": "str_mm_d4e5f6"
},
"message": "Multi-merchant group created successfully"
}
```
| Parameter | Type | Description |
|----------------------|--------|-------------|
| `status` | string | `SUCCESS` or `ERROR`. |
| `data.applicationId` | string | Use this ID to track the status of the group creation request. |
| `data.merchantId` | string | Group-level merchant ID. Use this when registering shared terminals (Step 3). |
| `data.storeId` | string | Group-level store ID. Use this when registering shared terminals (Step 3). |
| `message` | string | Human-readable description of the result. |
Save the `merchantId` and `storeId` from this response -- you will need them when registering devices in Step 3.
## Step 2: Add Merchants to the Group
Onboard individual merchants and link them to the multi-merchant group by passing the group's `multiMerchantId` in the standard Create Merchant request.
```
POST /partners/{partnerId}/merchants
```
**Request (minimal):**
```json
{
"country": "SE",
"organisation": {
"corporateId": "5566692092"
},
"multiMerchantId": "8353ffb4664d900d0e",
"controlFields": {
"transactionPricingPlan": "SP_SE_Fix129"
}
}
```
| Parameter | Type | Required | Description |
|----------------------------------------|--------|----------|-------------|
| `country` | string | Yes | Two-letter ISO country code in uppercase. |
| `organisation.corporateId` | string | Yes | Corporate/organisation ID of the merchant being added. |
| `organisation.legalName` | string | Conditional | Legal name of the organisation. Required for Payment Facilitator (PF) partners. |
| `organisation.mccCode` | string | Conditional | Merchant Category Code. Required for PF partners. |
| `multiMerchantId` | string | Yes | The `merchantId` returned from Step 1. Links this merchant to the shared group. |
| `controlFields.transactionPricingPlan` | string | Conditional | Billing plan for transaction costs. Required if more than one plan exists. |
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"applicationId": "app_merch_g7h8i9",
"webKybUrl": "https://kyb.surfboardpayments.com/app_merch_g7h8i9",
"merchantId": "mrc_vendor_001"
},
"message": "Merchant application created successfully"
}
```
| Parameter | Type | Description |
|----------------------|--------|-------------|
| `data.applicationId` | string | Track the merchant onboarding status with this ID. |
| `data.webKybUrl` | string | KYB (Know Your Business) link. Share this with the merchant to complete their verification. |
| `data.merchantId` | string | The merchant's individual ID (PF partners only -- returned when the merchant is created immediately). |
| `data.storeId` | string | The merchant's store ID (PF partners only). |
| `data.shortLinkUrl` | string | Shortened KYB URL, returned when `generateShortLink` is set to `true`. |
Repeat this step for every merchant that should be part of the group. Each merchant completes their own KYB verification independently.
### Adding Merchants via the Partner Portal
You can also add merchants through the Partner Portal UI:
1. Log in to your **Partner Portal** and go to the **Applications** section.
2. Click **Create Application**.
3. Enable the **MultiMerchant** toggle.
4. Select the group from the **MultiMerchantId or name** dropdown.
5. Fill in the required merchant details and click **Create Application**.
6. Share the generated WebKYB link with the merchant.
### Organisation and Address Details
For PF partners or when full merchant details are required, you can include comprehensive organisation information:
```json
{
"country": "SE",
"organisation": {
"corporateId": "5566692092",
"legalName": "Vendor AB",
"mccCode": "5812",
"address": {
"addressLine1": "Storgatan 10",
"city": "Stockholm",
"countryCode": "SE",
"postalCode": "11123"
},
"phoneNumber": {
"code": 46,
"number": "701234567"
},
"email": "vendor@example.com"
},
"multiMerchantId": "8353ffb4664d900d0e",
"controlFields": {
"transactionPricingPlan": "SP_SE_Fix129"
}
}
```
## Step 3: Register a Device to the Group
Once 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.
```
POST /merchants/{merchantId}/stores/{storeId}/devices
```
Use the **group-level** `merchantId` and `storeId` returned in Step 1, not an individual merchant's IDs.
**Request:**
```json
{
"registrationIdentifier": "250901",
"terminalName": "Kiosk One"
}
```
| Parameter | Type | Required | Description |
|--------------------------|--------|----------|-------------|
| `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. |
| `terminalName` | string | Yes | A friendly name for the terminal (e.g., "Food Court Register 1"). |
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"terminalId": "trm_shared_001",
"registrationStatus": "REGISTERED"
},
"message": "Terminal registered successfully"
}
```
| Parameter | Type | Description |
|---------------------------|--------|-------------|
| `data.terminalId` | string | Terminal ID of the registered device. |
| `data.registrationStatus` | string | `REGISTERED` for a new device, `ALREADY_REGISTERED` if the device was previously linked. |
## Processing Payments
After 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:
```json
{
"terminal$id": "trm_shared_001",
"orderLines": [...],
"totalOrderAmount": {...}
}
```
Key points for payment processing on shared terminals:
- **Orders** are tied to the individual merchant's `merchantId`, not the group.
- **Payouts** are settled to each merchant's own bank account.
- **Transaction history** is kept separate per merchant.
- The terminal displays the correct merchant name and receipt details for each transaction.
## Important Considerations
- **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.
- **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.
- **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.
- **Terminal limits.** You can register multiple terminals to the same group. There is no restriction on the number of devices per group.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create multi-merchant group | POST | `/partners/{partnerId}/multi-merchant` |
| Add merchant to group | POST | `/partners/{partnerId}/merchants` |
| Register shared terminal | POST | `/merchants/{merchantId}/stores/{storeId}/devices` |
### Store Management
Category: online | Tags: Online, API, Stores, Domain Verification, Management
URL: /developers/guides/store-management
## Overview
Stores are the organizational units that sit beneath merchants in the Surfboard hierarchy. Every terminal, whether physical or online, is registered under a store. This guide covers the full store lifecycle: creating in-store and online stores, retrieving store details, updating store information, verifying domains for online payments, listing terminals, and deactivating stores you no longer need.
A default store is often created automatically during merchant onboarding. Both merchants and partners can create additional stores at any time through the API or the Partner Portal.
## Prerequisites
- A registered **partner** and **merchant** in the Surfboard system
- Your `partnerId` and `merchantId`
- API credentials (API key and API secret)
## Create an In-Store (Physical) Store
Use the Create Store endpoint to add a new physical store under a merchant. The store will be assigned a unique `storeId` on creation.
```
POST /partners/:partnerId/merchants/:merchantId/stores
```
### Request
```json
{
"storeName": "Stockholm Flagship",
"email": "flagship@example.com",
"phoneNumber": {
"code": 46,
"number": "701234567"
},
"address": "Drottninggatan 10",
"city": "Stockholm",
"zipCode": "103 16",
"country": "SE"
}
```
### Key Request Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `storeName` | string | Yes | Name of the store |
| `email` | string | No | Store email. Mandatory for online payment support |
| `phoneNumber.code` | number | Yes | International dialing code (e.g., `46` for Sweden) |
| `phoneNumber.number` | string | Yes | Phone number, 5-15 digits |
| `address` | string | Yes | Address line 1 |
| `city` | string | Yes | City name |
| `zipCode` | string | Yes | Postal code |
| `country` | string | Yes | Two-letter ISO country code (e.g., `SE`) |
| `acquirerMID` | string | No | Acquirer Merchant ID, required for PF partners with store-based acquiring |
### Response
The response includes the new `storeId` along with the full store object:
```json
{
"status": "SUCCESS",
"data": {
"storeId": "store-abc-123",
"merchantId": "merchant-xyz-789",
"name": "Stockholm Flagship",
"address": {
"addressLine1": "Drottninggatan 10",
"city": "Stockholm",
"countryCode": "SE",
"postalCode": "103 16"
},
"phone": "+46701234567",
"email": "flagship@example.com"
},
"message": "Store created successfully"
}
```
## Create an Online Store
Online stores require additional properties in the `onlineInfo` object to enable e-commerce payment acceptance. You can either create a new online store directly or update an existing physical store to add online capabilities.
```
POST /partners/:partnerId/merchants/:merchantId/stores
```
### Request
```json
{
"storeName": "Web Store",
"email": "webstore@example.com",
"phoneNumber": {
"code": 46,
"number": "701234567"
},
"address": "Drottninggatan 10",
"city": "Stockholm",
"zipCode": "103 16",
"country": "SE",
"onlineInfo": {
"merchantWebshopURL": "https://shop.example.com",
"paymentPageHostURL": "https://shop.example.com/payment",
"termsAndConditionsURL": "https://shop.example.com/terms",
"privacyPolicyURL": "https://shop.example.com/privacy"
}
}
```
### Online Info Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `onlineInfo.merchantWebshopURL` | string | Yes | The merchant's webshop URL |
| `onlineInfo.paymentPageHostURL` | string | No | Payment page URL. Required for SDK mode integration |
| `onlineInfo.termsAndConditionsURL` | string | Yes | URL to terms and conditions (must include refund policy) |
| `onlineInfo.privacyPolicyURL` | string | Yes | URL to the privacy policy |
When an online store is created, the response includes two domain verification keys:
- `merchantURLDomainVerficationKey` -- used to verify ownership of the webshop domain
- `paymentPageURLDomainVerficationKey` -- used to verify the payment page domain (if provided)
You must complete domain verification before the store is approved for online payments.
## Domain Verification
After creating an online store, verify that you own the domains you provided. This is a two-step process.
### Step 1: Set DNS TXT Records
Take the verification keys returned during store creation and add them as **TXT records** on your domain's DNS configuration. Surfboard also performs automatic checks every 6 hours.
### Step 2: Trigger Verification
```
POST /partners/:partnerId/merchants/:merchantId/stores/:storeId/verify
```
```json
{
"domainType": "MERCHANT_WEBSHOP_URL"
}
```
The `domainType` value specifies which domain to verify. Use `MERCHANT_WEBSHOP_URL` for the webshop domain or `PAYMENT_PAGE_HOST_URL` for the payment page domain.
### Check Verification Status
You can retrieve the current domain verification status at any time:
```
GET /partners/:partnerId/merchants/:merchantId/stores/:storeId/online
```
Once verification succeeds, the store enters an internal approval process. After approval, the store can take online payments.
### Default Online Terminals
Creating an online store provisions two online terminals automatically: a **PaymentPage** terminal, used for payment links and hosted checkout, and a **MerchantInitiated** terminal, used for backend charges against a stored token. You do not register either one — list the store's terminals to pick up their IDs. They exist as soon as the store does, but cannot take a payment until the domains verify and the store is approved.
**SelfHostedPage** and **iFrame** terminals are not provisioned. Register those with the [Register Online Terminal](https://developers.surfboardpayments.com/api/terminals) endpoint when you need them:
```
POST /merchants/:merchantId/stores/:storeId/online-terminals
```
```json
{
"onlineTerminalMode": "SelfHostedPage"
}
```
## Fetch Store Details
Retrieve complete information about a specific store, including its status and online onboarding status.
```
GET /partners/:partnerId/merchants/:merchantId/stores/:storeId
```
### Response
```json
{
"status": "SUCCESS",
"data": {
"storeId": "store-abc-123",
"merchantId": "merchant-xyz-789",
"name": "Web Store",
"status": "ACTIVE",
"onlineOnboardingStatus": "APPROVED",
"address": {
"addressLine1": "Drottninggatan 10",
"city": "Stockholm",
"countryCode": "SE",
"postalCode": "103 16"
},
"phone": "+46701234567",
"email": "webstore@example.com",
"onlineInfo": {
"merchantWebshopURL": "https://shop.example.com",
"paymentPageHostURL": "https://shop.example.com/payment",
"termsAndConditionsURL": "https://shop.example.com/terms",
"privacyPolicyURL": "https://shop.example.com/privacy"
}
},
"message": "Store details fetched successfully"
}
```
Store status values: `ACTIVE`, `DEACTIVATED`, `BLOCKED`, `INACTIVE`.
Online onboarding status values: `APPROVED`, `INITIATED`, `FAILED`.
## List All Stores
Retrieve every store registered under a merchant to get a complete overview.
```
GET /partners/:partnerId/merchants/:merchantId/stores
```
The response returns an array of store objects, each with the same structure as the single-store response above.
## Update Store Details
Modify an existing store's name, contact information, address, or add online capabilities. Send only the fields you want to change.
```
PUT /partners/:partnerId/merchants/:merchantId/stores/:storeId
```
### Request
```json
{
"storeName": "Stockholm Flagship - Updated",
"email": "new-email@example.com",
"phoneNumber": {
"code": 46,
"number": "709876543"
}
}
```
All parameters are optional. You can also add `onlineInfo` to convert a physical store into an online store. Note that online info can only be added once.
If you add `onlineInfo` during an update, the response will include the domain verification keys, and you must complete domain verification as described above.
## Fetch Store Terminals
Retrieve all terminals registered under a specific store. You can optionally filter by terminal type.
```
GET /partners/:partnerId/merchants/:merchantId/stores/:storeId/terminals
```
Optional query parameter: `terminalType` (e.g., `surfpad`, `PaymentPage`, `SelfHostedPage`, `MerchantInitiated`).
### Response
```json
{
"status": "SUCCESS",
"data": [
{
"terminalId": "terminal-001",
"terminalType": "PaymentPage",
"terminalStatus": "ACTIVE",
"storeId": "store-abc-123",
"terminalName": "Online Checkout",
"startDate": "2025-06-15T10:00:00Z"
},
{
"terminalId": "terminal-002",
"terminalType": "MerchantInitiated",
"terminalStatus": "ACTIVE",
"storeId": "store-abc-123",
"startDate": "2025-06-15T10:00:00Z"
}
],
"message": "Terminals fetched successfully"
}
```
This is the call that hands you the IDs of the `PaymentPage` and `MerchantInitiated` terminals an online store comes with. An online store returns both from the moment it is created, alongside any physical or SDK terminals you registered yourself.
Terminal types include: `surfpad`, `surftouch`, `surfprint`, `checkoutPro`, `checkoutX`, `PaymentPage`, `SelfHostedPage`, `MerchantInitiated`, `printer`, `surftester`.
Terminal statuses: `REGISTERED`, `ACTIVE`, `IN_ACTIVE`, `DE_REGISTERED`.
## Deactivate a Store
Remove a store that is no longer needed. You can deactivate immediately or schedule deactivation for a future date.
```
DELETE /partners/:partnerId/merchants/:merchantId/stores/:storeId
```
Optional query parameter: `deactivationDate` in `yyyy-mm-dd` format. If omitted, the store is deactivated immediately.
> **Important:** A store can only be deactivated if it has no terminals registered to it. If active terminals exist, you must first delink them or move them to another store under the same merchant. Remember that an online store carries its two default terminals, `PaymentPage` and `MerchantInitiated`, so the terminal list is never empty by default — deactivate those before you deactivate the store.
### Response
```json
{
"status": "SUCCESS",
"message": "Store deactivated successfully"
}
```
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create store | POST | `/partners/:partnerId/merchants/:merchantId/stores` |
| Fetch store details | GET | `/partners/:partnerId/merchants/:merchantId/stores/:storeId` |
| List all stores | GET | `/partners/:partnerId/merchants/:merchantId/stores` |
| Update store | PUT | `/partners/:partnerId/merchants/:merchantId/stores/:storeId` |
| Verify domain | POST | `/partners/:partnerId/merchants/:merchantId/stores/:storeId/verify` |
| Fetch domain status | GET | `/partners/:partnerId/merchants/:merchantId/stores/:storeId/online` |
| Fetch store terminals | GET | `/partners/:partnerId/merchants/:merchantId/stores/:storeId/terminals` |
| Deactivate store | DELETE | `/partners/:partnerId/merchants/:merchantId/stores/:storeId` |
### Gift Cards & Promotions
Category: online | Tags: Online, API, Gift Cards, Promotions, Commerce
URL: /developers/guides/gift-cards-promotions
## Overview
Surfboard Payments provides APIs for two complementary commerce features: **gift cards** for stored-value and entitlement-based programs, and **promotions** for marketing campaigns displayed across merchant channels. This guide covers creating and managing both, with full API details and request/response examples.
## Gift Cards
Gift cards in Surfboard come in two types:
- **FUND** -- A stored monetary balance. Customers spend down the balance over one or more transactions.
- **ENTITLEMENT** -- A usage-limited card. Instead of a cash value, the card grants a fixed number of redemptions (e.g., "5 free coffees").
### Create a Gift Card
```
POST /gift-cards
```
#### FUND Type Request
```json
{
"cardType": "FUND",
"amount": 500,
"currency": "SEK",
"name": "Holiday Gift Card",
"accessControl": "OPEN",
"expiryDate": "12/31/2026",
"note": "Happy Holidays!"
}
```
#### ENTITLEMENT Type Request
```json
{
"cardType": "ENTITLEMENT",
"redemptionLimit": 10,
"name": "Loyalty Reward Card",
"accessControl": "OPEN",
"expiryDate": "06/30/2027",
"note": "Thank you for being a valued customer"
}
```
#### Request Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `cardType` | string | Yes | `FUND` or `ENTITLEMENT` |
| `amount` | number | Conditional | Monetary amount in smallest currency unit. Required for `FUND` type |
| `redemptionLimit` | number | Conditional | Number of allowed uses. Required for `ENTITLEMENT` type |
| `currency` | string | No | ISO currency code (e.g., `SEK`, `EUR`) |
| `name` | string | No | Display name for the gift card |
| `accessControl` | string | No | Access control level (e.g., `OPEN`) |
| `expiryDate` | string | No | Expiry date in `mm/dd/yyyy` or `mm-dd-yyyy` format |
| `note` | string | No | Optional note or message |
#### Response
```json
{
"status": "SUCCESS",
"data": {
"giftCardId": "gc-abc-123",
"pan": "6789012345678901",
"name": "Holiday Gift Card",
"cardType": "FUND",
"amount": 500,
"currency": "SEK",
"accessControl": "OPEN",
"status": "ACTIVE",
"expiryDate": "12/31/2026",
"shareableLink": "https://giftcards.surfboardpayments.com/gc-abc-123",
"formats": {
"qrCode": "data:image/png;base64,...",
"nfcData": "NFC_ENCODED_DATA",
"barcode": "data:image/png;base64,..."
},
"externalId": "ext-001",
"externalIdType": "CUSTOM"
},
"message": "Gift card created successfully"
}
```
The response includes multiple format representations (QR code, NFC data, barcode) for flexible distribution. The `shareableLink` provides a URL that can be sent directly to the recipient.
### List All Gift Cards
Retrieve a paginated list of all gift cards for a merchant, with optional filtering.
```
GET /gift-cards
```
#### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `type` | string | No | Filter by card type: `FUND` or `ENTITLEMENT` |
| `status` | string | No | Filter by card status |
#### Response
```json
{
"status": "SUCCESS",
"data": [
{
"giftCardId": "gc-abc-123",
"pan": "6789012345678901",
"name": "Holiday Gift Card",
"cardType": "FUND",
"amount": 500,
"currentAmount": 350,
"usageCount": 2,
"currency": "SEK",
"accessControl": "OPEN",
"status": "ACTIVE",
"expiryDate": "12/31/2026",
"lastTransactionAt": "2026-01-15T14:30:00Z",
"transactionCount": 2,
"totalRedeemed": 150
}
],
"message": "Gift cards fetched successfully"
}
```
Note the tracking fields: `currentAmount` shows the remaining balance for FUND cards, `usageCount` tracks how many times the card has been used, and `totalRedeemed` shows the cumulative amount spent.
### Get Gift Card Details
Retrieve full details for a single gift card, including customer information and format representations.
```
GET /gift-cards/:id
```
#### Response
```json
{
"status": "SUCCESS",
"data": {
"giftCardId": "gc-abc-123",
"pan": "6789012345678901",
"name": "Holiday Gift Card",
"cardType": "FUND",
"amount": 500,
"currentAmount": 350,
"usageCount": 2,
"currency": "SEK",
"status": "ACTIVE",
"expiryDate": "12/31/2026",
"lastTransactionAt": "2026-01-15T14:30:00Z",
"transactionCount": 2,
"totalRedeemed": 150,
"customerDetails": {
"customerId": "cust-456",
"firstName": "Anna",
"surname": "Svensson",
"countryCode": "SE",
"emails": [{ "email": "anna@example.com" }],
"phoneNumbers": [
{
"phoneNumber": {
"countryCode": "46",
"number": "701234567"
}
}
]
},
"shareableLink": "https://giftcards.surfboardpayments.com/gc-abc-123",
"formats": {
"qrCode": "data:image/png;base64,...",
"nfcData": "NFC_ENCODED_DATA",
"barcode": "data:image/png;base64,..."
}
},
"message": "Gift card details fetched successfully"
}
```
### Get Gift Card Transactions
View the transaction history for a specific gift card. Supports filtering by transaction type and pagination via the `x-page-number` header.
```
GET /gift-cards/:giftCardId/transactions
```
#### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `transactionType` | string | No | Filter by type: `ISSUED`, `CREDIT`, or `DEBIT` |
#### Response
```json
{
"status": "SUCCESS",
"data": [
{
"paymentId": "pay-789",
"transactionType": "DEBIT",
"transactionAmount": 150,
"currency": "SEK",
"valueBefore": 500,
"valueAfter": 350,
"orderId": "order-456",
"merchantId": "merchant-xyz-789",
"storeId": "store-abc-123",
"metadata": {}
},
{
"paymentId": "pay-001",
"transactionType": "ISSUED",
"transactionAmount": 500,
"currency": "SEK",
"valueBefore": 0,
"valueAfter": 500,
"merchantId": "merchant-xyz-789",
"metadata": {}
}
],
"message": "Transactions fetched successfully"
}
```
Each transaction record shows the `valueBefore` and `valueAfter` fields, giving a clear audit trail of the gift card balance over time.
## Promotions
Promotions let you create and manage marketing campaigns that appear across merchant channels, such as on payment terminals, receipts, and idle screens. Each promotion is scoped to a specific merchant and store.
### Create a Promotion
```
POST /merchants/:merchantId/stores/:storeId/promotions
```
#### Request
```json
{
"title": "Summer Sale",
"name": "summer-sale-2026",
"description": "50% off all summer items",
"assetUrl": "https://cdn.example.com/promo-summer.png",
"type": "RECEIPT_BIG",
"assetOpacity": "0.8",
"backgroundColor": "#1e3a5f",
"contentTextColor": "#ffffff",
"endProductUrl": "https://shop.example.com/summer",
"endProduct": "SUMMER-COLLECTION",
"buttonLabel": "Shop Now",
"priority": 1,
"startDate": "06-01-2026",
"endDate": "08-31-2026"
}
```
#### Request Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Unique name for the promotion |
| `type` | string | Yes | Promotion type (e.g., `RECEIPT_BIG`, `RECEIPT_SMALL`) |
| `priority` | number | Yes | Display priority. Lower numbers = higher priority |
| `startDate` | string | Yes | Start date in `MM-DD-YYYY` format |
| `endDate` | string | Yes | End date in `MM-DD-YYYY` format |
| `title` | string | No | Display title for the promotion |
| `description` | string | No | Brief description of the promotion |
| `assetUrl` | string | No | URL of the promotional image |
| `assetOpacity` | string | No | Image opacity, `0` (transparent) to `1` (opaque) |
| `backgroundColor` | string | No | Background color in hex format |
| `contentTextColor` | string | No | Text color in hex format |
| `endProductUrl` | string | No | URL of the promoted product |
| `endProduct` | string | No | Product ID linked to the promotion |
| `buttonLabel` | string | No | Label for the call-to-action button |
#### Response
```json
{
"status": "SUCCESS",
"data": {
"promotionId": "promo-abc-456"
},
"message": "Promotion created successfully"
}
```
### List All Promotions
Retrieve all promotions for a merchant's store to view, manage, and track active and past campaigns.
```
GET /merchants/:merchantId/stores/:storeId/promotions
```
#### Response
```json
{
"status": "SUCCESS",
"data": [
{
"promotionId": "promo-abc-456",
"merchantId": "merchant-xyz-789",
"storeId": "store-abc-123",
"name": "summer-sale-2026",
"title": "Summer Sale",
"description": "50% off all summer items",
"assetUrl": "https://cdn.example.com/promo-summer.png",
"endProduct": "SUMMER-COLLECTION",
"buttonLabel": "Shop Now",
"startDate": "2026-06-01T00:00:00Z",
"endDate": "2026-08-31T00:00:00Z",
"priority": "1",
"assetOpacity": "0.8",
"backgroundColor": "#1e3a5f",
"contentTextColor": "#ffffff",
"endProductUrl": "https://shop.example.com/summer"
}
],
"message": "Promotions fetched successfully"
}
```
### Get Promotion by ID
Retrieve the full configuration and current state of a single promotion.
```
GET /merchants/:merchantId/stores/:storeId/promotions/:promotionId
```
The response structure is identical to a single item in the list response above.
### Update a Promotion
Modify any attributes of an existing promotion. Send only the fields you want to change.
```
PUT /merchants/:merchantId/stores/:storeId/promotions/:promotionId
```
#### Request
```json
{
"description": "Up to 60% off all summer items - extended!",
"endDate": "09-30-2026",
"priority": 1
}
```
All fields are optional. The response confirms the update:
```json
{
"status": "SUCCESS",
"message": "Promotion updated successfully"
}
```
### Delete a Promotion
Permanently remove a promotion and its associated data.
```
DELETE /merchants/:merchantId/stores/:storeId/promotions/:promotionId
```
#### Response
```json
{
"status": "SUCCESS",
"message": "Promotion deleted successfully"
}
```
> **Warning:** Deletion is permanent. The promotion will no longer be active or visible on any channel.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create gift card | POST | `/gift-cards` |
| List all gift cards | GET | `/gift-cards` |
| Get gift card details | GET | `/gift-cards/:id` |
| Get gift card transactions | GET | `/gift-cards/:giftCardId/transactions` |
| Create promotion | POST | `/merchants/:merchantId/stores/:storeId/promotions` |
| List all promotions | GET | `/merchants/:merchantId/stores/:storeId/promotions` |
| Get promotion by ID | GET | `/merchants/:merchantId/stores/:storeId/promotions/:promotionId` |
| Update promotion | PUT | `/merchants/:merchantId/stores/:storeId/promotions/:promotionId` |
| Delete promotion | DELETE | `/merchants/:merchantId/stores/:storeId/promotions/:promotionId` |
### Product Catalog
Category: online | Tags: Online, API, Products, Catalog, Inventory
URL: /developers/guides/product-catalog
## Overview
The Product Catalog API lets you build a structured product hierarchy for your stores. You can create catalogs, add products with pricing and tax configuration, define variants (such as sizes and colours), manage stock levels, and pull sales statistics -- all through a single set of REST endpoints.
This guide walks through every operation in the catalog lifecycle, from creating an empty catalog to pulling performance analytics.
## Prerequisites
- A configured **store** with a valid `storeId`
- API credentials (API key, API secret)
## Step 1: Create a Product Catalog
A catalog is the top-level container that groups products for a store. Each store can have one or more catalogs.
### Create catalog
```json
POST /catalog
{
"storeId": "8136a645a2c2d1bb0f"
}
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"productcatalogId": "cat_91a3f..."
},
"message": "Product catalog created successfully"
}
```
### List catalogs
Retrieve all catalogs that exist under the store:
```
GET /catalog
```
The response returns `data.productcatalogId` as an array of catalog IDs associated with the store.
## Step 2: Add Products
With a catalog in place, add products to it. Each product requires a name, type, pricing, tax, and descriptive metadata.
```json
POST /catalog/:catalogId/products
{
"storeId": "8136a645a2c2d1bb0f",
"name": "SurfPad Purple Logo",
"type": "PRODUCT",
"unitType": "FIXED",
"costPrice": 20,
"sellingPrice": 45,
"currencyCode": "752",
"tax": [
{
"type": "VAT",
"percentage": "3"
}
],
"description": "SurfPad Payment Terminal in Purple",
"category": "electronics",
"unit": "nos",
"productImages": [
"https://example.com/images/surfpad-purple.png"
],
"hsnCode": "723453",
"barCode": "7812123454323"
}
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"productId": "prod_82f4a..."
},
"message": "Product created successfully"
}
```
### Product types and unit types
| Field | Values | Description |
|-------|--------|-------------|
| `type` | `PRODUCT`, `SERVICE` | Whether the item is a physical product or a service |
| `unitType` | `FIXED`, `VARIABLE`, `FREE_AMOUNT` | How quantity and pricing are determined |
### Fetch a single product
```
GET /catalog/:catalogId/products/:productId
```
Pass `storeId` as a query parameter. The response includes the full product object with pricing, tax, attributes, and inventory status.
### List all products in a catalog
```
GET /catalog/:catalogId/products
```
Returns an array of products including their variants, inventory, billing plans, campaign info, and tax breakdown.
## Step 3: Add Product Variants
Variants represent different versions of a product, such as colour or size options. Attach them to an existing product.
```json
POST /catalog/:catalogId/products/:productId/variants
{
"storeId": "8136a645a2c2d1bb0f",
"variants": [
{
"name": "SurfPad Blue Variant",
"description": "Blue variant of SurfPad",
"costPrice": 10,
"sellingPrice": 12,
"currencyCode": "752",
"productImages": [
"https://example.com/images/surfpad-blue.png"
],
"hsnCode": "123453",
"barCode": "1212123454323",
"attributeValues": [
{
"attributeKey": "colour",
"displayName": "blue",
"value": "#0000FF"
},
{
"attributeKey": "size",
"displayName": "medium",
"value": "M"
}
]
}
]
}
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"variants": ["var_73b1c..."]
},
"message": "Variants added successfully"
}
```
Each variant in the `attributeValues` array uses an `attributeKey` (e.g. `colour`, `size`) paired with a `displayName` and `value` so the storefront can render selectable options.
## Step 4: Link Related Products
Drive cross-sell and upsell opportunities by associating related products with a primary product.
```json
POST /catalog/:catalogId/products/:productId/related-products
{
"storeId": "8136a645a2c2d1bb0f",
"relatedProducts": [
{
"productId": "prod_82f4a...",
"relatedProductId": "prod_55d2b..."
}
]
}
```
The API returns a `SUCCESS` status when the association is saved.
## Step 5: Update Products and Variants
### Update a product
Use `PATCH` to modify any product field. Only the fields you include will be changed.
```json
PATCH /catalog/:catalogId/products/:productId
{
"storeId": "8136a645a2c2d1bb0f",
"name": "SurfPad Black Logo",
"sellingPrice": 15,
"description": "SurfPad Payment Terminal in Black"
}
```
### Update a variant
The same partial-update approach works for variants:
```json
PATCH /catalog/:catalogId/products/:productId/variants/:variantId
{
"storeId": "8136a645a2c2d1bb0f",
"name": "SurfPad Black Logo - Large",
"sellingPrice": 18,
"description": "SurfPad Payment Terminal in Black - Large Size"
}
```
Both endpoints return `{ "status": "SUCCESS" }` on success.
## Step 6: Manage Inventory
Track stock at both the product level and the individual variant level.
### Update product inventory
```json
PATCH /catalog/:catalogId/products/:productId/inventory
{
"storeId": "8136a645a2c2d1bb0f",
"inventory": {
"productId": "prod_82f4a...",
"inventory": {
"quantity": 10,
"reorderLevel": 5,
"reorderQuantity": 10
}
}
}
```
### Update variant inventory
```json
PATCH /catalog/:catalogId/products/:productId/variants/:variantId/inventory
{
"storeId": "8136a645a2c2d1bb0f",
"operation": "STOCK_UP",
"quantity": 15,
"unit": "nos"
}
```
The `operation` field controls how stock is modified (e.g. `STOCK_UP` to add inventory). The `unit` field accepts standard measurement units such as `nos`, `kg`, `l`, `m`, and many others.
## Step 7: View Statistics
### Product statistics
Get sales performance, inventory levels, and VAT breakdowns for a single product:
```
GET /catalog/:catalogId/products/:productId/statistics
```
Optionally pass `startDate` and `endDate` query parameters in `YYYY-MM-DD` format to filter by date range. The response includes:
- **Sales by currency** -- units sold, units returned, revenue, VAT, campaign discounts, order count, and average order value
- **Inventory status** -- current stock, stock in, stock out
- **VAT breakdown** -- amount and taxable total per VAT percentage
- **Variant-level stats** -- the same metrics broken down per variant
### Catalog statistics
Get an aggregate view across the entire catalog:
```
GET /catalog/:catalogId/products/statistics
```
This returns:
- **Summary** -- total products, total variants, and aggregated sales metrics by currency
- **VAT breakdown** -- catalog-wide tax totals
- **Top-selling products** -- ranked by units sold and revenue, with per-currency breakdowns
Both endpoints support optional `startDate` and `endDate` query parameters.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create catalog | POST | `/catalog` |
| List catalogs | GET | `/catalog` |
| Create product | POST | `/catalog/:catalogId/products` |
| Fetch product by ID | GET | `/catalog/:catalogId/products/:productId` |
| List all products | GET | `/catalog/:catalogId/products` |
| Update product | PATCH | `/catalog/:catalogId/products/:productId` |
| Add variants | POST | `/catalog/:catalogId/products/:productId/variants` |
| Update variant | PATCH | `/catalog/:catalogId/products/:productId/variants/:variantId` |
| Add related products | POST | `/catalog/:catalogId/products/:productId/related-products` |
| Update product inventory | PATCH | `/catalog/:catalogId/products/:productId/inventory` |
| Update variant inventory | PATCH | `/catalog/:catalogId/products/:productId/variants/:variantId/inventory` |
| Product statistics | GET | `/catalog/:catalogId/products/:productId/statistics` |
| Catalog statistics | GET | `/catalog/:catalogId/products/statistics` |
### Settlements & Reporting
Category: online | Tags: Online, API, Settlements, Reporting, Charges, Adjustments
URL: /developers/guides/settlements-reporting
## Overview
Once transactions are processed, you need visibility into what was settled, what fees were applied, and how to bill merchants for additional services. The Settlements and Reporting APIs give you that visibility.
This guide covers four related capabilities:
1. **Settlement reports** -- retrieve payout summaries for a merchant over a given period.
2. **Adjustments** -- view tips, surcharges, insurance, and other amounts applied to orders.
3. **Merchant charges** -- create, update, and list one-time or recurring charges billed to a merchant.
4. **Customer details** -- register customer profiles with addresses, contact information, and linked payment cards.
It also covers [reading a settlement report](#reading-a-settlement-report): why a monthly total and the payouts inside that month rarely match, and which figure answers which merchant question.
## Prerequisites
- A valid `partnerId` and `merchantId`
- API credentials (API key, API secret)
## Settlement Reports
Settlement reports summarize a merchant's settled transactions for a selected time period. Reports can be configured as `DAILY` or `MONTHLY` depending on the merchant's setup.
### Fetch settlement reports
```
GET /partners/:partnerId/merchants/:merchantId/reports
```
**Response:**
```json
{
"status": "SUCCESS",
"data": [
{
"payoutId": "po_83a1f...",
"merchantId": "m_91b2c...",
"transactionStartDate": "2026-01-01",
"transactionEndDate": "2026-01-31",
"settlementDate": "2026-02-03",
"reportType": "MONTHLY",
"url": "https://reports.surfboardpayments.com/settlements/po_83a1f...",
"totalSale": 1250000,
"totalRefund": 35000,
"fee": 18750,
"payout": 1196250
}
],
"message": "Settlement reports fetched successfully"
}
```
### Response fields
| Field | Type | Description |
|-------|------|-------------|
| `payoutId` | string | Identifies this specific payout |
| `transactionStartDate` | string | First transaction date covered (`YYYY-MM-DD`) |
| `transactionEndDate` | string | Last transaction date covered (`YYYY-MM-DD`) |
| `settlementDate` | string | Date the payout was issued (`YYYY-MM-DD`) |
| `reportType` | string | `MONTHLY` or `DAILY` |
| `url` | string | Direct link to view the full report |
| `totalSale` | number | Total sales amount in smallest currency unit |
| `totalRefund` | number | Total refunded amount |
| `fee` | number | Total fees deducted |
| `payout` | number | Net payout to the merchant |
Use the `url` field to download or redirect merchants to a detailed breakdown of every transaction in the settlement period.
## Reading a Settlement Report
This is the part support gets asked about most, so it is worth understanding before a merchant asks you.
A monthly report carries two fee totals, and they are usually different numbers:
- The **header figure** is the fee on transactions that happened in that calendar month. It is on a **transaction-date** basis.
- The **fee column in the payouts breakdown** sums the fees of the payouts issued during that month. It is on a **payout-date** basis.
Both are correct. They measure different things, and at a month boundary they cannot agree.
### Why the two totals differ
Payouts lag transactions by two to three days. A payout issued on 1 May settles transactions from the end of April, and the transactions from the last days of May are paid out in June. So the payout-date total borrows from the previous month at one end and loses to the next month at the other.
Take a merchant on daily payouts in May:
| Transactions | Paid out | Fee |
|---|---|---|
| 29--30 April | 1--2 May | 43.50 |
| 1--28 May | during May | 1,196.50 |
| 29--31 May | 1--3 June | 87.20 |
The monthly report header reads **1,283.70**, the fee on May's transactions: `1,196.50 + 87.20`. The fee column of the payouts breakdown reads **1,240.00**, the fee on May's payouts: `43.50 + 1,196.50`. Nothing has been charged twice, and neither figure is wrong.
The same shift applies to the sales and payout columns, not just fees. It is simply most visible on fees, because that is the number merchants ask about.
### Mapping a transaction to its report
One rule covers every case:
> **The monthly report follows the transaction date. The payouts breakdown follows the payout date.**
Every transaction is therefore counted in two places, and at a month boundary those two places are different months:
| Transaction happened | Paid out | Counted in the monthly report for | Appears in the payouts breakdown for |
|---|---|---|---|
| 30 April | 2 May | **April** | **May** |
| 15 May | 17 May | May | May |
| 31 May | 2 June | **May** | **June** |
The middle row is what people expect. The first and last rows are what the questions are about.
Drawn on a calendar, the two views are the same trading, shifted by the settlement lag:
```
transactions │ 29 Apr 30 Apr │ 01 May ... 30 May 31 May │
paid out │ 01 May 02 May │ 03 May ... 01 Jun 02 Jun │
└──────┬───────┘ └──────┬──────┘
April's trading, May's trading,
inside May's payouts inside June's payouts
```
To show a merchant where a specific transaction went, take its date, add the settlement lag, and read off both columns. That is the whole mapping.
### The fee is not taken out of the payout
A payout settles transactions. The Surfboard fee for the period is collected separately, once the month has closed, rather than being netted off each payout as it goes.
That matters when a merchant reconciles a bank statement. They see payouts arriving through the month, then one fee deduction afterwards. **The deduction that lands in early June is May's fees, and it matches the May monthly report header, not the sum of the May payout rows.** A merchant who compares the June deduction against the May payout breakdown is comparing two different periods and will always find a gap.
If you do see a fee deducted from an individual payout, that is not the normal arrangement -- check the merchant's billing setup before explaining it as expected behaviour.
### Which figure answers which question
| The merchant asks | Use |
|---|---|
| "What were my fees for May?" | The **monthly report header** fee. Transaction basis, the month they actually traded. |
| "What was deducted from my account in June?" | The **May monthly report** fee total. Fees are collected after the month closes. |
| "Why was this payout this amount?" | The **payout row**, or the daily report for that settlement date. |
| "What did I sell in May?" | The **monthly report header** sales figure, not the sum of May's payouts. |
The short version to give a merchant: *your monthly report tells you what you traded and what it cost you that month; your payouts tell you what arrived in the bank and when. The two are offset by a couple of days at each end of the month.*
### Refunds land in the period they were processed
A refund processed in June against a May sale reduces June's payouts. It does not reopen May. A merchant looking for a refund in the month of the original sale will not find it, and the monthly totals are not wrong for lacking it.
### Before escalating a mismatch
Work through this first -- it resolves most reports of a mismatch:
1. Take the two figures and subtract. Does the difference equal the fees or sales of the days either side of the month boundary? If so, the report is right and this is the transaction-date versus payout-date offset.
2. Is a refund or an adjustment sitting in a different period from its original sale?
3. Is the merchant comparing a fee deduction against the payouts of the same month rather than the month before?
If none of those explain it, raise it with support with the `payoutId` values and the two figures you are comparing. Both come from the same [settlement reports endpoint](#fetch-settlement-reports), so quoting the IDs is faster than describing the rows.
### Getting the numbers over the API
The report list gives you both bases without downloading a file. `transactionStartDate` and `transactionEndDate` are the transaction basis; `settlementDate` is the payout basis. Filter on the pair you mean:
```
GET /partners/:partnerId/merchants/:merchantId/reports
```
- Fees a merchant incurred in May: the `MONTHLY` report whose `transactionStartDate` falls in May.
- Fees inside payouts issued in May: sum `fee` across the reports whose `settlementDate` falls in May.
Reading those two into a support tool, side by side and labelled, answers the question before it gets asked.
## Adjustments
Adjustments represent additional amounts applied to orders during a transaction -- tips, surcharges, insurance payments, and similar line items. The Adjustments API lets you retrieve all adjustments at the merchant level for tracking and reconciliation.
### Fetch adjustments
```
GET /partners/:partnerId/merchants/:merchantId/adjustments?startDate=2026-01-01&endDate=2026-01-31
```
Both `startDate` and `endDate` are required query parameters in `YYYY-MM-DD` format.
**Response:**
```json
{
"status": "SUCCESS",
"data": [
{
"adjustmentId": "adj_44c2e...",
"adjustmentType": "TIP",
"amount": "2500"
},
{
"adjustmentId": "adj_55d3f...",
"adjustmentType": "SURCHARGE",
"amount": "1500"
}
],
"message": "Adjustments fetched successfully"
}
```
### Response fields
| Field | Type | Description |
|-------|------|-------------|
| `adjustmentId` | string | Unique identifier for the adjustment |
| `adjustmentType` | string | Type of adjustment (e.g. `TIP`, `SURCHARGE`, `INSURANCE`) |
| `amount` | string | Adjustment amount in smallest currency unit |
## Merchant Charges
Merchant charges let partners bill merchants for services, fees, or subscriptions. A charge can be one-time or recurring, and supports VAT.
### Create a charge
```json
POST /partners/:partnerId/merchants/:merchantId/charges
{
"description": "Monthly platform fee",
"currency": "752",
"amount": 5000000,
"vat": 35,
"billingDate": "2026-03-01",
"recurring": {
"frequency": "monthly",
"billingEndDate": "2027-03-01"
}
}
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"chargeId": "chg_72a4d..."
},
"message": "Charge created successfully"
}
```
### Create charge request fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `description` | string | Yes | Brief description of the charge |
| `currency` | string | Yes | Three-digit ISO currency code |
| `amount` | number | Yes | Charge amount in smallest currency unit |
| `vat` | number | No | VAT amount |
| `billingDate` | string | No | Effective date (`YYYY-MM-DD`) |
| `recurring.frequency` | string | No | Billing frequency (see table below) |
| `recurring.billingEndDate` | string | No | When to stop recurring charges (`YYYY-MM-DD`) |
### Frequency options
| Value | Cycle |
|-------|-------|
| `daily` | Every day |
| `twiceWeekly` | Twice per week |
| `weekly` | Every week |
| `tenDays` | Every 10 days |
| `fortNightly` | Every 2 weeks |
| `monthly` | Every month |
| `everyTwoMonths` | Every 2 months |
| `trimester` | Every 4 months |
| `quarterly` | Every 3 months |
| `twiceYearly` | Every 6 months |
| `annually` | Every year |
| `unscheduled` | No fixed schedule |
### Fetch a charge by ID
```
GET /partners/:partnerId/merchants/:merchantId/charges/:chargeId
```
The response includes subscription details, VAT, frequency, billing dates, and any associated `subCharges`. Sub-charges are individual billing instances generated from a recurring charge.
**Key response fields:**
| Field | Type | Description |
|-------|------|-------------|
| `isSubscriptionCharge` | boolean | Whether this is a recurring charge |
| `description` | string | Charge description |
| `amount` | number | Charge amount in smallest currency unit |
| `vat` | number | VAT applied |
| `frequency` | string | Billing frequency |
| `billingStartDate` | string | Start date (ISO 8601) |
| `billingEndDate` | string | End date (ISO 8601) |
| `subCharges` | array | Individual billing instances with their own `chargeId`, `amount`, `status`, and `billingDate` |
### Update a charge
Modify an existing charge's amount, VAT, or recurring configuration:
```json
PUT /partners/:partnerId/merchants/:merchantId/charges/:chargeId
{
"amount": 650000,
"vat": 15,
"recurring": {
"updateType": "onlyNext",
"billingEndDate": "2027-10-23"
}
}
```
The `recurring.updateType` field controls the scope of the update:
| Value | Behaviour |
|-------|-----------|
| `onlyNext` | Apply the change only to the next billing cycle |
| `allFuture` | Apply the change to all future billing cycles |
### List all merchant charges
```
GET /partners/:partnerId/merchants/:merchantId/charges
```
Returns a paginated list of all charges (one-time and recurring) for the merchant, including `chargeId`, `description`, `amount`, `vat`, `status`, `billingDate`, and whether the charge is subscription-based.
## Billing Plans
A merchant charge is what a merchant is billed. A billing plan is the pricing behind it: the rates that apply to a card brand, a payment method and a terminal type, broken down by where the card comes from and what kind of card it is. Plans are defined once at partner level and then assigned to merchants.
### Create billing plans
```json
POST /partners/:partnerId/billing-plans
{
"plans": [
{
"id": "SP_STANDARD_CARD",
"paymentMethod": "CARD",
"cardBrand": "VISA",
"terminalType": "STANDARD",
"planType": "FIXED",
"description": "Standard card pricing 2026",
"domesticDebitNonCommercial": 0.6,
"domesticCreditNonCommercial": 0.9,
"eeaDebitNonCommercial": 0.8,
"eeaCreditNonCommercial": 1.1,
"internationalDebitNonCommercial": 1.9,
"internationalCreditNonCommercial": 2.3,
"fixedCost": 30,
"vatPercentage": 25
}
]
}
```
`plans` is an array, so a full price list goes up in one call.
| Field | Description |
|-------|-------------|
| `id` | Your identifier for the plan. |
| `paymentMethod`, `cardBrand`, `terminalType` | What the plan applies to. One plan per combination. |
| `planType` | `FIXED` for a flat percentage or amount, `VARIABLE` for pricing that depends on transaction type. |
| `domestic*`, `eea*`, `international*` | Percentage rates, split by debit or credit and commercial or non-commercial. |
| `minimumCeiling` | Minimum amount for the rate to apply. |
| `fixedCost` | Fixed cost per transaction, in minor units. |
| `fixedPercentage` | Flat percentage across the board. |
| `vatPercentage` | VAT applied to the plan. |
The twelve rate fields are not padding. Interchange differs by card origin and card type, so a single blended rate either loses money on international commercial cards or overcharges on domestic debit. Price the grid.
### Manage plans
```
GET /partners/:partnerId/billing-plans
GET /partners/:partnerId/billing-plans/:id
DELETE /partners/:partnerId/billing-plans/:id
GET /partners/:partnerId/merchants/:merchantId/plans
```
The last one is the useful one in support: it returns the plans actually assigned to a merchant, which is the answer to "why was I charged this". Plans are attached to a merchant during onboarding through the `transactionPricingPlan` and `displayProducts` control fields — see [Merchant Onboarding](/developers/guides/merchant-onboarding) and [Order and Return Terminals](/developers/guides/terminal-logistics).
## Customer Details
The Customer API lets you create and retrieve customer profiles. Profiles store personal information, addresses, contact details, and linked payment cards, enabling richer order data and streamlined checkout experiences.
### Create a customer
```json
POST /customers
{
"firstName": "John",
"middleName": "Doe",
"birthDate": "1990/03/04",
"countryCode": "SE",
"address": [
{
"addressLine1": "Storgatan 12",
"city": "Stockholm",
"countryCode": "SE",
"postalCode": "111 23",
"role": "shipping"
}
],
"phoneNumbers": [
{
"phoneNumber": {
"code": "46",
"number": "701234567"
},
"role": "own"
}
],
"emails": [
{
"email": "john.doe@example.com",
"role": "personal"
}
],
"cardIds": [
"824c514bfe001805f0"
]
}
```
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"customerId": "cust_61e3b..."
},
"message": "Customer created successfully"
}
```
### Customer fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `firstName` | string | No | Customer's first name |
| `lastName` | string | No | Customer's last name |
| `birthDate` | string | No | Date of birth (`YYYY/MM/DD`) |
| `countryCode` | string | No | Two-letter ISO country code |
| `address` | array | No | Array of address objects (shipping, billing, etc.) |
| `address.addressLine1` | string | Yes | Primary address line |
| `address.city` | string | Yes | City |
| `address.countryCode` | string | Yes | Two-letter ISO country code |
| `address.postalCode` | string | Yes | Postal code |
| `address.role` | string | No | Address purpose (`shipping`, `billing`) |
| `emails` | array | No | Array of email objects with `email` and `role` |
| `phoneNumbers` | array | No | Array of phone objects with nested `phoneNumber` (`code`, `number`) and `role` |
| `cardIds` | array | No | Payment card identifiers to associate with the customer |
### Fetch a customer
```
GET /customers/:customerId
```
Returns the full customer profile including all addresses, emails, phone numbers, and linked card IDs.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Fetch settlement reports | GET | `/partners/:partnerId/merchants/:merchantId/reports` |
| Fetch adjustments | GET | `/partners/:partnerId/merchants/:merchantId/adjustments` |
| Create merchant charge | POST | `/partners/:partnerId/merchants/:merchantId/charges` |
| Fetch charge by ID | GET | `/partners/:partnerId/merchants/:merchantId/charges/:chargeId` |
| Update merchant charge | PUT | `/partners/:partnerId/merchants/:merchantId/charges/:chargeId` |
| List all merchant charges | GET | `/partners/:partnerId/merchants/:merchantId/charges` |
| Create billing plans | POST | `/partners/:partnerId/billing-plans` |
| Fetch billing plans | GET | `/partners/:partnerId/billing-plans` |
| Fetch billing plan by ID | GET | `/partners/:partnerId/billing-plans/:id` |
| Remove billing plan | DELETE | `/partners/:partnerId/billing-plans/:id` |
| Fetch a merchant's plans | GET | `/partners/:partnerId/merchants/:merchantId/plans` |
| Create customer | POST | `/customers` |
| Fetch customer by ID | GET | `/customers/:customerId` |
### Account & Service Provider Management
Category: online | Tags: Online, API, Accounts, Service Providers, Partners
URL: /developers/guides/account-management
## Overview
The Surfboard API provides administrative endpoints for managing accounts, service providers, and notification subscriptions. With these APIs you can:
- **Create user accounts** for merchants and partners with role-based access control
- **Register service providers** under a partner and track their onboarding lifecycle
- **Query service providers** linked to a merchant or partner
- **Subscribe to notifications** for automated report delivery via email, Slack, or SFTP
## User Accounts & Roles
Every account is assigned a role that controls access. The `role` field is optional when creating accounts.
| Role | Description |
|------|-------------|
| `SUPER_ADMIN` | Full access to all features and settings. Typically the account owner. |
| `ADMIN` | Can manage resources, invite users, and configure integrations. |
| `USER` | Read access with limited operational permissions. |
### Create a Merchant Account
```
POST /merchants/{merchantId}/accounts
```
```json
{ "email": "admin@merchant.com", "role": "ADMIN" }
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `email` | string | Yes | Email address. An invitation is sent to this address. |
| `role` | string | No | `SUPER_ADMIN`, `ADMIN`, or `USER`. Defaults if omitted. |
Returns `{ "status": "SUCCESS", "message": "..." }` on success.
### Create a Partner Account
Uses the same request body and response format, scoped to the partner:
```
POST /partners/{partnerId}/accounts
```
```json
{ "email": "admin@partner.com", "role": "ADMIN" }
```
## Service Provider Management
Service 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.
For 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).
### Register a Service Provider
Submit a new application. The response includes an `applicationId` and a `webKybUrl` for KYB verification.
```
POST /partners/{partnerId}/service-providers
```
```json
{
"country": "SE",
"organisation": { "corporateId": "3532007322" },
"controlFields": { "isServiceProvider": true }
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `country` | string | Yes | Two-letter ISO country code (e.g., `SE`, `NO`, `DK`). |
| `organisation.corporateId` | string | Yes | Corporate identification number. |
| `controlFields.isServiceProvider` | boolean | Yes | Must be `true`. |
**Response:**
```json
{
"status": "SUCCESS",
"data": {
"applicationId": "app-abc123",
"webKybUrl": "https://kyb.surfboardpayments.com/application/app-abc123"
},
"message": "Service provider application created"
}
```
Share the `webKybUrl` with the service provider so they can complete KYB.
### List All Applications
Retrieve all service provider applications, optionally filtered by lifecycle stage.
```
GET /partners/{partnerId}/service-providers/applications?applicationType=ONBOARDING
```
| Query Parameter | Type | Description |
|-----------------|------|-------------|
| `applicationType` | string | `ONBOARDING`, `RENEWAL`, or `ONBOARDING,RENEWAL`. Defaults to `ONBOARDING`. |
**Response** returns an array of application objects:
```json
{
"status": "SUCCESS",
"data": [{
"applicationId": "app-abc123",
"country": "SE",
"corporateId": "3532007322",
"applicationStatus": "APPLICATION_SUBMITTED",
"createdAt": "2025-06-15T10:30:00Z",
"lastUpdatedAt": "2025-06-16T08:00:00Z",
"legalName": "Acme Services AB"
}]
}
```
### Check Application Status
```
GET /partners/{partnerId}/service-providers/applications/{applicationId}
```
```json
{
"status": "SUCCESS",
"data": {
"onboardingStatus": "COMPLETED",
"applicationStatus": "MERCHANT_CREATED",
"serviceProviderId": "sp-789xyz"
}
}
```
The `serviceProviderId` is `null` while pending and populated once approved.
### Fetch Service Providers for a Partner
```
GET /partners/{partnerId}/service-providers
```
```json
{
"status": "SUCCESS",
"data": {
"serviceProviders": [{
"serviceProviderId": "sp-789xyz",
"corporateId": "3532007322",
"name": "Acme Services AB",
"email": "contact@acmeservices.com",
"address": { "addressLine1": "Birger Jarlsgatan 10", "city": "Stockholm", "countryCode": "SE", "postalCode": "114 34" },
"phoneNumber": { "code": "46", "number": "812345678" }
}]
}
}
```
### Fetch Service Providers for a Merchant
Same response structure as above, scoped to the merchant: `GET /merchants/{merchantId}/service-providers`
## External Notifications
Subscribe to automated event reports delivered via email, Slack, or SFTP. At least one channel is required per subscription.
### Subscribe to Reports
```
POST /merchants/{merchantId}/notifications/reports
POST /partners/{partnerId}/notifications/reports
```
```json
{
"event": "DAILY_FILE_TRANSFER",
"email": "finance@merchant.com",
"slackUrl": "https://hooks.slack.com/services/T00/B00/xxx",
"sftpInfo": {
"host": "sftp.merchant.com",
"user": "surfboard-reports",
"port": 22,
"remoteDirectory": "/reports/daily",
"separator": ","
}
}
```
**Response** returns a `notificationId` per channel. For SFTP, a `publicKey` is included -- add it to your server's authorized keys.
```json
{
"status": "SUCCESS",
"data": [
{ "notificationId": "notif-001", "NotificationChannel": "EMAIL" },
{ "notificationId": "notif-002", "NotificationChannel": "SLACK" },
{ "notificationId": "notif-003", "NotificationChannel": "SFTP", "publicKey": "ssh-rsa AAAA..." }
]
}
```
### Fetch Existing Notifications
Retrieve configured subscriptions with optional filters (`event`, `notificationChannel`, `notificationId`):
```
GET /merchants/{merchantId}/notifications
GET /partners/{partnerId}/notifications
```
### Unsubscribe
Remove a subscription by its ID:
```
DELETE /merchants/{merchantId}/notifications/{notificationId}
DELETE /partners/{partnerId}/notifications/{notificationId}
```
Returns `{ "status": "SUCCESS", "message": "..." }` on success.
## API Quick Reference
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create merchant account | POST | `/merchants/{merchantId}/accounts` |
| Create partner account | POST | `/partners/{partnerId}/accounts` |
| Register service provider | POST | `/partners/{partnerId}/service-providers` |
| List SP applications | GET | `/partners/{partnerId}/service-providers/applications` |
| Check SP application status | GET | `/partners/{partnerId}/service-providers/applications/{applicationId}` |
| Fetch SPs for partner | GET | `/partners/{partnerId}/service-providers` |
| Fetch SPs for merchant | GET | `/merchants/{merchantId}/service-providers` |
| Subscribe merchant reports | POST | `/merchants/{merchantId}/notifications/reports` |
| Subscribe partner reports | POST | `/partners/{partnerId}/notifications/reports` |
| Fetch merchant notifications | GET | `/merchants/{merchantId}/notifications` |
| Fetch partner notifications | GET | `/partners/{partnerId}/notifications` |
| Unsubscribe merchant | DELETE | `/merchants/{merchantId}/notifications/{notificationId}` |
| Unsubscribe partner | DELETE | `/partners/{partnerId}/notifications/{notificationId}` |
### Service Providers & Split Payouts
Category: online | Tags: Online, In-Store, API, Service Providers, Flow, Payouts, Partners
URL: /developers/guides/service-providers
## Overview
Surfboard 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.
There are three steps, all at the partner level:
1. **Onboard the service provider** -- submit an application, the recipient completes KYB (Know Your Business) or signs an agreement, and Surfboard issues a `serviceProviderId`
2. **Link it to a merchant** -- a service provider can only take a share from merchants it is linked to
3. **Set the split on the order** -- add the service provider and its share under `controlFunctions.serviceProviders`
```
Partner
├── Service provider (onboarded once, reused across merchants)
└── Merchant
├── link ──────► Service provider
└── Order
└── controlFunctions.serviceProviders[]
└── { serviceProviderId, amount }
```
The same mechanism works for in-store and online orders and for every payment method and acquirer.
## Prerequisites
1. A partner account with API credentials and your `partnerId` from the [Developer Portal](https://developers.surfboardpayments.com/)
2. At least one onboarded merchant. See [Merchant Onboarding](/developers/guides/merchant-onboarding)
3. Flow enabled on your partner account. Contact your Surfboard account manager if the service provider endpoints return `403`
Service 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).
## Step 1: Onboard a Service Provider
Every 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.
| Kind | Who | Verification | Endpoint |
|------|-----|--------------|----------|
| **Company** | A registered business (a franchisor, a software vendor, a marketplace operator) | Hosted web KYB, same as merchant onboarding | `POST /partners/{partnerId}/service-providers` |
| **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` |
### Company service provider
Submit the company's country and corporate ID:
```
POST /partners/{partnerId}/service-providers
```
```json
{
"country": "SE",
"organisation": {
"corporateId": "5560000000"
},
"controlFields": {
"isServiceProvider": true
}
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `country` | string | Yes | Two-letter ISO country code where the company is registered. |
| `organisation.corporateId` | string | Yes | Corporate registration number, validated against `country`. |
| `controlFields.isServiceProvider` | boolean | Yes | Must be `true`. Marks the application as a service provider rather than a merchant. |
The response returns the application and a hosted KYB link:
```json
{
"status": "SUCCESS",
"data": {
"applicationId": "838ca3a7c530200810",
"webKybUrl": "https://kyb.surfboardpayments.com/838ca3a7c530200810?pi=..."
},
"message": "Service provider company application created successfully"
}
```
Share `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.
### Individual service provider
The 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.
An 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:
```
POST /partners/{partnerId}/service-providers/individual
```
```json
{
"email": "anna@restaurant.example",
"countryCode": "SE",
"merchantId": "8385f437bc6d200b50",
"spType": "INDIVIDUAL",
"config": {
"deductApplicableTransactionFee": false
}
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `email` | string | Yes | Where the signing link is sent. |
| `countryCode` | string | Yes | Two-letter ISO country code of the individual. |
| `merchantId` | string | Yes | The merchant this individual will take a share from. |
| `spType` | string | No | Type of service provider, for example `INDIVIDUAL`. |
| `config.feePercentage` | number | No | Percentage of each applicable transaction that goes to the individual. |
| `config.feeFixedAmount` | number | No | Fixed amount per applicable transaction, in minor currency units. |
| `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. |
The response returns a signing session instead of a KYB link:
```json
{
"status": "SUCCESS",
"data": {
"applicationId": "838ca3a7c530200810",
"signingLink": "https://sign.surfboardpayments.com/838ca3a7c530200810",
"signingSessionId": "8391b7c2ad4e100722",
"status": "APPLICATION_INITIATED"
},
"message": "Individual SP onboarding initiated successfully"
}
```
The 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.
### Track the application
There 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:
```
GET /partners/{partnerId}/service-providers/applications/{applicationId}
```
```json
{
"status": "SUCCESS",
"data": {
"onboardingStatus": null,
"applicationStatus": "SERVICE_PROVIDER_CREATED",
"serviceProviderId": "839ab2f1c47d300a20"
},
"message": "Service provider application status fetched successfully"
}
```
`serviceProviderId` is `null` until the application is approved. The statuses follow the merchant application lifecycle:
| Status | Meaning |
|--------|---------|
| `APPLICATION_INITIATED` | Application created, link not yet opened. |
| `APPLICATION_STARTED` | The recipient has opened the KYB or signing link. |
| `APPLICATION_SUBMITTED` | Details submitted, awaiting signature or review. |
| `APPLICATION_PENDING_INFORMATION` | Surfboard needs more information from the recipient. |
| `APPLICATION_SIGNED` | All required signatures collected. |
| `APPLICATION_UNDER_REVIEW` | Compliance review in progress. |
| `APPLICATION_APPROVED` / `APPLICATION_COMPLETED` | Approved, service provider being created. |
| `SERVICE_PROVIDER_CREATED` | Done. `serviceProviderId` is populated. |
| `APPLICATION_REJECTED` | Did not pass review. |
| `APPLICATION_EXPIRED` | Not completed in time. Create a new application. |
To see every application under the partner, including renewals, list them:
```
GET /partners/{partnerId}/service-providers/applications?applicationType=ONBOARDING,RENEWAL
```
`applicationType` defaults to `ONBOARDING`. Each entry carries `applicationStatus`, `legalName`, `webKybUrl` while the link is still valid, and `endDate` for renewals.
Once created, service providers appear in the partner roster with their contact and address details:
```
GET /partners/{partnerId}/service-providers
```
## Step 2: Link the Service Provider to a Merchant
A 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.
Link an onboarded service provider:
```
POST /partners/{partnerId}/merchants/{merchantId}/service-providers/link
```
```json
{
"serviceProviderId": "839ab2f1c47d300a20"
}
```
```json
{
"status": "SUCCESS",
"message": "Service provider linked to merchant successfully"
}
```
One 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.
### Verify the link
```
GET /partners/{partnerId}/merchants/{merchantId}/service-providers
```
```json
{
"status": "SUCCESS",
"data": {
"activeServiceProviders": [
{
"merchantId": "8385f437bc6d200b50",
"partnerId": "8113d3f8403b380409",
"isActive": true,
"serviceProvider": {
"id": "839ab2f1c47d300a20",
"name": "Nordic Franchise AB"
}
}
]
},
"message": "Active service providers fetched successfully"
}
```
The merchant can see the same list through the merchant-scoped endpoint `GET /merchants/{merchantId}/service-providers`, which returns full contact details.
### Unlink
To stop a service provider from taking a share of a merchant's payments, remove the link. Orders already created keep their split.
```
DELETE /partners/{partnerId}/merchants/{merchantId}/service-providers/unlink
```
```json
{
"serviceProviderId": "839ab2f1c47d300a20"
}
```
### Linking at merchant creation
If 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:
```json
{
"country": "SE",
"organisation": { "corporateId": "5591631360" },
"controlFields": {
"merchantConfig": {
"serviceProvider": [
{
"serviceProviderId": "839ab2f1c47d300a20",
"deductApplicableTransactionFee": false,
"amount": {
"percentage": "5",
"fixed": 200,
"adjustmentTypes": ["TIPS"]
}
}
]
},
"store": { "...": "..." }
}
}
```
The `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.
### Partner Portal
Both 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.
## Step 3: Set the Split on the Order
With the service provider linked, name it on the order. Nothing else about the order changes: same line items, same totals, same payment initiation.
```
POST /orders
```
```json
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [
{
"id": "TABLE-12",
"name": "Dinner for two",
"quantity": 1,
"amount": {
"regular": 120000,
"total": 120000,
"currency": "752",
"tax": [{ "amount": 12857, "percentage": 12, "type": "VAT" }]
}
}
],
"totalOrderAmount": {
"regular": 120000,
"total": 120000,
"currency": "752",
"tax": [{ "amount": 12857, "percentage": 12, "type": "VAT" }]
},
"controlFunctions": {
"tipsMode": "STANDARD",
"serviceProviders": [
{
"serviceProviderId": "839ab2f1c47d300a20",
"amount": {
"percentage": "100",
"adjustmentTypes": ["TIPS"]
}
}
],
"initiatePaymentsOptions": {
"paymentMethod": "CARD"
}
}
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `serviceProviders[]` | array | No | One entry per recipient. Every `serviceProviderId` must be linked to the merchant in the `MERCHANT-ID` header. |
| `serviceProviderId` | string | Yes | The ID returned when the application was approved. |
| `amount` | object | No | The share for this order. Omit it to fall back to the share configured on the merchant link. |
| `amount.percentage` | string | No | Percentage of the order total, for example `"5"` for 5%. |
| `amount.fixed` | string | No | Fixed amount in minor currency units, for example `"200"` for 2 SEK. |
| `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. |
In 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.
`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.
Several service providers on one order each get their own entry:
```json
"serviceProviders": [
{ "serviceProviderId": "839ab2f1c47d300a20", "amount": { "percentage": "3" } },
{ "serviceProviderId": "83a1c4e7f20b100c11", "amount": { "fixed": "500" } }
]
```
### What happens next
Surfboard 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).
Refunds 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).
### Changing the split
The 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.
## Common Patterns
**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.
**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.
**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.
**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.
**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.
## Error Handling
| Error | Cause | Fix |
|-------|-------|-----|
| `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). |
| `400` Missing required parameter `serviceProviderId` | Link or unlink called without a body. | Send `{ "serviceProviderId": "..." }`. |
| `403` on service provider endpoints | Flow is not enabled for the partner. | Contact your account manager. |
| `404` Resource not found | Wrong `partnerId`, `merchantId`, or `applicationId`. | The merchant must belong to the partner in the path. |
| `APPLICATION_EXPIRED` | The KYB or signing link was not completed in time. | Create a new application. The old ID cannot be revived. |
## API Quick Reference
| Method | Endpoint | Purpose |
|--------|----------|---------|
| `POST` | `/partners/{partnerId}/service-providers` | Create a company service provider application |
| `POST` | `/partners/{partnerId}/service-providers/individual` | Onboard an individual for a merchant |
| `GET` | `/partners/{partnerId}/service-providers/applications` | List applications, filter by `applicationType` |
| `GET` | `/partners/{partnerId}/service-providers/applications/{applicationId}` | Application status and `serviceProviderId` |
| `GET` | `/partners/{partnerId}/service-providers` | All service providers under the partner |
| `POST` | `/partners/{partnerId}/merchants/{merchantId}/service-providers/link` | Link a service provider to a merchant |
| `DELETE` | `/partners/{partnerId}/merchants/{merchantId}/service-providers/unlink` | Remove the link |
| `GET` | `/partners/{partnerId}/merchants/{merchantId}/service-providers` | Active links on a merchant |
| `GET` | `/merchants/{merchantId}/service-providers` | Merchant-scoped view with contact details |
| `POST` | `/orders` | Set the split in `controlFunctions.serviceProviders` |
## Reference
- [Service Providers API](https://developers.surfboardpayments.com/api/service-providers)
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Create Merchant API](https://developers.surfboardpayments.com/api/merchants)
- [Surfboard Flow](/flow)
- [Partner Portal: Service Providers](/partner-portal/service-providers)
### Payment Methods
Category: online | Tags: Online, API, Payment Methods, In-Store
URL: /developers/guides/payment-methods
## Overview
Card 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.
## Available Payment Methods
| Method | Parameter | Description |
|--------|-----------|-------------|
| Card | `card` | Visa, Mastercard (enabled by default) |
| AMEX | `amex` | American Express |
| Swish | `swish` | Swedish mobile payments |
| Klarna | `klarna` | Buy now, pay later |
| B2B Invoice | `b2binv` | B2B invoice payments |
| Account-to-Account | `acc2acc` | Bank transfer |
| Vipps | `svipps` | Norwegian mobile payments |
| MobilePay | `smobilepay` | Danish mobile payments |
## Activate Payment Methods
Enable one or more payment methods for a merchant in a single request. Set each method to `true` to activate it.
```
POST /merchants/:merchantId/payment-methods
```
### Request
```json
{
"card": true,
"swish": true,
"klarna": true,
"b2binv": true
}
```
### Response
```json
{
"status": "SUCCESS",
"data": [
{
"method": "card",
"paymentMethodId": "pm_abc123",
"status": "SUCCESS"
},
{
"method": "swish",
"paymentMethodId": "pm_def456",
"status": "SUCCESS"
}
],
"message": "Payment methods activated successfully"
}
```
Each activated method returns a `paymentMethodId` you'll need for fetching details or deactivating later.
### Via Partner Portal
1. Log in to the **Partner Portal** > **Merchants** > select the merchant > **Payment Methods**
2. Select the payment methods you want to enable
3. Click **Activate Selected Methods**
## List All Payment Methods
Retrieve all available payment methods and their activation status for a merchant.
```
GET /merchants/:merchantId/payment-methods
```
### Response
```json
{
"status": "SUCCESS",
"data": [
{
"paymentMethodId": "pm_abc123",
"paymentMethod": "CARD"
},
{
"paymentMethodId": "pm_def456",
"paymentMethod": "SWISH"
}
],
"message": "Payment methods retrieved successfully"
}
```
## Get Payment Method Details
Fetch the configuration and status of a specific payment method.
```
GET /merchants/:merchantId/payment-methods/:paymentMethodId
```
### Response
```json
{
"status": "SUCCESS",
"data": {
"paymentMethodId": "pm_abc123",
"paymentMethod": "CARD",
"status": "ACTIVATED",
"acquirerMID": "12345678"
},
"message": "Payment method retrieved successfully"
}
```
The `status` field is either `ACTIVATED` or `DEACTIVATED`. For AMEX, the response includes an `amexMID`. For card payments, it includes an `acquirerMID`.
## Deactivate a Payment Method
Remove a payment method from a merchant using its `paymentMethodId`.
```
DELETE /merchants/:merchantId/payment-methods/:paymentMethodId
```
### Response
```json
{
"status": "SUCCESS",
"message": "Payment method deactivated successfully"
}
```
### Via Partner Portal
1. Log in to the **Partner Portal** > **Merchants** > select the merchant > **Payment Methods**
2. Find the method to remove and click the **Delete** icon
## Store-Level Configuration
You can scope payment methods to a specific store by including `storeId` in the activation request:
```json
{
"storeId": "YOUR_STORE_ID",
"swish": true,
"klarna": true
}
```
This lets different stores under the same merchant accept different payment methods.
### Client Auth Tokens
Category: online | Tags: Online, API, Authentication, Security
URL: /developers/guides/client-auth-tokens
## Overview
Client 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.
## What You Can Do With Client Tokens
Client tokens support operational API requests:
- Orders
- Payments
- Transactions
- Tips
- Reporting
- Branding
- Receipts
> **Note:** Client tokens cannot perform administrative tasks like creating merchants, managing stores, or other backend operations. Those still require full API credentials.
## Create a Token
Generate a token by sending a `POST` request with your auth provider credentials.
```
POST /partners/:partnerId/token
```
### Request
```json
{
"providerId": "YOUR_PROVIDER_ID",
"providerCertificate": "YOUR_PROVIDER_CERTIFICATE",
"externalUserId": "user_12345"
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `providerId` | string | Yes | Provider ID of the auth provider |
| `providerCertificate` | string | Yes | Certificate of the auth provider |
| `externalUserId` | string | Yes | Unique identifier for the user (e.g., a customer ID from your system) |
| `email` | string | No | Email address of the user |
> **Note:** To get your `providerId` and `providerCertificate`, contact [integrations@surfboard.se](mailto:integrations@surfboard.se) or reach out via Slack.
### Response
```json
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"validUntil": "3600",
"status": "SUCCESS",
"message": "Token created successfully"
}
```
The `validUntil` field indicates the token's validity period in seconds. When a token expires, generate a new one.
## Using Client Tokens
Include the token in the `Authorization` header as a Bearer token:
```
Authorization: Bearer
```
### Example: Initiate a Payment
```bash
curl -X POST YOUR_API_URL/payments \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer ' \
-d '{
"orderId": "o_RelSnor1A6gqgKzZxrbM7",
"paymentMethod": "CARD"
}'
```
## Token Lifecycle
1. **Generate** a token from your backend using full API credentials
2. **Pass** the token to your frontend application
3. **Use** the token for client-side API calls
4. **Refresh** the token when it expires by generating a new one from your backend
Keep your `API-KEY` and `API-SECRET` on the server side. Only the generated token should reach the client.
### Partner Branding
Category: online | Tags: Online, API, Branding, White-Label
URL: /developers/guides/partner-branding
## Overview
Surfboard is fully white-label. Use the Branding API to configure colors, fonts, logos, and images that apply to all terminals and customizable pages under your partner account. Branding can be set at the partner level and inherited by all merchants and stores beneath it.
## Set Partner Branding
Configure the visual appearance for your payment pages and terminals.
```
PATCH /partners/:partnerId/branding
```
### Request
```json
{
"backgroundColor": "#071132",
"brandColor": "#0e44e1",
"accentColor": "#00ffa7",
"footerColor": "#071132",
"rectShape": "rounded",
"fontType": "sans-serif",
"logoUrl": "https://your-cdn.com/logo.svg",
"iconUrl": "https://your-cdn.com/icon.png",
"primaryCoverImage": "https://your-cdn.com/cover-primary.jpg",
"secondaryCoverImage": "https://your-cdn.com/cover-secondary.jpg"
}
```
All fields are optional -- only include the ones you want to update.
### Branding Parameters
| Parameter | Description |
|-----------|-------------|
| `backgroundColor` | Background color for pages (hex) |
| `brandColor` | Primary brand color for buttons and accents (hex) |
| `accentColor` | Secondary color that complements the brand color (hex) |
| `footerColor` | Footer background color (hex) |
| `rectShape` | Button shape: `rounded`, `pill`, or `edgy` |
| `fontType` | Font family: `sans-serif`, `serif`, or `mono` |
| `logoUrl` | URL to your logo image |
| `iconUrl` | URL to your icon/favicon image |
| `primaryCoverImage` | URL to the primary cover image |
| `secondaryCoverImage` | URL to the secondary cover image |
### Response
```json
{
"status": "SUCCESS",
"message": "Branding updated successfully"
}
```
### Via Partner Portal
Navigate to **Settings** > **Set Partner Branding Config**, enter your branding values, and click **Save Changes**.
## Fetch Partner Branding
Retrieve the current branding configuration for your partner account.
```
GET /partners/:partnerId/branding
```
### Response
```json
{
"status": "SUCCESS",
"data": {
"backgroundColour": "#071132",
"brandColor": "#0e44e1",
"accentColor": "#00ffa7",
"footerColor": "#071132",
"rectShape": "rounded",
"fontType": "sans-serif",
"logoUrl": "https://your-cdn.com/logo.svg",
"iconUrl": "https://your-cdn.com/icon.png",
"primaryCoverImage": "https://your-cdn.com/cover-primary.jpg",
"secondaryCoverImage": "https://your-cdn.com/cover-secondary.jpg"
},
"message": "Branding retrieved successfully"
}
```
## How Branding Applies
Partner-level branding is the default for all merchants and stores under your account. It applies to:
- **Payment pages** -- hosted checkout UI
- **Terminals** -- on-screen branding for smart terminals
- **Receipts** -- logo and styling on digital receipts
This means your merchants' customers see your brand, not Surfboard's, across all payment touchpoints.
### Create Order Error Codes
Category: online | Tags: Online, API, Orders, Errors, Reference
URL: /developers/guides/create-order-error-codes
## Overview
This 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.
See 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.
## Error Response Shape
Errors return a `status` of `ERROR` along with an error code and message. Example:
```json
{
"status": "ERROR",
"errorCode": "OR_0042",
"message": "Terminal not found"
}
```
Error code prefixes indicate the source:
| Prefix | Source |
|--------|--------|
| `OR_` | Orders service (validation, order state, terminal/store checks) |
| `PS_` | Payment service (payment initiation, refund handling) |
| `GC_` | Gift card service |
| `SP_` | Service provider |
| `P_` | Platform-level validation, ahead of the services above |
Messages may contain placeholders like ``, ``, ``, or `` -- the API substitutes the real value at runtime.
### `P_0001`, where the message is the code
Payload validation that fails before the order reaches the Orders service comes back as `P_0001`, with the specific reason in free text:
```
P_0001: Input data validation failed.
```
`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:
| Message fragment | Cause | Fix |
|---|---|---|
| `Invalid item price for item id ` | `amount.total` sent as the line total | `total` is the **unit** price. See [Create an Order](/developers/guides/create-an-order) |
| `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) |
| `Cannot read properties of undefined (reading 'vatValue')` | A line is missing `amount.tax` | Send a `tax` entry on every line, including zero-rated ones |
The 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.
## Create Order Errors
Errors thrown while validating and creating the order.
### Validation and Schema
| Code | Message | Notes |
|------|---------|-------|
| `OR_0015` | Order validation failed | Wraps the raw schema validator error. Check required fields and types. |
| `OR_0022` | Invalid order line item. Missing item price | At least one line is missing `amount.regular` or `amount.total`. |
| `OR_0023` | Invalid item price for item id `[]`, `` | Price on the named line item failed validation. |
| `OR_0037` | Invalid total order price | `totalOrderAmount` does not reconcile with the order lines and adjustments. |
| `OR_0049` | *(empty message)* | Reserved / edge-case validation failure. |
### Merchant, Store, and Terminal
| Code | Message | Notes |
|------|---------|-------|
| `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. |
| `OR_0001` | No Merchant Details found for the Merchant ID | Merchant exists but is missing required configuration. |
| `OR_0002` | Order creation is not allowed for this merchant type `` | The merchant type does not permit direct order creation. |
| `OR_0028` | Store is not in an active state | Activate the store before creating orders. |
| `OR_0042` | Terminal not found | `terminal$id` does not match any terminal. |
| `OR_0007` | The Terminal ID is not associated with this merchant | Terminal exists but belongs to another merchant. |
| `OR_0006` | The terminal is in deregistered state | Re-register the terminal before use. |
| `OR_0050` | Order cannot be created, terminal is in an inactive state | Terminal is registered but not active. |
### Currency
| Code | Message | Notes |
|------|---------|-------|
| `OR_0003` | Currency Code is not present for this merchant | Merchant has no configured currency -- contact your onboarding contact. |
| `OR_0004` | The currency code is not associated with this merchant | Use a currency enabled on the merchant. |
| `OR_0005` | Currency is mandatory as the merchant has more than one currency | Multi-currency merchants must set `amount.currency` explicitly. |
| `OR_0048` | Currencies should be same in order lines | All line items must use the same currency. |
### Payment Method Locking
| Code | Message | Notes |
|------|---------|-------|
| `OR_0031` | No Payment Method found with the given payment method id | The `lockToPaymentMethods` id is unknown. |
| `OR_0032` | Lock to payment method is not allowed for this merchant | The merchant does not have the lock-to-method feature enabled. |
| `SP_0001` | Service provider IDs `[]` not associated with this merchant | One or more service provider IDs are invalid for this merchant. |
### Refunds (on create order)
Refunds are created as new orders with negative quantities and a `purchaseOrderId` on each line. These errors surface during that validation.
| Code | Message | Notes |
|------|---------|-------|
| `OR_0010` | No purchase order found with ID: `` | `purchaseOrderId` does not match any completed order. |
| `OR_0035` | Cannot refund from purchase order that is not completed. Status: `` | Only completed orders can be refunded. |
| `OR_0039` | Cannot refund more than the original order amount | Refund amount exceeds the remaining refundable balance. |
| `PS_0043` | Invalid order line item. Missing purchase order id for refund line | Every refund line must carry `purchaseOrderId`. |
| `PS_0010` | Unlinked refunds are only supported for CARD payment methods | Unlinked refunds cannot be issued on non-card methods. |
| `GC_0004` | Gift card refunds are not supported | Gift card payments cannot be refunded via this flow. |
## Initiate Payment Errors
When 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.
### Order State
| Code | Message | Notes |
|------|---------|-------|
| `PS_0057` | Order not found | The order referenced by the payment request does not exist. |
| `PS_0062` | Order already paid | The order has already been fully paid. |
| `OR_0046` | Order cancelled | The order is in a cancelled state and cannot accept payments. |
| `PS_0019` | Cannot pay `[]`, amount payable `[]` | Requested amount exceeds the outstanding balance on the order. |
### Amount and Validation
| Code | Message | Notes |
|------|---------|-------|
| `PS_0008` | Payment amount cannot be less than 0.01 | Send a positive amount in the smallest currency unit. |
| `PS_0033` | `empty payload - payment initiation rejected` or `Initiate payment validation failed with error ` | Payload is missing or failed schema validation. |
| `PS_0025` | Payment initiation failed: `` | Wraps unexpected exceptions from downstream services. See the *Handling `PS_0025`* section below. |
### Terminal, Merchant, and Store
| Code | Message | Notes |
|------|---------|-------|
| `OR_0042` | Terminal not found | Terminal ID unknown. |
| `OR_0006` | Terminal `[]` is not active | Re-activate the terminal. |
| `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. |
| `PS_0059` | Merchant not found | Merchant ID unknown. |
| `PS_0058` | Store not found | Store ID unknown. |
### Refund-Specific
| Code | Message | Notes |
|------|---------|-------|
| `OR_0053` | Mixed linked and unlinked refund lines are not supported | Split mixed refunds into separate requests. |
| `PS_0104` | Multiple `purchasePaymentId` values found in refund lines / This purchase order has partial payments | Specify which payment to refund using `purchasePaymentId`. |
| `PS_0017` | No valid purchase payments found for the specified payment IDs / Purchase payment not found | The `purchasePaymentId` does not match a refundable payment. |
| `PS_0010` | Payment 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 `[]` / Purchase payment does not support CARD\_NP refund | Refund is not allowed for the given method or configuration. |
| `PS_0032` | Cannot refund amount `` (not enough remaining / not enough in card / not enough in other methods / exceeds max for ``) | Refund would exceed the available balance. |
| `PS_0068` | Cannot refund amount `` as there are no card payments in the purchase order | No card payments are available to refund against. |
| `PS_0096` | Cannot initiate refund as there are no payments of type `` | No payments of the requested method exist on the order. |
| `PS_0034` | Message is missing | Card-not-present refund is missing the `message` field. |
| `PS_0044` | Cannot refund amount `` as message `` is not allowed / Purchase payment id is required, more than one purchase matches the criteria | Card-not-present refund message/ID disambiguation required. |
## Combined Flow Errors
When 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:
```json
{
"status": "ERROR",
"errorCode": "",
"message": "Payment failed",
"payment": {
"error": {
"errorMessage": "...",
"errorCode": ["..."],
"psErrorCodes": ["..."]
}
}
}
```
Merge `payment.error.errorCode` and `payment.error.psErrorCodes` to get the full list of processor codes for logging and support.
## Handling `PS_0025` -- Terminal Not Connected
A common `PS_0025` variant is:
```
PS_0025: Terminal is not connected to server so unable to send transactions
```
Re-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.
See [Interapp Integration](/developers/guides/interapp-integration) for the full recovery pattern.
## Handling Patterns
- **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.
- **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.
- **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.
- **Log the full response.** Capture `errorCode`, `message`, and `payment.error.*` together. Support tickets without the error code are very hard to trace.
## Reference
- [Create an Order](/developers/guides/create-an-order)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
- [Interapp Integration](/developers/guides/interapp-integration) -- `PS_0025` recovery
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
### Online Payment Link
Category: online | Tags: Online, API, Payment Link, Payment Page, Orders
URL: /developers/guides/online-payment-link
## Overview
An online payment link is an order created against an online terminal. The API returns a URL hosted by Surfboard; you send it to the customer by email, SMS, chat, or a redirect from your own site, and the card details never touch your infrastructure.
The order call is the same [Create Order](/developers/guides/create-an-order) call you use in store. What changes is everything around it: the terminal must be an **online** terminal, that terminal must sit in an **online store**, and the store's domains must be verified before Surfboard will approve it. This guide walks the whole path once, then covers the `controlFunctions.online` block that shapes the page the customer lands on.
If you want to render the card fields inside your own page instead of sending the customer away, use the [Self-Hosted Checkout](/developers/guides/self-hosted-checkout) guide. If you want to charge a stored card from your backend with no customer present, see [Server-to-Server API](/developers/guides/server-to-server-api).
## Prerequisites
1. A developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. A merchant that has completed onboarding and KYB
3. Control of the DNS for the webshop domain you are going to register
> **Demo environment:** payment page mode is the only online terminal type supported in demo, and only test cards work there. Real cards used in demo are voided automatically after 30 minutes and never settle.
## Step 1: Create an Online Store
Terminals live under stores, and an online terminal needs a store that carries an `onlineInfo` block. Create the store with the webshop details, or add `onlineInfo` to an existing physical store.
```json
POST /partners/:partnerId/merchants/:merchantId/stores
{
"storeName": "Web Store",
"email": "webstore@example.com",
"phoneNumber": { "code": 46, "number": "701234567" },
"address": "Drottninggatan 10",
"city": "Stockholm",
"zipCode": "103 16",
"country": "SE",
"onlineInfo": {
"merchantWebshopURL": "https://shop.example.com",
"paymentPageHostURL": "https://shop.example.com/payment",
"termsAndConditionsURL": "https://shop.example.com/terms",
"privacyPolicyURL": "https://shop.example.com/privacy"
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"storeId": "81d64e7174dcb00b0f",
"merchantId": "818712cdbcb670070e",
"name": "Web Store",
"merchantUrlDomainVerificationKey": "499470649f03b53fa1175659d4389743974710260b7f410313487e6062b3d559",
"paymentPageUrlDomainVerificationKey": "2179beab4f5e8c3960615205f042939a2ccc6c51a6e5923c9c068b3d9a645590"
},
"message": "Store Created Successfully"
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `onlineInfo.merchantWebshopURL` | Yes | The shop the customer is buying from. Verified by DNS. |
| `onlineInfo.termsAndConditionsURL` | Yes | Must include the refund policy. Rendered on the checkout page. |
| `onlineInfo.privacyPolicyURL` | Yes | Rendered on the checkout page. |
| `onlineInfo.paymentPageHostURL` | No | Only needed for SDK and iFrame modes. Verified by DNS when present. |
> **Online info can only be set once.** Get these URLs right before you send the call; they cannot be edited afterwards through the same route.
Terms, privacy policy and contact details must also be visible on the webshop itself. This is an acquiring requirement, not a Surfboard preference.
## Step 2: Verify the Domains
The response carries one verification key per URL. Publish each as a **TXT record** on the matching domain, then ask Surfboard to check it:
```json
POST /partners/:partnerId/merchants/:merchantId/stores/:storeId/verify
{
"domainType": "MERCHANT_WEBSHOP_URL"
}
```
Repeat with `"domainType": "PAYMENT_PAGE_HOST_URL"` if you registered a payment page host. Surfboard also re-checks automatically every six hours, so a record published late is picked up without another call.
Verification is what unlocks the online terminal types: until the webshop domain passes, there is nothing to register a terminal against. Once it passes, the store goes through an internal approval step at Surfboard. Poll the store to see where it stands:
```
GET /partners/:partnerId/merchants/:merchantId/stores/:storeId/online
```
See [Store Management](/developers/guides/store-management) for the full store lifecycle.
## Step 3: Pick Up the Terminal You Already Have
An online terminal is a mode, not a device — and for payment links you do not have to create one. Creating the online store provisions two terminals by default: a `PaymentPage` terminal, which is the one this guide uses, and a `MerchantInitiated` terminal for backend charges against a stored token. List the store's terminals and take the ID:
```
GET /partners/:partnerId/merchants/:merchantId/stores/:storeId/terminals
```
```json
// Response
{
"status": "SUCCESS",
"data": [
{
"terminalId": "813ca2cb12ce400405",
"terminalType": "PaymentPage",
"terminalStatus": "ACTIVE",
"storeId": "81d64e7174dcb00b0f"
},
{
"terminalId": "813ca2cb12ce400406",
"terminalType": "MerchantInitiated",
"terminalStatus": "ACTIVE",
"storeId": "81d64e7174dcb00b0f"
}
],
"message": "Terminals fetched successfully"
}
```
Store the `PaymentPage` `terminalId` against something identifiable in your system — it is the `terminal$id` every order in this guide is created against.
| Mode | Use it for | Provisioned with the store |
|------|------------|----------------------------|
| `PaymentPage` | Payment links and hosted checkout. This guide. | Yes |
| `MerchantInitiated` | Backend charges against a stored token, such as subscription renewals. | Yes |
| `SelfHostedPage` | Card fields rendered on your own page by the Online SDK. Returns a `publicKey` and `sdkUrl`. | No |
| `iFrame` | An embedded payment frame inside your site. | No |
The two default terminals exist from the moment the store does, but they cannot take a payment until the domains verify and the store is approved. The other two modes are registered when you need them, and a store can hold as many as you like:
```json
POST /merchants/:merchantId/stores/:storeId/online-terminals
{
"onlineTerminalMode": "SelfHostedPage"
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"terminalId": "813ca2cb12ce400407",
"registrationStatus": "REGISTERED"
},
"message": "Terminal registered successfully"
}
```
## Step 4: Create the Order
Now the familiar call. The `terminal$id` is the `PaymentPage` terminal from step 3, and the response carries the link.
```json
POST /orders
{
"terminal$id": "813ca2cb12ce400405",
"referenceId": "order-2026-0418",
"customer": {
"person": {
"name": { "firstName": "John", "lastName": "Doe" },
"email": "john@example.com",
"phoneNumber": { "code": "46", "number": "768100190" }
}
},
"orderLines": [
{
"id": "ITEM-001",
"name": "Annual Subscription",
"quantity": 1,
"amount": {
"regular": 99900,
"total": 99900,
"currency": "752",
"tax": [{ "amount": 19980, "percentage": 25, "type": "VAT" }]
}
}
],
"totalOrderAmount": {
"regular": 99900,
"total": 99900,
"currency": "752",
"tax": [{ "amount": 19980, "percentage": 25, "type": "VAT" }]
},
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "CARD",
"amount": 99900
},
"online": {
"paymentPageValidFor": "2h",
"enforce3DSecure": true,
"generateShortLink": true,
"payButtonType": "PAY",
"redirectUrl": "https://shop.example.com/thanks",
"failureRedirectUrl": "https://shop.example.com/checkout/failed"
}
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderId": "8455c12f9fd0620a010b",
"paymentPageLink": "https://pay.withsurfboard.com/8455c12f9fd0620a010b?pi=Dr4GoyMXF0zHvjca_Oa0vHgxcT-OD1qp7KdokyI7dkTwRwYJt8nkXyQm3bT6vqCfgraOw50Bf5uOp__3ckbMWOV6L9QbiTiSEFS3YmF4Eb8lr5pTWP2KFjm9Ukmd0000&add=IzFlNDBhZg==",
"shortLinkUrl": "https://srfb.se/Iq4JfPgHL"
},
"message": "Order created successfully"
}
```
`paymentPageLink` is the page to send the customer to. Treat it as opaque and pass it on whole: the query string carries the payment intent, and a link with a trimmed or re-encoded `pi` will not open. `shortLinkUrl` appears only when you asked for it with `generateShortLink`, and is the one to put in an SMS.
Keep the `orderId`. Everything afterwards is keyed on it, and unlike an in-store order there is no `paymentId` yet: nothing has been attempted until the customer opens the page. The `paymentId` arrives with the first attempt, in the status response and in the webhook.
Line items, tax, adjustments, customer objects and the order-level calculation rules all behave exactly as they do in store. [Create an Order](/developers/guides/create-an-order) covers them in full.
## Control Functions for Online Orders
Everything specific to the hosted page lives in `controlFunctions.online`.
| Field | Description |
|-------|-------------|
| `paymentPageValidFor` | How long the link works, as `` where the unit is `m`, `h` or `d` — for example `15m`, `2h`, `3d`. Defaults to one day. |
| `redirectUrl` | Where the customer lands after a successful payment. The `orderId` is appended as a query parameter. |
| `failureRedirectUrl` | Where the customer lands after a failure. Also carries the `orderId`. |
| `generateShortLink` | Returns `shortLinkUrl` alongside the full link. Default `false`. |
| `payButtonType` | The label on the button: `PAY`, `DONATE`, `BOOK`, `ORDER`, `CHECKOUT`, `CONTINUE`, `CONTRIBUTE`, `ADD_MONEY`, `RENT`, `SUPPORT`, `TIP`, `TOP_UP`. |
| `enforce3DSecure` | Force 3-D Secure where the issuer supports it. |
| `relaxed3ds` | Allow relaxed 3-D Secure handling. |
| `addressRequirements` | Ask for an address on the page. |
| `enforceTokenization` | Save the card for later use, overriding the terminal configuration. |
| `tokenisationIfPossible` | Tokenize when supported, but do not fail the payment if it is not. |
| `errorIfTokenizationFails` | Fail the flow when the card cannot be tokenized. |
| `subscription` | Mark the order as recurring-capable. |
| `generateOnlineLinkWith` | Generate the link with a different terminal than the one the order was created against. |
| `selfCardCharging` | Let the customer charge their own card. |
These sit next to the order-level controls that are not online-specific but matter here:
| Field | Description |
|-------|-------------|
| `delayCapture` | Authorize now, capture later. See [Capture a Payment](/developers/guides/capture-a-payment). |
| `authMode` | `AUTH` or `PRE-AUTH`. Choosing `PRE-AUTH` sets `delayCapture` for you. |
| `lockToPaymentMethods` | Restrict the page to the methods you list, e.g. `["CARD", "KLARNA"]`. |
| `delayPayout` | Hold the payout for a period, as ``. |
| `callBackUrl` | Per-order webhook URL for this order and its payments. |
### Recurring Orders
For a subscription, add the `recurring` object inside `online` and mark the order as one:
```json
{
"controlFunctions": {
"online": {
"subscription": true,
"enforceTokenization": true,
"recurring": {
"subscriptionAmountType": "fixed",
"frequency": "monthly",
"numberOfPayments": 12,
"uniqueReference": "sub-4471",
"validation": "validated"
}
}
}
}
```
The first payment is a customer-initiated transaction on the page, which is where the card is tokenized and 3-D Secure is satisfied. Every renewal after that is a merchant-initiated transaction against the stored token, charged from your backend through a `MerchantInitiated` terminal. [Recurring Payments](/developers/guides/recurring-payments) has the renewal side.
| Field | Description |
|-------|-------------|
| `subscriptionAmountType` | `fixed` or `variable`. |
| `maxAmount` | Ceiling in minor units, for `variable` subscriptions only. |
| `frequency` | `daily`, `twiceWeekly`, `weekly`, `tenDays`, `fortNightly`, `monthly`, `everyTwoMonths`, `trimester`, `quarterly`, `twiceYearly`, `annually`, `unscheduled`. Required. |
| `numberOfPayments` | How many payments the schedule expects. |
| `uniqueReference` | Your reference for the recurring order. |
| `validation` | `validated` or `notValidated`. |
## Step 5: Confirm the Payment
The redirect back to your site tells you the customer finished, not that the money moved. Confirm server-side, either by polling or, better, by subscribing to the webhook.
```
GET /orders/:orderId/status
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderStatus": "PAYMENT_COMPLETED",
"payments": [
{
"paymentId": "83a1ba3264bd500106",
"paymentStatus": "PAYMENT_COMPLETED",
"paymentMethod": "CARD",
"amount": 99900
}
]
}
}
```
The states are the same as in store. An order sits in `PENDING` while the link is unused, and a failed or cancelled payment leaves it there, so the same `orderId` can be paid again without creating a new order.
| Order status | Meaning |
|--------------|---------|
| `PENDING` | The link has not been paid yet, or the last attempt failed or was cancelled. |
| `PAYMENT_COMPLETED` | Paid in full. The order is closed. |
| `PARTIAL_PAYMENT_COMPLETED` | Part of the total is paid. See [Partial Payments](/developers/guides/partial-payments). |
| `PAYMENT_CANCELLED` | The order was cancelled. |
Subscribe to `order.paymentcompleted` and `order.paymentfailed` rather than polling on a timer. The payload carries the `orderId`, your `referenceId`, the amount and the transaction details, and each delivery carries a `metadata.eventId` you should use for idempotency — Surfboard retries a failed delivery twice, after 5 and 10 minutes.
```json
{
"eventType": "order.paymentcompleted",
"metadata": {
"eventId": "831fc2f040bf405fff",
"created": 1745821536443,
"retryAttempt": 0,
"terminalId": "813ca2cb12ce400405"
},
"data": {
"orderId": "83a1ba32774149710b",
"referenceId": "order-2026-0418",
"paymentId": "83a1ba3264bd500106",
"paymentStatus": "PAYMENT_COMPLETED",
"paymentMethod": "CARD",
"amount": "99900",
"type": "PURCHASE"
}
}
```
See [Webhooks](/developers/guides/webhooks-notifications) for subscription and signature verification.
## Sending the Link
The link is a URL, so how it reaches the customer is your call:
- **Redirect** from your own checkout, the closest thing to a hosted checkout flow.
- **Email or SMS** for invoices, deposits and quotes. Use `shortLinkUrl` in an SMS and keep `paymentPageValidFor` short enough that a stale link cannot be paid by mistake.
- **QR code** printed or shown on screen, for pay-at-table and self-service.
Two things to hold on to. Set `paymentPageValidFor` deliberately — a link that lives for three days is a link someone can pay three days late, after you have cancelled the order. And never treat the arrival at `redirectUrl` as proof of payment: a customer can reach that URL by other means. The webhook and the status call are the record.
## Error Handling
Create-order failures come back as `status: "ERROR"` with an `OR_*` or `PS_*` code. The ones you will meet setting this up:
| Code | Cause |
|------|-------|
| `OR_0042` | Terminal not found. The `terminal$id` is wrong, or it belongs to another merchant. |
| `OR_0037` | The total does not reconcile with the line items. |
| `OR_0048` | Line items mix currencies. |
If the store's terminal list comes back without a `PaymentPage` entry, the store was created without `onlineInfo` — the two default terminals only come with an online store. A terminal that will not register, or one that is there but refuses a payment, usually means the store has not cleared domain verification or is still in approval. Check the store's online status before you look at the terminal call.
The [Create Order Error Codes](/developers/guides/create-order-error-codes) reference lists the rest, including errors raised by the payment initiation that happens inside the same call.
## Next Steps
- [Payment Page](/developers/guides/payment-page) — the hosted checkout redirect in more detail
- [Self-Hosted Checkout](/developers/guides/self-hosted-checkout) — keep the customer on your own page
- [Server-to-Server API](/developers/guides/server-to-server-api) — charge a stored card with no customer present
- [Capture a Payment](/developers/guides/capture-a-payment) — finalize a delayed-capture authorization
- [Refund an Order](/developers/guides/refund-an-order) — return funds after settlement
## Reference
- [Stores API](https://developers.surfboardpayments.com/api/stores)
- [Terminals API](https://developers.surfboardpayments.com/api/terminals)
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Developer Portal](https://developers.surfboardpayments.com/)
### Customer Identification on Card Tap
Category: in-store | Tags: In-Store, API, Webhooks, Loyalty, Personalisation
URL: /developers/guides/customer-identification
## Overview
A 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.
The customer does nothing but pay. No app, no scan, no "are you a member with us?" at the till.
> **Android terminals only.** The feature is available for card payments on Surfboard's Android terminals. Support for further payment methods is on the roadmap.
## The Flow
| Step | Who | What happens |
|------|-----|--------------|
| 1 | You | Create the order as normal |
| 2 | Customer | Taps their card at the terminal |
| 3 | Surfboard | Sends `order.customer.identify` with the order and a card token |
| 4 | You | Look the token up, and update the order if you recognise it |
| 5 | You | Initiate the payment against the updated order |
The 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.
## Prerequisites
1. A registered Android terminal under an onboarded merchant and store
2. A webhook endpoint subscribed to `order.customer.identify` — see [Webhooks](/developers/guides/webhooks-notifications)
3. Somewhere to store card tokens against your customers
## Step 1: Create the Order
Nothing changes here. Create the order the way you always do, with the line items you have at the point of sale.
```json
POST /orders
{
"terminal$id": "8386af3b0f71b80b04",
"referenceId": "till-2-0418",
"orderLines": [
{
"id": "ITEM-001",
"name": "Nike Shoes",
"quantity": 1,
"amount": {
"regular": 50000,
"total": 50000,
"currency": "752",
"tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }]
}
}
],
"totalOrderAmount": {
"regular": 50000,
"total": 50000,
"currency": "752",
"tax": [{ "amount": 10000, "percentage": 25, "type": "VAT" }]
}
}
```
Leave `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.
## Step 2: The Customer Taps
The 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.
## Step 3: Receive `order.customer.identify`
```json
{
"eventType": "order.customer.identify",
"metadata": {
"eventId": "832cf9fe1806581dff",
"created": 1747553660038,
"retryAttempt": 0,
"webhookEventId": "81a214e74b107801ff"
},
"data": {
"orderId": "832cf9f93d2fd0410b",
"cardId": "c550c29e80908c887a"
}
}
```
`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.
Acknowledge 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`.
You can also pull the same card data from the order rather than waiting for the webhook:
```
GET /orders/:orderId/tokens
```
See [Tokens](/developers/guides/tokens) for what comes back.
### Matching the Token
The first time you see a `cardId` you will not recognise it, and that is the normal state of a new customer:
- **Known token** — load the customer, apply what they are entitled to, move to step 4.
- **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.
Never 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.
## Step 4: Update the Order
Apply what you found with the Update Order API. The order keeps its `orderId`.
```json
PUT /orders/:orderId
{
"terminal$id": "8386af3b0f71b80b04",
"customer": {
"customerId": "cus_88213",
"person": {
"name": { "firstName": "John", "lastName": "Doe" },
"email": "john@example.com"
}
},
"orderLines": [
{
"id": "ITEM-001",
"name": "Nike Shoes",
"quantity": 1,
"amount": {
"regular": 50000,
"campaign": 5000,
"total": 45000,
"currency": "752",
"tax": [{ "amount": 9000, "percentage": 25, "type": "VAT" }]
}
}
],
"totalOrderAmount": {
"regular": 50000,
"campaign": 5000,
"total": 45000,
"currency": "752",
"tax": [{ "amount": 9000, "percentage": 25, "type": "VAT" }]
},
"metadata": {
"loyaltyTier": "gold",
"memberSince": "2023-11-02"
},
"controlFunctions": {
"orderLineLevelCalculation": true
}
}
```
What you change depends on what you are giving them:
| Intent | Where it goes |
|--------|---------------|
| Member price or loyalty discount | `campaign` on the line, or an order-level `adjustment` |
| Points redeemed as money off | An `adjustment`, so it is visible as its own line in reporting |
| Attach the person to the order | `customer`, which also carries the receipt to their email |
| Anything your own systems need later | `metadata`, on the order or the line |
Recalculate `totalOrderAmount` to match. A total that does not reconcile with its lines is rejected with `OR_0037`.
> **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.
## Step 5: Initiate the Payment
```json
POST /payments
{
"orderId": "832cf9f93d2fd0410b",
"paymentMethod": "CARD",
"amount": 45000
}
```
The 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.
## Storing Tokens Responsibly
The card token turns anonymous footfall into a recognisable customer, so it deserves the treatment personal data gets:
- Store it against a customer record, not in a log line.
- Give the customer a way to be forgotten that removes the token as well as the profile.
- 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.
- The token is scoped to your merchant. It is not a national identifier, and it is not portable.
## Error Handling
| Symptom | Likely cause |
|---------|--------------|
| No webhook on tap | The endpoint is not subscribed to `order.customer.identify`, or the terminal is not an Android terminal. |
| `PUT /orders/:orderId` returns 404 | The `orderId` is wrong, or the order belongs to another merchant. |
| Update rejected after a tap | A payment has already been initiated for the order. The window has closed. |
| `OR_0037` on update | The new `totalOrderAmount` does not reconcile with the line items. |
## Reference
- [Order Customer Identity webhook](https://developers.surfboardpayments.com/references/webhooks)
- [Update Order API](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Tokens](/developers/guides/tokens)
- [Webhooks](/developers/guides/webhooks-notifications)
- [Create an Order](/developers/guides/create-an-order)
### Tokens
Category: online | Tags: Online, API, Tokenization, Cards, MIT
URL: /developers/guides/tokens
## Overview
A token is a reference to a card that Surfboard holds and you do not. The customer enters their details once, on a page or a terminal that is already in scope for PCI DSS, and you get back a `tokenId` you can charge later without the card ever touching your systems.
That is what makes the rest possible: subscription renewals, one-click repeat purchases, deposits settled after the fact, refunds routed back to the original card. This guide covers producing a token, reading it, and storing it. Charging one is [Server-to-Server API](/developers/guides/server-to-server-api), and scheduling the charges is [Recurring Payments](/developers/guides/recurring-payments).
| Where the card is entered | What tokenization gives you |
|---------------------------|------------------------------|
| Payment page or Online SDK | Card data handled inside Surfboard's PCI scope, never yours |
| MerchantInitiated terminal | A card you can charge with no customer present |
| Refunds | A reference back to the card that paid, without storing the card |
## Step 1: Ask for a Token
Set `enforceTokenization` when you create the order. On an online order it belongs inside `controlFunctions.online`:
```json
POST /orders
{
"terminal$id": "YOUR_TERMINAL_ID",
"orderLines": [
{
"id": "ITEM-001",
"name": "First month",
"quantity": 1,
"amount": { "regular": 19900, "total": 19900, "currency": "752" }
}
],
"totalOrderAmount": { "regular": 19900, "total": 19900, "currency": "752" },
"controlFunctions": {
"initiatePaymentsOptions": { "paymentMethod": "CARD", "amount": 19900 },
"online": {
"enforceTokenization": true
}
}
}
```
Three flags decide how hard you insist, and the difference matters when the card or the issuer will not play along:
| Flag | Behaviour |
|------|-----------|
| `enforceTokenization` | Tokenize the card for future use. Overrides the terminal's own configuration. |
| `tokenisationIfPossible` | Tokenize where it is supported, and carry on quietly where it is not. |
| `errorIfTokenizationFails` | Fail the whole payment if the card cannot be tokenized. |
Pick by what breaks if there is no token. A subscription with no token to renew against is worse than a failed first payment, so `errorIfTokenizationFails` is right there. A shop offering "save this card for next time" should not lose the sale over it, so `tokenisationIfPossible` is right there.
For subscriptions, pair tokenization with the `recurring` block so the first payment is authorised as the start of a series rather than a one-off — see [Online Payment Link](/developers/guides/online-payment-link#recurring-orders).
## Step 2: Fetch the Token
The token exists once the payment succeeds. Read it from the order:
```
GET /orders/:orderId/tokens
```
```json
// Response
{
"status": "SUCCESS",
"data": [
{
"cardBrand": "VISA",
"cardholderName": "Tom",
"tokenId": "822d544dc48c200308",
"createdAt": "2024-04-25T11:22:24.845Z",
"expiryMonth": 7,
"expiryYear": 2026,
"truncatedPan": "8907",
"cardArt": "iVBORw0KGgoAAAANSUhEUgAAAUQAAA......"
}
],
"message": "Fetched the card information."
}
```
| Field | What it is for |
|-------|----------------|
| `tokenId` | The handle you charge against. The only field that does anything. |
| `cardBrand`, `truncatedPan` | "Visa ending 8907" — how you show a saved card back to a customer. |
| `expiryMonth`, `expiryYear` | When the token stops working. Worth acting on before it does. |
| `cardholderName` | As given by the card. |
| `cardArt` | Base64 card image, if you want the saved card to look like the card. |
The response is an array. An order paid in parts, or retried on a second card, produces more than one token, so do not assume `data[0]`.
## Step 3: Store It
Store the `tokenId` against the customer in your own system, along with enough to describe it back to them — brand, last four, expiry. That pairing is the whole point: the token is meaningless without knowing whose card it is, and useless if the customer cannot tell which of their two Visas it is.
Store it where you would store an account identifier: your primary datastore, encrypted at rest, out of logs and analytics events. A token is not card data, but it is a bearer reference to someone's money.
## Charging a Stored Token
Pass it into a payment through `paymentMethodParams`:
```json
POST /payments
{
"orderId": "83a1ba32774149710b",
"paymentMethod": "CARD",
"amount": 19900,
"paymentMethodParams": {
"tokenId": "822d544dc48c200308"
}
}
```
A charge with no customer present is a Merchant Initiated Transaction, and it needs a terminal in `MerchantInitiated` mode — the terminal the customer paid on cannot do it. An online store is provisioned with one, so this is usually a matter of reading the store's terminal list rather than registering anything. [Server-to-Server API](/developers/guides/server-to-server-api) covers the setup and the rules that come with MIT.
## Expiry and Housekeeping
Tokens do not live forever, and the failure is silent until you try to charge:
- **The card expires.** You have `expiryMonth` and `expiryYear` at the point of tokenization, so you can warn a subscriber before the renewal that will fail rather than after it.
- **The card is replaced, lost or cancelled.** The token stops working with no notice to you. Handle the failure on the charge and ask the customer to re-authorise.
- **The customer asks you to forget them.** Delete your side of the mapping. A token with no customer attached is not usable and should not be kept.
Build the re-authorisation path before you need it: a link back to a payment page that tokenizes a fresh card and swaps the token on the subscription. It is the difference between a churned subscriber and a two-minute interruption.
## Reference
- [Fetch Tokens from Order](https://developers.surfboardpayments.com/api/orders)
- [Payments API](https://developers.surfboardpayments.com/api/payments)
- [Server-to-Server API](/developers/guides/server-to-server-api)
- [Recurring Payments](/developers/guides/recurring-payments)
- [Online Payment Link](/developers/guides/online-payment-link)
### Order and Return Terminals
Category: in-store | Tags: In-Store, API, Logistics, Terminals, Partners
URL: /developers/guides/terminal-logistics
## Overview
Before 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.
There are two moments to order from, and they are different calls:
| When | How |
|------|-----|
| During onboarding | Control fields on the Create Merchant call — the merchant picks from a catalogue you curate, or you preselect for them |
| Any time after | The Create Shipment call, against an existing merchant |
Returns are one call plus a waybill, and everything in flight reports its progress through a single webhook.
## Prerequisites
1. A partner account with API credentials and your `partnerId`
2. Product IDs and pricing plans for the hardware you resell — Surfboard provides both
3. A webhook endpoint subscribed to `logistics.orderupdate`
## Ordering During Onboarding
Terminals 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).
```json
POST /partners/:partnerId/merchants
{
"country": "SE",
"organisation": { "corporateId": "1234567890", "legalName": "Example AB" },
"controlFields": {
"showProductCatalogue": true,
"displayProducts": [
{ "productId": "815db2c5adc9b00301", "pricingPlans": ["816192c7efa2b0091a"] }
],
"preSelectProducts": [
{
"productId": "815db2c5adc9b00301",
"quantity": 2,
"pricingPlanId": "816192c7efa2b0091a"
}
],
"transactionPricingPlan": "816192c7efa2b0091a"
}
}
```
| Control field | What it does |
|---------------|--------------|
| `showProductCatalogue` | Shows the hardware catalogue during onboarding. |
| `displayProducts` | Restricts the catalogue to the products you list, each with the pricing plan that merchant gets. |
| `preSelectProducts` | Ships the listed products without asking. Use it when the hardware is part of the package rather than a choice. |
Curate `displayProducts` per segment rather than showing everything. A merchant choosing between two terminals decides; a merchant choosing between nine calls support.
## Ordering After Onboarding
For additional terminals, replacements, or accessories, create a shipment directly:
```json
POST /partners/:partnerId/merchants/:merchantId/shipment
{
"shippingAddress": {
"name": "John Doe",
"addressLine1": "Main Street 123",
"addressLine2": "Building C",
"city": "Stockholm",
"countryCode": "SE",
"postalCode": "123 45",
"phoneNumber": { "code": "46", "number": "771890089" },
"email": "store@example.com",
"deliveryInstruction": "Reception, ask for the store manager"
},
"lineItems": [
{ "productId": "815db2c5adc9b00301", "quantity": 1 }
]
}
```
```json
// Response
{
"status": "SUCCESS",
"data": { "orderId": "81376ad8ebedf80310" },
"message": "Order for shipping terminal successfully created"
}
```
`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.
| Line item field | Notes |
|-----------------|-------|
| `productId` | The Surfboard product ID, unique to you as a partner. |
| `quantity` | How many of that product. |
| `billingPlanId` | Optional. Falls back to the default plan for that product. |
| `replacementFor` | The `terminalId` of a device being replaced. |
### Replacements
Set `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.
```json
{
"lineItems": [
{
"productId": "815db2c5adc9b00301",
"quantity": 1,
"replacementFor": "816a0ff6bc0fb00404"
}
]
}
```
## Tracking the Shipment
Every change of state raises `logistics.orderupdate` against your webhook endpoint:
```json
{
"eventType": "logistics.orderupdate",
"metadata": {
"eventId": "81a214e74b107801ff",
"created": 1695793998732,
"retryAttempt": 0
},
"data": {
"merchantId": "81412e2e4102f80f0e",
"orderId": "81376ad8ebedf80310",
"orderStatus": "ORDER_SHIPPED",
"trackingUrl": "https://www.dhl.com/home/tracking.html",
"packageDetails": [
{ "productId": "817361bb0a23400701", "serial": "658364" }
]
}
}
```
| `orderStatus` | Meaning |
|---------------|---------|
| `ORDER_PLACED` | The order is accepted. |
| `ORDER_PENDING_FOR_STOCK` | Waiting on stock. Worth surfacing to the merchant — this is the status behind "where is my terminal". |
| `ORDER_SHIPPED` | In transit. Carries `trackingUrl` and `packageDetails`. |
| `ORDER_COMPLETED` | Delivered and fulfilled. |
`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).
Acknowledge with `200 OK` within 10 seconds, and deduplicate on `metadata.eventId`. Failed deliveries are retried twice, after 5 and 10 minutes.
## Returning a Terminal
When a merchant churns, downsizes, or has a device that will not come back to life:
```json
POST /partners/:partnerId/logistics/return
{
"terminalId": "816a0ff6bc0fb00404",
"name": "John Doe",
"email": "store@example.com",
"phoneNumber": { "code": "46", "number": "771890089" },
"address": {
"addressLine1": "Main Street 123",
"addressLine2": "Building C",
"city": "Stockholm",
"countryCode": "SE",
"postalCode": "123 45"
},
"deliveryInstruction": "Go left after the elevator",
"comment": "Merchant closed the second location",
"reasonForReturn": "NOT_USING_SERVICE"
}
```
The 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.
List what is in flight:
```
GET /partners/:partnerId/logistics/return
```
> **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).
## What to Build Around This
Three things repay the effort:
- **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.
- **Keep serial-to-store mapping from the shipped event.** It turns a later terminal fault into a lookup rather than an investigation.
- **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.
## Reference
- [Logistics API](https://developers.surfboardpayments.com/api/logistics)
- [Logistics Order Update webhook](https://developers.surfboardpayments.com/references/webhooks)
- [Merchant Onboarding](/developers/guides/merchant-onboarding)
- [Device Registration](/developers/guides/device-registration)
- [Terminal & Device Management](/developers/guides/terminal-device-management)
### Transactions and Reports
Category: online | Tags: Online, API, Transactions, Reporting, Reconciliation, In-Store
URL: /developers/guides/transactions-and-reports
## Overview
A 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.
This guide covers both halves: the transaction APIs you query directly, and how to read the monthly report that summarises a period.
## Fetching Transactions
### All Transactions
```
GET /transactions
```
Returns the merchant's transactions, newest first. Filter to a period with `startDate` and `endDate`:
```
GET /transactions?startDate=2026-04-01&endDate=2026-04-30
```
Responses 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:
```bash
curl 'YOUR_API_URL/transactions?startDate=2026-04-01&endDate=2026-04-30' \
-H 'API-KEY: YOUR_API_KEY' \
-H 'API-SECRET: YOUR_API_SECRET' \
-H 'MERCHANT-ID: YOUR_MERCHANT_ID' \
-H 'X-PAGE-NUMBER: 2'
```
```
< x-page-number: 2
< x-total-items: 230
```
Past 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.
### One Order, One Payment, One Transaction
```
GET /transactions/:id/list
```
Accepts 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.
### Search
```
GET /transactions/search?query=8208822
```
Free-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.
## The Monthly Report
The 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?*
**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.
### 1. Monthly Summary
The first page carries the totals.
**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.
**Total charges in the period** — everything deducted in the period:
| Line | What it is |
|------|------------|
| **Fees** | Charges tied to the payments themselves. |
| **Adjustments** | Other deductions affecting the payout, such as partner fees, itemised further down the report. |
| **VAT** | VAT on the fees and services that carry it. |
```
Fees + Adjustments + VAT = Total Charges
```
### 2. Total Payout in the Period
The 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.
### 3. Of Which: From Last Period
How much of this month's payouts came from the previous report period.
Sale 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*.
### 4. Unsettled Amount From Period
Amounts 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.
A 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.
### 5. Why Net Sales and Payout Differ
Net sales follows the **sale**. Payout follows the **money**. In any given month, the payouts contain:
- sales from the previous month paid out in this one,
- sales from this month paid out in the next one,
- and the fees and adjustments attached to each payout.
So `Total Net Sales − Total Charges` is not `Total Payout` for a single month. Both numbers are right; they answer different questions.
### 6. A Worked Example
| Line | Amount |
|------|--------|
| Total net sales | 100 000 kr |
| Total charges | 5 000 kr |
| Total payout | 92 000 kr |
| Of which: from last period | 2 000 kr |
| Unsettled from period | 5 000 kr |
Nothing 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.
### 7. Behind the Summary
Three detail sections explain any total on the summary page:
| Section | What it shows |
|---------|---------------|
| **Monthly Payouts** | The individual payouts, and which sales periods they cover. |
| **Monthly Adjustments** | The individual adjustments behind the Adjustments total. |
| **Sales breakdown** | How sales split by store, payment method and so on. |
When a summary figure needs explaining, the answer is in one of these three.
### 8. How to Reconcile a Period
1. Check **total net sales** against your own sales report. Manual payment methods are not included in Surfboard's sales figures.
2. Check **total charges** and its split into fees, adjustments and VAT.
3. Look at **total payout**, and how much of it is marked *from last period*.
4. Check **unsettled amount from period** — sales from this period that have not been paid out yet.
5. For a single figure that still looks wrong, use the detail sections to find the transactions, fees or adjustments behind it.
### 9. Around Month Boundaries
Transactions 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:
- **Transaction date** — which sales period the payment belongs to.
- **Payout date** — when the money actually left.
### If It Still Does Not Add Up
Come to support with the specifics, and it is usually resolved in one pass:
- Merchant or company name
- The report period
- The two amounts that disagree
- The transaction ID, if it is about one transaction
- Whatever you are comparing the report against
## Reconciling With the API
The report is the summary; the API is the ledger behind it. A reconciliation job that runs monthly usually does this:
1. Pull the period's transactions with `GET /transactions?startDate=&endDate=`, paging to the end.
2. Pull the settlement reports for the same period — `GET /partners/:partnerId/merchants/:merchantId/reports`. See [Settlements & Reporting](/developers/guides/settlements-reporting).
3. 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*.
4. Investigate the individual items by ID with `GET /transactions/:id/list`.
Automate the grouping and the two reconciling numbers, and month-end stops being a conversation.
## Reference
- [Reporting API](https://developers.surfboardpayments.com/api/reporting)
- [Settlements & Reporting](/developers/guides/settlements-reporting)
- [Partial Payments](/developers/guides/partial-payments)
- [Payment Lifecycle](/developers/guides/payment-lifecycle)
- [Notification Subscriptions](/developers/guides/notification-subscriptions) — settlement reports delivered by email or SFTP
### AI Product Images and Branding
Category: online | Tags: Online, API, AI, Catalog, Branding
URL: /developers/guides/ai-merchandising
## Overview
Two 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.
| Endpoint | What it takes | What it returns |
|----------|---------------|-----------------|
| `POST /ai/enhance-image` | A product photo and its name | Enhanced images, plain or in a scene |
| `POST /ai/branding` | A website URL | Colour schemes, shapes and fonts for that brand |
Both are ordinary authenticated calls — API key, secret and merchant ID — and both are rate limited. Ask support if you need the limit raised.
## Enhancing Product Images
A 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:
```json
POST /ai/enhance-image
{
"productName": "Wireless Bluetooth Headphones",
"url": "https://example.com/images/product-12345.jpg",
"mode": "STANDARD"
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"imageUrls": [
"https://cdn.example.com/enhanced/image-abc123-v1.jpg",
"https://cdn.example.com/enhanced/image-abc123-v2.jpg",
"https://cdn.example.com/enhanced/image-abc123-v3.jpg"
]
},
"message": "Image enhanced successfully"
}
```
| Field | Notes |
|-------|-------|
| `productName` | Context for the model. "Wireless Bluetooth Headphones" produces a better result than "IMG_4821". |
| `url` | Must be publicly reachable, in a normal web format — JPEG or PNG. |
| `mode` | `STANDARD` cleans up the photo. `SCENE` places the product in a generated setting. |
You 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.
`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.
### Wiring It Into Onboarding
The 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.
Treat 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.
## Generating Branding
A merchant's website already says what their brand is. Point the endpoint at it:
```json
POST /ai/branding
{
"url": "https://example-company.com"
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"options": [
{
"brandingOptions": [
{
"backgroundColor": "#FDFDFF",
"brandColor": "#38488F",
"accentColor": "#38488F",
"rectShape": "rounded",
"fontType": "mono",
"logoUrl": "",
"iconUrl": "",
"footerColor": "#F0F0F2"
}
],
"metadata": {
"inputTokens": 1043,
"outputTokens": 85,
"outputType": "text"
}
}
]
},
"message": "Branding generated successfully"
}
```
Each 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.
These 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.
Present 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.
## Practical Notes
- **Both endpoints are rate limited.** Queue bulk work rather than firing a request per row of an import.
- **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.
- **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.
- **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.
## Reference
- [AI API](https://developers.surfboardpayments.com/api/ai)
- [Product Catalog](/developers/guides/product-catalog)
- [Partner Branding](/developers/guides/partner-branding)
- [POS Templates](/developers/guides/pos-templates)
### POS Templates
Category: in-store | Tags: In-Store, API, POS, Templates, Configuration
URL: /developers/guides/pos-templates
## Overview
A 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.
The `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.
## Creating a Template
```json
POST /merchants/:merchantId/stores/:storeId/templates
{
"name": "Lunch service",
"paymentMethodOrder": ["SWISH"],
"productsPerPage": 10,
"product": [
{
"category": "lunch",
"productOrder": ["82674cfdf77f500001", "82674cfdf77f500002"]
},
{
"category": "drinks",
"productOrder": ["82674cfdf77f500003"]
}
],
"autoSet": [
{ "start": "11:00", "end": "14:00" }
],
"terminal": {
"primaryTerminal": "82674beadf0f700405",
"terminalOrder": ["82674beadf0f700405"]
},
"metaData": {
"till": "counter-2"
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": { "templateId": "8271dfa5782e380148" },
"message": "Pos-Template Created Successfully"
}
```
| Field | Required | What it does |
|-------|----------|--------------|
| `name` | Yes | The label staff see. Name it after the situation — "Lunch service", "Market stall" — not after the file. |
| `paymentMethodOrder` | No | Payment methods in the order they are offered. |
| `productsPerPage` | No | Products per page on the POS. |
| `product[].category` | Yes | A product category. |
| `product[].productOrder` | Yes | Product IDs in display order. |
| `autoSet[]` | No | Time windows, as `HH:MM` in 24-hour format, when this template applies. |
| `terminal.primaryTerminal` | No | The terminal this template sends payments to first. |
| `terminal.secondaryTerminal` | No | The fallback terminal. |
| `terminal.terminalOrder` | No | Terminal IDs in preferred order. |
| `metaData` | No | Free key-value pairs for your own use. |
**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.
Product IDs come from the catalog. See [Product Catalog](/developers/guides/product-catalog) for creating products, variants and categories.
## Managing Templates
```
GET /merchants/:merchantId/stores/:storeId/templates
GET /merchants/:merchantId/stores/:storeId/templates/:templateId
PUT /merchants/:merchantId/stores/:storeId/templates/:templateId
DELETE /merchants/:merchantId/stores/:storeId/templates/:templateId
```
Update 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.
## Designing Templates That Work
A till is used by someone who is being watched by a customer, so the rules are unforgiving:
- **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.
- **One template per situation, not per person.** Lunch, evening, and the summer terrace are situations. Individual staff preferences are not, and they multiply.
- **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.
- **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.
## Reference
- [Templates API](https://developers.surfboardpayments.com/api/templates)
- [Product Catalog](/developers/guides/product-catalog)
- [Payment Methods](/developers/guides/payment-methods)
- [Terminal & Device Management](/developers/guides/terminal-device-management)
### API Conventions
Category: online | Tags: Online, API, Pagination, Conventions, Authentication
URL: /developers/guides/api-conventions
## Overview
Every 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.
## Authentication
Most calls take a key and secret pair, plus the merchant they act on behalf of:
```
API-KEY: YOUR_API_KEY
API-SECRET: YOUR_API_SECRET
MERCHANT-ID: YOUR_MERCHANT_ID
```
**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.
Configuration endpoints — stores, terminals, tips, notifications, payment methods — *are* merchant-scoped and do carry `:merchantId` in the path.
Partner-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.
Three other schemes exist for cases where a long-lived secret cannot travel:
| Scheme | Header | Used by |
|--------|--------|---------|
| Bearer JWT | `Authorization` | Server-side integrations that already hold a session |
| API token | `X-Surfboard-Api-Token` | Scoped machine access |
| Nonce | `X-Surfboard-Nonce` | Self-hosted checkout pages, issued per order |
Anything 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.
## The Response Envelope
Every response is the same three fields.
```json
{
"status": "SUCCESS",
"data": { },
"message": "Order created successfully"
}
```
| Field | Notes |
|-------|-------|
| `status` | `SUCCESS` or `ERROR`. Check this, not just the HTTP code. |
| `data` | The payload. An object for a single resource, an array for a list. Absent or `null` on errors. |
| `message` | Human-readable. Log it; do not branch on it — the wording is not a contract. |
Errors keep the envelope and add a code where one applies:
```json
{
"status": "ERROR",
"message": "Invalid request body."
}
```
| HTTP | Meaning |
|------|---------|
| 400 | The body is malformed or a required field is missing. |
| 401 | Credentials are wrong, missing, or not valid for this account. |
| 403 | Authenticated, but not permitted to do this. |
| 404 | An identifier in the path does not resolve. |
| 500 | Server-side. Retry with backoff; if it persists, contact support. |
Order creation adds its own codes in the `OR_*`, `PS_*`, `GC_*` and `SP_*` families — see [Create Order Error Codes](/developers/guides/create-order-error-codes).
## Amounts and Currencies
**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.
**Currencies are numeric ISO 4217 codes, as strings.** SEK is `"752"`, EUR is `"978"`, NOK is `"578"`, DKK is `"208"`. Not `"SEK"`.
```json
"amount": {
"regular": 50000,
"total": 50000,
"currency": "752"
}
```
**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.
```json
"totalOrderAmount": {
"regular": 10999,
"total": 10999,
"currency": "752",
"tax": [{ "amount": 2200, "percentage": 25, "type": "VAT" }]
}
```
`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`.
If 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.
Countries, 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:
```json
"phoneNumber": { "code": "46", "number": "701234567" }
```
## Identifiers and Dates
Identifiers 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.
Dates and timestamps are ISO 8601 (`2026-04-04T10:20:30+02:00`). Durations, where a field takes one, are `` with the unit as `m`, `h` or `d`: `15m`, `2h`, `3d`.
## Pagination
List endpoints are page-based, and the rules are the same everywhere:
- Sorted newest to oldest by creation time.
- **Page size is fixed at 100.** There is no page-size parameter.
- The total is returned in a header, so the body keeps its shape.
Ask for a page with the `X-PAGE-NUMBER` request header. Without it you get the first page.
```bash
curl 'YOUR_API_URL/transactions' \
-H 'Content-Type: application/json' \
-H 'API-KEY: YOUR_API_KEY' \
-H 'API-SECRET: YOUR_API_SECRET' \
-H 'MERCHANT-ID: YOUR_MERCHANT_ID' \
-H 'X-PAGE-NUMBER: 2'
```
The response reports where you are and how much there is:
```
< x-page-number: 2
< x-total-items: 230
```
Past the last page you get a success, not an error — an empty array and a message saying so:
```json
{
"status": "SUCCESS",
"data": [],
"message": "No transactions available in the specified page"
}
```
So 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.
> **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.
## Environments
| Environment | Terminals | Cards |
|-------------|-----------|-------|
| **Demo** | Payment page mode | Test cards only |
| **Live** | All terminal types | Real cards, settled, paid out |
Demo 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.
**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:
```
https://developers.surfboardpayments.com/console/api-keys
```
Read 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`:
```
SURFBOARD_API_URL=
SURFBOARD_API_KEY=
SURFBOARD_API_SECRET=
SURFBOARD_MERCHANT_ID=
```
If 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.
> A real card used in the demo environment is voided automatically after 30 minutes. It is never captured and never settles.
Never 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.
## Reference
- [Client Auth Tokens](/developers/guides/client-auth-tokens)
- [Create Order Error Codes](/developers/guides/create-order-error-codes)
- [Transactions and Reports](/developers/guides/transactions-and-reports)
- [Developer Portal](https://developers.surfboardpayments.com/)
### B2B Invoices
Category: online | Tags: Online, API, Invoice, B2B, Payment Methods
URL: /developers/guides/b2b-invoices
## Overview
Above a few hundred euros, a business buyer generally will not pay by card. Procurement expects an invoice on terms, approved by someone who was not in the room when the order was placed, and paid by bank transfer thirty days later. A checkout that only takes cards quietly loses that business.
B2B invoicing in Surfboard is a payment method, not a separate product. It is the same [Create Order](/developers/guides/create-an-order) call against the same online terminal, with `paymentMethod` set to `B2BINV` and an `invoice` block that says how the invoice is delivered and when it falls due. Surfboard issues the document, distributes it as an e-invoice or by email, chases it with reminders if you ask, and gives the buyer bank details to settle against.
What changes is the timing. A card payment moves money at checkout; an invoice raises a claim at checkout and moves money on the due date. Everything downstream — reconciliation, credit notes, reporting — follows from that.
> **Scope:** B2BINV is an online payment method. Raise invoices against an online terminal — the `PaymentPage` or `MerchantInitiated` terminal your online store is provisioned with. It is not available on physical terminals.
## Prerequisites
1. A developer account at the [Developer Portal](https://developers.surfboardpayments.com/sign-up)
2. A merchant that has completed onboarding, with an approved online store — [Online Payment Link](/developers/guides/online-payment-link) covers store creation and domain verification
3. `b2binv` active on the merchant or the store
4. The `terminalId` of the store's online terminal
## Step 1: Activate B2B Invoicing
Card is on by default; `b2binv` is not. Activate it through the Payment Methods API:
```json
POST /merchants/:merchantId/payment-methods
{
"b2binv": true
}
```
You can also activate it from the Partner Portal or the Merchant Portal — the three paths write to the same configuration, so pick whichever suits how the merchant is managed. To scope invoicing to one store rather than the whole merchant, use the store-level endpoint:
```
POST /merchants/:merchantId/stores/:storeId/payment-methods
```
See [Payment Methods](/developers/guides/payment-methods) for the full activation and deactivation flow. Until the method is active, the payment initiation inside Create Order will fail, so do this before you send the first invoice.
## Step 2: Identify the Buyer
This is the part that has no equivalent in a card payment. You are not charging a person, you are billing a legal entity, and the invoice has to name it correctly and say where to send it. Two blocks do that work: `customer`, which identifies who owes the money, and `billing`, which is the address the invoice is issued to.
### The Customer
Send both `person` and `company`:
```json
"customer": {
"person": {
"name": { "firstName": "Elin", "lastName": "Berg" },
"email": "ap@radio-ocean.example",
"phoneNumber": { "code": "46", "number": "701234567" }
},
"company": {
"companyName": "Radio Ocean AB",
"vatId": "SE556000000001",
"registrationNumber": "5560000000"
}
}
```
| Field | Notes |
|-------|-------|
| `company.companyName` | The legal name of the entity being billed, as it should appear on the invoice. |
| `company.vatId` | VAT registration number, including the country prefix. |
| `company.registrationNumber` | Company registration number. |
| `person.email` | Where an `EMAIL` invoice is delivered. Use the buyer's accounts-payable address, not the salesperson's. |
| `person.name`, `person.phoneNumber` | The contact on the buying side. |
The company details identify who owes the money and are what the invoice is issued against. Get them from the buyer at checkout rather than inferring them from an email domain — a wrong registration number is an invoice the buyer's finance team can reject.
### The Billing Address
`billing` is optional on a Create Order call in general. **For a B2B invoice it is mandatory** — an invoice is a document addressed to somewhere, and there is no sensible default.
```json
"billing": {
"address": {
"addressLine1": "Surfgatan 1",
"city": "Stockholm",
"postalCode": "11122",
"countryCode": "SE"
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `billing.address.addressLine1` | Yes | Street address of the entity being billed. |
| `billing.address.city` | Yes | City. |
| `billing.address.postalCode` | Yes | Postal code. |
| `billing.address.countryCode` | Yes | ISO 3166-1 alpha-2, uppercase. |
| `billing.address.careOf` | No | Attention line — useful when invoices go to a named accounts-payable desk. |
| `billing.address.addressLine2`, `addressLine3` | No | Further address lines. |
| `billing.name`, `billing.email`, `billing.phoneNumber` | No | A billing contact distinct from `customer.person`. |
This is the buyer's registered billing address, which is not necessarily where the goods go. If you are shipping somewhere else, put that in `shipping` and leave `billing` as the address finance works from.
## Step 3: Create the Invoice Order
The call is Create Order with the invoice configuration carried in `controlFunctions.initiatePaymentsOptions.paymentMethodParams.invoice`:
```json
POST /orders
{
"terminal$id": "813ca2cb12ce400405",
"referenceId": "order-2026-0418",
"billing": {
"address": {
"addressLine1": "Surfgatan 1",
"city": "Stockholm",
"postalCode": "11122",
"countryCode": "SE"
}
},
"orderLines": [
{
"id": "83dddf1596c8d03937",
"name": "7'8 Radio Ocean Liner",
"description": "7'8 Radio Ocean Liner surfboard",
"quantity": 1,
"amount": {
"regular": 179200,
"total": 179200,
"currency": "752",
"tax": [{ "type": "VAT", "percentage": 25, "amount": 35840 }]
}
}
],
"customer": {
"person": {
"name": { "firstName": "Elin", "lastName": "Berg" },
"email": "ap@radio-ocean.example",
"phoneNumber": { "code": "46", "number": "701234567" }
},
"company": {
"companyName": "Radio Ocean AB",
"vatId": "SE556000000001",
"registrationNumber": "5560000000"
}
},
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "B2BINV",
"paymentMethodParams": {
"invoice": {
"invoiceDistribution": "EINVOICE",
"dueDate": "30d",
"reminder": false,
"invoicePaymentMethods": ["BANK", "DIRECT_BANK"]
}
}
}
}
}
```
```json
// Response
{
"status": "SUCCESS",
"data": {
"orderId": "845712d3b9674383020b",
"paymentId": "845712d3b9675f900206",
"interAppJWT": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"paymentPageLink": "https://pay.withsurfboard.com/845712d3b9674383020b?pi=ocaY_Xgzq9SqhTPR-ry9d8Ne2s3Cl9rB...",
"invoiceDetails": {
"invoiceId": 5100232680,
"invoicePdfUrl": "https://b2b.payer.se/api/v1/receiptViewer/invoice/pdf/1946a1ab-141f-441c-938d-8044278922ed",
"iban": "SE0000000000000000000000",
"accountHolderName": "Surfboard Payments AB",
"bic": "DNBASESX",
"bankgiro": "0000000",
"ocr": "00845712396759002065"
}
},
"message": "Order created successfully"
}
```
Amounts follow the same rules as every other order: minor units, a numeric ISO 4217 `currency` (`752` is SEK), and tax stated per line. `totalOrderAmount` is optional, but when you send it, it has to reconcile with the lines.
Give every line a `description` as well as a `name`. On a card payment nobody reads the line items; on an invoice they are the document, and the person approving it may never have seen the order. A line that says only "Liner" is a line someone has to email you about.
### The Invoice Block
| Field | Required | Notes |
|-------|----------|-------|
| `invoiceDistribution` | Yes | `EINVOICE` or `EMAIL`. E-invoice is routed to the company; email goes to `customer.person.email`. |
| `dueDate` | Yes | A relative duration such as `30d` for Net 30, or an absolute date. |
| `invoicePaymentMethods` | Yes | How the buyer may settle: `BANK`, `DIRECT_BANK`, `CARD`. Send the ones you will accept. |
| `reminder` | Yes | Whether Surfboard sends reminders as the due date passes. |
| `debtCollectionEnabled` | No | Hands a past-due invoice to the debt-collection flow. |
| `contractReference` | No | Your reference for the underlying contract, carried on the invoice. |
`dueDate` is a commercial decision, not a technical one. Net 30 is the common default for procurement; longer terms are a financing choice the merchant is making on the buyer's behalf. Set `reminder` and `debtCollectionEnabled` deliberately too — both change what the buyer receives after the due date, and both should match what the merchant agreed with them.
### What Comes Back
Because the payment is initiated inside the same call, the response carries the invoice itself in `invoiceDetails`:
| Field | Type | Description |
|-------|------|-------------|
| `invoiceId` | number | The invoice identifier. Note it is a **number**, not a string — store it as one. |
| `invoicePdfUrl` | string | The invoice document. This is the thing the buyer's finance team will actually open. |
| `iban` | string | The account to transfer to, for international settlement. |
| `bic` | string | Bank identifier code for that account. |
| `accountHolderName` | string | The account holder shown on the invoice. |
| `bankgiro` | string | Swedish bankgiro number, for domestic transfers. |
| `ocr` | string | The OCR reference the buyer quotes on the payment so it reconciles automatically. |
`bankgiro` and `ocr` are how a Swedish buyer settles a `BANK` transfer, and the OCR is what matches their payment back to this invoice without anyone reading a bank statement. `iban` and `bic` cover payment from outside the country. All of them appear on the PDF, so you do not have to surface them yourself — but store `invoiceId` and `ocr` against your own accounts-receivable record, because those are what reconciliation is keyed on later.
The response also carries the usual `orderId` and `paymentId`, plus a `paymentPageLink` — the same hosted page a card order returns, where a buyer can settle through whichever of `DIRECT_BANK` or `CARD` you allowed in `invoicePaymentMethods`.
If you initiate the payment separately rather than inside Create Order, the same `invoiceDetails` block comes back from the [Initiate Payment](https://developers.surfboardpayments.com/api/payments) call instead.
## Step 4: Confirm and Reconcile
Order status works the same as for any other order:
```
GET /orders/:orderId/status
```
Subscribe to `order.paymentcompleted` and `order.paymentfailed` rather than polling on a timer — see [Webhooks](/developers/guides/webhooks-notifications) for subscriptions, retries and signature verification.
The thing to hold on to is that an invoice settles on its own timetable. The order tells you the invoice was raised and where it stands; the money arriving is a separate event on the buyer's terms. Reconcile invoice revenue against [Settlements & Reporting](/developers/guides/settlements-reporting) rather than treating order creation as cash in the bank.
## Crediting an Invoice
When an invoice was wrong or the goods come back, the buyer gets a credit invoice. **How you raise it depends on whether the original invoice has been paid**, and this is the one thing to get right before you write any code:
| The original invoice | What you do | One call or two |
|----------------------|-------------|-----------------|
| Raised, not yet paid | Cancel the order | One call, no body |
| Paid | Create a return order | A full order payload |
Because an invoice sits unpaid for the whole of its term by design, the unpaid case is the one you will hit most.
### Unpaid: Cancel the Order
There is nothing to give back yet, so this is a cancellation rather than a refund. Cancelling the order raises a credit invoice by default:
```
DELETE /orders/:orderId
```
The endpoint takes no request body.
```json
// Response
{
"status": "SUCCESS",
"message": "Order cancelled successfully"
}
```
That is the whole operation. Use it for the ordinary cases — wrong amount, wrong entity, the buyer walked away after the invoice went out.
### Paid: Create a Return Order
Once the buyer has settled, crediting is a refund and takes the standard return-order shape: a new order with negative quantities, each line pointing back at the order and the payment it credits.
```json
POST /orders
{
"terminal$id": "813ca2cb12ce400405",
"referenceId": "credit-2026-0418",
"billing": {
"address": {
"addressLine1": "Surfgatan 1",
"city": "Stockholm",
"postalCode": "11122",
"countryCode": "SE"
}
},
"customer": {
"person": {
"name": { "firstName": "Elin", "lastName": "Berg" },
"email": "ap@radio-ocean.example",
"phoneNumber": { "code": "46", "number": "701234567" }
},
"company": {
"companyName": "Radio Ocean AB",
"vatId": "SE556000000001",
"registrationNumber": "5560000000"
}
},
"orderLines": [
{
"id": "83dddf1596c8d03937",
"name": "7'8 Radio Ocean Liner",
"description": "7'8 Radio Ocean Liner surfboard",
"quantity": -1,
"purchaseOrderId": "845712d3b9674383020b",
"purchasePaymentId": "845712d3b9675f900206",
"amount": {
"regular": 179200,
"total": 179200,
"currency": "752",
"tax": [{ "type": "VAT", "percentage": 25, "amount": 35840 }]
}
}
],
"controlFunctions": {
"initiatePaymentsOptions": {
"paymentMethod": "B2BINV",
"paymentMethodParams": {
"invoice": {
"invoiceDistribution": "EMAIL",
"dueDate": "30d",
"reminder": false,
"invoicePaymentMethods": ["BANK", "DIRECT_BANK"]
}
}
}
}
}
```
Key details:
- `quantity` goes negative on every credited line. `amount.total` stays positive.
- `purchaseOrderId` and `purchasePaymentId` both belong **on the line item**: the `orderId` and `paymentId` returned when the original invoice was created.
- Repeat the `billing` address and the `invoice` block. The credit is its own addressed document and can be distributed differently from the original — `EMAIL` here, where the original went out as `EINVOICE`.
- Credit only some of the lines and you have a partial credit. The mechanics are the same as [Partial Refund](/developers/guides/partial-refund).
> **`OR_0035: Cannot refund from purchase order that is not completed. Status: PENDING`** means exactly what the two paths above describe — the invoice has not been paid, so there is nothing to refund. Cancel the order instead.
The buyer keeps the original invoice and receives a credit against it. Both documents stand; the credit does not erase the original.
## Error Handling
Failures come back as `status: "ERROR"` with an `OR_*` or `PS_*` code, and the ones you will meet setting this up are mostly configuration rather than payload:
- **The method is not active.** `b2binv` has to be activated on the merchant or store before an invoice payment can initiate. This is the most common first failure.
- **The company block is missing.** A B2B invoice needs an entity to bill; a `customer` with only a `person` is not enough.
- **The billing address is missing.** `billing.address` is optional on Create Order generally and mandatory here. Easy to miss if you are adapting a working card payload.
- **The invoice block is incomplete.** `invoiceDistribution`, `dueDate`, `invoicePaymentMethods` and `reminder` are all required by the payment initiation, even though Create Order will accept the order without them.
- **You refunded an invoice nobody paid.** `OR_0035` on a return order means the original is still `PENDING`. Cancel it instead.
- **A credit line has no `purchaseOrderId`.** Every negative line must reference the order it credits, and carry the `purchasePaymentId` alongside it.
[Create Order Error Codes](/developers/guides/create-order-error-codes) lists the full set, including the errors raised by the payment initiation that happens inside the same call.
## Next Steps
- [Payment Methods](/developers/guides/payment-methods) — activating and deactivating `b2binv`
- [Create an Order](/developers/guides/create-an-order) — line items, tax and control functions in full
- [Refund an Order](/developers/guides/refund-an-order) — the card-side equivalent of a credit invoice
- [Settlements & Reporting](/developers/guides/settlements-reporting) — reconciling what has actually been paid
- [Online Payment Link](/developers/guides/online-payment-link) — the store and terminal setup this guide assumes
## Reference
- [Create Order API](https://developers.surfboardpayments.com/api/orders)
- [Cancel an Order API](https://developers.surfboardpayments.com/api/orders) — the one-call credit for an unpaid invoice
- [Initiate Payment API](https://developers.surfboardpayments.com/api/payments)
- [Payment Methods API](https://developers.surfboardpayments.com/api/payment-methods)
---
## Partner Portal Guides
Documentation for ISV partners using the Surfboard Payments Partner Portal to manage merchants, terminals, payment methods, shipments, and more.
### Dashboard Overview
Category: getting-started | Tags: Dashboard, Analytics, Overview
URL: /partner-portal/dashboard-overview
## Overview
The Partner Portal dashboard is the first thing you see after logging in. It provides a real-time snapshot of your entire payment operation, giving you instant visibility into your merchant portfolio's performance.

## Logging In
Access the Partner Portal at **[partner.surfboardpayments.com](https://partner.surfboardpayments.com/)**. Your account has a defined role that determines what you can do in the portal -- your primary job is to onboard merchants and manage their terminal distribution.
Once authenticated, you land on the dashboard, with the left-side navigation giving you access to all core partner functions: Applications, Merchants, Devices, Shipments, Returns, Branding, Billing Plans, Integrations, Settings, Tickets, and Messages.
> **Tip:** Open **Support → Messages** early and subscribe a shared email address. That is where Surfboard publishes settlement, integration, and service notices, and nothing is emailed until an address is added. See [Messages](/partner-portal/messages).
## The Partner Lifecycle
As a Surfboard partner, your core responsibility is to onboard merchants and ensure their payment terminals are operational. The lifecycle follows a clear sequence:
1. **Login** -- access the Partner Portal.
2. **Application** -- create a new merchant application; the system generates an Application ID and a signing link. See [Applications & Merchant Onboarding](/partner-portal/applications).
3. **Signing** -- the merchant signs the application, creating their Merchant ID.
4. **Terminal Distribution** -- ship or assign terminals to the merchant. See [Shipments & Returns](/partner-portal/shipments-and-returns).
5. **Activation** -- activate the terminal(s) against the merchant. See [Terminal Management](/partner-portal/terminals#activating-a-terminal).
6. **Registration** -- register the device so it's live and processing-ready.
7. **Post-Setup** -- configure apps, payments, branding, and reports.
8. **Tickets** -- raise [support tickets](/partner-portal/tickets) for any queries along the way.
## Key Metrics
At the top of the dashboard, four summary cards give you an instant overview:
| Metric | Description |
|--------|-------------|
| **Total Volume** | Combined sales volume across all currencies and merchants |
| **Applications** | Number of merchant onboarding applications |
| **Merchants** | Total active merchants in your portfolio |
| **Transactions** | Total transaction count across all merchants |
Below the summary cards, two additional cards show:
- **Purchases** -- total purchase transaction count
- **Returns** -- total return/refund transaction count
## Sales Trend
The Sales Trend chart visualizes your payment volume over time. Use the dropdown to switch between time intervals:
- **Weekly (Last 30 Days)** -- default view, shows weekly aggregated volumes
- **Monthly** -- longer-term trend across months
- **Daily** -- granular day-by-day breakdown
The chart helps you identify growth patterns, seasonal trends, and any unexpected dips in transaction volume.
## Application Status
The Application Status donut chart shows the distribution of your merchant onboarding applications by status:
- **Created** -- applications that have been initiated
- **Submitted** -- applications sent for review/KYB
- **Expired** -- applications that were not completed within the required timeframe
This helps you monitor your onboarding pipeline and follow up on pending applications.
## Currency Breakdown
The Currency Breakdown section shows how your transaction volume is distributed across different currencies. This is particularly useful for partners operating across multiple countries.
## Navigation
The sidebar provides quick access to all portal sections:
### Merchants
- **Applications** -- manage merchant onboarding
- **Merchants** -- browse and manage merchant accounts
- **Service Providers** -- manage service provider relationships
### Operations
- **Terminals** -- look up terminal information
- **Shipments** -- manage hardware shipments
- **Returns** -- handle device returns
### Configuration
- **Branding** -- customize your brand appearance
- **Billing Plans** -- manage pricing plans
- **Integrations** -- manage software applications
- **Settings** -- account and notification settings
## Global Search
Use the search bar at the top of the sidebar to quickly find merchants, applications, or terminals by name, ID, or email. The search works across your entire portfolio.
### Applications & Merchant Onboarding
Category: merchants | Tags: Applications, Onboarding, KYB, Merchants
URL: /partner-portal/applications
## Overview
The Applications page is where you initiate and track the onboarding process for new merchants. Each application represents a merchant going through the Know Your Business (KYB) verification process before they can start accepting payments.

## Application List
The applications table displays all your onboarding applications with the following information:
| Column | Description |
|--------|-------------|
| **Organisation** | The merchant's business name and application ID |
| **Country** | Country flag and code where the merchant operates |
| **Corporate ID** | The merchant's corporate registration number |
| **Status** | Current application status |
| **Last Activity** | Date of the most recent status change |
### Filtering Applications
You can filter applications by:
- **Date range** -- use the date pickers to narrow by creation or activity date
- **Status tabs** -- quickly filter by All, Expired, Submitted, or Merchant Created
- **Search** -- type a name, ID, or status in the search bar
## Application Statuses
Applications progress through several statuses:
| Status | Description |
|--------|-------------|
| **Created** | Application has been initiated but the KYB process has not started |
| **Submitted** | Application has been submitted for KYB review |
| **Merchant Created** | KYB approved -- the merchant account is now active |
| **Expired** | The application was not completed within the required timeframe |
## Creating a New Application
Click the **+ New Application** button in the top right corner to start onboarding a new merchant.

### Required Information
To create a new application, you need to provide:
1. **Country** -- the country where the merchant will operate
2. **Corporate ID** -- the merchant's official business registration number
3. **Organisation name** -- pulled automatically from company registries when available
4. **Contact details** -- email and phone number for the merchant
5. **Store information** -- at least one store must be configured during onboarding
The system will automatically look up the company details from public business registries based on the corporate ID.
### Onboarding Flow
1. Fill in the merchant's business details
2. Configure the initial store (name, email, phone) -- turn on the **Online payments** toggle if the merchant should run an online store and receive payments online
3. Submit the application
4. Share the signing link with the merchant
5. The merchant completes the KYB process
6. Once approved, the merchant account is created automatically
## Application Details Panel

Click on any application row to open the details panel on the right side. This shows:
- **Application ID** -- unique identifier for the application
- **Status** -- current application status with visual indicator
- **Merchant ID** -- assigned after merchant creation (links to the merchant page)
- **Store ID** -- assigned after the store is created
### Signing Links
The details panel also shows **Signing Links** for applications that require KYB completion. Each signing link includes:
- The signing configuration name
- The email address associated with the link
- Whether it's a **Combined** or **Separate** signing flow
- A **Copy Link** button to share the KYB URL with the merchant
You can share this link directly with the merchant so they can complete their KYB verification process.
> **Note:** The signing link is time-sensitive and valid for **72 hours**. Share it with the merchant promptly after creating the application. If it expires, the link can be copied and resent from the application detail panel.
## Best Practices
- **Follow up on pending applications** -- check the Submitted tab regularly and reach out to merchants who haven't completed their KYB
- **Monitor expired applications** -- if applications are expiring, consider reaching out to merchants sooner after creation
- **Use date filters** -- when managing a large portfolio, use date filters to focus on recent applications
### Merchant Management
Category: merchants | Tags: Merchants, Management, Stores, Profile
URL: /partner-portal/merchant-management
## Overview
The Merchants page is your central directory for managing all merchant accounts in your portfolio. From here you can browse merchants, access detailed profiles, and perform actions like editing merchant details, inviting users, or managing their payment setup.

## Merchant List
The merchant list displays all your onboarded merchants with the following columns:
| Column | Description |
|--------|-------------|
| **Merchant** | Business name, logo, and merchant ID |
| **Email** | Primary contact email |
| **Org. Nr** | Corporate registration number |
| **Country** | Country code |
| **Activity** | Date the merchant was created |
### Search & Sort
- **Search** -- find merchants by name, ID, or email using the search bar
- **Sort** -- use the sort dropdown (default: Name A-Z) to order merchants
- **View toggle** -- switch between list view and grid/card view
### Quick Actions
- **Export** -- download your merchant list
- **+ Onboard Merchant** -- start a new merchant application directly from the merchants page
---
## Merchant Detail Page
Click on any merchant to access their full profile. The merchant detail page is organized with tabs across the top for different management areas.

### Header Information
At the top of the merchant page you'll see:
- **Merchant name** and status badge (Active/Inactive)
- **Merchant ID** with copy button
- **Contact email**, country, and currency
- Quick action buttons: **Edit**, **Invite**, **Transactions**, **Terminals**
---
### Overview Tab
The default view shows a business overview with:
**Summary Cards** (past 3 months):
- Sales volume in the merchant's currency
- Transaction count
- Number of active payment methods
- Average transaction amount

**Sales Trend Chart** -- shows purchase volume over time with toggles for Quarterly, Monthly, Weekly, or Daily views.
**Payment Methods Distribution** -- shows which payment methods are used and their respective volumes (e.g., CARD, SWISH).
**Merchant Information Section**:
- Merchant ID and Acquirer MID
- Organisation number and MCC code
- Language and currency settings
**Contact Details**:
- Email and phone number
- Business address
---
### Transactions Tab

View all transactions for the merchant with powerful filtering and search:
- **Date range filtering** -- select start and end dates
- **Search** -- find transactions by payment ID, order ID, or transaction ID
- **Advanced filters** -- filter by payment method, card brand, card type, region, terminal, or store
- **Download** -- export transactions as CSV with all details including adjustments
Each transaction shows amount, terminal ID, currency, payment method, type, and timestamp. Click a transaction to view full details.
---
### Stores Tab

Manage the merchant's store locations:
- View all stores with name, type (Online/In-Store), email, phone, and status
- **+ Create Store** -- add a new store for the merchant
- Click a store to view and edit its details, manage domains (for online stores), and configure tips
---
### Terminals Tab

View and manage all terminals assigned to the merchant:
- See terminal ID, serial number, store assignment, status, and software version
- **Register Device** -- register a new physical terminal
- **Register Online Terminal** -- set up an online/virtual terminal
- Move terminals between stores or delink them
- Push latest configuration or software updates to individual terminals
---
### Printers Tab

Manage printer terminals separately from payment terminals. View printer terminal assignments and configurations.
---
### Reports Tab
Access merchant settlement and payout reports.

---
### Payment Methods Tab

Enable or disable payment methods for the merchant:
**Core Methods** (always available):
- CPOC (SoftPOS)
- CASH
- CARD
**Additional Payment Methods** (activate/deactivate as needed):
- Klarna
- American Express
- Vipps
- Swish
- MobilePay
- Account to Account
And more...
Each additional method shows its current status and availability. Click **+ Activate** to enable a method or **Deactivate** to disable it.
> **Note:** Some payment methods may show "Not available for your partner configuration" if they haven't been enabled for your partner account.
---
### Billing Tab
View and manage the merchant's billing plan assignment. Assign one of your configured billing plans to the merchant, and change it if needed.

### Catalog Tab
Manage the merchant's product catalog for in-store sales. Available for partners who have enabled the catalog feature.

---
### Promotions Tab
View and manage active promotions for the merchant.

---
### Charges Tab
View charges and fees associated with the merchant's account.

---
### Contracts Tab
Access and manage merchant contracts and agreements.

---
### Service Providers Tab
Lists the service providers linked to this merchant, the companies or individuals that receive a share of its payments through Surfboard Flow. Link a new provider or unlink an existing one here. See [Service Providers](/partner-portal/service-providers).
---
### Configuration Tab
Per-merchant configuration, including **Settlements**, where you request a change to how often the merchant is paid out. A settlement schedule change is part of the merchant agreement, so the portal generates a signature link for the merchant to sign -- see [Changing a Merchant's Settlement Schedule](/partner-portal/settlement-schedule).
---
## Editing Merchant Details
Click the **Edit** button on the merchant header to modify:
- Merchant contact information
- Email
- Phone number
- Default language
- Logotype

---
## Inviting Merchant Users
Click the **Invite** button to send portal access invitations to merchant team members. This allows merchants to access their own portal view for managing day-to-day operations.

### Service Providers
Category: merchants | Tags: Service Providers, Flow, Onboarding, Linking, Tips
URL: /partner-portal/service-providers
## Overview
A service provider is someone other than the merchant who should receive part of a payment: a platform taking a fee, a franchisor collecting a royalty, or a waiter who should get their tips directly instead of having them paid out to the restaurant, taxed, and shared through payroll. This is Surfboard Flow, the split-payout engine.
Setting up a split has three steps. The Partner Portal handles the first two. The third happens in the order your POS or webshop sends to the API.
1. **Onboard the service provider** -- an application is created, the recipient completes verification or signs an agreement, and Surfboard issues a service provider ID
2. **Link it to a merchant** -- a service provider can only take a share from merchants it is linked to
3. **Set the split on the order** -- your integration names the service provider and its share on each order. See the [Service Providers & Split Payouts](/developers/guides/service-providers) developer guide
Flow must be enabled on your partner account. If the Service Providers page is missing from the sidebar, contact your account manager.
## Service Provider Tabs
The Service Providers page has two tabs.
### Active Providers
Service providers that have completed onboarding and can be linked to merchants. Search by name, corporate ID, or email.
| Column | Description |
|--------|-------------|
| **Name** | Business or person name |
| **Corporate ID** | Business registration number, or the national ID for an individual |
| **Email** | Primary contact email |
| **Location** | Country |
Each row has two actions:
- **Link to Merchant** -- pick one of your merchants and link the provider to it, without leaving the list
- **Copy Service Provider ID** -- the ID your integration puts on the order in step 3
### Applications
Every service provider application and where it sits in onboarding.
| Column | Description |
|--------|-------------|
| **Application ID** | Identifier of the application |
| **Corporate ID** | Business registration number |
| **Status** | Current application status, see below |
| **Last Activity** | When the application last changed |
Click a row to open the details drawer. It shows a description of the current status, and for applications that are submitted or signed, the signing links for each signatory so you can chase a missing signature. While the application is still open you can also copy the onboarding link to resend it.
## Creating a Service Provider
Click **+ Create Service Provider**. The form has two modes, chosen at the top. They use different verification flows, so pick the right one before filling in the fields.
| Mode | Use it for | Verification |
|------|------------|--------------|
| **Company** | A registered business: a franchisor, a marketplace operator, your own platform entity for a platform fee | Web KYB (Know Your Business), the same hosted flow merchants go through |
| **Individual** | A private person tied to one merchant: a waiter receiving tips, a stylist, a driver | A signing flow where the person identifies themselves and signs a service provider agreement |
### Company
1. **Select the country** the company is registered in
2. **Search for the company** by name. Selecting a result fills in the corporate ID. You can also type the corporate ID directly
3. **Submit**
The success screen shows the application ID and an **Onboarding Link**. Copy it and send it to a signatory at the company. They fill in company details, contact information, and the bank account that will receive payouts, then sign. The link is personal to the application, so share it only with the company.
### Individual
1. **Select the country** the person is based in
2. **Enter their email**. The signing invitation is sent here
3. **Associate with merchant**. Pick the merchant this person works for. An individual belongs to one merchant, and is linked to it automatically once approved, so there is no separate link step
4. Optionally expand **Fee configuration** if the person should also receive a share of the order amount itself, such as a commission:
- **Percentage fee** -- percentage of each transaction
- **Fixed fee** -- fixed amount per transaction, in the smallest currency unit
- **Deduct applicable transaction fee** -- take Surfboard's transaction fee out of the person's share instead of the merchant's. Leave this off for tips so the tip arrives in full
5. **Submit**
The success screen shows the application status and a **Signing Link**. The person also receives it by email. Once they have signed and the application is approved, they appear under Active Providers.
For a waiter who should only receive tips, no fee configuration is needed. The tip routing is set on the order, by the POS, as described in the developer guide.
## Linking to Merchants
A company service provider must be linked to each merchant it should take a share from. One provider can be linked to many merchants: a franchisor is onboarded once and linked to every franchisee.
There are two ways to link:
- From the **Active Providers** tab, click **Link to Merchant** on the provider's row and pick the merchant
- From the merchant's page, open the **Service Providers** tab and click **Link Service Provider**. This tab also lists everything currently linked to the merchant, with its status
To stop a provider from taking a share of a merchant's payments, open the merchant's Service Providers tab and unlink it. Orders already created keep their split.
If your integration names a service provider on an order that is not linked to the merchant, order creation fails with error `SP_0001`.
## Application Status Tracking
Service provider applications follow the same lifecycle as merchant applications.
| Status | Meaning |
|--------|---------|
| **Application Initiated** | Application created and the onboarding or signing link generated |
| **Application Started** | The recipient has opened the link and begun filling in details |
| **Application Submitted** | Details submitted and awaiting signature or review |
| **Application Pending Information** | Under review, Surfboard needs more information from the recipient |
| **Application Signed** | All required signatures collected |
| **Application Completed** | Approved. The service provider ID has been generated and the provider appears under Active Providers |
| **Application Rejected** | Reviewed and rejected |
| **Application Expired** | Applications are valid for 30 days. If nothing happens in that time the application expires and a new one must be created |
## Related
- [Service Providers & Split Payouts](/developers/guides/service-providers) -- the developer guide, including how to set the split on the order
- [Merchant Management](/partner-portal/merchant-management) -- the merchant's Service Providers tab
- [Surfboard Flow](/flow)
### Terminal Management
Category: operations | Tags: Terminals, Devices, Configuration, Software
URL: /partner-portal/terminals
## Overview
The Terminals page provides a centralized lookup tool for finding and managing payment terminals across your entire merchant portfolio. Whether you need to check a terminal's status, view its transactions, or push a configuration update, this is where you start.
## Device Lifecycle States
Every terminal on the Surfboard platform moves through a defined set of lifecycle states. Understanding these states is essential for troubleshooting and merchant support. Click the **Devices** tab in the left sidebar to access your terminals.
| State | Meaning | Who acts next |
|-------|---------|---------------|
| **Your Stock** | The device is in your own inventory, ready to be assigned to a merchant. | Partner -- activate the terminal to a merchant. |
| **Activated** | The terminal has been activated and linked to a specific merchant. | Merchant or Partner -- register the device. |
| **Registered** | The device is fully registered, live, and operational on the merchant's account. | Merchant -- begin taking payments. |
> A terminal must be **activated** to a merchant before it can be **registered**, and it must be **registered** before it can take payments.
## Activating a Terminal
Activating links a terminal from your stock to a specific merchant, moving it to the **Activated** state. There are two ways to do it.
### Method A -- From the merchant

1. Click **Merchants** in the left sidebar and select the merchant you want to activate the terminal for.
2. Click the **Terminals** button at the top, then click **Activate**.
3. In the pop-up, enter the terminal **serial number** (found on the sticker on the back of the device) and click **Activate**.
4. To activate multiple terminals at once, click the **+** button in the pop-up before confirming.
The terminal is now linked to the merchant and moves to the **Activated** state.
### Method B -- From your stock

1. Click **Devices** in the left sidebar, then open **Your Stock**.
2. Click **Ship** on the terminal you want to send to a merchant.
3. Search for the merchant's name and click **Ship Device** -- the terminal is activated.
4. To activate multiple terminals at once, click the **+** button in the pop-up before confirming.
## Registering a Terminal

Registering moves an activated terminal to the **Registered** state, making it live and ready to take payments. Either the partner or the merchant can register the device.
1. Click the **Activated** tab (next to **Your Stock**) to see the list of activated devices.
2. Click the **three dots** next to the terminal and select **Register**.
> When you ship directly to a merchant, you can register the terminal on their behalf, or leave registration for the merchant to complete.
### Reassigning a terminal to another merchant
To move a device to a different merchant:
- Click the **three dots** under the **Activated** tab and select **Reassign Merchant**, or
- Open the **Registered** tab and select **Reassign Merchant**.


## Terminal Lookup
Enter a terminal ID in the search bar and click **Search** to look up any terminal in your portfolio. The lookup returns:
- Terminal ID and serial number
- Assigned merchant and store
- Terminal status (Active, Inactive, etc.)
- Current software version
- Last activity timestamp

## Managing Terminals Per Merchant
For more detailed terminal management, navigate to a specific merchant's **Terminals** tab. There you can:
### View Terminals
See all terminals assigned to a merchant, including:
- Terminal ID and serial number
- Store assignment
- Terminal type (physical device, SoftPOS, online terminal, printer)
- Current status
- Software version
### Register a New Device
To register a physical payment terminal:
1. Go to the merchant's Terminals tab
2. Click **Register Device**
3. Select the target store
4. Enter the device serial number
5. The terminal will be linked to the merchant and store
### Register an Online Terminal
For e-commerce or virtual terminals:
1. Click **Register Online Terminal**
2. Select the target store
3. Configure the terminal settings
4. The online terminal will be created and ready for use
### Move Terminals Between Stores
If a merchant needs to reassign a terminal to a different store:
1. Select the terminal
2. Choose **Change Store**
3. Select the new store from the dropdown
4. Confirm the move
### Move Terminals Between Merchants
Partners can move terminals between merchants within their portfolio:
1. Select the terminal
2. Choose **Move Terminal**
3. Enter the target merchant ID
4. Confirm the transfer
### Delink a Terminal
To remove a terminal from a merchant:
1. Select the terminal
2. Choose **Delink Terminal**
3. Confirm the deactivation
The terminal will be unlinked from the store and merchant.
## Terminal Configuration
Terminals can be configured at three levels, with inheritance from broader to more specific:
### Merchant Level
Set default configuration for all terminals under a merchant. Settings cascade down to all stores and terminals unless overridden.
### Store Level
Override merchant-level configuration for a specific store. All terminals in the store inherit these settings unless individually overridden.
### Terminal Level
Set configuration for a specific terminal. These settings take the highest priority.
### Configuration Options
Terminal configurations can include:
- Receipt settings
- Tip configuration
- Display preferences
- Payment method enablement
- Custom branding elements
- **Open POS on Reboot** -- when enabled (the default), the POS app launches automatically every time the terminal restarts
## Tips Configuration
Tipping can be configured at multiple levels, with more specific levels overriding broader ones:
- **Merchant level** -- in the merchant's **Configuration** tab, select **Tips** to apply tip settings across all of that merchant's terminals.
- **Store level** -- open a store from the **Stores** tab and use its **Configuration** button to set tips for that store.
- **Terminal level** -- configure tips individually per terminal via the terminal's own **Configuration** tab.
All levels offer the same settings:
| Setting | Description |
|---------|-------------|
| **Enable Tips** | Toggle tip functionality on or off. |
| **Display Format** | Show tip options as a **Percentage** or a fixed **Amount**. |
| **Preset Tip Levels** | Set up to three preset tip options (e.g. 10%, 20%, 25%) that customers can select at checkout. |
| **Allow Custom Amount** | Let customers enter their own tip value. A **Default Custom Amount** can also be pre-set. |
| **Show Calculated Amount** | Optionally display the calculated tip value beneath each preset option. |
Once configured, tap **Save Tip Config** to apply the changes.
## Pushing Updates
### Push Latest Configuration
Send the most recent configuration to a terminal. Useful after making changes to ensure the terminal picks up the new settings immediately.
### Push Latest Software
Trigger a software update on a specific terminal. The terminal will download and install the latest version on its next check-in.
## Terminal Naming
You can update a terminal's display name to make it easier to identify in reports and lists. Click on the terminal and edit its name.
### Pair a Terminal with the Multicomm Base
Category: operations | Tags: Multicomm Base, Docking station, Ethernet, SurfPrint Pro, SurfPrint Pro K, Terminals
URL: /partner-portal/multicomm-base-pairing
## Overview
The Multicomm Base is a charging dock with an ethernet port. Once the base has power and a network cable, it creates its own short-range wireless network, and a paired terminal uses that network to reach the wired ethernet connection in the base.
There are two bases, one per terminal, and they pair the same way:
| Base | Terminal | Base model |
|------|----------|------------|
| [SurfPrint Pro Multicomm Base](/products/fins/surfprint-base) | SurfPrint Pro | D51 |
| [SurfPrint Pro K Multicomm Base](/products/fins/surfprint-pro-k-base) | SurfPrint Pro K | D53 |
Out of the box the base only charges. The terminal does not know about the base until you pair the two, and pairing happens on the terminal, not in the Partner Portal. It takes about a minute and only needs to be done once per terminal and base.
This guide is written for the person standing at the counter with the terminal in hand: a partner setting up a merchant, or a merchant following your instructions.
## Before you start
- Plug the base into power and connect an ethernet cable to its RJ45 port. The base needs both before the terminal can use it for networking. Some deliveries ship the base without its own adapter: it takes the USB-C cable and adapter that came with the terminal.
- Have the terminal nearby, switched on, with the CheckoutX app open.
- Turn the base over. The code on its underside is what the terminal scans to pair: a barcode on the SurfPrint Pro base, a QR code marked **Scan connection** on the SurfPrint Pro K base.
- The base's LED should be blinking. A base that has never been paired blinks slowly, on and off in half-second steps. See [What the LED tells you](#what-the-led-tells-you) below.
> **Note:** Pairing is done in the terminal's device settings, which sit behind the CheckoutX cogwheel. A merchant does not need Partner Portal access to follow these steps.
## Step 1: Enable the wireless docking station
1. In the CheckoutX app, tap the **cogwheel** to open the device settings.
2. Find **Wireless docking station settings** and open it.
3. Switch on the **Wireless docking station** toggle.

When the toggle is on, the screen expands to show the pairing status and the binding controls. A terminal that has never been paired shows **Disconnected**.
## Step 2: Scan the code on the base
1. Tap **Scan bind**. The terminal's camera opens.
2. Point the camera at the barcode or QR code on the underside of the Multicomm Base.
3. As soon as the code is read, pairing starts on its own. Wait for the status between the base and terminal icons to change from **Disconnected** to **Connected**.


After a successful pairing, **Auto bind** is switched on. Leave it on so the terminal reconnects to this base by itself whenever the base is in range.
At this point the terminal and base talk to each other directly, over a link the base's manual calls Wi-Fi P2P. It carries charging status and the pairing, but not internet traffic. That comes in the next step.
## Step 3: Enable Ethernet and return to CheckoutX
1. Switch on the **Ethernet** toggle. The terminal asks you to confirm, because its own Wi-Fi is switched off while the base's ethernet is in use. Confirm. The terminal now routes its network traffic through the base's ethernet connection.
2. Leave **DHCP** on unless the merchant's network requires a fixed address.
3. Tap the **back** arrow to return to the payment application.

The terminal is now set up. From here on it connects to the Multicomm Base whenever the base is available, without any further action.
## How the connection behaves
- **Lifting the terminal off the base keeps the connection.** The ethernet link runs over the base's own wireless network, so the terminal stays connected while it is within range, not only while it sits in the cradle.
- **Charging is unchanged.** The base charges the terminal whether or not it is paired.
- **One base per terminal.** Pairing a terminal with a different base replaces the previous pairing. Scan the new base's code to move it.
- **The terminal's own Wi-Fi steps aside.** While Ethernet is on, the terminal uses the base for networking rather than the venue's Wi-Fi. Switch Ethernet off again to go back to Wi-Fi or 4G.
## Unpair or move a base
You only need this when a base changes hands: a terminal is moving to another counter, or a base is being replaced.
- **From the terminal:** open **Wireless docking station settings**, tap **Scan bind** and confirm. The terminal drops the current base and is ready to scan a new one.
- **From the base:** press the **RESET** button on the underside of the base briefly, for about two seconds. The base forgets the terminal and its LED goes back to the slow blink.
Hold the reset button down for longer only if you mean to reset the base itself: a long press resets it to factory settings, and holding it while plugging in the power starts a firmware update.
## What the LED tells you
The power LED on the base doubles as a status light.
| LED | Meaning |
|-----|---------|
| Steady on | A terminal is connected to the base. |
| Fast blink, on and off every 0.2 s | Ethernet mode is on and the base is paired, waiting for the terminal to connect. |
| Slow blink, on and off every 0.5 s | The base is not paired with any terminal. |
| Long on, then two quick flashes | The base is paired but Ethernet mode is off, waiting for the terminal to connect. |
## Troubleshooting
| Symptom | What to check |
|---------|---------------|
| Status stays **Disconnected** after scanning | Confirm the base has power and its LED is blinking. Wipe the code on the underside and scan again in good light. If the base was paired with another terminal before, press its reset button for two seconds and scan again. |
| **Connected**, but the terminal has no network | Check that the ethernet cable is plugged into the base and carries a live link. Confirm the **Ethernet** toggle is on: without it the link between base and terminal carries no internet traffic. |
| The terminal falls back to Wi-Fi or 4G | The terminal has moved out of range of the base. Bring it back within range, or rescan if it does not reconnect. |
| The LED blinks slowly although the terminal was paired | The base has been reset, or was paired with another terminal. Scan its code again. |
| The **Wireless docking station** setting is missing | The terminal is not a SurfPrint Pro or SurfPrint Pro K, or its software is out of date. Push the latest software from [Terminal Management](/partner-portal/terminals). |
## Related
- [SurfPrint Pro Multicomm Base](/products/fins/surfprint-base) for specifications and what is in the box.
- [SurfPrint Pro K Multicomm Base](/products/fins/surfprint-pro-k-base) for the SurfPrint Pro K version.
- [Terminal Management](/partner-portal/terminals) for activating, registering, and updating terminals from the Partner Portal.
### Cash Registers (ECR)
Category: operations | Tags: Cash Registers, ECR, Skatteverket, Z-report, Shifts
URL: /partner-portal/cash-registers
## Overview
The **Cash-Register** tab lets you manage a merchant's cash registers (ECR) directly from the partner portal. From here you can add and declare registers, view their details and control-unit information, check transactions and reports, and run daily operations like opening and closing shifts.
This guide walks through everything you can do with a merchant's cash registers, end to end.
> **Important (Swedish merchants):** Registering a cash register is mandatory under Swedish government regulations. A Swedish merchant without a connected cash register is not compliant with local tax law.
## Getting to the Cash-Register tab
Click the **Merchants** tab in the sidebar, select your merchant, then choose the **Cash-Register** sub-tab.
This tab is the home for all cash-register management. It shows every cash register under the merchant and is where you perform all cash-register actions -- adding registers, viewing details, checking transactions and reports, and operating shifts.

## Add a cash register
Click **+ Add Register** in the top right of the Cash Registers panel, then complete the form:
| Field | Required | Notes |
|-------|----------|-------|
| **Store** | Yes | The store the register belongs to. |
| **Terminal** | Yes | The terminal to use; this is used as the device ID. Select a store first to populate the list. |
| **Cash register name** | Yes | Letters and digits only. |
| **Notification emails** | No | Addresses that will receive ECR reports. Use **+ Add email** to add more. |
Then click **+ Add Register** to create it.
> **Note:** A charge applies for every cash register created under the merchant.

### Declare with Skatteverket
After the register is created, a **Cash register created** dialog confirms success. The register must be declared with the Swedish Tax Agency (Skatteverket) before it can be used, using the details in the register's **Info** tab.
- Click **Declare with Skatteverket** to declare it now, or
- Click **Do it later** to declare it from the Info tab afterward.

## View the cash-register list
The Cash Registers panel lists every cash register linked to the merchant. Each row shows the register name, register ID, store ID, and current status (for example, *Closed* or *No state*). After a cash register is added it's visible here. Click any row to open that register's details.
## View cash-register info
Selecting a register opens a side panel with four tabs: **Info**, **Transactions**, **Reports**, and **Operate**. The **Info** tab shows the register and control-unit details:
- **Cash register details** -- Cash Register ID, Cash Register Name, Activation Date, Designation, Model / Program, Address, and Email.
- **Control unit details** -- Control Unit Address, Manufacturer, Type, Model, and Serial Number.
These are the details you use to declare the cash register with Skatteverket.

### Notification recipients
The **Notifications** section of the Info tab lists the additional email addresses that receive this register's ECR reports (Z-report, X-report, and SIE files). The merchant's configured email always receives the reports by default -- the addresses listed here receive them in addition to it.
To add another recipient, enter the address in the **Add email** box and click **+ Add**.
> **Note:** Emails can only be added here -- they cannot be removed. If an address is entered by mistake, write to Surfboard to have it removed.

### Scheduled End of Day
Further down the Info tab, the **Scheduled End of Day** section shows when the End of Day action runs automatically each day (default *End of day (23:59)*, Swedish time). To change it, enter a new time under **Update End of Day time** and click **Update**.
### Delete a cash register
At the bottom of the Info tab, **Delete cash register** permanently removes the register.
> **Warning:** This action cannot be undone.

## Transactions
The **Transactions** tab lists the transactions made on this cash register. Each row shows the Order ID, Date, and Type (for example, *Purchase*). Use the pagination controls at the bottom to move through the list.

## Reports
The **Reports** tab fetches the reports generated for this cash register. Set the query parameters at the top, then click **Fetch reports**:
- **Type** -- *Z report*, *X report*, or *Journal Memory*.
- **Start date** and **End date** -- the date range to fetch.
Matching reports appear in a list below. Click **Open** on a report to view it.

## Operate
The **Operate** tab is where you run the register's daily operations. The actions available depend on whether a shift is currently open or closed.
### When the shift is closed
The tab shows the last shift's summary -- Shift status (*Closed*), Cashier, Shift closed time, Petty cash, and End of Day time -- with an **Open shift** button. Click **Open shift** to start a new shift so the register can accept payments.

### When the shift is open
The tab shows the current shift -- Shift status (*Open*), Shift opened time, and Petty cash -- with two actions:
- **Close shift** -- closes the current shift.
- **End of Day** -- generates the Z-report for the register.
> **Warning:** End of Day is a one-time action and cannot be undone. Once called, the cash register cannot accept payments for the rest of the day.

## Bookkeeping
On the cash-register tab there is a **Bookkeeping** block. Tap **Enable bookkeeping** if you want the merchant to receive SIE reports. SIE reports are generated on a per-merchant level and are sent out for every pay-out we send to the merchant.

### Shipments & Returns
Category: operations | Tags: Shipments, Returns, Hardware, Logistics
URL: /partner-portal/shipments-and-returns
## Overview
The Shipments and Returns sections handle the physical logistics of payment terminals -- ordering new hardware, tracking deliveries to merchants, and processing device returns.
## Shipments
The Shipments page shows all hardware orders and their delivery status. Use the search bar to find specific orders by Order ID or Product ID.
### Creating a Shipment
Click **+ Create Shipment** to order hardware for a merchant:
1. **Select a merchant** -- choose which merchant the hardware is for
2. **Choose products** -- select from the available product catalogue:
- Individual products (e.g., specific terminal models)
- Product bundles (pre-configured sets of devices and accessories)
3. **Specify quantities** -- set the number of each product
4. **Enter shipping details** -- delivery address and contact information
5. **Submit the order** -- the shipment will be created and tracked
### Distribution Methods
When you click **+ Create Shipment**, you choose one of two distribution methods -- both are fully supported on the platform.
#### Method 1 -- Bulk order to yourself
Receive terminals in bulk to distribute later. Each terminal must be **activated** to a merchant before it can be registered.
1. Select which terminal(s) you want shipped.
2. Fill in your **ship-to** address, review the order, and click **Submit**.
> **Note:** The bulk-order option is only available if you have a PCI-compliant storage facility and have signed the additional storage addendum.
#### Method 2 -- Ship to a merchant
Send terminals directly to a merchant's store address. You should activate and register the terminal; the merchant only needs to handle registration if you choose not to do it on their behalf.
1. Select which terminal(s) you want shipped.
2. Select the merchant you want to ship to.
3. Select the store.
4. Review and submit the request.
See [Activating a Terminal](/partner-portal/terminals#activating-a-terminal) and [Registering a Terminal](/partner-portal/terminals#registering-a-terminal) for the steps that follow delivery.
### Tracking Shipments
Once an order is placed, you can view the status of the terminals under the **Shipments** tab and track the courier by clicking the **Track Order** button.
Each shipment includes:
| Field | Description |
|-------|-------------|
| **Order ID** | Unique identifier for the shipment |
| **Merchant** | The merchant receiving the hardware |
| **Status** | Current delivery status |
| **Tracking URL** | Link to the delivery partner's tracking page |
| **Delivery Partner** | The logistics provider handling the shipment |
| **Package Details** | Product IDs and serial numbers in the shipment |
### Product Catalogue
The available products and bundles are configured for your partner account. Contact Surfboard Payments to add new products to your catalogue.
## Returns
The Returns page manages device return requests for terminals that need to be sent back -- whether for repairs, replacements, or end-of-service.
### Creating a Return Request
Click **+ Create Return** to initiate a device return:
1. **Select the merchant** -- choose which merchant is returning hardware
2. **Specify the device** -- enter the terminal serial number or ID
3. **Provide return reason** -- describe why the device is being returned
4. **Submit the request** -- a return label and instructions will be generated
### Tracking Returns
Search for return requests by merchant name, email, or ID. Each return request shows:
- Return request ID
- Merchant information
- Device details
- Current status
- Created date
### Return Statuses
| Status | Description |
|--------|-------------|
| **Created** | Return request has been submitted |
| **Label Sent** | Return shipping label has been sent to the merchant |
| **In Transit** | Device has been shipped back |
| **Received** | Device has been received at the warehouse |
| **Completed** | Return has been processed |
### Branding & Customization
Category: configuration | Tags: Branding, White-label, Customization, Design
URL: /partner-portal/branding
## Overview
The Branding page lets you customize the visual appearance of your partner experience. These settings control how your brand is presented on merchant-facing pages, onboarding flows, and customizable surfaces throughout the platform.
## Branding Settings
### Button Shape
Choose the shape of buttons on your branded pages:
- **Rounded** -- modern, rounded corner buttons (default)
- **Square** -- sharp-edged buttons for a more formal look
### Font Type
Select the font family used across your branded pages:
- **Sans-Serif** -- clean, modern typeface (default)
- **Serif** -- traditional, formal typeface
## Color Settings
Customize four color values to match your brand identity:
| Setting | Description | Default |
|---------|-------------|---------|
| **Background Color** | Main background color of branded pages | `#ffffff` |
| **Brand Color** | Primary brand color used for headers and key elements | Your brand's primary color |
| **Accent Color** | Secondary color used for highlights and interactive elements | Your brand's accent color |
| **Footer Color** | Background color of page footers | `#F5F5F5` |
Enter colors as hex values (e.g., `#0e44e1`) or click the color swatch to open a color picker.
## Brand Assets
Upload your brand's visual assets:
### Logo Image
Your full logo used in headers and prominent placements. This should be a high-quality image file (PNG or SVG recommended) with a transparent background.
Click **Upload Logo** to replace the current logo.
### Icon Image
A compact version of your brand mark used for favicons, mobile icons, and small placements. This should be a square image that works well at small sizes.
Click **Upload Icon** to replace the current icon.
## AI-Powered Branding
Click **Generate with AI** in the top right corner to automatically generate branding settings from your website. Enter your website URL and the system will:
1. Analyze your website's existing design
2. Extract brand colors, logo, and design patterns
3. Generate multiple branding options to choose from
4. Apply the selected option to your branding settings
This is a quick way to set up branding that matches your existing website design.
## Saving Changes
After making any changes to your branding configuration, click **Save Changes** to apply them. Changes will take effect across all branded surfaces.
## Preview
The preview panel on the left side shows how your branding settings will appear on customizable pages. This is available for ISV partners to see a live representation before saving.
## Where Branding Applies
Your branding configuration is used across:
- Merchant onboarding and KYB pages
- Payment pages and checkout flows (where applicable)
- Merchant-facing portal elements
- Email communications
- Terminal display branding (configured separately via Terminal Config)
### Billing Plans
Category: configuration | Tags: Billing, Pricing, Plans, Revenue
URL: /partner-portal/billing-plans
## Overview
Billing Plans let you define pricing structures for your merchants. Each plan specifies the fees and rates charged per payment method, allowing you to create different tiers of pricing and assign them to merchants based on their agreement.
## Understanding Billing Plans
A billing plan consists of:
- **Plan ID** -- a unique identifier (e.g., `SP_SE_Fix129`)
- **Payment methods** -- which methods are included (e.g., CARD, KLARNA, SWISH)
- **Channel type** -- whether the plan applies to in-store, online, or both
- **Pricing per method** -- individual fee structures for each payment method
## Viewing Plans
The billing plans list shows all your configured plans with:
- Plan ID/name
- Number of included payment methods
- Channel type badge (in-store / online)
- List of included payment methods
- Edit and delete actions
Use the search bar to filter plans by Plan ID.
## Creating a Billing Plan
Click **+ Add Plan** to create a new billing plan:
1. **Select payment methods** -- choose which payment methods to include
2. **Choose terminal types** -- decide whether the pricing applies to Standard (in-store) terminals, Online, or both (see [Setting different pricing per terminal type](#setting-different-pricing-per-terminal-type))
3. **Configure pricing** -- for each payment method, define:
- **Fixed %** -- the percentage of the transaction amount deducted as a fee
- **Fixed Fee** -- a constant amount added along with the percentage-based fee
- **Min Fee** -- the minimum fee charged per transaction
4. **Save the plan** -- the plan becomes available for assignment
### Supported Payment Methods
The system shows which payment methods are available for your partner account. You can include any combination of supported methods in a plan.
## Setting different pricing per terminal type
A single billing plan can hold separate pricing for **Standard** (in-store / card-present) and **Online** terminals. The **Apply fees to terminal types** section at the top of the pricing form controls which terminal type the fees you enter below are written to.
- **Tick more than one box** to fill in identical pricing for several terminal types at once. When both are selected you'll see a note such as *"Editing 2 terminal types -- fee changes apply to all of them."*
- **Tick a single box** to edit the pricing for just that terminal type, leaving the other untouched.
> **Standard pricing is mandatory.** Every plan must have Standard pricing configured. Online pricing is optional and sits on top -- so to offer Online, you need pricing for **both Standard and Online**. You cannot set Online prices without also having Standard prices in place.
### Setting different prices for Online and Standard
To give Online and Standard different fees within the same plan:
1. Under **Apply fees to terminal types**, make sure **Standard** is ticked (it's required) and fill in the **Fixed %**, **Fixed Fee**, and **Min Fee** for each payment method. These values are saved to the Standard terminal type.
2. Untick **Standard** and tick **Online** so only Online is selected.
3. Fill in the prices for Online -- these values are saved to the Online terminal type only.
4. **Save the plan.** Each terminal type keeps its own pricing, with Standard always present as the baseline.
> **Tip:** If most fees are the same and only a few differ, start with both boxes ticked to fill in the shared pricing, then select a single terminal type to adjust only the values that need to be different.
## Editing a Plan
Click the edit icon next to any plan to modify its pricing or included payment methods. Changes take effect for all merchants currently assigned to this plan.
## Deleting a Plan
Click the delete icon to remove a plan. Ensure no merchants are currently assigned to the plan before deleting.
## Assigning Plans to Merchants
To assign a billing plan to a merchant:
1. Navigate to the merchant's detail page
2. Go to the **Billing** tab
3. Select the desired billing plan from the dropdown
4. Confirm the assignment
The merchant will be billed according to the assigned plan going forward.
## Best Practices
- **Create tiered plans** -- set up different pricing tiers (e.g., Starter, Growth, Enterprise) based on transaction volume commitments
- **Separate in-store and online** -- when pricing differs by channel, you can set distinct fees for Standard and Online within a single plan (see [Setting different pricing per terminal type](#setting-different-pricing-per-terminal-type))
- **Review regularly** -- periodically review plan assignments to ensure merchants are on the right tier
### Integrations & Software
Category: configuration | Tags: Integrations, Software, Apps, Developer Portal
URL: /partner-portal/integrations
## Overview
The Integrations page (labeled "My Apps" in the portal) is where you manage software applications that run on your merchants' terminals. This is the bridge between the Developer Portal -- where software is built and signed -- and the Partner Portal -- where it gets deployed to your merchant fleet.
## How It Works
The software deployment flow works in two stages:
1. **Developer Portal** -- upload and sign your software application
2. **Partner Portal** -- push the signed software to merchants and terminals
> Software applications must first be uploaded and signed through the **Developer Portal** before they appear here. Once signed, you can push them to merchants and terminals.
## Viewing Software Packages
The software list shows all signed packages available for deployment:
- Search by ID, type, or version
- View package details including version numbers and signing status
## Pushing Software to Merchants
Click **Push Software** to deploy a software package:
1. **Select the software package** -- choose from your signed applications
2. **Select target merchants** -- pick which merchants should receive the software
3. **Confirm the push** -- the software will be queued for deployment
Terminals will download and install the software on their next check-in or when a push is triggered.
## Software Lifecycle
| Stage | Where | Action |
|-------|-------|--------|
| **Build** | Developer Portal | Create and upload your application |
| **Sign** | Developer Portal | Sign the package for production deployment |
| **Push** | Partner Portal | Deploy to merchants and terminals |
| **Install** | Terminal | Automatic installation on next check-in |
## Managing Software Per Merchant
You can also manage software at the individual merchant level:
1. Navigate to the merchant's detail page
2. Go to the **Terminals** tab
3. Select a terminal
4. Push latest software to that specific terminal
This is useful for staged rollouts or troubleshooting individual devices.
### Settings & Notifications
Category: configuration | Tags: Settings, Security, Team, Notifications, Slack
URL: /partner-portal/settings
## Overview
The Settings page provides account management, team collaboration, and notification configuration. Here you can update your password, invite team members, and set up automated alerts for important events.
## Security
### Changing Your Password
To update your password:
1. Enter your **Current Password**
2. Enter a **New Password** (minimum 8 characters)
3. **Confirm New Password** by entering it again
4. Click **Change Password**
Password requirements:
- Minimum 8 characters
- Recommended to use a mix of uppercase, lowercase, numbers, and special characters
## Team Members
Collaborate with your team by inviting members to your partner account.
### Inviting Team Members
Click **+ Invite** or **Invite Member** to add a new team member:
1. Enter the team member's **email address**
2. Assign a **role** with appropriate permissions
3. Send the invitation
The invited person will receive an email with a link to create their account and access the Partner Portal.
### Managing Team Members
The Team Members section displays all current members with their:
- Name and email
- Assigned role
- Account status (Active, Pending Invitation)
## Notifications
Configure automated notifications for key events in your partner portfolio. Notifications can be sent via:
- **Email** -- receive alerts to an email address
- **Slack** -- post notifications to a Slack channel via webhook URL
- **SFTP** -- send data files to an SFTP server for automated processing
### Available Events
Subscribe to notifications for events such as:
- **Merchant onboarding** -- when a new merchant completes KYB
- **Terminal registration** -- when a new terminal is activated
- **File transfers** -- when settlement or reporting files are ready
- **Application status changes** -- when applications change state
### Adding a Notification
Click **+ Add** to configure a new notification:
1. **Select the event** -- choose which event should trigger the notification
2. **Choose the channel** -- Email, Slack, or SFTP
3. **Enter the destination**:
- For Email: the recipient email address
- For Slack: the webhook URL for your Slack channel
- For SFTP: host, user, port, public key, and remote directory
4. **Save** -- the notification will be active immediately
### Removing a Notification
Click the remove button on any configured notification to stop receiving alerts for that event.
> **Important:** These are alerts about events in your own portfolio. Notices *from Surfboard* -- settlement changes, integration updates, maintenance -- are delivered on the **Messages** page under Support, and email delivery for those is set up there, not here. See [Messages](/partner-portal/messages) and subscribe a shared mailbox so your team hears about changes the day they are published.
## Terminal Configuration
Some partners may also have access to global terminal configuration settings:
- Default terminal behavior settings
- Receipt configuration
- Display preferences
These settings serve as defaults that cascade down to all merchants and terminals in your portfolio, unless overridden at the merchant, store, or terminal level.
### Transactions & Reporting
Category: merchants | Tags: Transactions, Reporting, CSV, Search, Filters
URL: /partner-portal/transactions-and-reporting
## Overview
Transaction management is accessible from each merchant's detail page under the **Transactions** tab. It provides a comprehensive view of all payment activity with powerful search, filtering, and export capabilities.
## Viewing Transactions
The transactions table shows recent payment activity for the selected merchant. Each row displays:
| Column | Description |
|--------|-------------|
| **Amount** | Transaction amount in the merchant's currency |
| **Terminal ID** | Which terminal processed the payment |
| **Payment Method** | Card, Swish, Klarna, etc. |
| **Type** | Purchase, Refund, Void, etc. |
| **Card Brand** | Visa, Mastercard, Amex, etc. (for card payments) |
| **RRN** | Retrieval Reference Number |
| **Timestamp** | Date and time of the transaction |
## Searching Transactions
Use the search bar to find specific transactions by:
- **Payment ID** -- the unique payment identifier
- **Order ID** -- the merchant's order reference
- **Transaction ID** -- the Surfboard transaction identifier
- **RRN** -- the Retrieval Reference Number
The search uses a global search index that matches across multiple fields and highlights which fields matched.
## Filtering
### Date Range
Select a start and end date to narrow transactions to a specific period. The default view shows the current month.
### Advanced Filters
Click **Filters** to access additional filtering options:
| Filter | Options |
|--------|---------|
| **Payment Method** | Card, Swish, Klarna, Vipps, MobilePay, etc. |
| **Card Brand** | Visa, Mastercard, Amex, Maestro |
| **Card Type** | Debit Consumer, Credit Consumer, Debit Commercial, etc. |
| **Region** | Domestic, Intra, International |
| **Terminal** | Filter by specific terminal |
| **Store** | Filter by specific store |
| **POS Entry Mode** | Chip, Contactless, Manual, etc. |
| **Currency** | Filter by transaction currency |
Filters can be combined for precise results. Click **Clear** to remove all filters.
### Filter Types
Filters are applied at two levels:
- **API filters** (date range, store, terminal) -- these filter at the data source for efficient querying
- **Client-side filters** (payment method, card brand, type, region) -- applied after data is fetched for quick toggling
When API filters are changed, you need to click **Search** to re-fetch data.
## Transaction Details
Click on any transaction to view its full details, including:
- Complete transaction identifiers (payment ID, order ID, transaction ID)
- Amount and currency
- Payment method and card details (brand, type, truncated PAN)
- Terminal and store information
- Settlement status and reference
- Interchange domain (Domestic, Intra, International)
- POS entry mode
- Void status
- Full timestamp
## Exporting Transactions
Click the **Download** button to export transactions as CSV. The export includes:
- Amount (formatted with decimal separators)
- Terminal ID, Currency, Merchant ID
- Order ID, Reference ID
- Payment method and card label
- Payment type and RRN
- Transaction ID and timestamp
- Adjustment count and amount (if applicable)
### Export Limits
- Maximum of **10,000 transactions** per export
- If the total exceeds this limit, narrow your date range for a complete export
- Client-side filters are applied before export when active
### Adjustments
The export automatically includes adjustment data (refunds, chargebacks, etc.) cross-referenced by Order ID. This gives you a complete picture of each transaction's lifecycle.
## Merchant-Level Analytics
The merchant overview page also provides aggregated analytics:
- **Sales volume** over the past 3 months
- **Transaction count** and average transaction value
- **Payment method distribution** by volume
- **Sales trend chart** with quarterly, monthly, weekly, and daily views
These analytics update automatically and provide at-a-glance performance metrics for each merchant.
## Downloadable Reports
Beyond the transaction list, each merchant's **Reports** tab provides downloadable reports:
- **Daily reports** -- end-of-day summaries of the merchant's activity.
- **ECR reports** -- electronic cash-register outputs (Z-report, X-report, and SIE files). ECR reports are available to subscribed merchants only.
Settlement and payout reports are also available from the merchant's Reports tab. For full cash-register reporting and how to fetch Z/X reports per register, see the [Cash Registers (ECR)](/partner-portal/cash-registers) guide.
### When a merchant asks for a different report interval
A merchant asking for "weekly reports" usually means one of two things. If they want a weekly *summary of what they sold*, they already have it -- reports cover any period they choose, whatever their settlement schedule. If they want to be *paid* weekly instead of daily, that is a settlement schedule change and needs the merchant's signature. [Changing a Merchant's Settlement Schedule](/partner-portal/settlement-schedule) covers both cases and the request flow.
### When a merchant says the totals do not match
A monthly report and the payouts inside that month are offset by two to three days at each end, because a payout settles transactions from a few days earlier. The monthly figure counts what the merchant traded that month; the payouts count what reached their bank. Fees are collected after the month closes, so a deduction seen in June is May's fees and matches May's monthly report rather than May's payouts.
[Reading a settlement report](/developers/guides/settlements-reporting#reading-a-settlement-report) works through the arithmetic, says which figure answers which merchant question, and lists what to check before escalating. The merchant-facing version is in [Reports](/merchant-portal/reports#why-two-totals-can-differ) if you would rather send the merchant a link than explain it yourself.
### Payment Methods
Category: merchants | Tags: Payment Methods, Klarna, Swish, Vipps, Cards, Amex
URL: /partner-portal/payment-methods
## Overview
Payment Methods management allows you to control which payment options are available for each merchant. You can activate additional payment methods with a single click -- no separate agreements needed for most methods since the flow goes through Surfboard.
## Accessing Payment Methods
Navigate to a merchant's detail page and click the **Payment Methods** tab.
## Core Methods
These methods are always available and cannot be deactivated:
| Method | Description |
|--------|-------------|
| **CPOC** | SoftPOS -- accept contactless payments on a smartphone |
| **CASH** | Cash payment recording |
| **CARD** | Standard card payments (Visa, Mastercard, etc.) |
Each core method shows its activation status and payment method ID.
## Additional Payment Methods
These methods can be activated or deactivated per merchant:
### Available Methods
| Method | Description |
|--------|-------------|
| **Klarna** | Buy Now, Pay Later and invoice payments |
| **American Express** | Amex card acceptance |
| **Vipps** | Norwegian mobile payment (where available) |
| **Swish** | Swedish mobile payment |
| **MobilePay** | Danish/Finnish mobile payment |
| **Account to Account** | Direct bank-to-bank payments |
### Availability States
Each additional payment method shows one of three states:
- **Active** (green badge) -- the method is enabled and ready for use
- **Ready to activate** -- the method is available for your partner configuration and can be activated
- **Not available for your partner configuration** -- the method hasn't been enabled for your partner account (contact Surfboard to enable)
### Activating a Payment Method
1. Find the payment method you want to enable
2. Verify it shows "Ready to activate"
3. Click **+ Activate**
4. The method will be activated and assigned a payment method ID
For some methods (like American Express or Klarna), additional configuration may be required during activation.
### Deactivating a Payment Method
1. Find the active payment method
2. Click **Deactivate**
3. Confirm the deactivation
> **Note:** Core methods (CASH, CPOC, CARD) cannot be deactivated.
## Payment Method Details
When a method is active, you can see:
- **Payment Method ID** -- unique identifier for the activated method
- **Status** -- current activation status
- **Method type** -- the payment method category
## Supported Payment Methods
The full list of payment methods that may be available depending on your configuration:
- Visa, Mastercard, Maestro
- American Express
- Apple Pay, Google Pay
- Klarna
- Swish (SSWISH, NSWISH variants)
- Vipps
- MobilePay
- Account to Account
- B2B Invoice
- Gift Cards
- Cash
> The specific methods available to your partner account depend on your agreement with Surfboard Payments and the countries you operate in.
### Support Tickets
Category: support | Tags: Support, Tickets, Help, Notifications
URL: /partner-portal/tickets
## Overview
The Support Ticketing system is available within the Surfboard Partner Portal and lets you raise, track, and respond to support requests directly with the Surfboard team. All communication related to an issue is kept in one place, making it easy to follow the full history of a conversation.
## Navigating to Support Tickets
To access the ticketing system, log in to the Partner Portal and locate the sidebar on the left-hand side of the screen. Under the **Support** section, click **Tickets**.
From the Tickets page you can:
- View all tickets and their current status
- Search for a specific ticket using the search bar at the top
- Filter tickets using the **+ Filter** option
- Switch between status tabs to focus on tickets requiring attention
- Create a new ticket using the **+ New Ticket** button in the top-right corner
## Ticket Statuses
Across the top of the ticket list, tabs group your tickets by their current status. The number displayed next to each tab shows how many tickets are in that state.
| Status | Meaning |
|--------|---------|
| 🟢 **Open** | Ticket has been submitted and is awaiting a response from Surfboard. |
| 🟡 **Action Needed** | Surfboard has responded and is waiting for further information from you. |
| 🔵 **In Progress** | Surfboard is actively investigating or resolving the issue. |
| ✅ **Resolved** | The issue has been resolved. Review and confirm, or reopen if needed. |
| ⚫ **Closed** | The ticket has been closed and no further action is required. |
## Ticket List Overview
Each ticket in the list displays the following information at a glance:
- Ticket title and priority badge (Urgent, High, Normal, or Low)
- Unique ticket ID (e.g. `SURF-ACTI04-000012`)
- Merchant name, if a merchant has been linked to the ticket
- Issue type (e.g. Terminal Issue, Payment Issue, Integration)
- Date and time the ticket was created
- Date and time of the most recent update
### Issue Types
Choosing the correct type routes your ticket to the most relevant team within Surfboard.
| Type | Use for |
|------|---------|
| **Terminal Issue** | Problems with a physical payment terminal |
| **Payment Issue** | Transaction failures, declined payments, or payment processing errors |
| **Account Issue** | Login, permissions, or account configuration problems |
| **Integration** | API, webhook, or third-party integration queries |
| **Billing** | Invoicing, fees, or billing plan queries |
| **Other** | Any issue that does not fit the above categories |
### Priority Levels
| Priority | When to use |
|----------|-------------|
| 🔴 **Urgent** | Critical issue causing immediate disruption to payments or operations. |
| 🟠 **High** | Significant issue affecting functionality. Needs prompt resolution. |
| 🟡 **Normal** | Standard issue with moderate impact. This is the default priority. |
| ⚪ **Low** | Minor issue or general enquiry with minimal operational impact. |
## Raising a New Ticket
To raise a support ticket, click the **+ New Ticket** button in the top-right corner of the Support page. A ticket creation form will appear.
### Completing the Form
| Field | Required | Notes |
|-------|----------|-------|
| **Ticket Title** | Yes | A clear, concise summary of the issue. The system will not let you submit without it. Supports bold, italic, underline, and link formatting. |
| **Description** | Yes | Explain the issue in detail (see below). Supports basic rich text: bold, italic, underline, and hyperlinks. |
| **Merchant** | No | If the issue is specific to one of your merchants, link them. Click the Merchant dropdown and type at least two characters to search. |
| **Attachments and Links** | No | Attach supporting files (drag, paste, or browse, 5 MB maximum per file) or add a hyperlink using the **Add link** option. |
In the description, include:
- What happened
- What you expected to happen
- Steps to reproduce the issue, if applicable
### Submitting the Ticket
Once you have completed the form, click the **Create ticket** button. The ticket is created immediately and assigned a unique ID. You will be taken directly to the ticket view, and the Surfboard support team will be notified.
> **Note:** If the **Create ticket** button is greyed out or the form does not submit, check that both the **Ticket title** and **Description** fields have been filled in, as these are required.
## Responding to a Ticket
When the Surfboard team replies to your ticket, you will receive a notification. To respond:
1. Open the ticket from the ticket list by clicking on the ticket title.
2. Scroll to the reply box at the bottom of the page.
3. Click into the **Write a reply...** area and type your message.
4. Use the formatting toolbar above the reply box to apply bold, italic, underline, or link formatting if needed.
5. Attach any relevant files using **Attach files**, or add a reference link using **Add link** (5 MB maximum per file).
6. Press **Enter** to send your reply, or use **Shift + Enter** to add a new line within your message before sending.
7. Click the **Reply** button to submit your response.
> **Note:** Replying to a ticket notifies the Surfboard support team immediately. Each reply creates a new message in the thread, so include any additional context or attachments before submitting.
## Notifications
The Partner Portal notifies you whenever there is new activity on your tickets. You don't need to manually check the ticket list, the notification system alerts you as soon as the Surfboard team responds.
### The Bell Icon
The bell icon is located in the top-right corner of the portal, to the left of the **+ New Ticket** button. When you have unread notifications, a blue badge appears on the bell showing the number of unread items.
Click the bell icon to open the Notifications panel, which shows activity from the last 10 days.
### Acting on a Notification
Click any notification entry to go directly to the relevant ticket. Once viewed, the notification is marked as read and the blue dot is removed.
> **Tip:** To stop receiving notifications for a specific ticket, open the ticket and click **Unwatch** in the **Watchers** section of the right-hand sidebar.
## Best Practices
Following these guidelines helps ensure your tickets are resolved as quickly and efficiently as possible.
- **Use a descriptive ticket title** -- avoid vague titles like "Issue" or "Problem". A clear title such as *"Terminal offline at Store #42, unable to process payments"* helps the team triage faster.
- **Set the correct priority** -- only use Urgent for issues causing immediate, widespread disruption. Raising the priority unnecessarily can delay resolution of genuinely critical tickets.
- **Select the right type** -- the correct issue type routes your ticket to the most relevant team within Surfboard.
- **Link a merchant where relevant** -- if the issue is specific to one merchant, always link them so the support team can look up their account details without needing to ask.
- **Include as much detail as possible upfront** -- describe what happened, what you expected, and the steps that led to the issue. Attach screenshots or logs where available.
- **Respond promptly when status is Action Needed** -- this status means the Surfboard team is waiting on information from you. Delays may affect how quickly your issue is resolved.
- **Monitor your notifications** -- keep an eye on the bell icon so you don't miss a reply. You can also check the **Action Needed** tab in the ticket list at any time.
> For queries you can also email [support@surfboardpayments.com](mailto:support@surfboardpayments.com).
### Changing a Merchant's Settlement Schedule
Category: merchants | Tags: Settlements, Payouts, Reports, Signing, Merchants
URL: /partner-portal/settlement-schedule
## Overview
A merchant's settlement schedule decides how often their money is paid out, and with it, how often a settlement report is produced. It is set when the merchant is onboarded and can be changed afterwards -- but not by you alone. The schedule is part of the merchant agreement, so a change has to be signed by the merchant.
You start the request from the partner portal. The portal generates a signature link. You send that link to the merchant, they sign, and the new schedule takes effect.
## First, Check What They Actually Want
"Can we get weekly reports?" is usually one of two different requests, and only one of them needs a signature:
| What the merchant means | What they need |
|---|---|
| "Pay us weekly instead of daily" | A settlement schedule change -- the flow below |
| "Send us a weekly summary of what we sold" | Nothing. Reports already cover any period they choose |
Downloadable reports in the merchant's **Reports** tab are independent of the settlement schedule. A merchant on daily settlement can still pull a weekly or monthly report whenever they like, and so can you. If all they want is the numbers for a week, point them at [Reports](/merchant-portal/reports) and you are done -- no signature, no waiting.
Ask which one it is before starting a contract change.
## Requesting the Change
1. Open the merchant from the **Merchants** list
2. Go to the **Configuration** tab
3. Open **Settlements**
4. Request the change to the new schedule
5. Generate the signature link
6. Share the link with the merchant
The merchant signs, and the schedule changes. Until they sign, the merchant stays on their current schedule -- the request on its own changes nothing.
> **You cannot sign on the merchant's behalf.** Settlement terms sit in the merchant agreement, which is why the signature is required rather than a formality. If the merchant does not sign, the request does not take effect.
## Available Schedules
| Schedule | Payout runs |
|---|---|
| `DAILY` | Every banking day |
| `WEEKLY` | Once per week |
| `MONTHLY` | Once per month |
These are the same values used by `settlementFrequency` when a merchant is created over the API -- see the [Merchants API](https://developers.surfboardpayments.com/api/merchants) for the onboarding side of this.
## What Changes, and What Does Not
**Changes with the schedule:**
- How often a payout reaches the merchant's bank account
- How often a settlement report is generated
**Does not change:**
- The transactions themselves, or their fees
- Access to daily, monthly, or cash-register reports in the Reports tab
- Anything already settled under the old schedule
A longer settlement period means fewer, larger payouts. Merchants sometimes ask for this to cut down on bank reconciliation work, then find it harder to match a payout back to a day's trading. [Reading a settlement report](/developers/guides/settlements-reporting#reading-a-settlement-report) is worth sending along with the change, because the arithmetic they are used to will shift.
## After the Change
The first payout on the new schedule covers the transactions that had not yet settled when the change took effect, so it can be an unusual size. That is expected, and it settles into the normal rhythm from the following period.
If a merchant asks why the first weekly payout does not match a week of trading, the offset explained in [When a merchant says the totals do not match](/partner-portal/transactions-and-reporting#when-a-merchant-says-the-totals-do-not-match) is the same effect.
## Related
- [Transactions & Reporting](/partner-portal/transactions-and-reporting) -- reports available per merchant
- [Merchant Management](/partner-portal/merchant-management) -- the merchant detail page and its tabs
- [Applications & Merchant Onboarding](/partner-portal/applications) -- signing links during onboarding
- [Settlements & Reporting](/developers/guides/settlements-reporting) -- the reports themselves, over the API
- [Reports](/merchant-portal/reports) -- the merchant-facing view
### Messages
Category: support | Tags: Messages, Notifications, Email, Notices
URL: /partner-portal/messages
## Overview
**Messages** is where Surfboard talks to you as a partner. When we publish a notice that affects your portfolio -- a settlement change, an integration or API update, planned maintenance, a service change -- it lands here, in the portal, addressed to your partner account.
You'll find it in the sidebar under **Support → Messages**. A badge on the menu item shows how many notices are unread.
> **Important:** Subscribe an email address on the Messages page as soon as your partner account is set up. An inbox only works if someone opens it. With a subscription, every notice also goes out as an email, so the people who run your integration and your merchant operations hear about a change the moment we publish it -- not the next time somebody logs in.
## Reading Messages
The page lists every notice sent to your partner account, newest first. Each one shows:
- **Title** and the date it was published
- **Severity** -- *Info*, *Warning*, or *Critical*
- An unread dot until it has been opened
Click a message to expand it. Some include a link to the relevant page in the portal, to a guide, or to the developer documentation. Once opened, a message is marked as read and the badge count goes down.
Critical notices also appear as a banner across the top of the portal until they are read.
## Subscribe to Email Notifications
At the bottom of the Messages page is the **Email notifications** card. Any address you add here receives every notice we publish to your partner account as a branded email, in addition to it appearing in the portal.
To subscribe an address:
1. Open **Support → Messages** and scroll to **Email notifications**.
2. Enter the address -- the placeholder suggests `notifications@yourcompany.com` -- and click **Add**.
3. We send a confirmation link to that address. Until it's clicked, the address shows as **Pending** and receives nothing.
4. Once confirmed, the status changes to **Active** and notifications start with the next notice.
If the confirmation email didn't arrive, click **Resend** next to the pending address. To stop emails to an address, use **Remove address** next to it.
### Which address to use
Subscriptions belong to your partner account, not to an individual user, and the address does not need to be a portal login. Use shared mailboxes so a notice survives holidays and staff changes:
- **finance@** for settlement and payout notices
- **integrations@** or your engineering on-call address, so API and integration changes reach the people who maintain your connection
- **support@** or your merchant-facing team, who are the ones merchants call when a terminal behaves differently
You can add several. Anyone on your team with portal access can add or remove addresses.
> **Tip:** Nothing is subscribed by default. A new partner account starts with an empty list, so if you skip this step the notices only live in the portal.
## Messages vs. Notifications
**Messages** are notices from Surfboard to you. The **Notifications** section under [Settings](/partner-portal/settings#notifications) is different: those are event alerts about your own portfolio -- merchant onboarding, terminal registration, file transfers, application status -- delivered by email, Slack, or SFTP. Set up both.
## Why It Matters
As a partner you sit between Surfboard and your merchants. When we change a settlement schedule, retire an API version, or schedule maintenance, your merchants will ask you first. A subscribed shared mailbox means your team already knows.
---
## Merchant Portal Guides
Documentation for merchants using the Surfboard Payments Merchant Portal to run their business, manage stores, terminals, products, sales, reports, campaigns, gift cards, financing, integrations, and settings.
### Dashboard Overview
Category: getting-started | Tags: Dashboard, Analytics, Overview
URL: /merchant-portal/dashboard-overview
## Overview
The dashboard is the first thing you see after logging in to the Merchant Portal. It gives you an immediate snapshot of how your business is performing -- without you having to dig for the data.

## Greeting and Monthly Sales
At the top, the portal greets you with a personalised welcome and shows the **Monthly sales (last 12 months)** chart. The graph visualises your payment volume month by month, so you can quickly spot growth, dips, and seasonal patterns.
## Latest Sales
The **Latest sales** card shows your most recent transactions in real time -- the date, time, amount, and whether it was a sale or a return. Click any row to open the full transaction detail page, or click **All sales »** to jump to the Sales section.
You also see two counters:
- **Sales** -- the number of recent successful payments
- **Returns** -- the number of recent refunds
## Currency Breakdown
The dashboard automatically separates your numbers by currency, which is useful if you operate in multiple markets. For each currency you'll see:
| Metric | What it means |
|--------|---------------|
| **Total sales** | The combined value of all successful transactions in that currency |
| **Total number of sales** | How many transactions made up that volume |
| **Average order value** | Mean transaction value (total ÷ count) |
| **Returns** | Total refunded amount and how many refunds |
This is your fastest way to compare performance across SEK, NOK, DKK, EUR, GBP, and any other currency you accept.
## Quarterly Analytics
The **Quarterly Analytics** chart at the bottom of the page plots Sales vs. Returns over the last several quarters. Use the currency tabs (SEK, GBP, EUR, DKK, etc.) to switch between markets. You can also export the chart as **SVG**, **PNG**, or **CSV** for reports and presentations.
## Sidebar Navigation
The sidebar gives you one-click access to every section of the portal:
- **Sales** -- transaction list and search
- **Reports** -- daily, monthly, and cash-register settlement reports
- **Terminals** -- manage your card terminals and printers
- **Stores** -- create and configure stores
- **Campaigns** -- run terminal-based marketing campaigns
- **Gift Cards** -- issue and track gift cards
- **Financing** -- access merchant financing offers
- **Support** -- get in touch with the team
- **Messages** -- notices from Surfboard; subscribe an email address here so your team gets them too. See [Messages](/merchant-portal/messages).
- **Settings** -- profile, team, branding, and notifications
You can also collapse the sidebar to free up screen space, switch language using the flag dropdown, and use the **Find transaction by ID** field to jump directly to any transaction.
## Profile Pill
In the top-right corner, the profile pill shows your merchant name and the user you're logged in as. From the dropdown you can quickly access **Settings**, **Support**, or **Log out**.
### Get Started Wizard
Category: getting-started | Tags: Onboarding, Get Started, Setup
URL: /merchant-portal/get-started-wizard
## Overview
The **Get Started** wizard is an agentic, chat-style assistant that walks new merchants through everything needed to take their first payment. Instead of clicking through dozens of settings screens, you have a guided conversation that configures the portal for you.
## Progress Tracker
A progress bar at the top tracks how far through setup you are. As you complete each step, the bar fills toward 100% and you'll see encouraging milestones (e.g. *"Almost there!"* once you pass 75%).
## What the Wizard Sets Up
The wizard typically covers:
- **Business profile** -- merchant name, contact details, and merchant type (in-store, online, or omnichannel)
- **Auto-branding** -- enter your website and the AI scrapes it to generate a colour palette, logo, and font choice for your branded checkout
- **First store** -- set up the first physical or online location
- **Terminal pairing** -- scan a QR code on a terminal to register it instantly, no manual setup required
- **Test transaction** -- run an automatic 1 kr void/test charge to confirm everything works end-to-end
## Manual Configuration
If you prefer to skip the conversation and configure things yourself, click **Configure manually** at the bottom -- you'll be sent straight to the dashboard, where every section is available from the sidebar.
You can also click **Skip onboarding** to dismiss the wizard entirely. You can always return to it from the **Get Started** entry in the sidebar.
## Completion
When all steps are done, a celebration screen confirms *"You're all set!"* and drops you on the dashboard, ready to start taking real payments.
## Why Use the Wizard?
- **Faster setup** -- typical merchants are live in under 10 minutes
- **No guesswork** -- the wizard asks for exactly what's needed at each step
- **Self-service** -- no calls to support required
- **Branded out of the box** -- AI-generated branding means your checkout looks like *your* business from the very first transaction
### Sales
Category: sales-reports | Tags: Sales, Transactions, Refunds
URL: /merchant-portal/sales
## Overview
The **Sales** section is your transaction ledger. Every payment that runs through your terminals, payment links, or online checkout shows up here -- in real time.

## Time Filters
The top of the page has quick filter buttons to scope what you're looking at:
- **Latest** -- the most recent transactions
- **Today** -- everything from today
- **Last 7 days** -- the past week
- **Current month** -- month-to-date
- **Last month** -- the previous calendar month
Need a custom range? Use the date pickers next to the quick filters to choose any window you like.
## Search
The **Search by** field lets you find specific transactions by:
- Order ID
- Terminal ID
- Card number (last four digits)
- Customer email or reference
- Amount
You can refine even further by combining the filter and the date range.
## Transaction Table
Each row in the list shows:
| Column | What it shows |
|--------|---------------|
| **Status** | Whether the sale was successful, voided, or refunded |
| **Order ID** | The unique reference for the transaction (clickable) |
| **Terminal** | Which device the sale was taken on |
| **Store** | Which location the terminal belongs to |
| **Amount & currency** | The total charged |
| **Date & time** | When the sale happened |
Click the **»** arrow on any row -- or the order ID -- to open the transaction detail view.
## Transaction Detail
The detail page shows everything about a single sale:
- Card brand, masked PAN, and authorisation code
- Acquirer response and approval status
- Tip amount (if applicable)
- Receipt and digital receipt link
- Terminal serial, store, and operator (if logged)
- Refund history and link to issue a new refund
You can also re-print or email a copy of the receipt directly from this page.
## Refunds and Returns
Returns appear in the same list, marked with a **Return** badge and a negative amount. Click into the original sale to issue a partial or full refund -- the portal handles the acquirer reversal automatically.
## Bulk Actions
Use the checkboxes on the left to select multiple transactions and export them to CSV for accounting or reconciliation.
## Surf AI Insights
Above the transaction list you'll see **Surf AI Insights** -- short, AI-generated commentary on your recent activity (e.g. busiest hour, average tip, returning customers). It's a fast way to surface patterns without having to build a custom report.
### Messages
Category: getting-started | Tags: Messages, Notifications, Email, Notices
URL: /merchant-portal/messages
## Overview
**Messages** is where Surfboard talks to you. When we publish a notice that concerns your business -- a settlement change, a renewal, planned maintenance, a service update -- it lands here, in the portal, addressed to your merchant account.
You'll find it in the sidebar between **Support** and **Settings**. A small counter on the menu item shows how many notices you haven't read yet.
> **Important:** Subscribe an email address on the Messages page as soon as you have access to the portal. The inbox only helps if someone opens it. With a subscription, every notice also arrives as an email, so the right people hear about a settlement or service change the moment we publish it -- not the next time somebody happens to log in.
## Reading Messages
The page lists every notice sent to your account, newest first. Each one shows:
- **Title** and the date it was published
- **Severity** -- *Info*, *Warning*, or *Critical*
- An unread dot until you open it
Click a message to expand it. Some include a link to the relevant page in the portal or to a guide. Once opened, a message is marked as read and the unread counter goes down.
Critical notices also appear as a banner across the top of the portal until they are read, so you won't miss them even if you never visit the Messages page.
## Subscribe to Email Notifications
At the bottom of the Messages page is the **Email notifications** card. Any address you add here receives every notice we publish to your account, as a branded email, in addition to it appearing in the portal.
To subscribe an address:
1. Open **Messages** from the sidebar and scroll to **Email notifications**.
2. Enter the address -- the placeholder suggests `notifications@yourcompany.com` -- and click **Add**.
3. We send a confirmation link to that address. Until it's clicked, the address shows as **Pending** and receives nothing.
4. Once confirmed, the status changes to **Active** and notifications start with the next notice.
If the confirmation email didn't arrive, click **Resend** next to the pending address. To stop emails to an address, click the **✕** next to it.
### Which address to use
Subscriptions belong to your merchant account, not to an individual user, and the address does not need to be a portal login. That makes a shared mailbox the best choice:
- **finance@** for anything about settlements and payouts
- **ops@** or **store@** so the people on the floor hear about terminal and service changes
- Your own address as well, if you're the one who acts on these notices
You can add several. Anyone on your team with portal access can add or remove addresses.
> **Tip:** Nothing is subscribed by default. A new merchant account starts with an empty list, so if you skip this step the notices only live in the portal.
## Messages vs. Notifications
**Messages** are notices from Surfboard to you. The **Notifications** tab under [Settings](/merchant-portal/settings#notifications) is different: those are alerts about your own activity -- transactions, settlement reports, application updates -- delivered by email, Slack, or webhook. Set up both.
## Why It Matters
Most of what we send is time-sensitive. A settlement schedule change, a certificate renewal, or a maintenance window matters far more before it happens than after. Two minutes spent subscribing a shared mailbox means the notice reaches the people who need it, on the day we send it.
### Reports
Category: sales-reports | Tags: Reports, Settlement, Accounting
URL: /merchant-portal/reports
## Overview
The **Reports** section gives you exportable settlement data for accounting, reconciliation, and bookkeeping. Each report rolls up a defined period of activity into a single downloadable file.
## Report Types
You can generate three kinds of reports:
| Type | What it covers | Typical use |
|------|----------------|-------------|
| **Daily** | One day of activity, settled to your bank | Day-end reconciliation |
| **Monthly** | A calendar month of activity | Monthly bookkeeping |
| **Cash register** | All activity tied to a specific terminal | Z-reports / per-shift cash-up |
## Report Columns
Every row in the report list shows:
- **Settlement date** -- when the funds will be (or were) paid out
- **Transaction dates** -- the period the report covers
- **Report type** -- Daily, Monthly, or Cash register
- **ID** -- the unique reference of the report
- **Sales** -- gross sales for the period
- **Returns** -- refunds for the period
- **Fee** -- processing fees deducted
- **Payout** -- net amount transferred to your bank account
- **Download** -- click to download the full report file
## Creating a Report
Click **+ Create report** at the top of the page. You can choose:
- The **report type** (daily, monthly, or cash register)
- The **date range** or specific terminal
- The **file format** (CSV, PDF)
The portal generates the report on demand and adds it to the list.
## Filtering and Sorting
Use the column headers to sort by settlement date, transaction date, report type, or amount. Pagination at the bottom lets you flip through historical reports -- the system keeps a full audit trail.
## What's in the File
The downloaded file includes line-by-line detail for every transaction in the report period: order ID, terminal, store, card brand, amount, fee, tip, and any adjustments. This is the same data your accountant or ERP system needs for VAT, settlement, and revenue reconciliation.
## Why Two Totals Can Differ
The most common question about these reports: the fee on the monthly report does not match the fees added up across that month's payouts. Both numbers are right. They count different things.
- The **monthly report** covers the transactions you took during that calendar month.
- The **payouts** are the transfers that reached your bank during that month.
Payouts arrive two to three days after the transactions they cover. So the payout that lands on 1 May pays for transactions from the end of April, and the sales you take in the last days of May arrive in your bank in June. Line the two up and they are offset by a couple of days at each end.
| Transactions | Paid out | Fee |
|---|---|---|
| 29--30 April | 1--2 May | 43.50 |
| 1--28 May | during May | 1,196.50 |
| 29--31 May | 1--3 June | 87.20 |
Your May monthly report shows a fee of **1,283.70** -- what May's trading cost you. Adding up the payouts that arrived in May gives **1,240.00** -- because that set starts with two days of April and stops before the last days of May.
### Which Report Is a Sale In?
One rule covers it:
> **Your monthly report follows the date you took the payment. Your payouts follow the date the money moved.**
So a sale is counted in two places, and around the turn of a month those are different months:
| You took the payment | It reached your bank | On the monthly report for | Among the payouts for |
|---|---|---|---|
| 30 April | 2 May | **April** | **May** |
| 15 May | 17 May | May | May |
| 31 May | 2 June | **May** | **June** |
To find where a particular sale went, take the date you took it, add two to three days, and read across.
## Fees Are Collected After the Month Closes
Fees are not taken out of each payout. Payouts arrive in full through the month, and the fee for the whole month is collected afterwards in a single deduction.
So the deduction on your statement in early June is **May's** fees, and it matches the fee on your May monthly report. Comparing that deduction against May's payouts compares two different periods, and will never balance.
## Refunds Count in the Month You Issued Them
A refund you process in June against a sale from May comes off June's figures. It does not change your May report. If a refund is missing from the month of the original sale, that is why.
## Answering the Common Questions
| You want to know | Look at |
|---|---|
| What you traded, and what it cost you, in a given month | That month's **monthly report** |
| What was deducted from your account this month | **Last month's** monthly report fee |
| Why a specific transfer was the amount it was | The **payout row**, or that day's daily report |
| What to give your accountant for VAT | The **monthly report** |
## Tips
- Run a **Daily** report at end-of-day to match till totals
- Use **Monthly** reports for bookkeeping and VAT returns
- Use **Cash register** reports per terminal for per-shift cash-up
- Schedule reports to download automatically through the API or webhook integration if you need to push them into another system
### Stores
Category: operations | Tags: Stores, Locations, Multi-store
URL: /merchant-portal/stores
## Overview
A **Store** is the central building block of the Merchant Portal. It represents a single physical location, an online shop, or an omnichannel hybrid. Terminals, products, payment links, and reports are all organised under stores.

## Store List
The **Stores** page lists every store in your business. Each row shows:
- **Name** and store ID
- **Type** -- Store (in-store), Omni (omnichannel), or Online
- **Address** -- street, postal code, and city
- **Email** -- store contact email
- **Phone** -- contact number
- **Status** -- Active or inactive
Click **Details »** on any row to open the store's configuration.
## Creating a Store

Click **+ Create new Store** at the top of the page. The wizard asks for:
1. **Store name** -- visible to customers and on receipts
2. **Store type** -- physical, online, or omnichannel
3. **Address** -- legal address of the location
4. **Contact details** -- email and phone for support and notifications
5. **Currency** -- the default currency for this store
Once saved, the store is immediately ready to accept payments.
## Store Detail Page

Opening a store gives you a tabbed view with everything you need to run it:
### Overview
A summary of recent activity, terminals, sales, and key metrics for that store.
### Terminals
Every card terminal registered to this store, with serial, status, and last-active time. You can register new terminals here too.

### Products
The product catalogue used at this store -- for menu-based or retail use cases.
### Payment Links
Online and email-based payment links generated from this store's online configuration.

### ECR Setup
Connect an Electronic Cash Register (ECR / POS system) to the store's terminals. Configure the integration, terminal IDs, and connection method.
### Storefront
If the store has online capability, manage the published online storefront URL, logo, banner, and product visibility from here.
### Branding
Per-store overrides for colours, fonts, logos, and button shapes -- so each location can match its own visual identity.
### Settings
Per-store configuration: default tip percentages, receipt footer, opening hours, and tax settings.
### Campaigns
Run terminal-displayed campaigns scoped to this single store (see the **Campaigns** guide for details).
## Why Stores Matter
Stores let you:
- Separate sales, reports, and payouts per location
- Apply different branding, products, and pricing per store
- Restrict team-member access by store
- Roll up multi-store performance on the dashboard
## Verifying a Store
When a store is first created, certain countries or acquirer requirements need an additional **Verify** step (KYB-style). The portal walks you through this from the store's settings tab if needed.
### Terminals
Category: operations | Tags: Terminals, Hardware, Tap to Pay
URL: /merchant-portal/terminals
## Overview
The **Terminals** section is where you see every device that can accept a payment for your business -- card terminals, smartphones running Tap to Pay, online checkouts, and ECR-attached devices. Each terminal is tied to a store.

## Tabs
The page is split into three tabs:
- **Terminals** -- physical and software terminals (card terminals, Tap to Pay phones, online endpoints)
- **Printers** -- networked receipt printers
- **CheckoutX** -- soft-POS Tap to Pay terminals running Surfboard's CheckoutX app
Counters at the top show how many terminals are **active** vs **registered** in total.
## Terminal Table
Every row in the list shows:
| Column | What it shows |
|--------|---------------|
| **Status** | Active, Registered, or Inactive |
| **Name** | Friendly name (you can rename any terminal) |
| **Type** | SurfTouch Pro, SurfPad, SurfprintPro, CheckoutX (Tap to Pay), Online, etc. |
| **Terminal serial number** | Manufacturer hardware serial |
| **Store** | Which store the terminal belongs to |
| **Terminal ID** | Internal unique reference |
| **Registered** | When the terminal was first paired |
| **Last turned on** | Last activity heartbeat |
Click **Details »** on any row to open the terminal's settings.
## Registering a New Terminal

Click **+ Register new Terminal** in the top-right corner. You then choose:
- **Terminal type** -- SurfTouch Pro, SurfPad, SurfprintPro, Tap to Pay (CheckoutX), Online, or Custom
- **Store** -- which store the terminal belongs to
- **Pairing method** -- QR code, activation code, or serial number
For physical terminals, you scan a QR code on the device screen. For Tap to Pay phones, you install the CheckoutX app from the App Store / Play Store and pair using the activation code.
## Terminal Detail Page

Opening a terminal gives you:
- **Status & last seen** -- whether it's online and when it last connected
- **Software version** -- payment app build, with one-click update if newer is available
- **Configuration** -- tipping behaviour, receipts, language, currency, ECR connection
- **Recent transactions** -- the last sales taken on this device
- **Move terminal** -- reassign to a different store
- **Deactivate** -- retire the device

## Online Terminals
"Online" terminals represent your online checkout endpoints. There are two types:
- **Payment page** -- hosted Surfboard checkout pages opened via a link
- **Merchant initiated** -- server-to-server payments triggered by your application
These show up in the same list so you have a unified view of every payment endpoint.
## Tap to Pay (CheckoutX)
Tap to Pay terminals are smartphones (iPhone or Android) running the **CheckoutX** app. They show up alongside hardware terminals so you can manage your entire fleet -- physical and software -- in one place.
## Tips
- Use clear naming (e.g. *"Front register"*, *"Bar 1"*) to keep the list tidy
- The **Last turned on** column is the fastest way to spot a stale or disconnected terminal
- All software updates can be pushed remotely from this page -- no need to physically touch the device
### Products & Catalog
Category: operations | Tags: Products, Catalog, Menu, Storefront
URL: /merchant-portal/products
## Overview
The **Products** section is your central catalogue. Items here power your terminal product menus, your online storefront, and your payment links. Each catalogue lives under a specific store.

## Adding Products
Click **+ Create new Product** to add an item manually. You can set:
- **Name** and description
- **Price** and currency
- **Image** -- shown on terminal, storefront, and receipts
- **Category** -- groups items in menus
- **Tax rate** and SKU
- **Variants** -- sizes, colours, modifiers

### Import from CSV
For larger catalogues, click **Import CSV** in the dropdown. Download the template, fill in your products, and upload -- the portal validates the file and creates everything in bulk.

### Scan Menu with AI
Have a printed menu or PDF? Use **Scan Menu** to let the AI read it and create the products for you. Snap a photo (or upload a PDF) and the assistant extracts items, prices, and categories automatically. Review the suggestions, edit anything that needs fixing, and confirm to save.
## Product Table
The catalogue table shows every product with its image, name, price, category, and status. You can:
- **Search and filter** by name, category, or status
- **Edit inline** -- click a row to update price or visibility
- **Bulk-toggle** -- show or hide multiple products from the storefront at once
- **Delete** -- remove an item from the catalogue
## Storefront Sync
If your store has online capability, the **catalogLiveOnStorefront** badge at the top shows your live storefront URL with a green pulse. Products you mark as visible appear on the storefront the moment you save them.

## Empty State
If a store has no products yet, you'll see an empty state with a quick **+ Create new product** button. The fastest way to get started is to use **Scan Menu** or **Import CSV** -- both populate the catalogue in seconds.
### Payment Links
Category: operations | Tags: Payment Links, Online, Checkout
URL: /merchant-portal/payment-links
## Overview
**Payment links** let you take a card payment from anywhere -- without a terminal. You generate a link, share it with the customer, and they pay through Surfboard's hosted checkout page on their phone or laptop.

## When to Use Payment Links
- Phone orders or callbacks
- Email invoices to clients
- SMS / WhatsApp / chat-based sales
- Quotes and deposits
- QR codes printed on tables or posters
## Creating a Payment Link
Click **Create payment link** in the top-right. The form asks for:
- **Store** -- which location the payment is attributed to
- **Amount and currency** -- the total to charge
- **Description / order reference** -- shown to the customer at checkout
- **Optional expiry** -- automatically void the link after a set time
Hit **Create** and you get a shareable URL. Copy it, open the QR code, or send it directly to the customer.

## Status Filters
The top of the page has tabs to filter the list:
- **All** -- every link ever created
- **Pending** -- waiting for the customer to pay
- **Paid** -- completed successfully
- **Expired / Cancelled** -- links that timed out or were voided
## Order Table
Each row shows:
| Column | What it shows |
|--------|---------------|
| **ID** | The unique order reference |
| **Status** | Pending, Paid, Failed, Refunded, Cancelled |
| **Terminal** | The online terminal that hosted the checkout |
| **Amount** | Charged amount in the link's currency |
| **URL** | The shareable checkout link, with a copy button |
Click any row to open the full order detail -- including the customer's payment method, any refunds, and the receipt.
## Hosted Checkout
When the customer opens the link, they see Surfboard's hosted checkout page -- branded with your colours, logo, and font. Supported payment methods include cards (Visa, Mastercard, Amex), Apple Pay, Google Pay, Pay by Bank, BNPL (Klarna, Svea), and local methods (Vipps, Swish, MobilePay, Dankort) where available.

### Campaigns
Category: growth | Tags: Campaigns, Marketing, Promotions
URL: /merchant-portal/campaigns
## Overview
The **Campaigns** section turns your terminals into a marketing surface. Display a promotion, a referral message, a website redirect, or a seasonal offer on the customer-facing screen between transactions and at checkout.
## Active vs Deactivated
The page splits campaigns into:
- **Active** -- currently running, scheduled, or paused
- **Deactivated** -- archived campaigns you've turned off (toggle to view)
Each card shows the campaign name, status, schedule, the assigned stores, and the headline copy.
## Creating a Campaign
Click **+ Create campaign** and choose between two modes:
### AI Mode
Describe what you want -- *"Summer 20% off coffees, link to our menu"* -- and the assistant generates:
- A **headline** and **body** copy
- A matching **image**
- A suggested **redirect URL**
- Recommended **products** to attach (if relevant)
Review the suggestions, tweak anything, then save.
### Manual Mode
Build the campaign yourself by entering:
- **Name** -- internal label for the campaign
- **Type** -- Redirect link, Image, or Promo
- **Headline & body** -- short copy shown on the terminal
- **Image URL** -- visual asset
- **Redirect URL** -- where a tap takes the customer (web, app, social)
## Scheduling
Every campaign has a **From** and **Until** date. Use the date pickers to set the run window -- the campaign will go live and pause itself automatically.
## Targeting
Campaigns can be applied across:
- **All terminals**, or
- **Specific stores**, or
- **Specific terminals within a store**
Use the **Assignments** section to pick where the campaign should appear. You can also link campaigns to specific products in your catalogue, so the promo only shows when a matching item is on the basket.
## Why Use Campaigns?
- Promote new products and limited-time offers
- Drive traffic to your website, social, or loyalty programme
- Cross-sell at the moment of payment
- Run seasonal or local promotions without changing terminal hardware
## Tips
- Keep headlines short -- under 6 words works best on the terminal screen
- High-contrast images render best on small displays
- Use scheduling to plan months ahead -- the portal handles the activation for you
### Gift Cards
Category: growth | Tags: Gift Cards, Entitlements, Loyalty
URL: /merchant-portal/gift-cards
## Overview
The **Gift Cards** section lets you issue branded gift cards, track their balance, and see every transaction redeemed against them. Cards work across your stores and terminals using the same payment rails as cards.
## Gift Card Table
The main view lists every card you've created with:
- **Card ID / code**
- **Original value** -- the issued amount
- **Remaining balance**
- **Status** -- active, redeemed, expired, void
- **Created** date
- **Customer / recipient** (if attached)
Click any row to open the **Details** modal.
## Creating a Gift Card
Click **Create Gift Card** in the top-right. The modal asks for:
- **Amount** and currency (defaults to your merchant's primary currency)
- **Recipient details** -- optional name and email if the card is for a specific person
- **Message** -- optional personal message for the recipient
- **Expiry** -- optional expiration date
Hit **Create** and the system generates a unique card code and value entitlement. A success modal confirms the new card and lets you copy or share it.
## Card Detail View
Opening a card shows:
- The full **card code** and QR code for redemption
- **Balance history** -- the original value, every redemption, and any top-ups
- **Linked transactions** -- every sale that drew down the balance
- **Recipient info** if entered
## Redemption
Customers redeem gift cards at the terminal -- the cashier selects "Gift Card" as the payment method and scans the QR code or enters the code manually. The portal automatically:
1. Checks the balance
2. Deducts the redeemed amount
3. Updates the card's remaining value
4. Logs the transaction in the card's history
## Why Issue Gift Cards?
- Drive new customer acquisition (gifted cards bring in new buyers)
- Boost cash flow -- you collect the value upfront
- Increase basket size -- gift card holders typically spend more than the card's value
- Support refunds-as-credit -- issue a gift card instead of a cash refund
### Order Hardware
Category: configuration | Tags: Hardware, Terminals, Order
URL: /merchant-portal/order-hardware
## Overview
The **Order Hardware** section is the in-portal shop for new terminals, printers, and accessories. Browse the catalogue, add items to a cart, and check out -- the order is shipped to your registered business address and the cost is billed via your normal merchant invoice.
## Catalogue
The catalogue is a grid of available products with:
- **Product image** -- photo of the device
- **Name** -- e.g. *SurfTouch Pro*, *SurfPrint Pro-K*
- **Category** -- terminal, printer, accessory
- **Description** -- key specs and use case
- **Price** -- in your merchant's currency
- **Quantity selector** -- + / - buttons to add to cart
## Cart
A cart icon in the top-right shows the running total and item count. Click it to open the cart drawer where you can:
- Review every line item
- Adjust quantities or remove items
- See subtotal, taxes, and shipping
- Review the **Checkout** total
## Checkout
When you're ready, click the **Checkout** button. The form asks for:
- **Shipping address** -- defaults to your registered business address; editable per order
- **Billing details** -- pulled from your merchant profile
- **Optional notes** -- delivery instructions, contact person, etc.
Confirm the order and the system places it with our hardware fulfilment partner. You'll get an email with tracking once the parcel ships.
## Tracking and History
Past orders appear with their status (placed, processing, shipped, delivered) and tracking link where available. You can also re-order any past purchase with a single click.
## Tips
- Order more terminals than you immediately need -- having a spare on the shelf prevents downtime if a device fails
- Pair each new terminal with a SurfPrint receipt printer for a complete in-store kit
- For high-volume locations, consider SurfPad Pro for larger screens and integrated printer
- All hardware ships with the latest payment software pre-installed -- pair via QR code and you're live in minutes
### Settings
Category: configuration | Tags: Settings, Profile, Team, Notifications
URL: /merchant-portal/settings
## Overview
The **Settings** page is your account control centre -- everything from your personal profile to merchant-wide branding lives here. The page uses tabs to keep things organised.

## Profile
The first tab shows your user profile, with editable fields for:
- **First name** and **last name**
- **Email**
- **Phone number**
- **Preferred language** -- English, Svenska, Dansk, Suomi, Norsk
- **Profile photo**
Below your personal details, you'll see the **merchant** profile -- legal name, address, business registration number, contact email, and merchant logo. These details flow through to receipts, invoices, and the customer-facing checkout page.
## Team
The Team tab lets you invite colleagues to the portal:
- **Invite by email** -- they receive a sign-up link
- **User level** -- choose **Admin** or **User**. See [User Levels](/merchant-portal/user-levels) for the full breakdown of what each one can do.
- **Per-store access** -- limit users to specific stores
- **Active sessions** -- see who's logged in and revoke access if needed
## Branding
The Branding tab applies merchant-wide visual identity:
- **Brand colour, accent colour, background colour**
- **Font** -- sans, serif, or mono
- **Logo and icon**
- **Button shape** -- rounded, edgy, or pill
These settings power your branded checkout page, hosted payment links, terminal screens, and any white-labeled portal experience for your customers.
> Tip: per-store branding overrides live in **Stores → [store] → Branding**.
## Notifications
Set up alerts for key events:
- **Email** -- transaction notifications, settlement reports, application updates
- **Slack** -- daily summaries posted to a channel
- **Webhooks** -- push events to your own backend
Toggle each notification type on or off and choose recipients per type.
> **Important:** These are alerts about your own activity. Notices *from Surfboard* -- settlement changes, renewals, service updates -- are delivered on the **Messages** page, and email delivery for those is set up there, not here. See [Messages](/merchant-portal/messages) and subscribe a shared mailbox so nothing is missed.
## Billing & Payment Methods
Manage the cards on file used for paid add-ons (Loyalty, premium integrations, hardware orders). Add a card, set the default, or remove old ones.
## Security
- **Change password**
- **Two-factor authentication** -- enable 2FA via authenticator app
- **API keys** -- generate, rotate, and revoke keys used by your custom integrations
- **Audit log** -- a record of important changes made to the account
## Logout
A red **Logout** button in the top-right ends your session and returns you to the login page. Use it whenever you're stepping away from a shared computer.
## Why It Matters
Most of these settings are "set once and forget" -- but they shape every customer-facing surface (receipts, checkout, terminal screens) and every notification you receive. Spending 10 minutes here at setup pays dividends every day after.
### User Levels
Category: configuration | Tags: Users, Roles, Permissions, Team
URL: /merchant-portal/user-levels
## Overview
The Merchant Portal has two user levels: **Admin** and **User**. Both can be invited from **Settings → Team**, and each invitee picks up the permissions of their assigned level the first time they log in.
The split is designed so that day-to-day staff can run sales without being able to change the shape of the merchant -- and so that admins keep full control of stores, hardware, billing, and the team itself.
## Quick Comparison
| Area | User | Admin |
|------|:----:|:-----:|
| Issue refunds, void payments, send receipts | ✅ | ✅ |
| Create and send payment links | ✅ | ✅ |
| Create / update products, inventory, images, product AI | ✅ | ✅ |
| Subscribe to notifications | ✅ | ✅ |
| Capture, cancel, or initiate payments | ❌ | ✅ |
| Update orders | ❌ | ✅ |
| Create, update, or delete stores | ❌ | ✅ |
| Register, move, rename, link, or delete terminals; set tips | ❌ | ✅ |
| Update merchant details and branding | ❌ | ✅ |
| Publish / unpublish / redeploy storefront | ❌ | ✅ |
| Manage payment methods, promotions, campaigns, gift cards, loyalty, integrations, shipments | ❌ | ✅ |
| Delete products | ❌ | ✅ |
| Unsubscribe from notifications | ❌ | ✅ |
| Invite and edit team members | ❌ | ✅ |
## 👤 User (`USER`) -- day-to-day operations
A **User** can do self-service and product/sales work, but not merchant administration or destructive actions. This is the right level for cashiers, store staff, and people running the shop floor.
**Allowed:**
- **Sales** -- issue refunds, void a payment (including a same-day mistaken refund), email or print receipts
- **Payment links** -- create and send them
- **Products** -- create and update products, update inventory, upload images, use Surf AI for product copy and images
- **Notifications** -- subscribe to alerts
**Blocked:** everything in the Admin-only list below.
## 🛡️ Admin (`ADMIN`) -- full merchant management
An **Admin** can do everything a User can do, plus the operations that change the shape of the merchant or affect money movement.
**Additionally allowed:**
- **Stores** -- create, update, delete
- **Terminals** -- register, deactivate, delete, move between stores, rename, link / delink, set tip behaviour
- **Payments** -- capture, cancel, initiate
- **Orders** -- update
- **Merchant & storefront** -- update merchant details, manage and delete branding, publish / unpublish / redeploy storefront
- **Commerce features** -- manage payment methods, promotions, campaigns, gift cards, loyalty, integrations, shipments
- **Products** -- delete
- **Notifications** -- unsubscribe
- **Users** -- invite and edit team members (the Users tab)
## Choosing the Right Level
A good rule of thumb:
- If the person is **running the till, handling customers, or merchandising products**, give them **User**.
- If the person is **responsible for the business itself** -- opening a new store, ordering or moving terminals, changing how you charge, or managing the team -- give them **Admin**.
You can change a team member's level at any time from **Settings → Team**. Changes take effect the next time the user reloads the portal.
## Why It Matters
The two-level model is intentionally narrow: it keeps the portal simple while still protecting the operations that are either destructive (deleting a store) or sensitive (capturing or cancelling a payment, changing payout details, inviting new admins).
If you need finer-grained control -- for example, restricting a User to a specific store -- combine user level with **per-store access**, which is also configured from the Team tab.
---
## Changelog
Product updates, new features, and improvements across Surfboard Payments products.
URL: /docs/changelog
### Carbon Platform v2026.9
Product: Carbon Platform | Version: 2026.9 | Date: 2026-09-04
URL: /docs/changelog/carbon-platform-v2026-9
Summary: Create Merchant now returns a prefilled web KYB link. Pass the corporate ID and a business description and Surfboard resolves the registry data, classifies the merchant, and works out which documents it needs. The merchant adds a bank account, the documents, and the signatures.
## 🚀 Prefilled Merchant Applications
**Category:** Merchants API
`POST /partners/{partnerId}/merchants` still creates the application and returns the web KYB link, but the link now comes back already populated. Two things happen before it is returned:
- **Registry prefill.** The legal name, registered address, and where available the directors and beneficial owners are resolved from the national business registry for the merchant's country. `organisation.legalName` and `organisation.address` can be omitted
- **Business classification.** The free-text `preEnteredInformation.businessDescription` is classified into a merchant category (MCC), which decides which documents the merchant must supply and any category-specific questions. Describe what the merchant actually takes payment for, not the general company purpose: *"Selling coffee and pastries at our café"*, not *"Food and beverage services"*. If you already know the MCC, pass `organisation.mccCode` instead
The merchant then only adds what a partner cannot know for them: the bank account, the category documents, and the signing. If any part of the prefill cannot be resolved, the call still succeeds and the merchant fills that section in the flow as before. Prefill is additive; nothing you send is discarded and nothing you omit blocks the link.
## 👥 People and Signing
**Category:** Merchants API
`preEnteredInformation` can now carry the people involved, so the signing invitations go out without the merchant typing anyone in.
- `applicant`: the main contact, with `isSignatory`, `isUbo` and `isChairman` flags. A person can hold more than one role
- `signatories[]`, `ubos[]` and `chairpersons[]`: additional people, each with at least `name` and `email`
- Beneficial owners take `ownershipPercent` and `ownershipType` (`direct` or `indirect`). An indirect owner must name the intermediary company in `entityName`
- **Only signatories and beneficial owners receive a signing link.** An applicant or chairperson signs only if they also hold one of those roles
## 🧩 New Request Fields
**Category:** Merchants API
- `localeSelected`: language of the web KYB (`sv`, `da`, `fi`, `en`); defaults to the country's language
- `country` now accepts `SE`, `NO`, `DK`, `FI` and `IE`, with `corporateId` validated per country
- `controlFields.store.paymentChannels`: `{ physical, online, physicalSharePercent }`, where the share only matters when both channels are used
- `controlFields.disableFields.onlineInfo`: locks the webshop URLs against merchant edits; requires `store.onlineInfo` in the same request
- `controlFields.linkUsers`: existing user IDs to link to the new merchant
- `controlFields.directMerchantCreation`: creates the merchant directly instead of an application; only for programmes with a direct acquirer agreement
- `controlFields.acquirerConfig.acquirerIID` alongside `acquirerMID`
- `merchantConfig.settlementFrequency` values are now `daily`, `twiceWeekly`, `weekly`, `tenDays`, `fortNightly`, `monthly`, `everyTwoMonths`, `trimester`, `quarterly` and `twiceYearly`
- `preEnteredInformation.giftcards.revenueSharePercent` replaces `amountPerYear`
- `preEnteredInformation.fundsInfo.estimatedFrequencyOfTransactions` is one of `DAILY`, `WEEKLY`, `MONTHLY`, `YEARLY`
## 📨 Response and Status
**Category:** Merchants API
- Create Merchant returns `validUntil`, the expiry of the onboarding link, next to `applicationId`, `webKybUrl`, `shortLinkUrl`, `merchantId` and `storeId`
- `GET /partners/{partnerId}/merchants/{applicationId}/status` carries the current `webKybUrl` while the application is open, so a lost link is recovered from status rather than by creating a second application. Each Create Merchant call creates a new application
- Status `paymentMethods[]` entries now read `{ paymentMethod, enabledSchemes, status }`, and `domainVerification[]` lists the merchant's online-store verification records
## 📗 Documentation
**Category:** Documentation
- [Create Merchant](https://developers.surfboardpayments.com/api/merchants) and [Check Application Status](https://developers.surfboardpayments.com/api/merchants) references rewritten around the prefill flow, with the full café example. The OpenAPI document at `/openapi/carbon.json` and the MCP server carry the same fields
- [Merchant Onboarding](/developers/guides/merchant-onboarding) guide rewritten: the minimum request, the store, what to prefill and why, what the merchant still completes, and the status lifecycle
- The [Surfboard Flow](/flow) page now links to the [Service Providers & Split Payouts](/developers/guides/service-providers) guide
- The [developer guides](/developers/guides) index has a search palette: press ⌘K or Ctrl+K and type a topic, endpoint or keyword to jump to the right guide
### Developer Docs v2026.9
Product: Developer Docs | Version: 2026.9 | Date: 2026-09-04
URL: /docs/changelog/developer-docs-v2026-9
Summary: The hosted MCP server and the site's API reference catch up with the npm package. 237 endpoints across 29 sections, six new sections, five new webhook events, and one version number read from one place.
## 🔢 One Version, Read From One Place
**Category:** Developer Experience
The server card at `/.well-known/mcp` reported 1.3.0 after `@surfboardpayments/surf-mcp` 1.3.1 had shipped, and would have kept saying so after a redeploy: the endpoint carried the version as a string constant, while the sibling `server-card.json` asked the npm registry at build time. The two could disagree, and did.
- Both cards, and the MCP `initialize` response, now read the version from the installed npm package at build time. Bumping the dependency is the whole release step
- No registry lookup during a build, so a build is reproducible and cannot silently fall back to an old number
## 📚 Reference Synced With Upstream
**Category:** Documentation
The hosted MCP server, the developer search, the AI endpoints and the Carbon OpenAPI document all answer from the site's own copy of the reference, and that copy had stopped at 157 endpoints while the npm package moved on. The site is now synced with the same upstream documentation the package is built from.
- **237 endpoints across 29 sections**, up from 157 across 23
- Six new sections: **Electronic Cash Register (ECR) V2**, **Partners**, **RFID**, **Webhooks API**, **Transactions** and **Bin Ranges**
- Expanded sections: Terminals 21 to 33, Product Catalog 16 to 25, Merchants 12 to 19, Stores 8 to 12, Payments 5 to 10, Logistics 7 to 10, Receipts 5 to 8, Service Providers 5 to 8, Payment Methods 4 to 7
- **NFC Reading** removed, superseded upstream by the RFID API, along with ten orphaned pages the index no longer listed
- Every reference page regenerated from upstream: path parameter tables, consistent prerequisites, and fuller error sections
- The `/openapi/carbon.json` document grows from 148 to 236 operations over 179 paths, and still passes the reference test suite
- The Create Merchant and Check Application Status pages keep the prefill documentation added in [Carbon Platform v2026.9](/docs/changelog/carbon-platform-v2026-9), merged with upstream's richer status response tables
## 🔔 Five New Webhook Events
**Category:** Documentation
The webhook catalog gains two sections, Stores and Terminals, and the hosted MCP's `search_webhook_docs` finds them.
- `store.activated` and `store.onboarded`
- `terminal.hardware.registered`, `terminal.online.registered` and `terminal.softpos.registered`
### Carbon Platform v2026.8
Product: Carbon Platform | Version: 2026.8 | Date: 2026-08-31
URL: /docs/changelog/carbon-platform-v2026-8
Summary: Messages in both portals now come with email subscriptions. Add a shared mailbox on the Messages page and every Surfboard notice — settlements, integrations, maintenance — arrives as an email. Nothing is subscribed by default.
## 📬 Email Subscriptions for Messages
**Category:** Partner Portal, Merchant Portal
**Messages** is the inbox where Surfboard publishes notices to your account: settlement changes, integration and API updates, planned maintenance, renewals, service changes. Until now a notice only lived in the portal, so it waited for the next login. Both portals now carry an **Email notifications** card at the bottom of the Messages page, and every address on it receives each notice as a branded email the moment it is published.
- Add an address and click **Add**. It does not need to be a portal login — a shared mailbox such as `finance@` or `integrations@` is the point
- We send a confirmation link. The address shows as **Pending** and receives nothing until it is clicked, then flips to **Active** and picks up from the next notice
- **Resend** for a confirmation that never arrived, **Remove address** to stop
- Subscriptions belong to the partner or merchant account, not to the user who added them. Anyone with portal access can add or remove addresses, and several can be active at once
- **Nothing is subscribed by default.** A new account starts with an empty list. If you skip this step, the notices stay in the portal
The inbox itself is unchanged: notices listed newest first with a severity of *Info*, *Warning* or *Critical*, an unread badge on the sidebar item, and a banner across the top of the portal for critical notices until they are read.
## 🔔 Messages vs. Notifications
**Category:** Partner Portal, Merchant Portal
The two are easy to confuse, so the guides now draw the line. **Messages** are notices from Surfboard to you. **Notifications** under Settings are alerts about your own activity — merchant onboarding, terminal registration, file transfers, application status — delivered by email, Slack or SFTP. Set up both.
## 📗 New Guides
**Category:** Documentation
- [Partner Portal → Messages](/partner-portal/messages) — the inbox under **Support → Messages**, the subscription flow, and which shared mailboxes to use for settlement, integration and merchant-facing notices
- [Merchant Portal → Messages](/merchant-portal/messages) — the same walk-through for merchants, placed in Getting Started so it is read before the first notice lands
- The Settings and Dashboard guides in both portals now link across to Messages and explain that the Notifications they describe are about your own activity, not notices from us
### Developer Docs v2026.8.2
Product: Developer Docs | Version: 2026.8.2 | Date: 2026-08-24
URL: /docs/changelog/developer-docs-v2026-8-2
Summary: Two OpenAPI documents, a live MCP endpoint, agent instructions, and the reconciliation explainer for why a monthly report and its payouts never add up.
## 🤖 Machine-Readable Everything
**Category:** Developer Experience
Everything an agent needs was already documented; nothing said so in a format a machine reads. This publishes the missing half.
- **OpenAPI in two documents.** `/openapi.json` describes this domain's public surface — 32 operations, no credentials. `/openapi/carbon.json` describes the payments API — 148 operations over 107 paths, generated from the reference so it cannot drift from it. Both are served as YAML too, and both pass `redocly lint`
- The Carbon base URL stays a **server variable**: it is issued per account in the console, so there is no host to publish and an agent must not guess one
- **`/.well-known/mcp` is a live MCP server** over Streamable HTTP, stateless and unauthenticated, exposing the same five tools as the npm package over the same corpus
- **`/agent-instructions.md`** says when to reach for Surfboard Payments and when not to, in the order an integration actually happens
- **`/developers/mcp`** exists so the server and the specs can be found by name rather than by guessing a URL
## 🧯 Errors That Say What To Do
**Category:** Developer Experience
- Every JSON endpoint now fails in one shape — `{ error: { code, status, message, hint } }` — with a code to branch on and a hint that says what to do about it
- 404s answer in the caller's language: a browser gets the designed page, a fetcher or crawler gets markdown pointing at `llms.txt`, the sitemap and the spec, and anything under an API prefix gets JSON
## 📊 Reports and Payouts, Reconciled
**Category:** Documentation
Partners kept asking why a monthly report and the payouts inside that month never agree, and the answer kept being given one thread at a time.
- One rule: the **monthly report follows the transaction date**, the **payouts breakdown follows the payout date**, and payouts lag transactions by two to three days
- A three-row table showing a transaction on 30 April, one mid-month, and one on 31 May, and which month each lands in under both views
- Verified against settlement data before it was written down
## 🎨 Portal Guides
**Category:** Documentation
- The merchant and partner portal guides now use the same rail as the developer guides — a list against a hairline, with independent scroll, scrollspy, and scroll-into-view on long rails
### Developer Docs v2026.8
Product: Developer Docs | Version: 2026.8 | Date: 2026-08-14
URL: /docs/changelog/developer-docs-v2026-8
Summary: Eleven new guides — online payment links, tokens, terminal logistics, B2B invoices, ESC/POS printing and more — and a corrected order path that eighteen guides had wrong.
## 📗 Eleven New Guides
**Category:** Documentation
Every guide was diffed against the docs tree and the API section list. The gaps were not edge cases: a partner could not find out how to get a terminal shipped, how to search a transaction, or how to read the monthly report they are asked about every month.
- **Online payment link** — the guide the whole online path hangs off, starting where the work starts: an online store with verified domains, then a PaymentPage terminal, then the order
- **Customer identification** — the token that arrives by webhook on card tap, and the short window it gives you to price an order for a returning customer
- **Tokens** — the three enforcement flags, expiry, and re-authorisation
- **Terminal logistics** — ordering and returning terminals
- **Transactions and reports** — including what net sales, charges, payout, "from last period" and "unsettled from period" each mean
- **API conventions** — what holds across every endpoint, including the pagination nobody had written down
- **POS templates**, **AI merchandising**, **B2B invoices**, **ESC/POS printing**, and **device registration**
## 🧾 B2B Invoices
**Category:** Documentation
Invoicing was a row in the payment methods table and nothing else, so the one method a B2B buyer expects had no path to follow.
- Activation, the company and billing blocks a card payload does not carry, and the invoice terms
- What comes back: **bankgiro and OCR** for a domestic transfer, **IBAN and BIC** from abroad
- Crediting splits in two: an unpaid invoice is cancelled with one call and the credit is raised for you; only a paid one takes a return order. Asking for the wrong one earns `OR_0035`
- `bankgiro` and `ocr` added to the Initiate Payment reference — they arrive in every invoice response
## 🖨️ ESC/POS Printing
**Category:** Documentation
- Documents the **UTF-8 opt-in contract**: one field on the print request buys up-front validation and consistent output across current and legacy terminals
- Per-size line budgets, the supported command set, QR and image encoding, and the error codes
- A worked receipt whose base64 is generated by the code printed above it, with real Swedish characters
- A **preflight validator** that catches the three things that break integrations: an out-of-contract command, non-UTF-8 text, and an over-long line
## 🐛 Fixed — The Order Path
**Category:** Documentation
Eighteen guides documented `POST /merchants/:merchantId/orders`, which 404s. The merchant travels in the `MERCHANT-ID` header and the path is `POST /orders`.
- Four path shapes were wrong, not one: orders, order status, order tokens, and payments. All four corrected
- Merchant-scoped configuration endpoints — stores, terminals, tips, notifications, payment methods — are genuinely merchant-scoped and are untouched
- API conventions now states that the merchant is a **header**, not a path segment, and that the 404 says nothing about path shape
- `amount.total` is the **unit price**, not the line total. The parameter table said "Total line amount" and the calculation section below it already disagreed. Invisible until a cart has a quantity above one, so the new example carries two lines and a quantity of two
- Terminal registration removed from guides that told developers to register terminals they already have
### Developer Docs v2026.6
Product: Developer Docs | Version: 2026.6 | Date: 2026-06-05
URL: /docs/changelog/developer-docs-v2026-6
Summary: A Device Registration guide covering all four ways to register a terminal to a store, plus refund parameter and inter-app token corrections.
## 🔗 Device Registration Guide
**Category:** Documentation
Registering a terminal to a store is the step before anything else works, and it had no guide. There are four ways to do it, and which one you want depends on who is holding the terminal.
- **Rotating 6-digit code** (~90s TTL) entered in the merchant or partner portal
- **QR or registration link** scanned from the terminal's registration screen
- **Pre-shipped `registrationCode`** with longer validity, for near zero-touch setup
- **In-app (inter-app) registration** for SoftPOS and CheckoutX
## 💸 Refunds
**Category:** Documentation
- Documented `refundProcessingParams` in both the refund an order and partial refund guides
## 📲 Inter-App Integration
**Category:** Documentation
- Clarified that `interAppJWToken` is required on **both** Android and iOS, not Android alone
### Developer Docs v2026.4
Product: Developer Docs | Version: 2026.4 | Date: 2026-04-24
URL: /docs/changelog/developer-docs-v2026-4
Summary: A CheckoutX SoftPOS guide, a create-order error code reference, and the order and payment status flow written out end to end.
## 📲 CheckoutX SoftPOS Guide
**Category:** Documentation
- New guide covering SoftPOS acceptance with CheckoutX, from device requirements through to the first transaction
- Inter-app integration guide extended with the SoftPOS entry point
## ⚠️ Create Order Error Codes
**Category:** Documentation
Errors were the thing developers hit first and the thing least written down.
- New reference listing the error codes returned by **both** the create order and initiate payment flows
- What each code means, what causes it, and what to change
- Cross-linked from the create order guide at the point of first use
## 🔄 Order and Payment Status Flow
**Category:** Documentation
- Documented the progressive status flow: `INITIATED` → `PROCESSING` → `PROCESSED` → terminal state
- Clarified that `PARTIAL_PAYMENT_COMPLETED` is an **order-level** status, not a payment one
- Noted that `PAYMENT_CANCELLED` and `PAYMENT_FAILED` can follow `PAYMENT_INITIATED` directly
## 🐛 Fixed
- The console link in the webhooks guide pointed at the wrong domain instead of the developer portal
### Carbon Platform v2026.3.1
Product: Carbon Platform | Version: 2026.3.1 | Date: 2026-03-24
URL: /docs/changelog/carbon-platform-v2026-3-1
Summary: Gift Cards v1.0 with full lifecycle management, Promotions v1.0 with campaign rules and cross-channel tracking, and Split Payments v1.0 for multi-method transaction splitting on in-store terminals.
## 🎁 Gift Cards v1.0
**Category:** Additional Services
Issue, manage, and redeem gift cards through Carbon APIs. Supports full lifecycle management including creation, balance tracking, partial redemption, and expiry.
- Works across in-store terminals, online checkout, and the merchant portal
- Cross-location redemption supported under the same merchant
- **API:** `POST /v1/gift-cards`
## 🏷️ Promotions v1.0
**Category:** Additional Services
Create and manage promotional campaigns across terminals and online checkout.
- Supports percentage and fixed-amount discounts
- Time-limited campaigns with per-customer and total redemption limits
- Includes statistics and value-reporting endpoints for campaign performance tracking
- **API:** `POST /v1/promotions`
## 💳 Split Payments v1.0
**Category:** Payment Features
Accept split payments on in-store terminals. A single transaction can be divided across multiple payment methods or cards.
- Each partial payment is processed and settled individually
- Requires CheckoutX v5 or later
### Developer Docs v2026.3
Product: Developer Docs | Version: 2026.3 | Date: 2026-03-18
URL: /docs/changelog/developer-docs-v2026-3
Summary: Webhooks guide rewritten with signature verification and retry behaviour, notification subscriptions split into their own guide, and Tap to Pay on iPhone rebuilt with availability and supported devices.
## 🔔 Webhooks, Rewritten
**Category:** Documentation
The webhooks guide was a short page that assumed you already knew the shape of an event. It now documents the whole contract.
- Full **event type** list with payloads
- **Signature verification** samples in several languages
- **Retry logic** written down, including what a failed delivery does next
- **Duplicate handling** — why the same event can arrive twice, and how to be idempotent about it
- Console setup walked through end to end
## 📬 Notification Subscriptions
**Category:** Documentation
Subscriptions were buried inside the webhooks guide and are a different job, so they are a guide of their own now.
- **Email, Slack, and SFTP** channels documented separately
- Merchant and partner subscription endpoints cross-linked to the reference
## 📱 Tap to Pay on iPhone
**Category:** Documentation
- Guide migrated from a hand-built page to Markdown, so it is searchable and sits on the guides rail
- New sections on **availability**, **supported devices**, and integration best practices
- Added the disclaimer covering Apple's own entitlement and review requirements
## 🔄 Payment Lifecycle
**Category:** Documentation
- Corrected the description of when a payment status actually changes, which the create-order guide contradicted
### Carbon Online SDK v1.0
Product: Carbon Online SDK | Version: 1.0 | Date: 2026-03-17
URL: /docs/changelog/carbon-online-sdk-v1-0
Summary: Generally available embeddable JavaScript SDK for online payment acceptance with PCI-compliant payment forms, multi-basket checkout support, and domain-based authentication.
## 🌐 Carbon Online SDK v1.0
**Category:** Online Features
Generally available JavaScript SDK for accepting online payments in web applications.
- **PCI-compliant** embeddable payment form, no sensitive card data touches your servers
- **Multi-basket support** for complex checkout flows with multiple order groups
- **Domain-based authentication** for seamless, secure integration without API keys in the browser
- Minimal bundle size for fast page loads
### Developer Docs v2026.2
Product: Developer Docs | Version: 2026.2 | Date: 2026-02-25
URL: /docs/changelog/developer-docs-v2026-2
Summary: The full Carbon API reference indexed on surfboardpayments.com — 157 endpoints across every section — so developer search and the AI endpoints answer from the reference, not just the guides.
## 📚 Full API Reference Indexed
**Category:** Documentation
Every Carbon endpoint is now part of the site's documentation corpus. Until now a search here could find a guide, but never the endpoint that guide described.
- **157 endpoints** covering orders, payments, terminals, stores, tips, merchants, billing, branding, logistics, notifications, NFC reading, adjustments, admin functions, and the AI endpoints
- Each entry carries the path, method, auth, parameters, request body, and a worked response
- Grouped, machine-readable index at `api-index.json`
## 🔍 Developer Search
**Category:** Documentation
- Search now spans guides **and** the endpoint reference in one result list
- New `GET /api/index.json` and an expanded `GET /api/ai/docs.json` expose the same corpus to agents
- `llms.txt` updated to point at the reference
### CheckoutX v5.0.4 (Build 521)
Product: CheckoutX | Version: 5.0.4 | Build: 521 | Date: 2026-02-11
URL: /docs/changelog/checkoutx-v5-0-4
Summary: Optional physical keyboard support, enhanced inter-app communication protocols, and critical security integrity checks for production environments.
## ⌨️ New Features
- **Physical Keyboard Support:** Added optional support for hardware keypads on Android terminals, allowing users to enter amounts and operators (+, -, *, /) directly via physical keys.
- **Enhanced Inter-App Flow:**
- Standardized communication using the new InterAppResponse model for consistent transaction feedback to calling apps.
- Fixed a URI construction bug that caused data loss when redirect URLs contained fragments.
- Implemented mandatory failure callbacks to prevent users from being stuck on failure screens.
## 🛡️ Security & Integrity
- **Automated Device Auditing:** Introduced automated checks for **USB Debugging**, **Developer Mode**, and **Root/Jailbreak** status in production environments.
- **Security Alert Blocking:** Users are now blocked from initiating transactions via a SecurityAlertModal if security risks are detected on the device.
- **Graceful App Exit:** Refactored the Android exit process to automatically launch the Surfboard admin launcher with administrative access before terminating the app process.
## 🌍 UI & Localization
- **Terminal Setup Localization:** Fully localized terminal configuration error screens.
- **Dynamic UI Messaging:** Refactored payment status messages (e.g., "Authorizing," "Tap Card") to use localization keys, ensuring real-time language switching during the payment flow.
- **Selectable Terminal ID:** Users can now copy the Terminal ID directly from the home screen or settings menu.
- **Conditional Visibility:** The product/free-amount toggle button now hides automatically if the current store has no products available.
## 📊 Data & Stability
- **Reliable Product Sync:** Updated the product synchronization logic to use a "Clear + Reseed" strategy within a database transaction, ensuring backend deletions are accurately reflected in the app.
- **Faster Startup:** Improved app initialization to prevent freezing during startup on slower devices.
- **Printer Hardware Check:** The receipt screen now dynamically verifies if hardware printing is supported by the device before displaying the print icon.
- **Failure Screen Fix:** Resolved a race condition where manual retries and automatic redirection timers could conflict, causing duplicate callback executions.
### SurfTouch 4.9.6.0
Product: SurfTouch | Version: 4.9.6.0 | Date: 2025-06-06
URL: /docs/changelog/2025-06-06-surftouch-surftouch-4-9-6-0
Summary: Introduced a revamped Change Payment Method screen for a more intuitive user experience.
## 🆕 New
- Introduced a revamped Change Payment Method screen for a more intuitive user experience.
- Added a PIN verification screen to securely exit the app when lockToPos is enabled.
## 🐛 Fixed
- Included missing localizations to ensure complete multilingual support.
- Resolved an issue where the terminal would automatically log out upon reconnecting, if it was deregistered while offline.
### SurfTouch 4.9.5.0
Product: SurfTouch | Version: 4.9.5.0 | Date: 2025-05-29
URL: /docs/changelog/2025-05-29-surftouch-surftouch-4-9-5-0
Summary: Refreshed UI texts across the cash register for improved clarity.
## 🆕 New
- Refreshed UI texts across the cash register for improved clarity.
- Unified payment animation screen design across both standalone and attached modes for a consistent experience.
- Card brands are now displayed during card payment presentation for better transparency.
## 🔧 Updates
- Addressed localization issues in the "Add Product Category" section.
- Fixed the error message display on transaction failure screens when receipt printing is disabled.
### SurfTouch 4.9.4.0
Product: SurfTouch | Version: 4.9.4.0 | Date: 2025-05-26
URL: /docs/changelog/2025-05-26-surftouch-surftouch-4-9-4-0
Summary: You can now cancel payments made with Swish and Klarna using the new cancellation button.
## 🆕 New
- You can now cancel payments made with Swish and Klarna using the new cancellation button.
### SurfTouch 4.9.2.0
Product: SurfTouch | Version: 4.9.2.0 | Date: 2025-05-05
URL: /docs/changelog/2025-05-05-surftouch-surftouch-4-9-2-0
Summary: Refined transaction animations for a more seamless user experience.
## 🆕 New
- Refined transaction animations for a more seamless user experience.
- Optimized tap area detection for improved responsiveness on both SurfTouch and SurfPrint.
### Payment Failure and Payment Cancelled webhooks updated
Product: API | Version: All | Date: 2025-04-30
URL: /docs/changelog/2025-04-30-api-payment-failure-and-payment-cancelled-webhooks-updated
Summary: Enhanced webhook payload to include payment details for failed payments
## Updated Payment Failure webhook
### 🔧 Updates
- Enhanced webhook payload to include payment details for failed payments
## Updated Payment Cancelled webhook
### 🔧 Updates
- Enhanced webhook payload to include payment details for cancelled payments
### Added interAppJWT to payment API
Product: API | Version: Lithium | Date: 2025-03-07
URL: /docs/changelog/2025-03-07-api-added-interappjwt-to-payment-api
Summary: Added interAppJWT to the Initiate Payment API to improve inter-app transaction initiation performance
## 🔧 Updates
- Added interAppJWT to the [Initiate Payment API](https://developers.surfboardpayments.com/api/payments?lang=cURL#Initiate-a-Payment) to improve inter-app transaction initiation performance
### Transaction details in webhooks and status checks
Product: API | Version: All | Date: 2025-03-06
URL: /docs/changelog/2025-03-06-api-transaction-details-in-webhooks-and-status-checks
Summary: Added transaction details to the 'order.paymentcompleted' event data.
## Added transaction details to webhook
### 🔧 Updates
- Added transaction details to the '[order.paymentcompleted](https://developers.surfboardpayments.com/webhooks/orders#Order-Payment-Completed)' event data.
## Added transaction details to status check calls
### 🔧 Updates
- Added transaction details in the response of [Check Payment Status API](https://developers.surfboardpayments.com/api/order#Fetch-Order-Status) and [Fetch Order Status API](https://developers.surfboardpayments.com/api/payments#Check-Payment-Status).
- Note that the transaction details are only available when the status is 'PAYMENT_COMPLETED'.
## New Remove Branding API's
### 🆕 New
- Added new API's to [Remove Branding](https://developers.surfboardpayments.com/api/branding#Remove-Branding-for-Merchant) configuration at partner, merchant and store level.
### Online Terminal Events
Product: Webhooks | Version: All | Date: 2025-03-03
URL: /docs/changelog/2025-03-03-webhooks-online-terminal-events
Summary: Added new Online Terminal related events to the Order Terminal Event Webhook.
## 🆕 New
- Added new Online Terminal related events to the [Order Terminal Event](https://developers.surfboardpayments.com/webhooks/orders#Order-Terminal-Event) Webhook.
### Tap to Pay on iPhone
Product: CheckoutX | Version: 1.1.0 | Date: 2025-02-18
URL: /docs/changelog/2025-02-18-checkoutx-tap-to-pay-on-iphone
Summary: Tap to Pay on iPhone accepts physical debit and credit cards, as well as Apple Pay and other digital wallets. No extra hardware needed. It’s easy, private, secure.
## 🆕 New
- Tap to Pay on iPhone accepts physical debit and credit cards, as well as Apple Pay and other digital wallets. No extra hardware needed. It’s easy, private, secure.
### New Billing API's
Product: API | Version: Lithium | Date: 2025-02-17
URL: /docs/changelog/2025-02-17-api-new-billing-api-s
Summary: Added new API's to create, update and fetch Charges for Merchants.
## 🆕 New
- Added new API's to create, update and fetch [Charges](https://developers.surfboardpayments.com/api/billing) for Merchants.
### Added new NFC reading API's
Product: API | Version: Lithium | Date: 2025-01-05
URL: /docs/changelog/2025-01-05-api-added-new-nfc-reading-api-s
Summary: Added new *NFC Reading APIs* that allow you to scan and read NFC tags associated with an order.
## 🆕 New
- Added new *[NFC Reading APIs](https://developers.surfboardpayments.com/api/nfc-reading)* that allow you to scan and read NFC tags associated with an order.
### Console Module Added
Product: Surfboard for Developers | Version: 1.0.0.0 | Date: 2025-01-02
URL: /docs/changelog/2025-01-02-surfboard-for-developers-console-module-added
Summary: Added Console module to the Documentation site, this will allow you to create api keys and configure webhooks by yourself. To get access to console please signUp and fill up the developer…
## 🆕 New
- Added [Console](https://developers.surfboardpayments.com/console/dashboard) module to the Documentation site, this will allow you to create api keys and configure webhooks by yourself. To get access to console please signUp and fill up the developer onboarding form.
### New Gift Cards API
Product: API | Version: Carbon | Date: 2024-11-17
URL: /docs/changelog/2024-11-17-api-new-gift-cards-api
Summary: Added Create Gift Card API for creating gift cards with monetary value (FUND) or usage-based limits (ENTITLEMENT).
## 🆕 New
- Added **[Create Gift Card API](https://developers.surfboardpayments.com/api/gift-cards#Create-Gift-Card)** for creating gift cards with monetary value (FUND) or usage-based limits (ENTITLEMENT).
- Added **[Get All Gift Cards API](https://developers.surfboardpayments.com/api/gift-cards#Get-All-Gift-Cards)** for retrieving paginated list of gift cards with filtering by type and status.
- Added **[Get Gift Card Details API](https://developers.surfboardpayments.com/api/gift-cards#Get-Gift-Card-Details)** for retrieving detailed gift card information including customer details and transaction history.
- Added **[Get Gift Card Transactions API](https://developers.surfboardpayments.com/api/gift-cards#Get-Gift-Card-Transactions)** for retrieving paginated transaction history with filtering by transaction type (ISSUED, CREDIT, DEBIT).
### Update in Merchant APIs
Product: API | Version: Lithium | Date: 2024-11-08
URL: /docs/changelog/2024-11-08-api-update-in-merchant-apis
Summary: Added Fetch Transaction Analytics in Merchants APIs.
## 🆕 New
- Added **[Fetch Transaction Analytics](https://developers.surfboardpayments.com/api/merchants#Fetch-Transaction-Analytics)** in Merchants APIs.
### Payment Methods API V2 Update
Product: API | Version: Carbon | Date: 2024-10-17
URL: /docs/changelog/2024-10-17-api-payment-methods-api-v2-update
Summary: Updated Activate Payment Method API to support unified payment method registration in a single request.
## 🔧 Updates
- Updated **[Activate Payment Method API](https://developers.surfboardpayments.com/api/payment-methods#Activate-Payment-Method)** to support unified payment method registration in a single request.
### Product Catalogue, Service Providers, and Terminals APIs
Product: API | Version: Carbon | Date: 2024-10-10
URL: /docs/changelog/2024-10-10-api-product-catalogue-service-providers-and-terminals-apis
Summary: Added comprehensive Product Catalogue API with 17 endpoints for complete product catalog management including AI-powered features.
## Product Catalogue API Updates
### 🆕 New
- Added comprehensive **[Product Catalogue API](https://developers.surfboardpayments.com/api/product-catalogue)** with 17 endpoints for complete product catalog management including AI-powered features.
- Added **[Create Product Catalog API](https://developers.surfboardpayments.com/api/product-catalogue#Create-Product-Catalog)** for creating product catalogs under stores.
- Added **[Fetch Product Catalogs API](https://developers.surfboardpayments.com/api/product-catalogue#Fetch-Product-Catalogs)** for retrieving all catalogs under a store.
- Added **[Create Product API](https://developers.surfboardpayments.com/api/product-catalogue#Create-Product)**, **[Update Product API](https://developers.surfboardpayments.com/api/product-catalogue#Update-Product)**, and **[Fetch Product by Id API](https://developers.surfboardpayments.com/api/product-catalogue#Fetch-Product-by-Id)** for complete product management.
- Added **[Add Variant For Product API](https://developers.surfboardpayments.com/api/product-catalogue#Add-Variant-For-Product)**, **[Update Variant API](https://developers.surfboardpayments.com/api/product-catalogue#Update-Variant)**, and **[Update Variant Inventory API](https://developers.surfboardpayments.com/api/product-catalogue#Update-Variant-Inventory)** for product variant management.
- Added **[Generate Product Description API](https://developers.surfboardpayments.com/api/product-catalogue#Generate-Product-Description)** for AI-powered product description generation based on product name and language preferences.
- Added **[Generate Product Images API](https://developers.surfboardpayments.com/api/product-catalogue#Generate-Product-Images)** for AI-powered product image generation with custom prompts and specifications.
- Added **[Generate Product Catalog from Images API](https://developers.surfboardpayments.com/api/product-catalogue#Generate-Product-Catalog-from-Images)** for extracting structured product information from menu images or catalog photos using AI.
## Service Providers API
### 🆕 New
- Added **[Create Service Provider API](https://developers.surfboardpayments.com/api/service-providers#Create-Service-Provider)** for creating new service provider applications with company information.
- Added **[Fetch Service Provider for Partner API](https://developers.surfboardpayments.com/api/service-providers#Fetch-Service-Provider-for-Partner)** and **[Fetch Service Provider for Merchant API](https://developers.surfboardpayments.com/api/service-providers#Fetch-Service-Provider-for-Merchant)** for retrieving service provider information.
- Added **[Get All Service Provider Applications API](https://developers.surfboardpayments.com/api/service-providers#Get-All-Service-Provider-Applications)** with filtering support for ONBOARDING and RENEWAL application types.
- Added **[Fetch Service Provider Application Status API](https://developers.surfboardpayments.com/api/service-providers#Fetch-Service-Provider-Application-Status)** for retrieving application status and onboarding progress.
## Terminals API Update
### 🆕 New
- Added **[Get Device Registration Code API](https://developers.surfboardpayments.com/api/terminals#Get-Device-Registration-Code)** for generating registration codes and deep links for device onboarding.
### Admin Functions and Notifications APIs
Product: API | Version: Carbon | Date: 2024-10-07
URL: /docs/changelog/2024-10-07-api-admin-functions-and-notifications-apis
Summary: Added Create Merchant Account API for creating new merchant accounts with email and role assignment.
## New Admin Functions API
### 🆕 New
- Added **[Create Merchant Account API](https://developers.surfboardpayments.com/api/admin-functions#Create-Merchant-Account)** for creating new merchant accounts with email and role assignment.
- Added **[Create Partner Account API](https://developers.surfboardpayments.com/api/admin-functions#Create-Partner-Account)** for creating new partner accounts with email and role assignment.
## New Notifications API
### 🆕 New
- Added **[Subscribe to Merchant Reports API](https://developers.surfboardpayments.com/api/notifications#Subscribe-to-Merchant-Reports)** for configuring automated merchant event reports via SFTP.
- Added **[Subscribe to Partner Reports API](https://developers.surfboardpayments.com/api/notifications#Subscribe-to-Partner-Reports)** for configuring automated partner event reports via SFTP.
### Activate Payment Method
Product: API | Version: Carbon | Date: 2024-09-30
URL: /docs/changelog/2024-09-30-api-activate-payment-method
Summary: New Activate Payment Method API to activate available payment methods (AMEX, SWISH, KLARNA, VIPPS, MOBILEPAY, ACCTOACC) for a merchant.
## 🆕 New
- New [Activate Payment Method API](https://developers.surfboardpayments.com/api/payment-methods#Activate-Payment-Method) to activate available payment methods (AMEX, SWISH, KLARNA, VIPPS, MOBILEPAY, ACCTOACC) for a merchant.
### New AI API
Product: API | Version: Carbon | Date: 2024-09-29
URL: /docs/changelog/2024-09-29-api-new-ai-api
Summary: Added Generate Branding Options API to create AI-powered branding themes and color schemes based on website URLs.
## 🆕 New
- Added **[Generate Branding Options API](https://developers.surfboardpayments.com/api/ai#Generate-Branding-Options)** to create AI-powered branding themes and color schemes based on website URLs.
### Update in Stores API
Product: API | Version: Lithium | Date: 2024-07-12
URL: /docs/changelog/2024-07-12-api-update-in-stores-api
Summary: Added Verify Store Domain , Fetch Store Domains in Stores APIs.
## 🔧 Updates
- Added **[Verify Store Domain](https://developers.surfboardpayments.com/api/stores#Verify-Store-Domain)** , **[Fetch Store Domains](https://developers.surfboardpayments.com/api/stores#Fetch-Store-Domains)** in Stores APIs.
### Update in Orders API
Product: API | Version: Lithium | Date: 2024-07-08
URL: /docs/changelog/2024-07-08-api-update-in-orders-api
Summary: Added Fetch Online Orders in Order APIs.
## 🔧 Updates
- Added **[Fetch Online Orders](https://developers.surfboardpayments.com/api/orders#Fetch-Online-Orders)** in Order APIs.
### Update in Orders API
Product: API | Version: Lithium | Date: 2024-06-14
URL: /docs/changelog/2024-06-14-api-update-in-orders-api
Summary: Added new includeAdjustments , delayCapture , enforceTokeization , enforce3DSecure , lockToPaymentMethods , callBackUrl , throwErrorIfTerminalInactiveFor , paymentPageValidFor , authMode params…
## 🔧 Updates
- Added new `includeAdjustments` , `delayCapture` , `enforceTokeization` , `enforce3DSecure` , `lockToPaymentMethods` , ` callBackUrl` , ` throwErrorIfTerminalInactiveFor` , `paymentPageValidFor` , `authMode` params under **[Create New Order](https://developers.surfboardpayments.com/api/orders#Create-New-Order)** and **[Update Order](https://developers.surfboardpayments.com/api/orders#Update-Order)** APIs
### Update in Orders API
Product: API | Version: Lithium | Date: 2024-06-07
URL: /docs/changelog/2024-06-07-api-update-in-orders-api
Summary: Added new unit , generateShortLink , delayPayout , payButtonType params under Create New Order and Update Order APIs
## 🔧 Updates
- Added new `unit` , `generateShortLink` , `delayPayout` , ` payButtonType` params under **[Create New Order](https://developers.surfboardpayments.com/api/orders#Create-New-Order)** and **[Update Order](https://developers.surfboardpayments.com/api/orders#Update-Order)** APIs
### New API in Terminal APIs
Product: API | Version: Lithium | Date: 2024-05-06
URL: /docs/changelog/2024-05-06-api-new-api-in-terminal-apis
Summary: Added Get Interapp Details in Terminal APIs.
## 🔧 Updates
- Added **[Get Interapp Details](https://developers.surfboardpayments.com/api/terminals#Get-Interapp-Details)** in Terminal APIs.
### Update in Transaction Search API
Product: API | Version: Lithium | Date: 2024-04-29
URL: /docs/changelog/2024-04-29-api-update-in-transaction-search-api
Summary: Included additional details about the transaction in Transaction Search API .
## 🔧 Updates
- Included additional details about the transaction in **[Transaction Search API](https://developers.surfboardpayments.com/api/transactions#Transaction-Search)** .
### New terminal type and a new Payments API
Product: API | Version: Lithium | Date: 2024-04-25
URL: /docs/changelog/2024-04-25-api-new-terminal-type-and-a-new-payments-api
Summary: Introduced new terminal type softpos under Fetch All Merchant Terminals , Fetch All Store Terminals , Fetch Terminal by ID.
## New terminal type introduced
### 🆕 New
- Introduced new terminal type softpos under **[Fetch All Merchant Terminals](https://developers.surfboardpayments.com/api/merchants#Fetch-All-Merchant-Terminals)** , **[Fetch All Store Terminals](https://developers.surfboardpayments.com/api/stores#Fetch-All-Store-Terminals)** , **[Fetch Terminal by ID](https://developers.surfboardpayments.com/api/terminals#Fetch-Terminal-by-ID)**.
## New API in Payments APIs
### 🆕 New
- Added new **[Capture Payment](https://developers.surfboardpayments.com/api/payments#Capture-Payment)** , **[Check Capture Status](https://developers.surfboardpayments.com/api/payments#Check-Capture-Status)** , **[Complete Payment](https://developers.surfboardpayments.com/api/payments#Complete-Payment)** in Payment APIs.
### New API in Transactions and Terminals API
Product: API | Version: Lithium | Date: 2024-04-16
URL: /docs/changelog/2024-04-16-api-new-api-in-transactions-and-terminals-api
Summary: Added Transaction Search in Transactions API.
## 🆕 New
- Added **[Transaction Search](https://developers.surfboardpayments.com/api/transactions#Transaction-Search)** in **Transactions API**.
- Added **[Get Interapp Code](https://developers.surfboardpayments.com/api/terminals#Get-Interapp-Code)** in **Terminals API**.
### Fetch All Transactions parameters and an Orders API update
Product: API | Version: Lithium | Date: 2024-04-05
URL: /docs/changelog/2024-04-05-api-fetch-all-transactions-parameters-and-an-orders-api-update
Summary: Added referenceId , truncatedPan , cardLabel , posEntryMode , issuerApplication , terminalVerificationCode , aid , customerResponseCode , cvmMethod , authMode , cardBrand response parameters to…
## New parameters added in Fetch all Transactions
### 🆕 New
- Added `referenceId` , `truncatedPan` , `cardLabel` , `posEntryMode` , `issuerApplication` , `terminalVerificationCode` , `aid` , `customerResponseCode` , `cvmMethod` , `authMode` , `cardBrand` response parameters to **[Fetch All Transactions](https://developers.surfboardpayments.com/api/transactions#Fetch-All-Transactions)**.
## Update in Orders API
### 🔧 Updates
- Added new `orderLineLevelCalculation` param under **[Create New Order](https://developers.surfboardpayments.com/api/orders#Create-New-Order)** and **[Update Order](https://developers.surfboardpayments.com/api/orders#Update-Order)** APIs
- This boolean value when set to true, enables support for row level campaign and shipping calculations.
### New Printing API
Product: API | Version: Lithium | Date: 2024-03-19
URL: /docs/changelog/2024-03-19-api-new-printing-api
Summary: New Printing API added under Transaction API's.
## 🆕 New
- New **[Printing API](https://developers.surfboardpayments.com/api/transactions#Print-Receipt)** added under Transaction API's.
### New Branding API's
Product: API | Version: Lithium | Date: 2024-03-12
URL: /docs/changelog/2024-03-12-api-new-branding-api-s
Summary: New Branding API's added to customize all customer facing pages provided by Surfboard.
## 🆕 New
- New **[Branding API's](https://developers.surfboardpayments.com/api/branding)** added to customize all customer facing pages provided by Surfboard.
### New API in Merchant API's
Product: API | Version: Lithium | Date: 2024-03-08
URL: /docs/changelog/2024-03-08-api-new-api-in-merchant-api-s
Summary: New Activate Terminal API for merchant added under Merchant API's.
## 🆕 New
- New **[Activate Terminal](https://developers.surfboardpayments.com/api/merchants#Activate-Terminal)** API for merchant added under Merchant API's.
### SurfPad 4.0.15.0
Product: SurfPad | Version: 4.0.15.0 | Date: 2024-03-08
URL: /docs/changelog/2024-03-08-surfpad-surfpad-4-0-15-0
Summary: Added support for operator selection on the terminal.
## 🆕 New
- Added support for operator selection on the terminal.
## 🔧 Updates
- Ensures transaction goes to success/failure on certain Network disconnect edge cases.
- Wifi Symbol is shown even if signal strength is very weak.
### SurfTouch 2.8.7.0
Product: SurfTouch | Version: 2.8.7.0 | Date: 2024-03-08
URL: /docs/changelog/2024-03-08-surftouch-surftouch-2-8-7-0
Summary: Added the functionality to change tips config using APIs.
## 🆕 New
- Added the functionality to change tips config using APIs.
## 🔧 Updates
- Removed cancelling of transaction in the tips screen which lead to a race condition, instead provided cancelling of the tip.
## 🐛 Fixed
- Code optimization to ensure data flow and data sync with backend.
- Other bug fixes and improvements.
### New parameters in Transaction, Stores, and Orders APIs
Product: API | Version: Lithium | Date: 2024-03-06
URL: /docs/changelog/2024-03-06-api-new-parameters-in-transaction-stores-and-orders-apis
Summary: Added issuerCountry , interchangeDomain , cardCategory , cardUsage to response parameters of the Fetch One Transaction , Fetch All Transactions , Fetch Transactions by ID.
## New Parameters added in Transaction APIs
### 🔧 Updates
- Added `issuerCountry` , `interchangeDomain` , `cardCategory` , `cardUsage` to response parameters of the **[Fetch One Transaction](https://developers.surfboardpayments.com/api/transactions#Fetch-One-Transaction)** , **[Fetch All Transactions](https://developers.surfboardpayments.com/api/transactions#Fetch-All-Transactions)** , **[Fetch Transactions by ID](https://developers.surfboardpayments.com/api/transactions#Fetch-Transactions-by-ID)**.
## New Parameters added in Stores APIs
### 🔧 Updates
- New response parameters added under **[Create Store](https://developers.surfboardpayments.com/api/stores#Create-Store)** , **[Update Store Details](https://developers.surfboardpayments.com/api/stores#Update-Store-Details)** , **[Fetch Store Details](https://developers.surfboardpayments.com/api/stores#Fetch-Store-Details)** , **[Fetch Stores](https://developers.surfboardpayments.com/api/stores#Fetch-Stores)** to setup online payments.
- `merchantWebShopUrl` is the web-shop URL of the merchant.
- `paymentPageHostURL` is the URL of the payment page.
- `termsAndConditionsURL` is the URL of the T&C of the merchant’s web-shop, it has to contain the refund policy.
- `privacyPolicyURL` is the URL of the privacy policy of the merchant.
## New Parameters added in Order APIs
### 🔧 Updates
- Added `includeAdjustments` , `delayCapture` , `enforceTokenization` , `enforce3DSecure` , `lockToPaymentMethod` , `callBackUrl` , `throwErrorIfTerminalInactiveFor` , `paymentPageValidFor` response parameters to **[Create New Order](https://developers.surfboardpayments.com/api/orders#Create-New-Order)** and **[Update Order](https://developers.surfboardpayments.com/api/orders#Update-Order)**.
### New parameters added in Fetch Terminal by ID
Product: API | Version: Lithium | Date: 2024-02-23
URL: /docs/changelog/2024-02-23-api-new-parameters-added-in-fetch-terminal-by-id
Summary: Added lastAliveAt , isCharging , batteryPercentage , powerSource , deviceNetwork , turnOnTime to response parameters of the Fetch Terminal by ID.
## 🔧 Updates
- Added `lastAliveAt` , `isCharging` , `batteryPercentage` , `powerSource` , `deviceNetwork` , `turnOnTime` to response parameters of the **[Fetch Terminal by ID](https://developers.surfboardpayments.com/api/terminals#Fetch-Terminal-by-ID)**.
### Adjustments, Tips, and a new Orders API
Product: API | Version: Lithium | Date: 2024-02-19
URL: /docs/changelog/2024-02-19-api-adjustments-tips-and-a-new-orders-api
Summary: Added new Adjustments API and Tips API.
## New Adjustments and Tips API's
### 🆕 New
- Added new **[Adjustments API](https://developers.surfboardpayments.com/api/adjustments)** and **[Tips API](https://developers.surfboardpayments.com/api/tips)**.
- Adjustments API takes care of handling additional amounts to orders like tips, surcharges, insurance payments etc included while performing a transaction.
- Tips APIs enable merchants to integrate tipping functionality directly into the payment terminals.
## New API added under Orders API
### 🆕 New
- Added **[Fetch Order Adjustments](https://developers.surfboardpayments.com/api/orders#Fetch-Order-Adjustments)** in **[Orders API](https://developers.surfboardpayments.com/api/orders)**.
- Fetch all adjustments created under an order.
### Response parameter updates in Transaction and Payment API's
Product: API | Version: Lithium | Date: 2024-02-12
URL: /docs/changelog/2024-02-12-api-response-parameter-updates-in-transaction-and-payment-api-s
Summary: Added voided flag to response parameters of the Fetch Transactions by ID API, Fetch All Transactions API, Fetch One Transaction API and Get Payment by ID API.
## 🔧 Updates
- Added `voided` flag to response parameters of the **[Fetch Transactions by ID API](https://developers.surfboardpayments.com/api/transactions#Fetch-Transactions-by-ID)**, **[Fetch All Transactions API](https://developers.surfboardpayments.com/api/transactions#Fetch-All-Transactions)**, **[Fetch One Transaction API](https://developers.surfboardpayments.com/api/transactions#Fetch-One-Transactions)** and **[Get Payment by ID API](https://developers.surfboardpayments.com/api/payments#Get-Payment-by-ID)**.
- The voided flag denotes if the transaction has been voided or not.
### New Terminal and Transaction APIs
Product: API | Version: Lithium | Date: 2024-01-30
URL: /docs/changelog/2024-01-30-api-new-terminal-and-transaction-apis
Summary: Added new Fetch APN List API to Terminal APIs.
## New API in Terminal APIs
### 🆕 New
- Added new **[Fetch APN List API](https://developers.surfboardpayments.com/api/terminals#Fetch-APN-List)** to **Terminal APIs**.
- You can use this API to fetch APN list for an active terminal.
## New API in Transaction APIs
### 🆕 New
- Added new **[Email Receipt API](https://developers.surfboardpayments.com/api/transactions#Email-Receipt)** to **Transaction APIs**.
- You can use this API to email receipts to users.
### New API in Orders APIs
Product: API | Version: Lithium | Date: 2024-01-09
URL: /docs/changelog/2024-01-09-api-new-api-in-orders-apis
Summary: Added new Add Receipt Information API to Orders APIs.
## 🆕 New
- Added new **[Add Receipt Information API](https://developers.surfboardpayments.com/api/orders#Add-Receipt-Information)** to **Orders APIs**.
- You can use this API to store cash register details for receipts.
### Response parameter update in Transaction API
Product: API | Version: Lithium | Date: 2024-01-08
URL: /docs/changelog/2024-01-08-api-response-parameter-update-in-transaction-api
Summary: Added paymentId field to the response parameters of the Fetch All Transactions API
## 🔧 Updates
- Added `paymentId` field to the response parameters of the **[Fetch All Transactions API](https://developers.surfboardpayments.com/api/transactions#Fetch-All-Transactions)**
- This field returns the unique payment ID of the transaction.
### Transaction response fields and a new Merchants API
Product: API | Version: Lithium | Date: 2023-12-23
URL: /docs/changelog/2023-12-23-api-transaction-response-fields-and-a-new-merchants-api
Summary: Added paymentId and rrn field to the response parameters of the Fetch One Transaction API and Fetch Transactions by ID API.
## Response parameter updates in Transaction APIs
### 🔧 Updates
- Added `paymentId` and `rrn` field to the response parameters of the **[Fetch One Transaction API](https://developers.surfboardpayments.com/api/transactions#Fetch-One-Transaction)** and **[Fetch Transactions by ID API](https://developers.surfboardpayments.com/api/transactions#Fetch-Transactions-by-ID)**.
- These fields return the unique payment ID of the transaction and the Retrieval Reference Number(RRN) of the transaction respectively.
## New API in Merchants APIs
### 🆕 New
- Added new **[Fetch All Merchant Contracts API](https://developers.surfboardpayments.com/api/merchants#Fetch-All-Merchant-Contracts)** to **Merchants APIs**.
- You can use this API to retrieve a list of all contracts associated with the merchant.
### Response parameter updates in Merchants, Stores, and Transactions APIs
Product: API | Version: Lithium | Date: 2023-12-12
URL: /docs/changelog/2023-12-12-api-response-parameter-updates-in-merchants-stores-and-transactions-apis
Summary: Added terminalType and osType fields to the response parameters of the Fetch All Merchant Terminals API and Fetch All Store Terminals API.
## 🔧 Updates
- Added `terminalType` and `osType` fields to the response parameters of the **[Fetch All Merchant Terminals API](https://developers.surfboardpayments.com/api/merchants#Fetch-All-Merchant-Terminals)** and **[Fetch All Store Terminals API](https://developers.surfboardpayments.com/api/stores#Fetch-All-Store-Terminals)**.
- The `terminalType` field returns the type of the terminal.
- The `osType` field returns the type of operating system running on the terminal.
- Added `storeId` field to the response parameters of the **[Fetch All Transactions API](https://developers.surfboardpayments.com/api/transactions#Fetch-All-Transactions)**.
### New API in Merchants APIs
Product: API | Version: Lithium | Date: 2023-11-09
URL: /docs/changelog/2023-11-09-api-new-api-in-merchants-apis
Summary: Added new Fetch All Multi-Merchant Groups API to Merchants APIs.
## 🆕 New
- Added new **[Fetch All Multi-Merchant Groups API](https://developers.surfboardpayments.com/api/transactions#Fetch-All-Multi-Merchant-Groups)** to **[Merchants APIs](https://developers.surfboardpayments.com/api/merchants)**.
- You can use this API to retrieve a list of all multi-merchant groups associated with a specific partner.
### Response parameter updates in Transaction APIs
Product: API | Version: Lithium | Date: 2023-11-08
URL: /docs/changelog/2023-11-08-api-response-parameter-updates-in-transaction-apis
Summary: Added cardBrand field to the response parameters of the Fetch One Transaction API and Fetch Transactions by ID API.
## 🔧 Updates
- Added `cardBrand` field to the response parameters of the **[Fetch One Transaction API](https://developers.surfboardpayments.com/api/transactions#Fetch-One-Transaction)** and **[Fetch Transactions by ID API](https://developers.surfboardpayments.com/api/transactions#Fetch-Transactions-by-ID)**.
- This field returns the card brand used for the transaction such as Visa, Mastercard, or Amex.
### Support for querying in Transaction APIs
Product: API | Version: Lithium | Date: 2023-10-22
URL: /docs/changelog/2023-10-22-api-support-for-querying-in-transaction-apis
Summary: You can now use queries in the Fetch All Transactions API to retrieve all transaction within a particular date range.
## 🆕 New
- You can now use queries in the **[Fetch All Transactions API](https://developers.surfboardpayments.com/api/transactions#Fetch-All-Transactions)** to retrieve all transaction within a particular date range.
- Specify the start and end dates in the `startDate` and `endDate` parameters to retrieve all transactions within that date range.
### Response parameter updates in Fetch All Transactions API
Product: API | Version: Lithium | Date: 2023-09-20
URL: /docs/changelog/2023-09-20-api-response-parameter-updates-in-fetch-all-transactions-api
Summary: Added settlement data to Fetch All Transactions API.
## 🔧 Updates
- Added settlement data to **[Fetch All Transactions API](https://developers.surfboardpayments.com/api/transactions#Fetch-All-Transactions)**.
- Settlement data includes, settlement status ( `settlementStatus` ), payout ( `payout` ), and fee ( `fee` ).
### New terminal configurations
Product: API | Version: Lithium | Date: 2023-09-17
URL: /docs/changelog/2023-09-17-api-new-terminal-configurations
Summary: Added support for new terminal configurations at merchant, store, and terminal level in Terminals APIs
## 🆕 New
- Added support for new terminal configurations at **[merchant](https://developers.surfboardpayments.com/api/terminals#Set-Merchant-Terminal-Config)**, **[store](https://developers.surfboardpayments.com/api/terminals#Set-Store-Terminal-Config)**, and **[terminal level](https://developers.surfboardpayments.com/api/terminals#Set-Terminal-Config)** in **[Terminals APIs](https://developers.surfboardpayments.com/api/terminals)**
- Tap before amount ( `tapBeforeAmount` ) : Configure card tapping before amount is displayed.
- Power mode ( `powerMode` ) : Configure performance modes for terminals.
- Status bar background color ( `statusBarBackground` ) : Configure status bar background color.
- Status bar foreground color ( `statusBarForeground` ) : Configure status bar background color.
- Status bar battery low icon color ( `statusBarBatteryLow` ) : Configure custom color for the status bar battery icon when the battery level drops to 20% or lower.
### New API in Transactions APIs
Product: API | Version: Lithium | Date: 2023-09-10
URL: /docs/changelog/2023-09-10-api-new-api-in-transactions-apis
Summary: Added new Fetch Transactions by ID API in Transactions APIs.
## 🆕 New
- Added new **[Fetch Transactions by ID API](https://developers.surfboardpayments.com/api/transactions#Fetch-Transactions-by-ID)** in **[Transactions APIs](https://developers.surfboardpayments.com/api/transactions)**.
- You can use the **[Fetch Transactions by ID API](https://developers.surfboardpayments.com/api/transactions#Fetch-Transactions-by-ID)** to retrieve a list of all transactions associated with an order.
- This is useful in cases where a single order was paid through multiple **[partial payments](https://developers.surfboardpayments.com/docs/payments/partialpayments)**.
### Request parameter updates in Logistics APIs
Product: API | Version: Lithium | Date: 2023-09-06
URL: /docs/changelog/2023-09-06-api-request-parameter-updates-in-logistics-apis
Summary: Email address ( email ) is now mandatory for creating a shipment order using Create Shipment API.
## 🔧 Updates
- Email address ( `email` ) is now mandatory for creating a shipment order using **[Create Shipment API](https://developers.surfboardpayments.com/api/logistics?lang=cURL#Create-Shipment)**.
### Support for pre-entered information in KYB
Product: API | Version: Lithium | Date: 2023-09-03
URL: /docs/changelog/2023-09-03-api-support-for-pre-entered-information-in-kyb
Summary: Added support for pre-entered information to simplify the KYB process for merchants.
## 🆕 New
- Added support for pre-entered information to simplify the KYB process for merchants.
- This allows partners to pre-enter opening information, gift cards, prepayments, and funds information fields in the KYB.
## 🔧 Updates
- Updated **[Create Merchant API](https://developers.surfboardpayments.com/api/merchants?#Create-Merchant)** to support optional object parameter `preEnteredInformation` in the control fields.
- Partners can use this parameter to pre-enter `openingInfo` , `giftcards` , `prePayments` , and `fundsInfo`.
### API features and availability updates
Product: API | Version: Lithium | Date: 2023-08-31
URL: /docs/changelog/2023-08-31-api-api-features-and-availability-updates
Summary: Added support for partial payments in Payments APIs.
## 🆕 New
- Added support for **[partial payments](https://developers.surfboardpayments.com/docs/payments/partialpayments)** in **[Payments APIs](https://developers.surfboardpayments.com/api/payments)**.
- Added new **[Update Terminal Name API](https://developers.surfboardpayments.com/api/terminals#Update-Terminal-Name)** for updating terminal name.
- **[Logistics APIs](https://developers.surfboardpayments.com/api/logistics)** are now available.
## 🔧 Updates
- Added optional parameter `amount` for partial payments in **[Initiate a Payment API](https://developers.surfboardpayments.com/api/payments#Initiate-a-Payment)**.
- Added new order status(`orderStatus`)value `PARTIAL_PAYMENT_COMPLETED` in **[Fetch Order Status API](https://developers.surfboardpayments.com/api/orders#Fetch-Order-Status)**.
### Added Support for Terminal names
Product: API | Version: Lithium | Date: 2023-08-29
URL: /docs/changelog/2023-08-29-api-added-support-for-terminal-names
Summary: Added support for using Terminal name(terminalName) to identify individual terminals.
## 🆕 New
- Added support for using **Terminal name**(`terminalName`) to identify individual terminals.
## 🔧 Updates
- Added new optional request parameter `terminalName` to the **[Register Terminal API](https://developers.surfboardpayments.com/api/terminals#Register-Terminal)**.
- Added new response parameter `terminalName` to the **[Fetch All Merchant Terminals API](https://developers.surfboardpayments.com/api/merchants#Fetch-All-Merchant-Terminals)**.
- Added new response parameter `terminalName` to the **[Fetch All Store Terminals API](https://developers.surfboardpayments.com/api/stores?lang=cURL#Fetch-All-Store-Terminals)**.
### New API in Payments APIs
Product: API | Version: Lithium | Date: 2023-08-13
URL: /docs/changelog/2023-08-13-api-new-api-in-payments-apis
Summary: Added Reboot Terminal API to the Terminals APIs.
## 🆕 New
- Added **[Reboot Terminal API](https://developers.surfboardpayments.com/api/terminals#Reboot-Terminal)** to the **[Terminals APIs](https://developers.surfboardpayments.com/api/terminals)**.
### Response parameter updates
Product: API | Version: Lithium | Date: 2023-08-08
URL: /docs/changelog/2023-08-08-api-response-parameter-updates
Summary: Added new response parameters totalNumberOfTransaction, totalAmountOfTransaction, lastTransactionAt, phoneNumber, and acquirerMID to the Fetch Merchant Details API.
## 🔧 Updates
- Added new response parameters `totalNumberOfTransaction`, `totalAmountOfTransaction`, `lastTransactionAt`, `phoneNumber`, and `acquirerMID` to the **[Fetch Merchant Details API](https://developers.surfboardpayments.com/api/merchants#Fetch-Merchant-Details)**.
### Response parameter updates
Product: API | Version: Lithium | Date: 2023-08-07
URL: /docs/changelog/2023-08-07-api-response-parameter-updates
Summary: Added transactionId response parameter to the Check Payment Status API.
## 🔧 Updates
- Added `transactionId` response parameter to the **[Check Payment Status API](https://developers.surfboardpayments.com/api/payments#Check-Payment-Status)**.
- `transactionId` will be returned when the payment status is ‘PAYMENT_COMPLETED’.
### User guide for SurfPad
Product: SurfPad | Version: 3.0.1.36 | Date: 2023-07-30
URL: /docs/changelog/2023-07-30-surfpad-user-guide-for-surfpad
Summary: Added user guide for SurfPad.
## 🆕 New
- Added user guide for **[SurfPad](https://developers.surfboardpayments.com/docs/terminals/surfpad)**.
### Response parameter updates
Product: API | Version: Lithium | Date: 2023-07-20
URL: /docs/changelog/2023-07-20-api-response-parameter-updates
Summary: Added rrn response parameter to the Fetch All Transactions API.
## 🔧 Updates
- Added `rrn` response parameter to the **[Fetch All Transactions API](https://developers.surfboardpayments.com/api/transactions#Fetch-All-Transactions)**.
## 🐛 Fixed
- Various bug fixes.
### New language support and various performance improvements
Product: SurfPad | Version: 3.0.1.36 | Date: 2023-07-17
URL: /docs/changelog/2023-07-17-surfpad-new-language-support-and-various-performance-improvements
Summary: Added language support for Finnish and Danish.
## 🆕 New
- Added language support for **Finnish** and **Danish**.
- Added power modes to improve performance when the battery is charged and there is an external power source connected.
- Added visual event using LEDs when configuration is changed.
## 🔧 Updates
- Made changes to the status bar immediately visible.
## 🐛 Fixed
- Various bug fixes.
### New APIs and request parameter updates
Product: API | Version: Lithium | Date: 2023-07-16
URL: /docs/changelog/2023-07-16-api-new-apis-and-request-parameter-updates
Summary: Added Set Terminal Config API, Set Merchant Terminal Config API, Set Store Terminal Config API APIs to the Terminals APIs.
## 🆕 New
- Added **[Set Terminal Config API](https://developers.surfboardpayments.com/api/terminals#Set-Terminal-Config)**, **[Set Merchant Terminal Config API](https://developers.surfboardpayments.com/api/terminals#Set-Merchant-Terminal-Config)**, **[Set Store Terminal Config API](https://developers.surfboardpayments.com/api/terminals#Set-Store-Terminal-Config)** APIs to the **[Terminals APIs](https://developers.surfboardpayments.com/api/terminals)**.
## 🔧 Updates
- Removed the `merchantType` response parameter from the **[Fetch All Merchants API](https://developers.surfboardpayments.com/api/merchants#Fetch-All-Merchants)**.
- Added `merchantLanguage` and `mccCode` response parameters to the **[Fetch All Merchants API](https://developers.surfboardpayments.com/api/merchants#Fetch-All-Merchants)**.
## 🐛 Fixed
- Various bug fixes.
### Request parameter updates
Product: API | Version: Lithium | Date: 2023-07-02
URL: /docs/changelog/2023-07-02-api-request-parameter-updates
Summary: Updated the type of request parameter address from string to object in the Fetch All Merchants API and Fetch Merchant Details API.
## 🔧 Updates
- Updated the type of request parameter `address` from string to object in the **[Fetch All Merchants API](https://developers.surfboardpayments.com/api/merchants#Fetch-All-Merchants)** and **[Fetch Merchant Details API](https://developers.surfboardpayments.com/api/merchants#Fetch-Merchant-Details)**.
## 🐛 Fixed
- Various bug fixes.
### New API and parameter updates
Product: API | Version: Lithium | Date: 2023-06-30
URL: /docs/changelog/2023-06-30-api-new-api-and-parameter-updates
Summary: Added new Get Payment by ID API to the Payments APIs.
## 🆕 New
- Added new **[Get Payment by ID API](https://developers.surfboardpayments.com/api/payments#Get-Payment-by-ID)** to the **[Payments APIs](https://developers.surfboardpayments.com/api/payments)**.
## 🔧 Updates
- Renamed the request parameter `pfMerchantId` to `acquirerMID` in **[Create Merchant API](https://developers.surfboardpayments.com/api/merchants#Create-Merchant)** .
- Support for `pfMerchantId` will be continued for the time being, but we recommend transitioning to `acquirerMID` for future-proof compatibility.
## 🐛 Fixed
- Various bug fixes.
---
## Payment Methods
Surfboard supports global card schemes, mobile wallets, account-to-account, BNPL and invoice, local card schemes, and meal-benefit cards through a single Payment Methods system. Methods are enabled per merchant, store, or terminal, through the Partner Portal (partners), the Merchant Portal (merchants), or the Payment Methods API (developers). All three paths hit the same backend; Surfboard handles scheme registration, certifications, and the operational layer. Methods are only available in markets where Surfboard accepts merchants, coverage statements describe Surfboard's acceptance footprint, not the underlying scheme's global issuance.
All methods index: /payment-methods
### Global card schemes
Worldwide-acceptance card networks. Default acceptance for any merchant.
#### Mastercard
Tagline: The global card scheme accepted in 210+ countries
Mastercard credit, debit, and prepaid cards are accepted on every Surfboard channel, in-store, online, SoftPOS, and unattended.
Mastercard is one of the two dominant global card schemes. Acceptance is non-negotiable for any merchant operating in Europe, North America, or globally. Surfboard supports Mastercard credit, debit, prepaid, and commercial cards across every channel through one integration.
- Channels: in-store, online, softpos, unattended
- Footprint: Accepted on every Surfboard channel, in every market we acquire
- When to use: Default acceptance for any merchant. Without Mastercard, a meaningful share of consumers can't transact at all.
- Customer base: Every consumer segment. Mastercard issues across credit, debit, and prepaid product lines, covering teens to corporates.
- Use cases:
- Standard in-store card-present payments
- Online card-not-present checkout with 3DS
- Mobile wallet provisioning (Apple Pay / Google Pay)
- Recurring billing with network tokens
- Cross-border commerce with DCC support
- Related: visa, american-express, apple-pay, google-pay
- External: https://www.mastercard.com
- URL: /payment-methods/mastercard
#### Visa
Tagline: The world's largest card scheme
Visa credit, debit, and prepaid cards processed on Surfboard infrastructure across in-store, online, SoftPOS, and unattended.
Visa is the largest card scheme by volume worldwide. Like Mastercard, it's a baseline expectation for any merchant. Surfboard handles Visa credit, debit, prepaid, and commercial cards across every channel, including Visa Direct payouts where supported.
- Channels: in-store, online, softpos, unattended
- Footprint: Accepted on every Surfboard channel, in every market we acquire
- When to use: Default acceptance for any merchant. Visa typically represents the largest share of card transactions in most European markets.
- Customer base: Every consumer segment. Visa issues across the same credit / debit / prepaid product lines as Mastercard.
- Use cases:
- Standard in-store card-present payments
- Online card-not-present checkout with 3DS
- Mobile wallet provisioning (Apple Pay / Google Pay)
- Recurring billing with network tokens
- Cross-border commerce with DCC support
- Related: mastercard, american-express, apple-pay, google-pay
- External: https://www.visa.com
- URL: /payment-methods/visa
#### American Express
Tagline: Premium card scheme with affluent and corporate buyers
Accept American Express credit and corporate cards across in-store, online, SoftPOS, and unattended channels.
American Express has lower overall market share than Visa or Mastercard but a disproportionately high share of premium consumer and corporate spend. Travel, hospitality, B2B, and high-end retail merchants benefit most from accepting AMEX. Surfboard supports AMEX as a separately activatable payment method per merchant.
- Channels: in-store, online, softpos, unattended
- Footprint: Accepted on every Surfboard channel, in every market we acquire
- When to use: Recommended for travel, hospitality, B2B, premium retail, and any merchant with significant cross-border or corporate-card volume. AMEX customers tend to have higher average transaction values.
- Customer base: Premium consumers, business cardholders, frequent travelers, and corporate expense accounts.
- Use cases:
- Hotel and hospitality check-in
- B2B and corporate-card transactions
- Premium retail with high average ticket
- Travel and cross-border merchants
- Restaurants in tourist-heavy locations
- Related: mastercard, visa, b2b-invoice
- External: https://www.americanexpress.com
- URL: /payment-methods/american-express
#### Discover
Tagline: North American card scheme with growing global acceptance
Accept Discover Network cards including Diners Club, JCB, UnionPay, and other partner cards through Surfboard.
Discover Network includes Discover-branded cards plus reciprocal acceptance with Diners Club, JCB, UnionPay, and BC Card via the Discover Global Network. Most useful for European merchants serving North American, Japanese, or Chinese travelers.
- Channels: in-store, online, softpos, unattended
- Footprint: Accepted via Discover Global Network (incl. JCB, UnionPay) on every Surfboard channel
- When to use: Recommended for European merchants with significant inbound tourism from North America or Asia. Adds JCB and UnionPay acceptance through one activation.
- Customer base: North American Discover cardholders, Japanese JCB cardholders, Chinese UnionPay cardholders, and Diners Club holders globally.
- Use cases:
- Tourist-heavy retail and hospitality
- Airport and travel-hub merchants
- Cross-border e-commerce serving Asian markets
- Premium retail with international foot traffic
- Related: mastercard, visa, american-express
- External: https://www.discover.com
- URL: /payment-methods/discover
### Local card schemes
National debit card networks. Lower-cost routing for domestic transactions.
#### Dankort
Tagline: Denmark's national debit card scheme
Accept Dankort on Surfboard, the dominant debit card in Denmark and a default expectation for any DK merchant.
Dankort is Denmark's national debit card scheme, issued by every major Danish bank. Most Danish consumers carry a Dankort as their primary card and many cards are co-badged Dankort/Visa for international acceptance. Surfboard supports Dankort across every channel.
- Channels: in-store, online, softpos, unattended
- Footprint: Denmark
- When to use: Mandatory for any merchant operating in Denmark. Without Dankort acceptance you'll lose Danish consumers at checkout.
- Customer base: Danish consumers, essentially every banked adult holds a Dankort.
- Use cases:
- All Danish in-store retail and hospitality
- Online checkout for Danish e-commerce
- Unattended payments at Danish locations
- SoftPOS deployments in Denmark
- Related: mobilepay, visa, mastercard
- External: https://www.dankort.dk
- URL: /payment-methods/dankort
#### BankAxept
Tagline: Norway's national debit card scheme
Accept BankAxept on Surfboard, the dominant debit card in Norway and a default expectation for any NO merchant.
BankAxept is Norway's national debit card scheme, jointly owned by Norwegian banks. Most Norwegian bank cards are co-badged BankAxept/Visa or BankAxept/Mastercard. BankAxept is the lower-cost domestic-routing option preferred by Norwegian merchants. Surfboard supports BankAxept across every in-store channel.
- Channels: in-store, softpos, unattended
- Footprint: Norway
- When to use: Recommended for every merchant operating in Norway. Routing through BankAxept rather than Visa/Mastercard typically reduces interchange fees on domestic Norwegian transactions.
- Customer base: Norwegian consumers, virtually all banked adults hold a BankAxept-enabled card.
- Use cases:
- All Norwegian in-store retail and hospitality
- Unattended payments at Norwegian locations
- SoftPOS deployments in Norway
- Cost-optimized routing for high-volume Norwegian merchants
- Related: vipps, visa, mastercard
- External: https://www.bankaxept.no
- URL: /payment-methods/bankaxept
### Mobile wallets
Tokenized contactless payments on phones and watches. Higher conversion rates.
#### Apple Pay
Tagline: Tokenized contactless payments on iPhone and Apple Watch
Apple Pay works on every contactless-enabled Surfboard terminal and on online checkout out of the box.
Apple Pay is a mobile wallet that tokenizes the customer's underlying credit or debit card. From a merchant perspective, it processes as a card transaction with the issuing bank's network, but with stronger biometric authentication and higher authorization rates. Surfboard supports Apple Pay across every contactless terminal and on hosted/embedded online checkout.
- Channels: in-store, online, softpos, unattended
- Footprint: Accepted on every Surfboard channel, in every market we acquire
- When to use: Always enable. Apple Pay drives higher online conversion, faster in-store checkout, and works on every Surfboard terminal that accepts contactless. There's no reason not to accept it.
- Customer base: iPhone and Apple Watch users. In some markets (US, Nordics, UK), Apple Pay represents 30%+ of contactless transactions.
- Use cases:
- Faster contactless checkout in retail
- Higher conversion rates on online checkout
- Tap to Pay on iPhone, accept Apple Pay without dedicated hardware
- Subscription and recurring billing
- Unattended self-checkout flows
- Related: google-pay, visa, mastercard
- External: https://www.apple.com/apple-pay/
- URL: /payment-methods/apple-pay
#### Google Pay
Tagline: Tokenized contactless payments on Android devices
Google Pay works on every contactless-enabled Surfboard terminal and on online checkout.
Google Pay is a mobile wallet that tokenizes the customer's underlying credit or debit card on Android devices. Like Apple Pay, it processes as a card transaction with strong device-level authentication. Surfboard supports Google Pay across every contactless terminal and on hosted/embedded online checkout.
- Channels: in-store, online, softpos, unattended
- Footprint: Accepted on every Surfboard channel, in every market we acquire
- When to use: Always enable. Google Pay drives higher online conversion and faster in-store checkout, particularly in markets with high Android share like Eastern Europe and parts of Asia.
- Customer base: Android device users globally. Higher penetration in markets where Android dominates.
- Use cases:
- Faster contactless checkout in retail
- Higher conversion rates on online checkout
- Tap to Pay on Android via SoftPOS
- Subscription and recurring billing
- Unattended self-checkout flows
- Related: apple-pay, visa, mastercard
- External: https://pay.google.com
- URL: /payment-methods/google-pay
### Account-to-account
Direct bank-account transfers. Lower fees, no chargebacks, fast settlement.
#### Swish
Tagline: Sweden's mobile payment standard
Accept Swish on Surfboard for fast, low-cost, account-to-account payments, the dominant payment method in Sweden.
Swish is Sweden's mobile payment system, owned by a consortium of major Swedish banks. It moves money directly between bank accounts in seconds via BankID authentication. Adoption is near-universal among Swedish consumers, over 80% use Swish regularly. Surfboard supports Swish for both in-store (QR or send-to-phone) and online checkout.
- Channels: in-store, online, unattended
- Footprint: Sweden
- When to use: Mandatory for any merchant operating in Sweden. Many Swedish consumers expect Swish as the default payment option and may abandon checkout if it's missing, particularly for small in-store transactions and online purchases.
- Customer base: Swedish consumers (essentially all banked adults). Particularly strong among younger demographics where Swish often replaces cash.
- Use cases:
- Online checkout for Swedish e-commerce
- Small-value in-store transactions where customers prefer not to use cards
- Peer-style payments at events, markets, food halls
- QR-based payments at unattended kiosks
- Invoicing flows with Swish payment links
- Related: mobilepay, vipps, pay-by-bank
- External: https://www.swish.nu
- URL: /payment-methods/swish
#### Vipps
Tagline: Norway's mobile payment standard, now part of Vipps MobilePay
Accept Vipps on Surfboard for the dominant payment method in Norway, fast, low-cost, account-to-account.
Vipps is Norway's leading mobile payment service, used by ~75% of the Norwegian adult population. In 2022 Vipps merged with Denmark's MobilePay to form the Vipps MobilePay group, but the consumer-facing brands remain separate in their home markets. Surfboard supports Vipps for in-store (QR) and online checkout in Norway.
- Channels: in-store, online, unattended
- Footprint: Norway
- When to use: Mandatory for any merchant operating in Norway. Vipps is to Norway what Swish is to Sweden, Norwegian consumers expect it at every checkout.
- Customer base: Norwegian consumers (over 75% of adults). Particularly strong among younger demographics where it often replaces cash.
- Use cases:
- Online checkout for Norwegian e-commerce
- Small-value in-store transactions
- Peer-style payments at events and markets
- QR-based payments at unattended kiosks
- Invoicing flows with Vipps payment links
- Related: swish, mobilepay, bankaxept
- External: https://vipps.no
- URL: /payment-methods/vipps
#### MobilePay
Tagline: Denmark's and Finland's mobile payment standard
Accept MobilePay on Surfboard for the dominant mobile payment method in Denmark and Finland.
MobilePay is the leading mobile payment app in Denmark and a major player in Finland. In 2022 MobilePay merged with Norway's Vipps to form the Vipps MobilePay group, but the consumer-facing brand remains MobilePay in DK and FI. Adoption is near-universal among Danish adults. Surfboard supports MobilePay for in-store and online checkout in DK and FI.
- Channels: in-store, online, unattended
- Footprint: Denmark; Finland
- When to use: Mandatory for any merchant operating in Denmark or Finland. MobilePay is the default expectation for both online and small in-store transactions.
- Customer base: Danish and Finnish consumers. In Denmark over 90% of the adult population uses MobilePay regularly.
- Use cases:
- Online checkout for Danish and Finnish e-commerce
- Small-value in-store transactions
- Peer-style payments at events and markets
- Subscription and recurring billing
- QR-based payments at unattended kiosks
- Related: vipps, swish, dankort
- External: https://www.mobilepay.dk
- URL: /payment-methods/mobilepay
#### Pay by Bank
Tagline: Direct bank-to-bank payments via open banking
Accept account-to-account payments directly from the customer's bank account, with no card scheme fees and no chargebacks.
Pay by Bank uses open banking (PSD2) APIs to move money directly from the customer's bank account to the merchant, no card scheme intermediary, no card data, no chargebacks, and significantly lower fees than card processing. Best suited to high-ticket transactions where the savings on interchange fees are meaningful, and to use cases where chargebacks are problematic.
- Channels: online, in-store
- Footprint: Sweden; Norway; Denmark; Finland; UK; Germany; and growing
- When to use: Recommended for high-ticket online transactions, B2B payments, government and utility payments, and anywhere chargebacks are a meaningful operational cost. Particularly attractive in markets with strong open banking adoption.
- Customer base: Banked consumers in markets with mature open banking infrastructure. Skews toward customers comfortable with modern banking apps.
- Use cases:
- High-ticket retail and electronics
- Travel and hospitality booking
- B2B and wholesale payments
- Government, utility, and public-sector billing
- Subscription services where lower fees matter
- Use cases where chargebacks are operationally painful
- Related: swish, vipps, mobilepay, b2b-invoice
- URL: /payment-methods/pay-by-bank
### BNPL & invoice
Buy-now-pay-later, instalment, and invoice-based payment methods.
#### Klarna
Tagline: Pay later, pay in 3, or pay over time
Offer Klarna's full BNPL suite, Pay in 30 days, Pay in 3 instalments, and longer-term financing, on Surfboard's online checkout.
Klarna is the leading buy-now-pay-later provider in Europe and a major player in the US and UK. Offering Klarna at checkout typically lifts conversion and average order value, particularly for retail, fashion, electronics, and home goods. Surfboard supports Klarna's full product suite, Pay in 30 days, Pay in 3 instalments, and longer-term financing, on online checkout.
- Channels: online
- Footprint: Sweden; Norway; Denmark; Finland; Germany; Netherlands; UK; US; and more
- When to use: Recommended for any retail, fashion, electronics, or home-goods merchant. BNPL converts customers who would otherwise abandon at the price point, and lifts average order value because shoppers add more knowing they don't pay upfront.
- Customer base: Consumers who prefer to spread payments, defer payment until after delivery, or avoid using credit cards. Skews younger and more online-native.
- Use cases:
- Fashion and apparel e-commerce
- Electronics and high-ticket retail
- Home goods and furniture
- Travel and experience bookings
- Subscription services with optional instalments
- Related: svea, b2b-invoice, pay-by-bank
- External: https://www.klarna.com
- URL: /payment-methods/klarna
#### Svea
Tagline: Nordic invoice and BNPL specialist with strong B2B coverage
Accept Svea invoice and instalment payments, particularly strong for B2B and Swedish-market specific use cases.
Svea is a Nordic financial services group offering invoice payment, partial payment, and credit-account products for both B2C and B2B. Less consumer-recognized than Klarna but particularly strong in B2B contexts and as a complement to Klarna for credit-conscious merchants. Surfboard supports Svea invoice payments on online checkout.
- Channels: online
- Footprint: Sweden; Norway; Denmark; Finland
- When to use: Recommended for B2B-heavy merchants, Swedish-market specialty retailers, and anyone wanting BNPL coverage beyond Klarna.
- Customer base: Swedish and Nordic consumers and businesses. Strong B2B presence.
- Use cases:
- B2B invoicing and procurement
- Swedish specialty retail
- Wholesale and trade catalog merchants
- BNPL diversity alongside Klarna
- Related: klarna, b2b-invoice, pay-by-bank
- External: https://www.svea.com
- URL: /payment-methods/svea
#### B2B Invoice
Tagline: Net 30 / Net 60 invoicing for business buyers
Issue invoices on standard business payment terms, a default expectation for B2B procurement.
B2B buyers expect invoices on standard payment terms (typically Net 30 or Net 60), not card transactions. Surfboard's invoice payment method covers the full lifecycle, issuing the invoice, distribution by email or e-invoice, configurable due dates, automated reminders, and integration with debt-collection partners when payments go past due. Frees the merchant from managing card limits, chargebacks, or per-transaction interchange on large B2B sales.
- Channels: online
- Footprint: Sweden; Nordic and EU markets
- When to use: Mandatory for any merchant with meaningful B2B sales. Most business buyers either cannot or will not pay by card for purchases above a few hundred euros, they require an invoice.
- Customer base: Business buyers. Procurement teams, finance departments, professional services clients, and wholesale customers.
- Use cases:
- Wholesale and trade catalogs
- Professional services and consulting
- B2B SaaS billing
- Industrial and equipment sales
- Public-sector and government procurement
- Related: svea, klarna, pay-by-bank
- URL: /payment-methods/b2b-invoice
### Meal & wellness benefits
Employer-funded benefit cards for meals, wellness, sports, and culture.
#### Epassi
Tagline: Finland's leading employee benefit payment platform
Accept Epassi for employee meal, sport, wellness, and culture benefits, a baseline expectation for Finnish F&B and wellness merchants.
Epassi is Finland's leading employer-sponsored employee benefit platform. Employers fund Epassi accounts for their staff, and employees use Epassi to pay for meals, sports activities, wellness services, culture, and commuting. For F&B, gyms, and wellness merchants in Finland, accepting Epassi is essentially mandatory. Surfboard supports Epassi at the point of sale.
- Channels: in-store, online
- Footprint: Finland; Sweden (growing); Other Nordic and Baltic markets
- When to use: Mandatory for F&B, gym, wellness, sports, and culture merchants in Finland. Highly recommended for the same vertical in Sweden where adoption is growing rapidly.
- Customer base: Finnish employees with Epassi-funded employer benefits. Often used during weekday lunch hours and evening leisure time.
- Use cases:
- Restaurants, cafes, and lunch counters
- Gyms, sports clubs, and padel centers
- Massage, spa, and wellness services
- Cultural venues and event tickets
- Commuting and transport (selected partners)
- Related: mobilepay, swish, b2b-invoice
- External: https://www.epassi.fi
- URL: /payment-methods/epassi
### Supported Payment Methods (legacy short list)
- Visa
- Mastercard
- American Express
- JCB
- Discover
- UnionPay
- Apple Pay
- Google Pay
- Dankort
- Vipps
- Pay by Bank
- BNPL
---
## Portals
- **Partner Portal** (/shacks/partner-portal): For ISV partners to manage their integration and merchants, including enabling and disabling payment methods on each downstream merchant. Full documentation at /partner-portal
- **Developer Portal** (/shacks/developer-portal): API access, documentation, and sandbox. The Payment Methods API enables / disables methods at merchant, store, or terminal scope.
- **Merchant Portal** (/shacks/merchant-portal): For merchants to run their business, sales, terminals, stores, products, campaigns, gift cards, financing, settings, and self-service enablement of payment methods. Full documentation at /merchant-portal
---
## Company Pages
### About Us
Founded in 2019 by people with great experience in the payment and security industry. Team of ~50 employees across Stockholm and Chennai. The company values collaboration, innovation, and constantly seeking new ways to improve.
URL: /about
### Contact
Book a meeting or fill out a contact form to discuss payment solutions.
URL: /contact
### Press Kit
Company logos, brand assets, and media resources.
URL: /presskit
### Knowledge Hub
85+ articles covering payment technology, company news, product launches, industry analysis, and compliance topics.
URL: /knowledge-hub
### Legal
- Privacy Policy: /privacy-policy
- Terms and Conditions: /terms-and-conditions
- Cookies: /cookies
- SLA: /sla
---
## Partnerships
- **Worldline:** Strategic partnership: Seamless payment solutions
- **Cardstream Group:** Partnership: Innovative payment delivery
- **Apple:** Technology partner: Tap to Pay on iPhone across Europe
- **Google Cloud:** Infrastructure partner: Cloud platform
- **PCI Security Standards Council:** Participating organization
- **Payer:** Payment method partnership
- **Ping (Sports):** Simplified payments for sports clubs
- **BookSalon:** Integrated payments for salon booking software
- **Kajsas Fisk:** In-store payment solution for restaurant
---
## Recent Milestones (2025–2026)
### 2026
- Feb 2026: Closed oversubscribed €4M funding round (exceeding €3M target by 33%)
- Feb 2026: Adopted Universal Commerce Protocol (UCP) and Agent Payments Protocol (AP2) for agentic commerce
- Feb 2026: Published AI-optimized developer docs with MCP server support
### 2025
- Dec 2025: Tap to Pay on iPhone launched in Poland and Hungary (now 12 markets)
- Oct 2025: Tap to Pay on iPhone launched in Ireland, Estonia, Latvia, Lithuania
- Oct–Jan 2026: Carbon platform release, 10 drops including alternative payment methods, self-hosted digital receipts, customer management, SFTP reports, PRO series terminals, billing, subscriptions, offline payments
- Q4 2025: 122% recurring revenue growth year-over-year (Q4 2025)
- 164% CAGR (Oct 2024 – Dec 2025)
- ~8% month-over-month for 15 consecutive months (Oct 2024 – Dec 2025)
- Terminal base: ~4,000 units
- One new partner signing per week
- AI integration: ~70% of merchant support conversations resolved automatically
- Pay by Bank, Dankort, and Vipps payment methods launched
- Q1 2025 report: Revenue growth driven by partners
- Q3 2025 report: +22% QoQ recurring revenue, +64% processed volume vs Q1
- CFO: Johannes Akermark
---
## AI & Technology
Surfboard embraces an AI-first culture:
- AI assists developers for faster development cycles and higher quality
- ~70% of merchant support conversations resolved automatically by AI
- AI-powered branding API for automatic checkout customization
- MCP server for AI-assisted developer integration
- Platform includes AI APIs for streamlining operations
- Adopted Universal Commerce Protocol (UCP) and Agent Payments Protocol (AP2)
- Google Merchant Center Content API integration for product catalog alignment
- AI-optimized developer documentation with llms.txt and structured JSON endpoints
---
## Contact Information
- **Website:** https://www.surfboardpayments.com
- **Contact Page:** https://www.surfboardpayments.com/contact
- **Developer Portal:** https://developers.surfboardpayments.com
- **Stockholm Office:** Barnhusgatan 4, Stockholm, Sweden
- **Chennai Office:** Chennai, India
- **Social Media:** @surfboardpay