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

# Quickstart

> Make your first authenticated calls to the Feddi Partner API: probe health, read capabilities, exchange your key for a terminal JWT, and identify a customer.

## What you build here

You make your first authenticated calls against the Feddi Partner API. By the end you have probed the platform with no key, read what your integration can transact in, exchanged your `x-api-key` for a short-lived terminal JWT, and resolved a customer credential to a profile.

This is the on-ramp. When you are ready to charge a wallet, continue to [Make your first payment](/getting-started/make-your-first-payment).

<Info>
  Every path lives under `/v1/partner`. The base URL is `https://api.feddi.io/v1/partner` for production and `https://api.dev.feddi.io/v1/partner` for dev. Build and self-test against dev first.
</Info>

## Before you start

You need a Feddi API key. Request sandbox credentials from your Feddi contact.

The key is platform- or merchant-scoped and formatted as `fddi_...`. The tenant boundary is enforced on it: `context.merchant_id` must resolve to a merchant the key is authorized for. A `merchant_id` outside the key's authority is rejected, never honored.

<Warning>
  The raw key is shown exactly once when it is created. Store it. A lost key is regenerated, never recovered. See [Authentication](/api-reference/authentication) for the full key lifecycle.
</Warning>

## The four steps

`GET /health` and `GET /capabilities` are GA (stable). The token exchange and identify are Beta: callable today, contract may still change additively. Treat `GET /capabilities` and `GET /openapi` as the runtime source of truth for what is mounted and enabled for your integration.

<Steps>
  <Step title="Probe health, no key required" icon="heart-pulse">
    `GET /health` is unauthenticated. It returns immediately with no database read or tenant resolution, so a POS terminal can gate feature availability without holding a key.
  </Step>

  <Step title="Read your capabilities" icon="list-checks">
    `GET /capabilities` takes your `x-api-key` and returns the currencies, credential types, features, and rate limits enabled for your integration. Read this before assuming any enum is enabled.
  </Step>

  <Step title="Exchange your key for a terminal JWT" icon="key">
    `POST /auth/token` exchanges your `x-api-key` for a short-lived terminal-scoped bearer token. Refresh it before any money call.
  </Step>

  <Step title="Identify a customer" icon="fingerprint">
    `POST /identify` resolves a customer credential (phone, card fingerprint, short code, QR, or provider customer id) to a profile carrying wallet state and balance. Identify never returns a debit token.
  </Step>
</Steps>

## Step 1: Probe health

Call health first. A `200` with `status` of `up` and an empty `degraded_subsystems` array means the platform is nominal. A non-empty array signals a partial outage: surface a warning at the POS, but you may continue operations that are not degraded.

<CodeGroup>
  ```bash Request theme={null}
  curl https://api.dev.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, platform 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>
  Health carries no tenant data by design. It is mounted ahead of the auth layer, so it is the cheapest liveness probe.
</Info>

## Step 2: Read capabilities

Capabilities is the authoritative descriptor of what your integration can transact in. Send your `x-api-key`. Read this before assuming any currency, credential type, or feature flag exists.

<ParamField header="x-api-key" param-type="string" required="true">
  Your platform- or merchant-scoped API key (`fddi_...`). The integration is resolved from this key.
</ParamField>

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

  ```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", "short_code", "qr"],
      "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>

<ResponseField name="supported_currencies" field-type="array" required="true">
  ISO 4217 codes of currencies this integration can transact in. An empty array means no wallet program is provisioned yet, not that currencies are unsupported globally.
</ResponseField>

<ResponseField name="supported_identify_credential_types" field-type="array" required="true">
  The credential types this integration accepts for identity resolution (who the customer is), for example `phone`, `short_code`, and `qr`. This array is the authoritative enabled set. Sending an unsupported type returns `CREDENTIAL_TYPE_UNSUPPORTED` with the supported set in `error.details`.
</ResponseField>

<ResponseField name="supported_payment_credential_types" field-type="array" required="true">
  The credential types this integration accepts as wallet-debit authorization (proof the customer approves this payment), for example `otp` and `qr`. Identity resolution and payment authorization are separate sets: a type listed in one is not implicitly enabled in the other.
</ResponseField>

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

<ResponseField name="rate_limits" field-type="object" required="true">
  Per-operation rate limit descriptors with a `per_minute` integer. These match the `X-RateLimit-*` headers enforced on each endpoint.
</ResponseField>

<Tip>
  Cache this response, honor its `Cache-Control` header, and re-fetch on a `CREDENTIAL_TYPE_UNSUPPORTED` or `CURRENCY_NOT_SUPPORTED` typed error.
</Tip>

## Step 3: Exchange your key for a terminal JWT

Per-terminal money and identity calls use a POS terminal JWT, not the raw `x-api-key`. You exchange the key for a JWT, then send `Authorization: Bearer <jwt>` on those calls.

The JWT is short-lived: TTL 600 seconds. It carries scope claims (`integration_id`, `enterprise_id`, `brand_id`, `branch_id`) derived from the key, plus an optional server-validated `cashier_id`. Refresh it before any money call.

The exchange takes the canonical envelope. Send `meta` and `context`, and an `Idempotency-Key` header. A retried exchange within the validity window replays the same JWT.

<ParamField header="Idempotency-Key" param-type="string" required="true">
  A partner-generated UUID. The single deduplication key for mutating endpoints (24h TTL). Same key with the same payload replays a byte-identical response. Same key with a different payload returns `IDEMPOTENCY_KEY_REUSED` (HTTP `422`).
</ParamField>

<ParamField body="meta" param-type="object" required="true">
  Request metadata. Requires `partner_request_id` (a UUID, for correlation only, never the deduplication key) and `api_version`. Send `occurred_at` and `sent_at` when you have them.
</ParamField>

<ParamField body="context" param-type="object" required="true">
  Request context. Requires `merchant_id` (key-enforced, never free-text). `branch_id`, `terminal_id`, and `cashier_id` are optional.
</ParamField>

<ParamField body="cashier_id" param-type="string" required="false">
  Optional cashier identifier to sign into the JWT scope. Server-validated: 64 characters or fewer, no control characters.
</ParamField>

<CodeGroup>
  ```bash Request wrap="true" theme={null}
  curl -X POST https://api.dev.feddi.io/v1/partner/auth/token \
    -H "x-api-key: fddi_your_key_here" \
    -H "Idempotency-Key: 6f1d2c3a-0000-4abc-9def-000000000001" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": { "partner_request_id": "9a1c4e22-7b3f-4d0a-8e21-0f9c2d3b4a5e", "occurred_at": "2026-06-05T09:00:00Z", "sent_at": "2026-06-05T09:00: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", "cashier_id": "cashier-42" },
      "cashier_id": "cashier-42"
    }'
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...signature",
      "token_type": "Bearer",
      "expires_in": 600,
      "expires_at": "2026-06-05T09:10:00Z",
      "scope": {
        "integration_id": "33333333-3333-3333-3333-333333333333",
        "enterprise_id": "11111111-1111-1111-1111-111111111111",
        "brand_id": "44444444-4444-4444-4444-444444444444",
        "branch_id": "22222222-2222-2222-2222-222222222222",
        "cashier_id": "cashier-42"
      },
      "sandbox": true
    },
    "error": null,
    "meta": { "request_id": "req_tk1", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```

  ```json 401 Unauthorized theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "INVALID_API_KEY",
      "message": "Missing, invalid, revoked, or expired x-api-key, or the key's integration is not active."
    },
    "meta": { "request_id": "req_tk1", "api_version": "2026-06-01" }
  }
  ```
</CodeGroup>

<ResponseField name="token" field-type="string" required="true">
  The compact JWT string. Send it as `Authorization: Bearer <token>` on subsequent terminal-scoped calls.
</ResponseField>

<ResponseField name="token_type" field-type="string" required="true">
  Always `Bearer`.
</ResponseField>

<ResponseField name="expires_in" field-type="integer" required="true">
  Seconds until expiry (600).
</ResponseField>

<ResponseField name="expires_at" field-type="string" required="true">
  Absolute expiry timestamp (RFC 3339).
</ResponseField>

<ResponseField name="scope" field-type="object" required="true">
  Scope claims derived from the key: `integration_id`, `enterprise_id`, and the nullable `brand_id`, `branch_id`, `cashier_id`.
</ResponseField>

<ResponseField name="sandbox" field-type="boolean" required="true">
  Mirrors the originating key's sandbox flag, carried as a JWT claim.
</ResponseField>

<Warning>
  A returned `401` with `error.code` of `INVALID_API_KEY` means the key is missing, invalid, revoked, expired, or its integration is not active. Errors are typed string codes in `error.code`, never a bare HTTP number. See [Idempotency and errors](/concepts/idempotency-and-errors).
</Warning>

## Step 4: Identify a customer

Identify resolves one customer credential to a profile. Send the terminal JWT from Step 3 as `Authorization: Bearer <jwt>`. The standalone endpoint also accepts `x-api-key` for balance-check panels and cashier lookup screens where there is no active checkout.

The call carries the canonical envelope and an `Idempotency-Key` header. The credential is a discriminated union on `credential_type`. The enabled set is gated by `GET /capabilities`.

<ParamField body="credential" param-type="object" required="true">
  Exactly one credential, discriminated by `credential_type`. One of `phone`, `card_fingerprint`, `short_code`, `qr`, or `provider_customer_id`. Provide the matching value field alongside it, for example `phone` for `+97433001122`.
</ParamField>

<ParamField body="meta" param-type="object" required="true">
  Request metadata. Requires `partner_request_id` and `api_version`.
</ParamField>

<ParamField body="context" param-type="object" required="true">
  Request context. Requires `merchant_id`.
</ParamField>

The `data.resolution_state` field returns one of three states:

<ResponseField name="resolution_state" field-type="string" required="true">
  One of `registered` (balance readable and spendable), `pending_proof` (enrolled but not yet identity-verified, balance visible, spend may be gated by the POS), or `not_found` (no wallet user for this credential, returned as HTTP `200` with a null balance so polling UIs distinguish it from a server error).
</ResponseField>

<Info>
  A credential that matches a customer enrolled in multiple active programs is not a resolution state. It returns the typed `WALLET_PROGRAM_AMBIGUOUS` error (HTTP `422`) with the candidate `wallet_program_id` values in `error.details.candidates`.
</Info>

<CodeGroup>
  ```bash Request wrap="true" theme={null}
  curl -X POST https://api.dev.feddi.io/v1/partner/identify \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...signature" \
    -H "Idempotency-Key: 6f1d2c3a-0000-4abc-9def-000000000002" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": { "partner_request_id": "7f3a1e20-1b2c-4d5e-8f90-a1b2c3d4e5f6", "occurred_at": "2026-06-05T09:01:00Z", "sent_at": "2026-06-05T09:01: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", "cashier_id": "cashier-42" },
      "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"
      }
    },
    "error": null,
    "meta": { "request_id": "req_id1", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```

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

Key response fields:

<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`. An ambiguous match returns `WALLET_PROGRAM_AMBIGUOUS` (HTTP `422`) instead of this field.
</ResponseField>

<ResponseField name="wallet_id" field-type="string" required="false">
  The ledger id. Null when `resolution_state` is `not_found` or the wallet has no credits yet.
</ResponseField>

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

<ResponseField name="balance" field-type="object" required="false">
  The wallet balance breakdown, or null when `resolution_state` is `not_found`. Money is always minor units with an explicit currency: `actual_minor` (real money), `promo_available_minor` (spendable promotional credit), `promo_locked_minor` (promotional credit gated on an activation condition), `pending_topups_minor` (in-flight, not spendable), and `currency` (ISO 4217).
</ResponseField>

<Warning>
  Identify never returns a debit token, an authorization token, or any artifact that permits a ledger mutation. It reads identity and balance only. A spend happens on a separate, idempotent payment call.
</Warning>

The credential types follow a discriminated-union pattern on `credential_type`. Read [Identity and credentials](/concepts/identity-and-credentials) for the full set, and confirm what your integration accepts via `GET /capabilities`.

## Where to go next

<Columns cols="2">
  <Card title="Make your first payment" href="/getting-started/make-your-first-payment" icon="credit-card" horizontal="false">
    Identify, charge promo-first, and recover from insufficient funds, worked end to end.
  </Card>

  <Card title="Authentication" href="/api-reference/authentication" icon="key-round" horizontal="false">
    The two API credentials, the key lifecycle, and JWT scope claims.
  </Card>

  <Card title="Build with an AI agent" href="/getting-started/build-with-an-agent" icon="bot" horizontal="false">
    Read capabilities and the OpenAPI spec at runtime, validate bodies, and handle every typed error.
  </Card>

  <Card title="Idempotency and errors" href="/concepts/idempotency-and-errors" icon="shield-check" horizontal="false">
    The deduplication contract, typed error codes, and recovery actions.
  </Card>
</Columns>
