> ## Documentation Index
> Fetch the complete documentation index at: https://docs.feddi.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Cashier-panel customer lookup: identity + balance + activity + badge

> The cashier-panel read model, hit at the start of nearly every order. Resolve a customer by `phone` (E.164) or `provider_customer_id` (the POS provider's customer id) and return a single at-a-glance projection: the identity `status`, the customer's name and member-since, both money classes (`balance.spendable_minor`, the actual cashable balance, and `balance.locked_promo_minor`, the `LOCKED` cashback awaiting register-to-claim), lifetime activity (spend, visit count, last visit), and a derived `badge` (`new` / `regular` / `vip` / `lapsed`).

**All four states resolve to `200`, never `404`.** A `not_found` (stranger) is a valid business answer that drives the cashier's 'register this customer' call to action; returning `404` would route a normal lookup through the error pipeline and deny the cashier the register affordance. A `disabled` (operator-shut-off) identity is surfaced as `not_found` deliberately: no PII, no register CTA. A `pending_proof` proto-wallet returns `unclaimed_cashback` (the register-to-claim hook). Tenant isolation is structural: every query is scoped to the enterprise resolved from the verified POS-terminal JWT, never from request input, so the same phone at a different enterprise resolves a different row set and cross-enterprise reads are impossible (they surface as `not_found`). `branch_id` is echoed unspoofably from the JWT scope. Read-only and rate-limited. The `pending_proof` state gates all money side effects: this read surfaces the proto-wallet but never credits.



## OpenAPI

````yaml /api-reference/openapi.yaml get /customers/lookup
openapi: 3.1.0
info:
  title: Feddi Partner API
  version: '2026-06-01'
  description: >-
    Operate a Feddi closed-loop wallet and loyalty program at the point of sale:
    identify customers, read balances, accept wallet payments, run top-ups, and
    reconcile transactions.


    All amounts are integer minor units with an explicit ISO-4217 currency.
    Balances are merchant-held and closed-loop.


    Every response uses a typed envelope (`ok` / `data` / `error` / `meta`) with
    string error codes (never a bare HTTP number) and an idempotency-replay
    flag. Authenticate with the `x-api-key` header; exchange it for a
    short-lived POS terminal JWT for hot-path calls.


    Self-validate against this document (`GET /openapi`) and read `GET
    /capabilities` for the credential types, currencies, and features enabled
    for your integration before assuming any enum.


    Operations are tagged `x-feddi-availability: ga` (stable) or `beta`
    (callable, contract may still change additively); confirm what is enabled
    for your credentials via `GET /capabilities`.


    Sandbox access is granted per partner agreement; request credentials from
    your Feddi contact.
servers:
  - url: https://api.feddi.io/v1/partner
    description: production
  - url: https://api.dev.feddi.io/v1/partner
    description: dev
security:
  - ApiKeyAuth: []
tags:
  - name: platform
    description: >-
      Platform cross-cutting: health, capabilities, openapi self-serve, merchant
      provisioning, settlement, reconciliation.
  - name: checkout_session
    description: >-
      Checkout sessions: every interaction opens a session, then identity and
      basket attach to it, and payment, top-up, and offers run against it.
  - name: auth
    description: >-
      Partner authentication + onboarding: API key lifecycle, POS terminal JWT
      exchange, terminal heartbeat.
  - name: customers
    description: >-
      Customer lookup + identification: resolve identity, cashier-panel
      summaries, preferences, GDPR export/erase.
  - name: enrollment
    description: >-
      Enrollment + signup: OTP enroll, cashback claims, identity/consent,
      customer correction + merge.
  - name: payments
    description: >-
      Payments + redemption: debit wallet (promo-first), balance-check, void,
      refund, QR mint.
  - name: topup
    description: >-
      Wallet top-up: 2-step prepare/confirm, reload-bonus grants, SKU top-up,
      settlement + reconciliation.
  - name: incentives
    description: >-
      Incentives + offers: offer feeds, apply/redeem/release locks, proposals,
      budget envelopes, points, grant clawback.
  - name: transactions
    description: >-
      Transactions + receipts: transaction detail, void, receipts, disputes,
      settlements, reconciliation, exports.
  - name: webhooks
    description: >-
      Webhooks + events: subscriptions, delivery history + retry, event catalog,
      polling fallback.
paths:
  /customers/lookup:
    get:
      tags:
        - customers
      summary: 'Cashier-panel customer lookup: identity + balance + activity + badge'
      description: >-
        The cashier-panel read model, hit at the start of nearly every order.
        Resolve a customer by `phone` (E.164) or `provider_customer_id` (the POS
        provider's customer id) and return a single at-a-glance projection: the
        identity `status`, the customer's name and member-since, both money
        classes (`balance.spendable_minor`, the actual cashable balance, and
        `balance.locked_promo_minor`, the `LOCKED` cashback awaiting
        register-to-claim), lifetime activity (spend, visit count, last visit),
        and a derived `badge` (`new` / `regular` / `vip` / `lapsed`).


        **All four states resolve to `200`, never `404`.** A `not_found`
        (stranger) is a valid business answer that drives the cashier's
        'register this customer' call to action; returning `404` would route a
        normal lookup through the error pipeline and deny the cashier the
        register affordance. A `disabled` (operator-shut-off) identity is
        surfaced as `not_found` deliberately: no PII, no register CTA. A
        `pending_proof` proto-wallet returns `unclaimed_cashback` (the
        register-to-claim hook). Tenant isolation is structural: every query is
        scoped to the enterprise resolved from the verified POS-terminal JWT,
        never from request input, so the same phone at a different enterprise
        resolves a different row set and cross-enterprise reads are impossible
        (they surface as `not_found`). `branch_id` is echoed unspoofably from
        the JWT scope. Read-only and rate-limited. The `pending_proof` state
        gates all money side effects: this read surfaces the proto-wallet but
        never credits.
      operationId: customersLookup
      parameters:
        - name: phone
          in: query
          required: false
          description: >-
            E.164 phone to resolve the customer (e.g. `+97433001122`),
            pre-format to E.164 before calling. At least one of `phone` /
            `provider_customer_id` is required; the typed phone is authoritative
            and `provider_customer_id` is the no-phone fallback. The server
            applies the SAME routine to this query value that the write path
            applied when storing the stored normalized phone, so the two sides
            byte-match by construction; an input that fails E.164 normalization
            resolves to `not_found` (never an error).
          schema:
            type: string
            example: '+97433001122'
        - name: provider_customer_id
          in: query
          required: false
          description: >-
            The POS provider's own customer id (provider-agnostic), used when no
            phone is available. Resolved within the calling integration's
            enterprise only.
          schema:
            type: string
      responses:
        '200':
          description: >-
            The customer projection. ALWAYS 200, inspect `data.status`
            (`registered` | `pending_proof` | `not_found`). `disabled`
            identities surface as `not_found`. `customer` is null when
            `status=not_found`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/CustomerSummary'
              example:
                ok: true
                data:
                  status: registered
                  customer:
                    name: Layla Hassan
                    phone: '+97433001122'
                    state: VERIFIED
                    member_since: '2025-11-02T08:14:00Z'
                    balance:
                      spendable_minor: 12500
                      locked_promo_minor: 0
                      currency: QAR
                    activity:
                      lifetime_spend_minor: 184000
                      visit_count: 27
                      visit_count_last_90d: 9
                      last_visit_at: '2026-06-01T19:42:00Z'
                    badge: vip
                  unclaimed_cashback: null
                  branch_id: 22222222-2222-2222-2222-222222222222
                  fetched_at: '2026-06-05T09:10:00Z'
                error: null
                meta:
                  request_id: req_lk1
                  idempotency_replayed: false
                  api_version: '2026-06-01'
                  data_completeness_score: 80
        '400':
          description: >-
            `VALIDATION_ERROR` (HTTP 400), neither `phone` nor
            `provider_customer_id` supplied, or a malformed value.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '401':
          description: >-
            `INVALID_API_KEY` (HTTP 401), missing/invalid POS terminal JWT
            (expired, malformed, or wrong audience).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '403':
          description: >-
            `FORBIDDEN` (HTTP 403), the token is valid but not authorized for
            this merchant/branch scope. NOTE: a cross-enterprise customer is NOT
            a 403 here, it resolves to `status:not_found` (HTTP 200) to avoid a
            PII leak. 403 is reserved for an unauthorized merchant/branch scope
            on the token itself.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
      security:
        - PosTerminalJWT: []
components:
  schemas:
    ResponseEnvelope:
      type: object
      description: >-
        The standard response envelope that every enveloped endpoint serializes
        through. `ok` is a boolean discriminator: when `ok` is `true`, the typed
        result is carried in `data`; when `ok` is `false`, a typed error object
        is returned instead. The `meta` object is uniform across the entire API
        surface.
      required:
        - ok
        - meta
      properties:
        ok:
          type: boolean
        data:
          type:
            - object
            - 'null'
        error:
          oneOf:
            - $ref: '#/components/schemas/Error'
            - type: 'null'
        meta:
          $ref: '#/components/schemas/Meta'
    CustomerSummary:
      type: object
      description: >-
        The cashier-panel read model returned by the customer-lookup endpoint.
        The identity `status` enum drives the cashier UI, and **all four states
        return HTTP `200`, never `404`**: a stranger is a valid business answer
        that drives the 'register this customer' affordance rather than an
        error. The projection carries the customer's identity, both money
        classes (`balance.spendable_minor` and `balance.locked_promo_minor`),
        lifetime activity, and a derived `badge`.
      required:
        - status
        - customer
        - fetched_at
      properties:
        status:
          type: string
          enum:
            - registered
            - pending_proof
            - not_found
          description: >-
            identity (a DISABLED identity collapses to `not_found`, so only
            these three surface). `registered` = known/verified customer;
            `pending_proof` = proto-wallet (render the register-to-claim CTA);
            `not_found` = stranger (render the register CTA) OR an
            operator-DISABLED identity (no PII, no CTA) OR a cross-enterprise
            customer (isolation).
        customer:
          oneOf:
            - $ref: '#/components/schemas/CustomerSummaryCustomer'
            - type: 'null'
          description: The customer projection, or null when `status=not_found`.
        unclaimed_cashback:
          oneOf:
            - $ref: '#/components/schemas/UnclaimedCashback'
            - type: 'null'
          description: >-
            Present (non-null) only when LOCKED cashback is waiting, the
            register-to-claim hook (typically with `status=pending_proof`).
        branch_id:
          type:
            - string
            - 'null'
          format: uuid
          description: >-
            Echoed unspoofably from the POS-terminal JWT scope; null for
            enterprise/brand-scoped tokens.
        fetched_at:
          type: string
          format: date-time
          description: When this projection was read (live aggregate timestamp).
    Error:
      type: object
      description: >-
        The typed error object returned with every non-2xx response. `code` is a
        string enum (for example `WALLET_PROGRAM_AMBIGUOUS`,
        `CREDENTIAL_TYPE_UNSUPPORTED`, `INSUFFICIENT_FUNDS`,
        `IDEMPOTENCY_KEY_REUSED`, `CURRENCY_NOT_SUPPORTED`), **never a bare HTTP
        status number**. Inspect `code` for programmatic branching, not the HTTP
        status. Some codes echo the valid set in `details` so clients can
        present or reconcile the accepted values: for example
        `CREDENTIAL_TYPE_UNSUPPORTED` lists the supported credential types,
        `CURRENCY_NOT_SUPPORTED` lists the supported ISO-4217 currencies, and
        `WALLET_PROGRAM_AMBIGUOUS` lists the candidate `wallet_program_id`
        values that matched the request.
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - INSUFFICIENT_FUNDS
            - INVALID_API_KEY
            - CREDENTIAL_TYPE_UNSUPPORTED
            - CURRENCY_NOT_SUPPORTED
            - IDEMPOTENCY_KEY_REUSED
            - WALLET_PROGRAM_AMBIGUOUS
            - REQUIRES_DYNAMIC_CREDENTIAL
            - NOT_FOUND
            - FORBIDDEN
            - VALIDATION_ERROR
            - RATE_LIMITED
            - CONFLICT
            - INTERNAL_SERVER_ERROR
        message:
          type: string
        details:
          type: object
          additionalProperties: true
    Meta:
      type: object
      description: >-
        The uniform response metadata block returned on every response.
        `data_completeness_score` is computed per call and reports the
        completeness of the returned data. `decision_trace_id` is present on
        responses that carry decision or insight output and can be used to
        correlate the response with its reasoning. `capabilities` is an
        additive, response-level array of hints advertising features the caller
        may use, and may be extended over time without notice.
      required:
        - request_id
        - api_version
      properties:
        request_id:
          type: string
          description: Feddi-issued correlation id for this response.
        idempotency_replayed:
          type: boolean
          description: True when this response was replayed from the idempotency store.
        api_version:
          type: string
          description: The single API version field.
          example: '2026-06-01'
        data_completeness_score:
          type: integer
          minimum: 0
          maximum: 100
          description: >-
            0-100, computed per call (basket/identity/tax/category/consent
            presence).
        decision_trace_id:
          type:
            - string
            - 'null'
          description: Opaque trace id on intelligence-bearing responses; one per response.
        capabilities:
          type: object
          additionalProperties: true
          description: Additive response-level capability hints.
    CustomerSummaryCustomer:
      type: object
      description: >-
        The resolved customer's projection. Present (non-null) for `registered`
        and `pending_proof` states.
      required:
        - phone
        - state
        - member_since
        - balance
        - activity
        - badge
      properties:
        name:
          type:
            - string
            - 'null'
          description: >-
            Customer name (from the enterprise-scoped wallet owner, or the
            global User by the already-known phone).
        phone:
          type: string
          description: E.164 phone the summary resolved to.
        state:
          type: string
          enum:
            - VERIFIED
            - PENDING_PROOF
            - DISABLED
          description: >-
            The underlying wallet identity state machine value. (DISABLED is
            never returned in a customer body, a DISABLED identity surfaces as
            top-level `status:not_found` with a null customer.)
        member_since:
          type: string
          format: date-time
          description: >-
            When this customer first became known at the merchant (the wallet
            creation).
        balance:
          $ref: '#/components/schemas/CustomerBalanceSummary'
        activity:
          $ref: '#/components/schemas/CustomerActivitySummary'
        badge:
          $ref: '#/components/schemas/CustomerBadge'
    UnclaimedCashback:
      type: object
      description: >-
        LOCKED promotional cashback awaiting the customer's registration (the
        register-to-claim hook).
      required:
        - amount_minor
      properties:
        amount_minor:
          type: integer
          description: Sum of LOCKED promotional grant remaining, minor units.
        currency:
          type:
            - string
            - 'null'
          description: ISO-4217 currency, or null.
    CustomerBalanceSummary:
      type: object
      description: >-
        A customer's wallet money in a single wallet program, broken into its
        two spendable classes. The `balance.spendable_minor` field reports total
        spendable balance as a minor-units integer (for example, `1050` for
        `10.50`), and the response declares its currency explicitly as an
        ISO-4217 code. Money is split between promotional credit and the
        customer's own funds. Promotional credit is spent first (promo-first,
        FIFO ordering) before own funds are drawn. Promotional grants progress
        through `LOCKED`, `RELEASED`, `CLAWED_BACK`, and `EXPIRED` states, and a
        grant must be registered to claim before its value becomes spendable.
      required:
        - spendable_minor
        - locked_promo_minor
      properties:
        spendable_minor:
          type: integer
          description: >-
            Spendable (actual, cashable) balance, the canonical Wallet.balance
            scalar, minor units. The customer's claim against the merchant's
            deferred-revenue liability.
        locked_promo_minor:
          type: integer
          description: >-
            Sum of LOCKED promotional grants (cashback awaiting
            register-to-claim), minor units. Non-cashable until the customer
            verifies.
        currency:
          type:
            - string
            - 'null'
          description: >-
            ISO-4217 of the money above, or null when the customer has no money
            of either kind.
    CustomerActivitySummary:
      type: object
      description: Lifetime + recent activity aggregates feeding the badge classifier.
      required:
        - lifetime_spend_minor
        - visit_count
        - visit_count_last_90d
      properties:
        lifetime_spend_minor:
          type: integer
          description: Lifetime COMPLETED spend (PAYMENTs out of the wallet), minor units.
        visit_count:
          type: integer
          description: Lifetime count of COMPLETED purchases.
        visit_count_last_90d:
          type: integer
          description: COMPLETED purchases in the last 90 days.
        last_visit_at:
          type:
            - string
            - 'null'
          format: date-time
          description: Most recent COMPLETED activity (pay OR top-up), or null.
    CustomerBadge:
      type: string
      enum:
        - new
        - regular
        - vip
        - lapsed
      description: >-
        An at-a-glance classification of a customer for display to cashiers,
        derived from the customer's activity aggregates (purchase history, visit
        count, lifetime spend, and recency). Exactly one badge value is returned
        per customer. **Precedence is fixed: the lapse check is evaluated before
        the VIP check**, so a high-value but inactive customer reads `lapsed`
        rather than `vip` (the win-back signal out-ranks `vip`). Values: `new`
        indicates no purchases, or a single purchase within the new-member
        window. `vip` indicates lifetime spend at or above the configured
        threshold, or visit count at or above the configured threshold. `lapsed`
        indicates no activity for at least the configured lapse-days threshold.
        `regular` indicates any customer not matching the preceding cases.
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Partner API key (platform- or merchant-scoped). Contract key-enforces
        context.merchant_id.
    PosTerminalJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        POS terminal JWT minted by /auth/token; carries integrationId +
        terminal/cashier claims (server-trusted).

````