Forloy API

v1

Customer Management

The customer management endpoints let a merchant owner fetch, update, and erase their enrolled clients (cardholders). All four endpoints are owner-only and operate on a single merchant identified by a required merchant_id query parameter.

Owner credentials required

These endpoints require merchant owner (admin) credentials. System user tokens used by POS integrations are rejected with 403 OWNER_REQUIRED.

merchant_id must be a merchant you own

Every request must include a merchant_id query parameter. It is verified against the authenticated owner's merchants — passing a merchant you do not own returns 403 MERCHANT_NOT_AUTHORIZED. This explicit scoping exists because one owner can own multiple merchants, so the target merchant can never be inferred.

Endpoints

Customers are keyed by customer_id. A customer exists for this API as long as they hold any card instance of any status (active, expired, or archived) under the verified merchant.

MethodPathPurpose
GET/api/v1/customersPaginated, per-customer grouped list with card summaries. Search supported.
GET/api/v1/customers/{customerId}One customer: profile plus all their cards under this merchant.
PATCH/api/v1/customers/{customerId}Update the customer's contact and demographic profile fields.
DELETE/api/v1/customers/{customerId}Erase profile PII and archive the customer's cards under this merchant.

Listing & Search

The list endpoint returns the merchant's customers grouped per customer_id, each with a card_count, a first_enrollment_date (earliest enrollment across their cards), and a summary of every card they hold under this merchant.

Query Parameters

  • merchant_id Required. The merchant to list customers for. Must be a merchant you own.
  • page 1-based page number. Defaults to 1.
  • page_size Results per page, between 1 and 50. Values above 50 are rejected.
  • search Case-insensitive filter matching customer name, email, phone, or card barcode. Minimum 3 characters.
Request — List with search
GET /api/v1/customers?merchant_id=8f14e45f-ceea-467e-8b0f-1a2b3c4d5e6f&page=1&page_size=20&search=jane
Authorization: Bearer <access_token>
Response
{
  "success": true,
  "data": {
    "customers": [
      {
        "customer_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "customer_name": "Jane Doe",
        "email": "jane@example.com",
        "card_count": 2,
        "first_enrollment_date": "2025-11-02T09:15:00.000Z",
        "cards": [
          {
            "card_instance_id": "d4c3b2a1-f6e5-0987-dcba-0987654321fe",
            "barcode_value": "FRL-2026-000841",
            "card_name": "Lunch Cashback",
            "card_type": "cashback",
            "status": "active",
            "current_balance": 12.5,
            "max_balance": null,
            "currency_symbol": "$",
            "enrollment_date": "2026-01-10T14:00:00.000Z"
          },
          {
            "card_instance_id": "c1d2e3f4-a5b6-7890-abcd-ef1234567890",
            "barcode_value": "FRL-2025-000317",
            "card_name": "Coffee Rewards",
            "card_type": "punch",
            "status": "active",
            "current_balance": 4,
            "max_balance": 10,
            "currency_symbol": null,
            "enrollment_date": "2025-11-02T09:15:00.000Z"
          }
        ]
      }
    ],
    "total_count": 1,
    "total_pages": 1,
    "current_page": 1
  },
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-01-15T10:30:00.000Z"
  }
}

Updating a Customer Profile

PATCH updates the customer's profile across all of their card profiles under the verified merchant. Only contact and demographic fields may be changed. locale and consent_marketing remain customer-controlled and are rejected here.

Editable Fields

  • customer_name Display name. Non-empty, up to 100 characters. Cannot be cleared (non-nullable).
  • email Email address. Up to 255 characters, or null to clear.
  • phone Phone number. Up to 20 characters, or null to clear.
  • birthday ISO date (YYYY-MM-DD), or null to clear.
  • gender One of male, female, other, prefer_not_to_say, or null to clear.

Null-clear and empty-body semantics

email, phone, birthday, and gender accept null to clear the field. customer_name is non-nullable and cannot be set to null. An empty body ({}) is rejected with 400 VALIDATION_ERROR — at least one field is required. Any unknown or forbidden key (including locale and consent_marketing) is also rejected with 400.

PATCH edits profile PII only. It never changes the customer's login identity (their auth account) — editing the profile email does not change the email they sign in with.

After an erase, the customer still exists (their archived cards remain) but has no profile rows, so PATCH returns 200 with updated: 0.

Request — Update profile (phone cleared)
PATCH /api/v1/customers/a1b2c3d4-e5f6-7890-abcd-ef1234567890?merchant_id=8f14e45f-ceea-467e-8b0f-1a2b3c4d5e6f
Content-Type: application/json
Authorization: Bearer <access_token>

{
  "customer_name": "Jane A. Doe",
  "email": "jane.doe@example.com",
  "phone": null
}
Response
{
  "success": true,
  "data": {
    "updated": 2
  },
  "meta": {
    "request_id": "req_def456",
    "timestamp": "2026-01-15T10:31:00.000Z"
  }
}

Deleting Customer Data

DELETE is a merchant-scoped client removal, not a GDPR-complete erasure. In a single transaction it erases the customer's profile PII and archives their cards under this merchant. It is idempotent: a repeat DELETE returns 200 with zero counts.

What DELETE removes

  • Profile PII (customer_name, email, phone, birthday, gender) — the profile rows are deleted.
  • The customer's cards under this merchant are archived (status set to archived), not hard-deleted.
  • Push registrations (device tokens) for those cards are deleted.
  • Merchant-scoped customer sessions (which hold IP address and user agent) are deleted.

What DELETE intentionally retains

  • Card transactions, including any free-text transaction notes — financial history is never cascade-wiped.
  • Punch rewards earned by the customer.
  • The customer's login identity (their auth account) — they may hold cards at other merchants.

Wallet pass behavior

Apple Wallet passes receive no final push notification on DELETE (the device tokens are deleted in the same transaction). Apple passes pick up the archived state on their next pass web service poll. Google Wallet passes are updated through the registration-independent path.

Response

DELETE returns four counts: cards_archived, profiles_deleted, push_registrations_deleted, and sessions_deleted. A second DELETE returns all zeros and sends no wallet notifications.

Request — Delete customer data
DELETE /api/v1/customers/a1b2c3d4-e5f6-7890-abcd-ef1234567890?merchant_id=8f14e45f-ceea-467e-8b0f-1a2b3c4d5e6f
Authorization: Bearer <access_token>
Response
{
  "success": true,
  "data": {
    "cards_archived": 2,
    "profiles_deleted": 2,
    "push_registrations_deleted": 3,
    "sessions_deleted": 1
  },
  "meta": {
    "request_id": "req_ghi789",
    "timestamp": "2026-01-15T10:32:00.000Z"
  }
}

Error Codes

These endpoints may return the following endpoint-specific error codes.

CodeStatusDescription
OWNER_REQUIRED403The caller is not a merchant owner (a system user token, or a user owning no merchant).
MERCHANT_NOT_AUTHORIZED403The caller is an owner but does not own the requested merchant_id.
CUSTOMER_NOT_FOUND404The customer has no card instance of any status under the verified merchant.
VALIDATION_ERROR400A query parameter or request body failed validation (missing merchant_id, empty PATCH body, unknown field, or an invalid value).