> ## 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.

# The checkout session

> A checkout session groups the calls of one customer interaction. Learn its states, attach identity to it, and read the CustomerContext it returns.

A checkout session groups the calls of one customer interaction at the point of sale: identifying the customer, then paying against their wallet. Each call carries the session id so the interaction is tracked as one record.

You do not create or close a session directly today. The one session operation you call is identify: it attaches a customer identity to a session and returns their wallet context. The rest of this page documents that call, its request and response fields, and the states a session moves through.

<Info>
  A standalone entry point that opens or closes a session is not yet available. Today you attach identity to a session with the call below, and you settle by calling `POST /v1/partner/payments`. See [Payments and redemption](/guides/payments).
</Info>

## Session states

A session moves through four states:

```text theme={null}
OPEN ──▶ IDENTIFIED ──▶ AUTHORIZED ──▶ CLOSED / ABANDONED
```

* `OPEN`: the session exists and carries an `anonymous_session_id`, but no identity is attached yet.
* `IDENTIFIED`: a credential resolved the customer. The session now carries the customer's wallet context.
* `AUTHORIZED`: written only by a successful session payment. You never set this directly.
* `CLOSED`: settled into a receipt of record.
* `ABANDONED`: the interaction ended without settlement. A session that times out auto-abandons at its expiry.

The identify call moves a session from `OPEN` to `IDENTIFIED`. It accepts a session that is `OPEN` or already `IDENTIFIED` (a re-identify on an identified session is allowed and idempotent). A session that is `CLOSED` or `ABANDONED` cannot be identified and returns `VALIDATION_ERROR`.

The session lifecycle runs identify, then settle, then close:

```mermaid theme={null}
sequenceDiagram
    participant POS as POS / Cashier
    participant Feddi as Feddi Partner API
    participant Cust as Customer

    Note over POS,Feddi: Session is OPEN
    Cust->>POS: Present credential
    POS->>Feddi: POST /checkout/sessions/{id}/identify
    Feddi-->>POS: CustomerContext (balance, resolution_state)
    Note over POS,Feddi: Session is IDENTIFIED

    POS->>Feddi: POST /payments (payment credential)
    Feddi-->>POS: Payment result (promo + actual split, balance_after)
    Note over POS,Feddi: Session is AUTHORIZED

    POS->>Cust: Print receipt of record
    Note over POS,Feddi: Session is CLOSED
```

## Attach identity to a session

`POST /v1/partner/checkout/sessions/{id}/identify` attaches a customer identity to an open session and returns their `CustomerContext`. It dispatches on a credential discriminated union (`phone`, `card_fingerprint`, `short_code`, `provider_customer_id`, or `qr`) and merges the session's `anonymous_session_id` into the resolved customer (a one-way, idempotent merge).

<Warning>
  This call never returns a debit token or payment token. Identity resolution and money movement are strictly separated. To charge a customer, call `POST /v1/partner/payments` with a payment credential. See [Payments and redemption](/guides/payments).
</Warning>

### Request

This call is authenticated with a POS terminal JWT. Send exactly one credential block per call; `credential_type` is the discriminator.

<ParamField header="Idempotency-Key" param-type="string" required="true">
  Partner-generated UUID, the one dedup key. Stable for this one logical operation, 24h TTL. Same key plus same payload replays the original response; same key plus a different payload returns `IDEMPOTENCY_KEY_REUSED` (HTTP 422).
</ParamField>

<ParamField path="id" param-type="string" required="true">
  The checkout session id (UUID) to attach identity to. The session must be `OPEN` or `IDENTIFIED`. When you also send `context.feddi_session_id`, it must match this path value.
</ParamField>

<ParamField body="context" param-type="object" required="true">
  Request context. Carries `merchant_id` (UUID, required) and the optional `branch_id`, `terminal_id`, `cashier_id`, `partner_session_id`, and `feddi_session_id`. The `merchant_id` is enforced against the calling credential's authority, not trusted as free text.
</ParamField>

<ParamField body="customer" param-type="object" required="true">
  One credential block, discriminated by `credential_type`. One of `phone`, `card_fingerprint`, `short_code`, `provider_customer_id`, or `qr`. Set the matching value field: `phone` (E.164), `provider_customer_id`, `card_fingerprint` (masked-PAN hash), `short_code` (5-character wallet code), or `qr` (raw QR payload). The enabled set for your integration is in `GET /v1/partner/capabilities` under `supported_identify_credential_types`.
</ParamField>

<ParamField body="wallet_program_id" param-type="string" required="false">
  Disambiguate when the identity resolves to multiple wallet programs. Omit it on the first call; if the response is `WALLET_PROGRAM_AMBIGUOUS`, re-send with the chosen `wallet_program_id`.
</ParamField>

<ParamField body="meta" param-type="object" required="false">
  Request metadata. Carries `partner_request_id` (UUID, your correlation id, never the dedup basis), `api_version`, and the optional `occurred_at` and `sent_at` timestamps.
</ParamField>

### Response

A `200` returns a `CustomerContext`. It never contains a debit token.

<ResponseField name="resolution_state" field-type="string" required="true">
  One of `registered`, `pending_proof`, `not_found`. `registered` is fully enrolled, balance readable and spendable. `pending_proof` is enrolled but not yet identity-verified; balance is visible but spend may be gated by the POS. `not_found` means no wallet user resolved for this credential under this enterprise, returned as HTTP `200` to distinguish it from a server error. Render a register CTA, not an error.
</ResponseField>

<ResponseField name="identity_trace_id" field-type="string" required="true">
  Opaque correlation id for this identity resolution. Persist it and echo it where a call accepts one.
</ResponseField>

<ResponseField name="wallet_user_id" field-type="string">
  The customer's `wallet_user_id`. Null when `resolution_state` is `not_found`.
</ResponseField>

<ResponseField name="wallet_program_id" field-type="string">
  The wallet program binding. Null when `resolution_state` is `not_found`. An ambiguous match returns `WALLET_PROGRAM_AMBIGUOUS` (HTTP 422) rather than this field.
</ResponseField>

<ResponseField name="wallet_id" field-type="string">
  The canonical `wallet_id` (ledger row). Null when `resolution_state` is `not_found` or the customer's wallet has not been created yet (no credits).
</ResponseField>

<ResponseField name="display_name" field-type="string">
  Customer display name. Null when unknown.
</ResponseField>

<ResponseField name="badge" field-type="string">
  Loyalty tier badge label. Null when the program has no tiering.
</ResponseField>

<ResponseField name="balance" field-type="object">
  Wallet balance breakdown, or null when `resolution_state` is `not_found`. Carries the two money classes and a per-grant breakdown.
</ResponseField>

The `balance` object splits the wallet into two money classes. See [Money: actual vs promotional](/concepts/money-classes) for the full model.

<Expandable title="balance fields">
  <ResponseField name="actual_minor" field-type="integer" required="true">
    Real money balance in minor units, funded by top-ups.
  </ResponseField>

  <ResponseField name="promo_available_minor" field-type="integer" required="true">
    Released promotional credits available to spend, in minor units. Spent promo-first before `actual_minor`.
  </ResponseField>

  <ResponseField name="promo_locked_minor" field-type="integer" required="true">
    Locked promotional credits not yet spendable. See [PENDING\_PROOF and promo release](/concepts/pending-proof).
  </ResponseField>

  <ResponseField name="pending_topups_minor" field-type="integer" required="true">
    Sum of top-up value in flight, not yet spendable.
  </ResponseField>

  <ResponseField name="currency" field-type="string" required="true">
    ISO 4217 currency code for every minor-unit field in this balance.
  </ResponseField>

  <ResponseField name="promo_grants" field-type="array" required="true">
    Per-grant breakdown, sorted FIFO by expiry. Each grant carries `source` (one of `CASHBACK`, `RELOAD_BONUS`, `SKU_TOPUP_BONUS`, `GATEWAY_BONUS`, `SIGNUP_BONUS`), `state` (one of `LOCKED`, `RELEASED`, `CLAWED_BACK`, `EXPIRED`), and `remaining_minor`.
  </ResponseField>
</Expandable>

The response `meta` block carries two opaque fields. `data_completeness_score` is an opaque integer Feddi may use internally; no action required. `decision_trace_id` is an opaque correlation id; persist and echo it where a call accepts one.

### Example

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.feddi.io/v1/partner/checkout/sessions/33333333-3333-3333-3333-333333333333/identify \
    -H "Authorization: Bearer $POS_TERMINAL_JWT" \
    -H "Idempotency-Key: 9a01b2c3-0c4a-4f9b-9d1e-1a2b3c4d5e6f" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": {
        "partner_request_id": "9a01b2c3-0002-4a3b-9c2d-1a2b3c4d5e6f",
        "occurred_at": "2026-06-05T09:02:00Z",
        "sent_at": "2026-06-05T09:02:00Z",
        "api_version": "2026-06-01"
      },
      "context": {
        "merchant_id": "11111111-1111-1111-1111-111111111111",
        "branch_id": "22222222-2222-2222-2222-222222222222",
        "terminal_id": "POS-360-0007",
        "feddi_session_id": "33333333-3333-3333-3333-333333333333"
      },
      "customer": { "credential_type": "phone", "phone": "+97433001122" },
      "wallet_program_id": null
    }'
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "resolution_state": "registered",
      "identity_trace_id": "idt-trace-xyzabc",
      "wallet_user_id": "wu-aabbccdd-1234",
      "wallet_program_id": "wp-55667788-0001",
      "wallet_id": "wlt-99001122-ffff",
      "display_name": "Ahmed Al-Rashid",
      "badge": "Gold",
      "balance": {
        "actual_minor": 12500,
        "promo_available_minor": 500,
        "promo_locked_minor": 200,
        "pending_topups_minor": 0,
        "currency": "QAR",
        "promo_grants": []
      }
    },
    "error": null,
    "meta": {
      "request_id": "req_cs_id1",
      "idempotency_replayed": false,
      "api_version": "2026-06-01",
      "data_completeness_score": 82,
      "decision_trace_id": null
    }
  }
  ```

  ```json 422 Unsupported credential theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "CREDENTIAL_TYPE_UNSUPPORTED",
      "message": "Credential type 'card_fingerprint' is not enabled for this integration.",
      "details": {
        "requested": "card_fingerprint",
        "supported": ["phone", "provider_customer_id", "short_code"]
      }
    },
    "meta": {
      "request_id": "req_cs_id_unsup",
      "idempotency_replayed": false,
      "api_version": "2026-06-01"
    }
  }
  ```
</CodeGroup>

### Errors

Inspect `error.code`, not the HTTP status, for programmatic branching. See [Idempotency and errors](/concepts/idempotency-and-errors) for the full catalog.

| HTTP | `error.code`                  | When                                                                                                                                   | Recovery                                                                                                       |
| ---- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| 400  | `VALIDATION_ERROR`            | Malformed body, or the credential value is missing for the declared `credential_type`.                                                 | Fix the request body. `details` carries the field errors.                                                      |
| 400  | `VALIDATION_ERROR`            | The session is `CLOSED` or `ABANDONED` and cannot be identified.                                                                       | Identify only an `OPEN` or `IDENTIFIED` session.                                                               |
| 401  | `INVALID_API_KEY`             | Missing or invalid POS terminal JWT (expired, malformed, or wrong audience).                                                           | Re-mint the terminal JWT.                                                                                      |
| 403  | `FORBIDDEN`                   | The key is valid but not authorized for this session's merchant or branch scope.                                                       | Use a credential scoped to this merchant.                                                                      |
| 404  | `NOT_FOUND`                   | The session does not exist or belongs to another integration. Cross-enterprise identity lookups also resolve here to avoid a PII leak. | For a cross-enterprise miss, the customer surfaces as `resolution_state: not_found` on a 200, not this error.  |
| 422  | `CREDENTIAL_TYPE_UNSUPPORTED` | The credential type is not enabled for this integration.                                                                               | `details` lists the supported set. Read `GET /v1/partner/capabilities` first.                                  |
| 422  | `WALLET_PROGRAM_AMBIGUOUS`    | The identity resolves to multiple wallet programs.                                                                                     | `details` lists the candidate `wallet_program_id` values. Re-send with the chosen one.                         |
| 422  | `IDEMPOTENCY_KEY_REUSED`      | Same `Idempotency-Key`, different payload.                                                                                             | Your key generation is buggy. A key must be stable for one logical operation and unique across different ones. |

## Invariants

Hold these when you render the result:

* Identify never returns a debit token. To debit, call `POST /v1/partner/payments` with a payment credential.
* `not_found` is HTTP `200`, not `404`, with `resolution_state: not_found`. Render a register CTA, not an error.
* A `card_fingerprint` link is probationary until the phone is verified. A masked-PAN collision never auto-acts on money.
* Identify never releases a `LOCKED` promotional grant on its own. Promo release stays gated on a verified identity plus the claim flow. See [PENDING\_PROOF and promo release](/concepts/pending-proof).
* A customer's balance is scoped per merchant.

<Tip>
  For a balance-check panel with no active session, use `GET /v1/partner/customers/lookup` or the standalone `POST /v1/partner/identify` instead. See [Customers and identification](/guides/customers).
</Tip>

## Next

<Columns cols="2">
  <Card title="Money: actual vs promotional" icon="coins" href="/concepts/money-classes">
    The two-class balance model that runs through every endpoint.
  </Card>

  <Card title="Identity and credentials" icon="fingerprint" href="/concepts/identity-and-credentials">
    The credential types and how a call resolves who the customer is.
  </Card>

  <Card title="Payments and redemption" icon="credit-card" href="/guides/payments">
    The promo-first wallet-debit endpoint you settle against.
  </Card>

  <Card title="Make your first payment" icon="route" href="/getting-started/make-your-first-payment">
    The identify-to-pay loop, worked end to end.
  </Card>
</Columns>
