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

# Platform and discovery

> Probe liveness, read what your integration can transact in, fetch the live contract, and resolve a customer credential before you build against the API.

## Overview

The platform endpoints answer three questions before you write a transactional call: is the API reachable, what is this integration allowed to do, and what does the contract say. All three are live and safe to call today.

We recommend reading `GET /capabilities` before you assume any enum, currency, or feature flag. It is tenant-scoped and authoritative. The contract at `GET /openapi` is served live from the running API, so it never drifts from the surface you call.

This page also covers standalone identity resolution with `POST /identify`, which resolves a customer credential to their wallet state without opening a checkout session.

## Operations

| Operation         | Method and path                | Auth                        |
| ----------------- | ------------------------------ | --------------------------- |
| `getHealth`       | `GET /v1/partner/health`       | None                        |
| `getCapabilities` | `GET /v1/partner/capabilities` | `x-api-key`                 |
| `getOpenApiSpec`  | `GET /v1/partner/openapi`      | None                        |
| `identify`        | `POST /v1/partner/identify`    | `x-api-key` or terminal JWT |

<Info>
  The authoritative list of mounted routes is always `GET /capabilities` and `GET /openapi`, never a static table. Call them at runtime to discover what your integration can transact in.
</Info>

## Health versus capabilities

These two endpoints look similar and serve different jobs. Use each for its own purpose.

<Columns cols="2">
  <Card title="GET /health is liveness" icon="activity" horizontal="false">
    Unauthenticated, stateless, no database read, no tenant resolution. Mounted ahead of the auth layer. Carries zero tenant data. Use it for load-balancer probes and POS terminals that gate feature availability without holding a key.
  </Card>

  <Card title="GET /capabilities is read-before-you-assume" icon="list-checks" horizontal="false">
    Authenticated and tenant-scoped. Returns the supported currencies, credential types, feature flags, and rate limits for the integration your key authenticated as. Read it before you build against any enum.
  </Card>
</Columns>

A liveness probe answers instantly under load and carries no tenant fields, so it does no database work. A capability descriptor must be tenant-accurate, so it requires a key and a tenant resolution. The two stay split.

## GET /health

Check that the platform is reachable. The response is `{ ok: true, status: "up", degraded_subsystems: [] }` when everything is nominal.

A non-empty `degraded_subsystems` array means a partial outage. Surface a warning on the POS, and continue for operations that do not depend on the impaired subsystem.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.feddi.io/v1/partner/health
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "status": "up",
    "degraded_subsystems": []
  }
  ```
</CodeGroup>

<ResponseField name="ok" field-type="boolean" required="true">
  Always `true` on HTTP 200. The platform is reachable.
</ResponseField>

<ResponseField name="status" field-type="string" required="true">
  One of `up` (all subsystems nominal) or `degraded` (one or more subsystems impaired but the platform is still serving).
</ResponseField>

<ResponseField name="degraded_subsystems" field-type="array" required="true">
  Named subsystems currently impaired, for example `["promo_engine", "webhook_fanout"]`. Empty when `status` is `up`.
</ResponseField>

<Info>
  `GET /health` returns a bare liveness object, not the standard response envelope. It is mounted before auth so it can stay this lean. Every other operation on this page returns the `{ ok, data, error, meta }` envelope.
</Info>

## GET /capabilities

Read what your integration can transact in. This is the runtime source of truth for currencies, credential types, feature flags, and rate limits, scoped to the integration your `x-api-key` resolves to.

Read this before assuming any enum. Empty collections mean a feature is not yet provisioned for this integration, not that it is unsupported globally. The descriptor is additive: it never over-claims.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.feddi.io/v1/partner/capabilities \
    -H "x-api-key: fddi_live_xxxxxxxxxxxxxxxx"
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "api_version": "2026-06-01",
      "integration_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
      "provider": "ODOO",
      "supported_currencies": ["QAR", "SAR"],
      "supported_identify_credential_types": ["phone", "qr", "short_code"],
      "supported_payment_credential_types": ["otp", "qr"],
      "features": {
        "identify": true,
        "checkout": true,
        "topup": false,
        "promo_grants": false
      },
      "rate_limits": {
        "identify": { "per_minute": 120 },
        "payments": { "per_minute": 60 }
      }
    },
    "error": null,
    "meta": {
      "request_id": "req_a1b2c3",
      "idempotency_replayed": false,
      "api_version": "2026-06-01"
    }
  }
  ```

  ```json 401 Unauthorized theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "INVALID_API_KEY",
      "message": "Missing or invalid x-api-key."
    },
    "meta": {
      "request_id": "req_a1b2c3",
      "api_version": "2026-06-01"
    }
  }
  ```
</CodeGroup>

<ParamField header="x-api-key" param-type="string" required="true">
  Your integration's API key (format `fddi_...`). The descriptor returned is scoped to the integration this key authenticates as.
</ParamField>

<ResponseField name="api_version" field-type="string" required="true">
  The single canonical API version string, for example `2026-06-01`.
</ResponseField>

<ResponseField name="integration_id" field-type="string" required="true">
  The integration UUID the key authenticated as.
</ResponseField>

<ResponseField name="provider" field-type="string" required="true">
  Provider name, for example `ODOO` or `FOODICS`.
</ResponseField>

<ResponseField name="supported_currencies" field-type="array" required="true">
  ISO 4217 codes this integration can transact in, derived from active wallet program assignments. Empty means none provisioned yet.
</ResponseField>

<ResponseField name="supported_identify_credential_types" field-type="array" required="true">
  Credential types the integration's wallet programs accept for identity resolution (who the customer is). This array, alongside the `openapi` schema, is the authoritative enabled set. Empty means none provisioned yet.
</ResponseField>

<ResponseField name="supported_payment_credential_types" field-type="array" required="true">
  Credential types accepted as wallet-debit authorization (proof the customer approves this payment), for example `otp` and `qr`. A type enabled for identification is not implicitly enabled for payment; the two sets are independent.
</ResponseField>

<ResponseField name="features" field-type="object" required="true">
  Named feature flags keyed by operation slug, with boolean values. A missing key or `false` means the feature is not available for this integration.
</ResponseField>

<ResponseField name="rate_limits" field-type="object" required="true">
  Per-operation limits, each carrying a `per_minute` integer that matches the `X-RateLimit-*` headers enforced on the endpoint.
</ResponseField>

<Tip>
  Cache the descriptor, honor its `Cache-Control` header, and re-fetch when you hit a `CREDENTIAL_TYPE_UNSUPPORTED` or `CURRENCY_NOT_SUPPORTED` typed error. Those errors mean your cached view of the integration has gone stale.
</Tip>

A `404` with `RESOURCE_NOT_FOUND` means the integration resolved from the API key is not found or not active.

## GET /openapi

Fetch the CI-validated OpenAPI 3.1 contract for every mounted `/v1/partner/*` route. Validate your integration against this document rather than against generated TypeScript types.

Every mounted route has an entry in the spec, so it stays in sync with the live surface. It is unauthenticated so tooling (Postman, Swagger UI, custom SDK generators) can bootstrap without a key.

```bash theme={null}
curl https://api.feddi.io/v1/partner/openapi
```

The response is a valid OpenAPI 3.1 JSON document. Pair it with `GET /capabilities`: `/openapi` tells you what shapes exist, and `/capabilities` tells you which of them your integration can call right now.

## POST /identify

Resolve a customer credential to their wallet state without opening a checkout session. Use this for balance-check panels, cashier lookup screens, and any POS-display surface where there is no active session.

The request carries exactly one credential, discriminated by `credential_type`. The credential types are `phone`, `card_fingerprint`, `short_code`, `qr`, and `provider_customer_id`. Read `GET /capabilities` and the `openapi` schema for the set enabled on your integration. The response returns the customer's wallet state, both money classes, the wallet program binding, and an `identity_trace_id` for downstream correlation.

<Warning>
  This endpoint never returns a debit token, an authorization token, or any artifact that permits a ledger mutation. It is read-only identity resolution. To pay or top up, use the [payments](/guides/payments) or [topup](/guides/topup) flows.
</Warning>

### Resolution states

The response carries the outcome in `data.resolution_state`, one of three values. An ambiguous match does not return a resolution state: it returns a typed `WALLET_PROGRAM_AMBIGUOUS` error (HTTP 422) instead. See [Identify ambiguity](#identify-ambiguity) below.

<Steps>
  <Step title="registered" icon="user-check" title-type="p">
    Fully enrolled. Balance is readable and spendable through the normal payment flow.
  </Step>

  <Step title="pending_proof" icon="user-cog" title-type="p">
    Enrolled but not yet identity-verified. Balance is visible, but spend may be gated by the POS until the customer proves their phone. See [PENDING\_PROOF and promo release](/concepts/pending-proof).
  </Step>

  <Step title="not_found" icon="user-x" title-type="p">
    No wallet user for this credential under this enterprise. Returned as HTTP 200 with a null `balance`, not 404, so polling UIs can distinguish 'no wallet' from a server error.
  </Step>
</Steps>

### Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.feddi.io/v1/partner/identify \
    -H "x-api-key: fddi_live_xxxxxxxxxxxxxxxx" \
    -H "Idempotency-Key: 7f3a1e20-1b2c-4d5e-8f90-a1b2c3d4e5f6" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": {
        "partner_request_id": "7f3a1e20-1b2c-4d5e-8f90-a1b2c3d4e5f6",
        "occurred_at": "2026-06-05T10:00:00Z",
        "sent_at": "2026-06-05T10:00:01Z",
        "api_version": "2026-06-01"
      },
      "context": {
        "merchant_id": "11111111-1111-1111-1111-111111111111",
        "branch_id": "22222222-2222-2222-2222-222222222222",
        "terminal_id": "POS-360-0001",
        "cashier_id": "cashier-7",
        "partner_session_id": null,
        "feddi_session_id": null
      },
      "credential": {
        "credential_type": "phone",
        "phone": "+97433001122"
      }
    }'
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "resolution_state": "registered",
      "wallet_user_id": "wu-aabbccdd-1234",
      "wallet_program_id": "wp-55667788-0001",
      "wallet_id": "wlt-99001122-ffff",
      "identity_trace_id": "idt-trace-xyzabc",
      "display_name": "Ahmed Al-Rashid",
      "badge": "Gold",
      "balance": {
        "actual_minor": 15000,
        "promo_available_minor": 500,
        "promo_locked_minor": 1200,
        "pending_topups_minor": 0,
        "currency": "QAR",
        "promo_grants": [
          { "source": "CASHBACK", "state": "RELEASED", "remaining_minor": 500, "expires_at": "2026-09-05T00:00:00Z" },
          { "source": "RELOAD_BONUS", "state": "LOCKED", "remaining_minor": 1200, "expires_at": "2026-09-10T00:00:00Z" }
        ]
      }
    },
    "error": null,
    "meta": {
      "request_id": "req_b2c3d4",
      "idempotency_replayed": false,
      "api_version": "2026-06-01",
      "decision_trace_id": null
    }
  }
  ```

  ```json 422 Unprocessable Entity theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "WALLET_PROGRAM_AMBIGUOUS",
      "message": "Credential matches a customer enrolled in multiple active wallet programs.",
      "details": {
        "candidates": ["wp-55667788-0001", "wp-55667788-0002"]
      }
    },
    "meta": {
      "request_id": "req_b2c3d4",
      "api_version": "2026-06-01"
    }
  }
  ```
</CodeGroup>

### Request fields

<ParamField header="Idempotency-Key" param-type="string" required="false">
  A client-generated UUID. A retry with the same key replays the original result. Deduplication is keyed exclusively on this header, never on `partner_request_id`.
</ParamField>

<ParamField body="meta" param-type="object" required="true">
  Request metadata. Requires `partner_request_id` (a correlation UUID, not the dedup key) and `api_version`. Optional `occurred_at` (when the event happened at the POS) and `sent_at` (when you sent the request); both are RFC 3339 timestamps. Send both timestamps.
</ParamField>

<ParamField body="context" param-type="object" required="true">
  Request context. Requires `merchant_id` (a UUID that the calling credential must be authorized for, never a free-text trust field). Optional `branch_id`, `terminal_id`, `cashier_id`, `partner_session_id`, and `feddi_session_id`. `cashier_id` is server-trusted only when signed into the terminal JWT.
</ParamField>

<ParamField body="credential" param-type="object" required="true">
  A discriminated union selected by `credential_type`. Exactly one credential field must be present alongside `credential_type`:

  ```json theme={null}
  { "credential_type": "phone", "phone": "+97433001122" }
  { "credential_type": "card_fingerprint", "card_fingerprint": "fp_a1b2c3d4e5f6" }
  { "credential_type": "short_code", "short_code": "F3A9" }
  { "credential_type": "qr", "qr": "feddi://qr/v1/9f8e7d6c5b4a" }
  { "credential_type": "provider_customer_id", "provider_customer_id": "cust_77f0a1" }
  ```

  The `card_fingerprint` link is probationary and reversible until the phone is verified, so a masked-PAN match never auto-acts on money.
</ParamField>

### Response fields

<ResponseField name="resolution_state" field-type="string" required="true">
  One of `registered`, `pending_proof`, or `not_found`. See [Resolution states](#resolution-states).
</ResponseField>

<ResponseField name="wallet_user_id" field-type="string" required="false">
  The POS-facing identity id. Null when `resolution_state` is `not_found`.
</ResponseField>

<ResponseField name="wallet_program_id" field-type="string" required="false">
  The wallet program binding. Null when `resolution_state` is `not_found`. Ambiguity raises HTTP 422 `WALLET_PROGRAM_AMBIGUOUS` instead of returning this field.
</ResponseField>

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

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

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

<ResponseField name="badge" field-type="string" required="false">
  Current loyalty tier badge label, for example `Gold`. Null when the program has no tiering.
</ResponseField>

<ResponseField name="balance" field-type="object" required="false">
  Wallet balance breakdown. Null when `resolution_state` is `not_found`. Fields:

  * `actual_minor` (integer): real money funded by top-ups, in minor units.
  * `promo_available_minor` (integer): released promotional credit, spendable, in minor units.
  * `promo_locked_minor` (integer): locked promotional credit not yet spendable, in minor units.
  * `pending_topups_minor` (integer): top-up value in flight, not spendable, in minor units.
  * `currency` (string): ISO 4217 code for all minor-unit fields above.
  * `promo_grants` (array): per-grant breakdown, sorted FIFO by `expires_at`.

  Each entry in `promo_grants` carries `source` (one of `CASHBACK`, `RELOAD_BONUS`, `SKU_TOPUP_BONUS`, `GATEWAY_BONUS`, `SIGNUP_BONUS`), `state` (`LOCKED` or `RELEASED`), `remaining_minor` (integer), and `expires_at` (RFC 3339 timestamp, or null if the grant does not expire).
</ResponseField>

The `balance` object exposes both money classes distinctly. Treat `actual_minor` as real money and `promo_available_minor` as released spendable promotional credit; `promo_locked_minor` is gated on signup or first top-up. Never conflate them. Read [Money: actual vs promotional](/concepts/money-classes) for the full model.

<Info>
  `POST /identify` is the standalone, sessionless variant. The session-anchored form, which writes the resolved identity into an open checkout session, is documented under [The checkout session](/concepts/the-checkout-session).
</Info>

### Errors

| Code                       | Status | Recovery                                                                                                                    |
| -------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------- |
| `VALIDATION_ERROR`         | 400    | Malformed body, unrecognized `credential_type`, or missing required credential field. Fix the request and resend.           |
| `INVALID_API_KEY`          | 401    | Missing or invalid `x-api-key` or terminal JWT. Re-authenticate.                                                            |
| `FORBIDDEN`                | 403    | The key is valid but not authorized for this merchant scope. Use a credential authorized for `merchant_id`.                 |
| `WALLET_PROGRAM_AMBIGUOUS` | 422    | The credential matches a customer in multiple active programs. Read `error.details.candidates` and present a disambiguator. |
| `RATE_LIMITED`             | 429    | Per-integration identify quota exceeded. Back off using `Retry-After` and `X-RateLimit-*` headers, then retry.              |

<a id="identify-ambiguity" />

`identify` is rate-limited per integration to prevent enumeration. A `429` response carries `Retry-After` and `X-RateLimit-*` headers; back off and retry.

<Info>
  The `meta.data_completeness_score` field, when present, is an opaque integer Feddi may use internally. No action required. The `meta.decision_trace_id` field is an opaque correlation id. Persist it and echo it where a call accepts one.
</Info>

## Where to go next

<Columns cols="2">
  <Card title="Customers and identification" href="/guides/customers" icon="fingerprint" horizontal="false">
    The cashier-panel lookup and the four-state customer read model in depth.
  </Card>

  <Card title="Authentication" href="/api-reference/authentication" icon="key" horizontal="false">
    Platform key versus terminal JWT, and how the tenant boundary is enforced.
  </Card>

  <Card title="Idempotency and errors" href="/concepts/idempotency-and-errors" icon="shield-check" horizontal="false">
    The Idempotency-Key header and the typed error codes you handle on every mutating call.
  </Card>

  <Card title="Conventions" href="/api-reference/conventions" icon="book-open" horizontal="false">
    The response envelope, money-as-minor-integers, versioning, and pagination.
  </Card>
</Columns>
