POS
Issue gift cards, redeem gift cards, and look up cards by QR code from a point of sale terminal.
The POS API is what the point of sale terminal app uses to sell gift cards in-store, redeem them against a purchase, and look up a card's balance before redeeming.
All three endpoints require an authenticated merchant session with the
portal.pos permission.
Issue a gift card
POST /api/bff/pos/issueCreates a local order, then issues the gift card upstream with the card
network. If the upstream issue fails, the local order is marked
ISSUE_FAILED and the error is surfaced to the caller; no gift card is
created in that case.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
value | string or number | yes | Amount to issue, in decimal currency units (for example "25.00"). Must be greater than 0. |
giftCardOfferId | string or number | no | The catalog offer to issue against. If omitted, issuance falls back to a default offer. |
giftCardOfferType | string | no | Overrides the offer type if not resolvable from giftCardOfferId. Normalised to GIFTCARD_DIGITAL or REFUND_DIGITAL. |
currency | string | no | Defaults to the offer's currency, then the merchant's default currency. |
retailStoreId | string or number | no | Defaults to "0". |
deviceId / terminalId | string | no | Identifies the POS device. terminalId is accepted as an alias for deviceId. |
paymentReference | string | no | Max 80 characters. Falls back to externalReference, then an auto-generated POS-<timestamp>-<random> reference. |
externalReference | string | no | Used as a fallback for paymentReference. |
merchantId | string | no | Only meaningful for callers with access to multiple merchants; otherwise the session's merchant is used. |
deliveryDetails | object | no | See below. |
email | string | no | Legacy alias for deliveryDetails.email. |
deliveryDetails:
| Field | Type | Required | Notes |
|---|---|---|---|
deliveryType | string | no | EMAIL, SMS, or QR. Defaults to EMAIL. Case-insensitive. |
email | string | required if deliveryType is EMAIL | Must be a valid email address. |
phoneNumber | string | required if deliveryType is SMS |
QR delivery requires neither email nor phone; the card is displayed on the
device instead.
If the selected offer has fixed pricing, value must match the offer's fixed
amount exactly. If the offer has variable (flexible) pricing, value must
fall within the offer's min/max range.
Response
{
"ok": true,
"data": {
"reservationCode": "...",
"orderHash": "...",
"giftCardId": "...",
"cardNo": "...",
"qrCode": "...",
"orderId": "local-order-id",
"paymentReference": "POS-20260626120000-ABC123"
}
}The exact upstream fields (reservationCode, orderHash, giftCardId,
cardNo, qrCode) are passed through from the card network's issue
response.
Errors
| Status | Cause |
|---|---|
| 401 | No session. |
| 404 | Session user has no associated merchant. |
| 403 | Session user's role lacks the portal.pos permission. |
| 400 | Missing email/phoneNumber for the selected delivery type, invalid amount, amount outside offer min/max, amount not matching a fixed-price offer, or the merchant has no card-network account configured. |
| varies | Other upstream issuance failures pass through the upstream status code, with error.details containing the upstream response. |
Redeem a gift card
POST /api/bff/pos/redeemRedeems an amount from a gift card against a purchase and records the redemption. On success, this also triggers settlement creation for the redemption (best-effort; a settlement failure is logged but does not fail the request).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
qrCode | string | yes | |
redeemAmount | number | yes | Must be greater than 0. |
purchaseAmount | number | yes | Must be 0 or greater. The total purchase amount the redemption is applied to. |
retailStoreId | string or number | no | |
retailStoreName | string | no | Defaults to "Merchant Portal POS". |
currency | string | no | Defaults to the merchant's default currency, then USD. |
Looser validation than issue
Unlike pos/issue, this endpoint does not use a zod schema; fields are read
directly from the request body and only checked for presence/basic numeric
validity. Error responses here use a bare { "error": "..." } shape, not the
{ "ok": false, "error": { ... } } envelope used elsewhere.
Response
Success:
{ "ok": true, "data": { }, "redemptionId": "redemption-id" }data is the raw response from the card network's redeem endpoint.
Failure:
{ "error": "Redeem failed", "details": { } }A failed redemption is still recorded (with status: FAILED) for audit
purposes; only successful redemptions are linked to a GiftCard record when
one matches the QR code.
Errors
| Status | Cause |
|---|---|
| 401 | No session. |
| 404 | Session user has no associated merchant. |
| 403 | Session user's role lacks the portal.pos permission. |
| 400 | Missing qrCode, redeemAmount, or purchaseAmount; invalid redeemAmount (not finite or not greater than 0); invalid purchaseAmount (not finite or negative); merchant has no card-network account configured. |
| varies | Upstream redeem failures pass through the upstream status code. |
| 500 | Unexpected internal error. |
Look up a card by QR code
GET /api/bff/pos/card/{qrcode}Looks up a gift card's current state directly from the card network, for use at the POS terminal before redeeming.
Two card lookup endpoints
This is the POS-specific lookup, gated by portal.pos. There is a separate
endpoint, /api/bff/cards/{qr},
gated by portal.gift_cards, used by the gift cards management screens
rather than the POS terminal. Both call the same underlying card network
lookup; use whichever matches the permission your client already holds.
Path parameters
| Param | Required |
|---|---|
qrcode | yes |
Response
{ "ok": true, "data": { } }data is the raw card record from the card network (balance, status, and
related fields as returned upstream).
Errors
| Status | Cause |
|---|---|
| 401 | No session. |
| 404 | Session user has no associated merchant. |
| 403 | Session user's role lacks the portal.pos permission. |
| 400 | Missing qrcode path parameter. |
| varies | Card not found or other upstream lookup failure; passes through the upstream status code. |