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

# Request & Response Conventions

> The wire shape every Partner API endpoint shares: request and response envelopes, money as minor integers, the Idempotency-Key header, and pagination.

## One envelope, every endpoint

Every mutating call sends the same two-part request envelope, and every response, success or error, comes back in the same response envelope. Learn this shape once and it holds across the entire surface.

The base URL is `https://api.feddi.io/v1/partner`. A dev environment is available at `https://api.dev.feddi.io/v1/partner`. All paths sit under `/v1/partner`.

<Tip>
  The envelope conventions on this page apply to every endpoint regardless of its own stability. Call `GET /capabilities` and `GET /openapi` at runtime to confirm which operations and enums are enabled for your credentials. See [API stability](#api-stability) below.
</Tip>

## The request envelope

A mutating request carries two top-level objects: `meta` (correlation and timing) and `context` (who and where). Your resource-specific body fields sit alongside them.

```json theme={null}
{
  "meta": {
    "partner_request_id": "9b1f3a2c-7e44-4f0a-bb21-0d9e2c8a4f10",
    "occurred_at": "2026-06-05T14:22:31Z",
    "sent_at": "2026-06-05T14:22:31.840Z",
    "api_version": "2026-06-01"
  },
  "context": {
    "merchant_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "branch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "terminal_id": "POS-360-T12",
    "cashier_id": "emp-4471",
    "partner_session_id": "ORD-20260605-009931",
    "feddi_session_id": "c0a8011e-9d3b-4a77-8f2e-1b6d4e5a9c10"
  }
}
```

### meta: correlation and timing

<ParamField body="meta.partner_request_id" param-type="string" required="true">
  Your correlation id for this call, a UUID. It threads your logs to Feddi's. It is not the dedup key. A fresh `partner_request_id` with a reused `Idempotency-Key` is a replay, not a new action.
</ParamField>

<ParamField body="meta.occurred_at" param-type="string" required="false">
  RFC 3339 timestamp for when the event happened at the POS.
</ParamField>

<ParamField body="meta.sent_at" param-type="string" required="false">
  RFC 3339 timestamp for when you transmitted the call. Send both `occurred_at` and `sent_at`.
</ParamField>

<ParamField body="meta.api_version" param-type="string" required="true">
  The single version field. See [Versioning](#versioning) below.
</ParamField>

### context: who and where

<ParamField body="context.merchant_id" param-type="string" required="true">
  The resolved Feddi tenant, a UUID. On a platform integration this is key-enforced to a merchant your credential is authorized for, never a free-text trust field. A `merchant_id` outside your credential's authority is rejected. See [Identity and credentials](/concepts/identity-and-credentials).
</ParamField>

<ParamField body="context.branch_id" param-type="string" required="false">
  The branch within the merchant, a UUID. Null when not applicable.
</ParamField>

<ParamField body="context.terminal_id" param-type="string" required="false">
  The physical or logical terminal. Scoped into the terminal JWT.
</ParamField>

<ParamField body="context.cashier_id" param-type="string" required="false">
  The operator. Server-trusted only when it is signed into the terminal JWT.
</ParamField>

<ParamField body="context.partner_session_id" param-type="string" required="false">
  Your own session or order reference. This is the field that carries your order number into Feddi.
</ParamField>

<ParamField body="context.feddi_session_id" param-type="string" required="false">
  The `checkout_session` UUID this call belongs to. Null on standalone calls that do not run inside a session.
</ParamField>

<Tip>
  Your order number is not its own envelope field. Put it in `context.partner_session_id` and read it back the same way. There is exactly one mapping.
</Tip>

## The response envelope

Every response, whether `ok` is true or false, returns the same four top-level keys.

```json theme={null}
{
  "ok": true,
  "data": {},
  "error": null,
  "meta": {
    "request_id": "feddi-7c2a9f01-3e88-4b6d-9a14-5f0c1d2e3b44",
    "idempotency_replayed": false,
    "api_version": "2026-06-01",
    "data_completeness_score": 80,
    "decision_trace_id": null,
    "capabilities": {}
  }
}
```

<ResponseField name="ok" field-type="boolean" required="true">
  True on success, false on a typed error. Branch on this first.
</ResponseField>

<ResponseField name="data" field-type="object">
  The resource payload on success. Null when `ok` is false.
</ResponseField>

<ResponseField name="error" field-type="object">
  Null on success. On failure, a typed object of `code`, `message`, and optional `details`, where `code` is a string enum, never a bare HTTP number. See [Errors](/api-reference/errors).
</ResponseField>

<ResponseField name="meta.request_id" field-type="string" required="true">
  Feddi's id for this response. Quote it in support requests.
</ResponseField>

<ResponseField name="meta.idempotency_replayed" field-type="boolean">
  True when this response is a byte-identical replay of an earlier call with the same `Idempotency-Key`. Do not double-act on a replay.
</ResponseField>

<ResponseField name="meta.api_version" field-type="string" required="true">
  The version that served this response.
</ResponseField>

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

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

<ResponseField name="meta.capabilities" field-type="object">
  Additive, response-level capability hints. Treat unknown keys as forward-compatible, never as an error.
</ResponseField>

On paginated reads, `meta` also carries `next_cursor` and `has_more`. See [Pagination](#pagination).

## Money is always minor integers

Every money field is an `*_minor` integer paired with an explicit ISO-4217 `currency`. Feddi never accepts or returns a decimal-formatted amount in a body.

```json theme={null}
{ "amount_minor": 3402, "currency": "QAR" }
```

`amount_minor: 3402` with `currency: "QAR"` means 34.02 Qatari riyal. The `currency` code is exactly three characters.

Money is server-authoritative: Feddi recomputes its own total from the basket lines you submit and treats that as the basis for any debit or incentive. Your submitted total is captured and reconciled. A divergence is flagged for reconciliation, never silently overridden.

<Warning>
  Never send `"34.02"` or `34.02` for an amount. Floating-point currency is the most common integration bug. Always use an integer minor-units amount plus a `currency` string.
</Warning>

## The Idempotency-Key header

Put a partner-generated UUID in the `Idempotency-Key` HTTP header on every mutating call. It is the one and only dedup key.

<ParamField header="Idempotency-Key" param-type="string" required="true">
  A partner-generated UUID, stable for one logical operation and unique across different operations. The TTL is 24 hours. Same key plus same payload replays the original response. Same key plus a different payload returns the typed `IDEMPOTENCY_KEY_REUSED` conflict, HTTP 422.
</ParamField>

Three read-shaped POSTs are the exception: `identify`, the redeem balance check, and incentive evaluation are side-effect-free, so the header is optional on them. Sending it anyway is fine, uniform client middleware that stamps every POST is accepted, and replays are reported via `meta.idempotency_replayed`. Every other mutating operation still requires it.

The key lives in the HTTP header, not the envelope. There is no `meta.idempotency_key` field. For the full replay, concurrency, and conflict rules, read [Idempotency and errors](/concepts/idempotency-and-errors).

## Versioning

There is one version field, `api_version`, sent in `meta` and echoed back in `meta`. `/v1` is additive-only.

Feddi grows the contract by adding nullable attributes, never by renumbering or renaming a top-level field. Build a forward-compatible client: ignore response keys you do not recognize, and send new fields only when your POS genuinely has them.

<Info>
  A breaking change ships as a new dated `api_version`, announced in the [Changelog](/changelog). Inside a version, you only ever see additions.
</Info>

## Pagination

List endpoints are cursor-paginated. Pass `limit` to set the page size and `cursor` to fetch the next page. The response carries `meta.next_cursor` and `meta.has_more`.

<ParamField query="limit" param-type="integer" required="false">
  Page size. Defaults to 50, with a maximum of 200.
</ParamField>

<ParamField query="cursor" param-type="string" required="false">
  An opaque cursor from a prior page's `meta.next_cursor`. Omit it on the first page.
</ParamField>

<ResponseField name="meta.next_cursor" field-type="string">
  The cursor to pass as `cursor` on the next request. Null on the final page.
</ResponseField>

<ResponseField name="meta.has_more" field-type="boolean">
  True when more pages remain. Stop paging when this is false.
</ResponseField>

Loop until `has_more` is false, passing the previous response's `next_cursor` each time.

## API stability

GA operations are stable: the shape will not shift under you. Beta operations are callable today and may still change additively before reaching GA, so pin your `api_version`.

This site documents the designed surface, not what is mounted in your environment right now. Call `GET /capabilities` and `GET /openapi` at runtime for the authoritative list of what is available to your credentials, including the enabled credential types, currencies, and feature flags.

<Danger>
  Confirm the live, mounted set at runtime via `GET /capabilities` and `GET /openapi` before you call a money or identity path. Never assume an operation exists because this site or a guide names it.
</Danger>

## A full request and response

This is a standalone identity resolution, the cashier-panel balance check (`POST /v1/partner/identify`). It shows the envelope on both sides, with money in minor units and the `Idempotency-Key` header in place. The header is optional on this read-shaped call; it is shown here as a client that stamps every POST would send it.

<CodeGroup>
  ```bash cURL wrap="true" theme={null}
  curl -X POST https://api.feddi.io/v1/partner/identify \
    -H "Authorization: Bearer <terminal-jwt>" \
    -H "Idempotency-Key: 0d3f7b9a-2c61-4e84-9b07-5a1c8e2f4d63" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": {
        "partner_request_id": "9b1f3a2c-7e44-4f0a-bb21-0d9e2c8a4f10",
        "occurred_at": "2026-06-05T14:22:31Z",
        "sent_at": "2026-06-05T14:22:31.840Z",
        "api_version": "2026-06-01"
      },
      "context": {
        "merchant_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
        "branch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "terminal_id": "POS-360-T12",
        "cashier_id": "emp-4471"
      },
      "credential": { "credential_type": "phone", "phone": "+97455512345" }
    }'
  ```

  ```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": 12500,
        "promo_available_minor": 3000,
        "promo_locked_minor": 2000,
        "pending_topups_minor": 0,
        "currency": "QAR",
        "promo_grants": [
          {
            "source": "CASHBACK",
            "state": "RELEASED",
            "remaining_minor": 3000,
            "expires_at": "2026-09-05T00:00:00Z"
          },
          {
            "source": "RELOAD_BONUS",
            "state": "LOCKED",
            "remaining_minor": 2000,
            "expires_at": "2026-09-10T00:00:00Z"
          }
        ]
      }
    },
    "error": null,
    "meta": {
      "request_id": "feddi-7c2a9f01-3e88-4b6d-9a14-5f0c1d2e3b44",
      "idempotency_replayed": false,
      "api_version": "2026-06-01",
      "data_completeness_score": 65,
      "decision_trace_id": null,
      "capabilities": {}
    }
  }
  ```

  ```json 422 Conflict theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "IDEMPOTENCY_KEY_REUSED",
      "message": "This Idempotency-Key was already used with a different payload."
    },
    "meta": {
      "request_id": "feddi-9f0c1d2e-3b44-4a77-8f2e-1b6d4e5a9c10",
      "idempotency_replayed": false,
      "api_version": "2026-06-01"
    }
  }
  ```
</CodeGroup>

## Where to go next

<Columns cols="2">
  <Card title="Errors" href="/api-reference/errors" icon="alert-triangle">
    Every typed `error.code`, its HTTP status, and what to do about it.
  </Card>

  <Card title="Idempotency and errors" href="/concepts/idempotency-and-errors" icon="repeat">
    The full replay, concurrency, and conflict rules for the Idempotency-Key header.
  </Card>

  <Card title="Authentication" href="/api-reference/authentication" icon="key">
    Exchange your API key for the terminal JWT every mutating call carries.
  </Card>

  <Card title="Transactions" href="/guides/transactions" icon="list">
    The cursor-paginated reads that return your settled ledger.
  </Card>
</Columns>
