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

# Build with an AI Agent

> A guide for AI coding agents integrating the Feddi Partner API: read the live contract, validate against the served spec, and handle every typed error.

## A checklist for the agent doing the integration

If you are an AI coding agent wiring a point-of-sale platform, payment gateway, or commerce platform to Feddi, read this before you generate integration code. It is a short set of disciplines that keep a generated client correct against a live, tenant-scoped API.

The Feddi Partner API is REST over JSON. The base URL is `https://api.feddi.io/v1/partner` for production and `https://api.dev.feddi.io/v1/partner` for dev. Every path in this guide is relative to that base.

<Warning>
  Do not assume any enum, currency, credential type, or feature flag from prose. Read `GET /capabilities` and `GET /openapi` for the live, tenant-scoped set. A wrong guess on a money path is worse than a question.
</Warning>

## Read the live contract first

Your highest-value first action is to read what is enabled for the integration the API key authenticates as. Two endpoints are the runtime source of truth, and both are stable.

<Steps>
  <Step title="Read GET /capabilities for the tenant-scoped enabled set" icon="list-checks">
    Returns `api_version`, `provider`, `supported_currencies`, `supported_identify_credential_types` (credential types accepted to resolve who a customer is), `supported_payment_credential_types` (credential types accepted to authorize a wallet debit), a `features` map, and `rate_limits`. This is tenant-specific. Never hardcode a currency, a `credential_type`, or a feature flag. An empty collection means the feature is not provisioned for this integration, so branch on the response, not on a hardcoded list.
  </Step>

  <Step title="Read GET /openapi for the served contract" icon="file-code">
    Returns the current OpenAPI 3.1 document. This is the contract a generated client validates against. Where this guide and the served spec disagree, the spec wins.
  </Step>
</Steps>

A capability descriptor that you cache should honor its `Cache-Control` header. Re-fetch it when you receive a `CREDENTIAL_TYPE_UNSUPPORTED` or `CURRENCY_NOT_SUPPORTED` typed error.

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

  ```json 200 response theme={null}
  {
    "ok": true,
    "data": {
      "api_version": "2026-06-01",
      "integration_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
      "provider": "POS_PROVIDER",
      "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 },
      "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"
    }
  }
  ```
</CodeGroup>

## Validate generated code against the served spec

Generated TypeScript types are a convenience, not the contract. The contract is the document served at `GET /openapi`. Validate your generated client and your request bodies against that document, not against a hand-written type.

<Steps>
  <Step title="Fetch the served spec" icon="download">
    `GET /openapi` returns the current OpenAPI 3.1 document for the mounted surface. It is unauthenticated, so SDK generators and contract tests can bootstrap without a key.
  </Step>

  <Step title="Validate every request body against its schema" icon="shield-check">
    The envelope, the credential discriminated unions, and the money fields all have schemas. Validate against them. Money is always a minor-units integer paired with an explicit ISO-4217 `currency` (for example `amount_minor: 3500`, `currency: QAR`), never a float.
  </Step>

  <Step title="Reconcile drift toward the spec" icon="git-compare">
    If your types and the served document disagree, regenerate from the served document. The spec is the source of truth.
  </Step>
</Steps>

## Authenticate, then exchange for a terminal token

You authenticate with the `x-api-key` header. For hot-path calls (identity and money), exchange the API key for a short-lived POS terminal JWT and present it as a bearer token.

<Steps>
  <Step title="Exchange the API key for a terminal JWT" icon="badge-check">
    `POST /auth/token` with your `x-api-key` returns a bearer JWT scoped to the terminal (`token_type: Bearer`, `expires_in: 600`). The token carries scope claims derived from the key. It is never a debit token and never carries customer PII.
  </Step>

  <Step title="Refresh before the TTL expires" icon="timer-reset">
    The JWT has a 600-second TTL. Call `GET /auth/token/validate` to read `remaining_seconds` and refresh before starting a checkout, so you avoid a mid-transaction `401`. A malformed or expired token returns HTTP 200 with `valid: false`, not a `401`: it is a freshness check, not an auth gate.
  </Step>
</Steps>

<Info>
  Sandbox access is granted per partner agreement. Request sandbox credentials from your Feddi contact. Sandbox keys run against mock providers and never touch production money or PII, so you can exercise every money path while you build.
</Info>

## Send the canonical envelope on every mutating call

Every mutating call carries a `meta` block and a `context` block. Every response is the envelope `ok`, `data`, `error`, `meta`. Errors are typed string codes in `error.code`, never a bare HTTP number. Idempotency is the `Idempotency-Key` HTTP header, and only that.

<CodeGroup>
  ```json Request meta + context theme={null}
  {
    "meta": {
      "partner_request_id": "bb22ccdd-1111-2222-3333-444455556666",
      "occurred_at": "2026-06-05T09:05:00Z",
      "sent_at": "2026-06-05T09:05:01Z",
      "api_version": "2026-06-01"
    },
    "context": {
      "merchant_id": "11111111-2222-3333-4444-555566667777",
      "branch_id": "22222222-3333-4444-5555-666677778888",
      "terminal_id": "POS-360-0007",
      "cashier_id": "cashier-42",
      "partner_session_id": "pos-txn-90021",
      "feddi_session_id": "9a7c1e10-0001-4a2b-bb33-aa44cc55dd66"
    }
  }
  ```

  ```json Response envelope theme={null}
  {
    "ok": true,
    "data": {},
    "error": null,
    "meta": {
      "request_id": "req_8f31a2",
      "idempotency_replayed": false,
      "api_version": "2026-06-01",
      "data_completeness_score": 80,
      "decision_trace_id": "dt_a91c"
    }
  }
  ```
</CodeGroup>

<ParamField header="Idempotency-Key" param-type="string" required="true">
  A partner-generated UUID, one per logical mutating operation (24h TTL). Same key plus the same payload replays the original byte-identical response with `meta.idempotency_replayed: true`. Same key plus a different payload returns `IDEMPOTENCY_KEY_REUSED`. `meta.partner_request_id` is your correlation id, not the deduplication basis.
</ParamField>

<ParamField body="merchant_id" param-type="string (uuid)" required="true">
  The resolved Feddi tenant. Key-enforced: a `merchant_id` outside the credential's authority is rejected, never honored. It is not a free-text trust field.
</ParamField>

<ParamField body="partner_request_id" param-type="string (uuid)" required="true">
  Your own correlation id, echoed in logs and responses. Not the idempotency basis.
</ParamField>

<ResponseField name="data_completeness_score" field-type="integer">
  An opaque integer Feddi may use internally. No action required.
</ResponseField>

<ResponseField name="decision_trace_id" field-type="string">
  An opaque correlation id. Persist it and echo it where a call accepts one.
</ResponseField>

See [Conventions](/api-reference/conventions) and [Idempotency & Errors](/concepts/idempotency-and-errors) for the full envelope and replay rules.

## Handle every typed error code

Branch on `error.code`, the typed string enum, never the HTTP status. Some codes echo the valid set in `error.details` so you can present or reconcile the accepted values.

<CodeGroup>
  ```json INSUFFICIENT_FUNDS (HTTP 402) theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "INSUFFICIENT_FUNDS",
      "message": "Wallet balance is insufficient for this payment.",
      "details": {
        "shortfall_minor": 1200,
        "available_actual_minor": 1800,
        "available_promo_minor": 500,
        "currency": "QAR"
      }
    },
    "meta": {
      "request_id": "req_p2",
      "idempotency_replayed": false,
      "api_version": "2026-06-01"
    }
  }
  ```

  ```json CREDENTIAL_TYPE_UNSUPPORTED (HTTP 422) 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>

The `error.code` enum your client must handle:

* `INSUFFICIENT_FUNDS` (HTTP 402): the wallet balance is less than `amount_minor`. The credential is not consumed, so a retry is valid after a top-up. Route this to recovery, never a dead end.
* `WALLET_PROGRAM_AMBIGUOUS` (HTTP 422): the identity matches more than one active wallet program. `details.candidates` lists the `wallet_program_id` values so you can present a disambiguator.
* `CREDENTIAL_TYPE_UNSUPPORTED` (HTTP 422): the credential is not enabled for this integration. `details.supported` lists the accepted set. Re-read `GET /capabilities`.
* `REQUIRES_DYNAMIC_CREDENTIAL`: a money call needs a fresh dynamic credential (OTP or QR), not a static identifier.
* `IDEMPOTENCY_KEY_REUSED` (HTTP 422): the same `Idempotency-Key` was presented with a different payload.
* `CURRENCY_NOT_SUPPORTED`: the currency is not provisioned for this integration. `details` lists the supported ISO-4217 codes.
* `INVALID_API_KEY` (HTTP 401), `FORBIDDEN` (HTTP 403), `NOT_FOUND` (HTTP 404), `VALIDATION_ERROR` (HTTP 400), `RATE_LIMITED` (HTTP 429), `CONFLICT`, `INTERNAL_SERVER_ERROR`.

<Tip>
  We recommend handling `INSUFFICIENT_FUNDS` explicitly on every money path. The customer can top up and the same payment retries. Confirm a top-up against a provider settlement reference with `POST /topup/confirm`, then re-issue the payment.
</Tip>

See [Errors](/api-reference/errors) for the full code table with status and recovery for each.

## Identity and money are separate

An identity call resolves a credential to a customer context and returns balance state. It never returns a debit token, an authorization token, or any artifact that permits a ledger mutation. To charge a wallet, you make a separate payment call with a fresh dynamic credential.

<Steps>
  <Step title="Identify the customer" icon="fingerprint">
    `POST /identify` resolves a credential and returns a `resolution_state` of `registered`, `pending_proof`, or `not_found`. The `not_found` state is HTTP 200 with a null balance, not a `404`, so a polling UI can tell 'no wallet' from a server error. Read `GET /capabilities` for the enabled `supported_identify_credential_types` before you send one.
  </Step>

  <Step title="Charge the wallet" icon="credit-card">
    `POST /payments` debits the wallet for `amount_minor` in `currency`, promo balance first, then actual balance. The result carries `debited_promo_minor` and `debited_actual_minor` so the two are always reconcilable, plus the full `balance_after`.
  </Step>
</Steps>

A customer's balance is scoped per merchant. Promotional money is a separate, expiring, non-cashable class distinct from the actual balance; the two are never merged. See [Money: actual vs promotional](/concepts/money-classes) and [Identity & credentials](/concepts/identity-and-credentials).

## Where to go next

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

  <Card title="Authentication" icon="key" href="/api-reference/authentication">
    The API-key lifecycle and the terminal JWT exchange.
  </Card>

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

  <Card title="Errors" icon="triangle-alert" href="/api-reference/errors">
    Every typed code, its HTTP status, and its recovery action.
  </Card>
</Columns>
