Developers Guides Customer Identification on Card Tap

Customer Identification on Card Tap

Recognise a returning customer from the card they tap, before the payment runs. Apply their discount, points or price list to the open order, then take the money.

In-StoreAPIWebhooksLoyaltyPersonalisation

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

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

StepWhoWhat happens
1YouCreate the order as normal
2CustomerTaps their card at the terminal
3SurfboardSends order.customer.identify with the order and a card token
4YouLook the token up, and update the order if you recognise it
5YouInitiate 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
  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.

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

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

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:

IntentWhere it goes
Member price or loyalty discountcampaign on the line, or an order-level adjustment
Points redeemed as money offAn adjustment, so it is visible as its own line in reporting
Attach the person to the ordercustomer, which also carries the receipt to their email
Anything your own systems need latermetadata, 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

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

SymptomLikely cause
No webhook on tapThe endpoint is not subscribed to order.customer.identify, or the terminal is not an Android terminal.
PUT /orders/:orderId returns 404The orderId is wrong, or the order belongs to another merchant.
Update rejected after a tapA payment has already been initiated for the order. The window has closed.
OR_0037 on updateThe new totalOrderAmount does not reconcile with the line items.

Reference

Ready to get started?

Create a sandbox account and start building your integration today.