Developers Guides Create an Order

Create an Order

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

OnlineAPIOrdersIn-Store

Add this to your codebase

Paste it into Claude Code, Codex, Cursor or any coding agent. It points the agent at this guide in machine-readable form, so it writes against the real API instead of a guess. Wire up the MCP server once and it can read the rest of the platform too.

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
  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 for the full header set.

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

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"
    }
  }
}
// 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:

FieldRequiredDescription
idYesUnique line item identifier
nameYesProduct name
quantityYesQuantity (negative for refunds)
amount.regularYesUnit price in smallest currency unit
amount.totalYesUnit price after shipping and campaign (regular + shipping - campaign). Not the line total
amount.currencyYesNumeric ISO 4217 code (e.g., "752" for SEK)
amount.taxYesTax array for the line. Required even at zero rate — send a 0 entry rather than omitting it

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

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

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

"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 if you are coming from a sales-tax market.

Customer, Billing, and Shipping

Include customer, billing, and shipping details when available:

{
  "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.

SettingFormulaExample
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:

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

Adjustments

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

{
  "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:

{
  "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 guide for the full flow.

Check Order Status

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

GET /orders/:orderId/status
// 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 StatusOrder StatusDescription
PAYMENT_COMPLETEDPAYMENT_COMPLETEDPayment succeeded — the order is closed.
PAYMENT_CANCELLEDPENDINGPayment was cancelled — the order remains open and a new payment can be initiated using the existing orderId.
PAYMENT_FAILEDPENDINGPayment 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 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:

Reference

Ready to get started?

Create a sandbox account and start building your integration today.