# Feddi Partner API (`/v1/partner/*`) > You are most likely an AI agent integrating a point-of-sale or commerce platform. This file orients you; the machine contract is the OpenAPI 3.1 spec next to it (`openapi.yaml`), served at `GET /v1/partner/openapi`. Self-validate request and response bodies against that spec, not against generated types. Where this file and `openapi.yaml` disagree, the spec wins. Read `GET /capabilities` at runtime for the currencies, credential types, and features enabled for your integration before assuming any enum. ## What the API does The Feddi Partner API lets your POS platform accept and reconcile closed-loop wallet payments and run loyalty at the point of sale. You identify the customer, read their balance, accept a wallet payment (promotional credit spent first), run top-ups, and reconcile transactions. Balances are merchant-held, closed-loop store value: spendable only at the issuing merchant, never withdrawable as cash, and Feddi never holds, escrows, or moves the funds. Every money endpoint is a ledger and reporting surface. A customer's balance is scoped per merchant. ## Base URL and auth - Production: `https://api.feddi.io/v1/partner` - Dev: `https://api.dev.feddi.io/v1/partner` Authenticate with the `x-api-key` header (your platform key). For per-terminal hot-path calls, exchange the key for a short-lived POS terminal JWT: `POST /auth/token` returns a bearer token (~600s TTL). Manage keys with the key-lifecycle endpoints (create, list, revoke, regenerate, delete). Sandbox credentials are issued by your Feddi contact under a partner agreement; build against the dev base URL first. ## The response envelope Every response is `{ ok, data, error, meta }`. - `ok` is a boolean. When `false`, `error` carries a typed object (`code`, `message`, `details`) and `data` is `null`. - `meta` is always present: `request_id`, `idempotency_replayed` (boolean), `api_version`, and two opaque correlation values (`decision_trace_id`, `data_completeness_score`) that you persist and echo where a call accepts them but otherwise take no action on. - Every mutating call sends a request `meta` (`partner_request_id` UUID, `occurred_at`, `sent_at`, `api_version`) and a `context` (`merchant_id`, `branch_id`, `terminal_id`, `cashier_id`, and `feddi_session_id` when the call anchors to a checkout session). ## Money model The wallet exposes two value classes that are never conflated. Amounts are always integer minor units with an explicit ISO-4217 `currency` (for example `amount_minor: 1800, currency: QAR`), never a float. - `actual_minor` — real money funded by customer top-ups. Spendable, non-cashable, closed-loop. - `promo_available_minor` — released promotional credit (a `PromoGrant` in state `RELEASED`). Spendable, expirable, clawback-able, non-cashable. - `promo_locked_minor` — promotional credit accrued but gated (a `PromoGrant` in state `LOCKED`). Not spendable until released through the claim flow. - `pending_topups_minor` — top-ups in flight, informational only. Promotional credit is spent first (FIFO by expiry across released grants), then actual money. `POST /payments` returns the split: `debited_promo_minor` plus `debited_actual_minor` sum to the amount, with `balance_after`. A `PromoGrant` that is `LOCKED` for an unverified (`PENDING_PROOF`) customer is released only on `VERIFIED` plus the claim gate, never by a session merge alone. A clawback adjusts promotional credit and never touches the customer's actual balance. ## Idempotency Idempotency is the `Idempotency-Key` header, and only that. Put a partner-generated UUID in `Idempotency-Key` on every mutating call (24h TTL). Same key plus same payload replays the original byte-identical response with `meta.idempotency_replayed: true`. Same key plus a different payload returns `IDEMPOTENCY_KEY_REUSED` (422). `meta.partner_request_id` is your correlation id, not the dedup basis: a fresh `partner_request_id` with a reused header is still a replay (no new debit). ## Typed errors Errors are typed string codes in `error.code`, never a bare HTTP number in the body. The canonical set: `INSUFFICIENT_FUNDS`, `INVALID_API_KEY`, `CREDENTIAL_TYPE_UNSUPPORTED`, `CURRENCY_NOT_SUPPORTED`, `IDEMPOTENCY_KEY_REUSED`, `WALLET_PROGRAM_AMBIGUOUS`, `REQUIRES_DYNAMIC_CREDENTIAL`, `NOT_FOUND`, `FORBIDDEN`, `VALIDATION_ERROR`, `RATE_LIMITED`, `CONFLICT`, `INTERNAL_SERVER_ERROR`. Typed errors echo the valid set in `details` (for example `CURRENCY_NOT_SUPPORTED` lists supported currencies; `WALLET_PROGRAM_AMBIGUOUS` lists the candidate `wallet_program_id` values). Handle `INSUFFICIENT_FUNDS` (HTTP 402, carries `shortfall_minor`; the credential is not consumed) as a top-up recovery trigger, then retry. ## Tenant scoping `context.merchant_id` is enforced against the merchant your credential is authorized for; it is never a free-text trust field. A platform integrating many merchants mints per-merchant scoped credentials, never one shared tenant. Cross-tenant access collapses to `NOT_FOUND` or `FORBIDDEN` with no existence or PII leak. If a customer has wallets in more than one program or currency, pass `wallet_program_id`; ambiguity returns `WALLET_PROGRAM_AMBIGUOUS` with the candidates. ## The callable surface Grouped by domain. Read `GET /openapi` for the authoritative, current list and the full request and response schemas. - Platform: `GET /health`, `GET /capabilities`, `GET /openapi`. - Identify: `POST /identify` (standalone, cashier panel) and `POST /checkout/sessions/{id}/identify` (anchored to a checkout session). A credential is one of `phone`, `provider_customer_id`, `card_fingerprint` (probationary until phone-verified), `short_code`, or `qr`. Identification resolves a customer context and never returns a debit token. - Auth: `POST /auth/keys`, `GET /auth/keys`, `POST /auth/keys/{keyId}/revoke`, `POST /auth/keys/{keyId}/regenerate`, `POST /auth/keys/{keyId}/delete`, `POST /auth/token`, `POST /auth/token/validate`. - Customers: `GET /customers/lookup` (cashier-panel summary; a not-found lookup is a 200 with a `not_found` state, not a 404). - Enrollment: `POST /enroll/initiate`, `POST /enroll/verify`, `POST /enroll/resend`, `POST /claims` (release claimable promotional credit on verified phone proof). - Payments: `POST /payments` (promo-first wallet debit with an `otp`, `qr`, or `short_code` credential), `GET /payments/{id}`, `POST /payments/{id}/capture`, `POST /qr/mint`. - Top-up: `POST /topup/confirm`, `POST /topup/sku/{skuId}` (cashier-sold denomination), `POST /topup/reload-bonus/config`. - Incentives (the promotional-grant ledger): `GET /customers/{customerId}/grants`, `POST /grants/{id}/clawback`. - Transactions: `GET /{integrationId}/transactions`, `GET /{integrationId}/transactions/{transactionId}`. ## A typical integration sequence 1. Present your `x-api-key`, then `POST /auth/token` for a terminal JWT. 2. Read `GET /capabilities` for the enabled currencies, credential types, and features. Never hardcode an enum. 3. Identify the customer with whatever credential the POS has (`POST /identify` or the session-anchored variant). 4. Take the payment with `POST /payments`. On `INSUFFICIENT_FUNDS`, run a top-up (`POST /topup/confirm` or `POST /topup/sku/{skuId}`) and retry the original payment. 5. Reconcile with `GET /{integrationId}/transactions` and `GET /{integrationId}/transactions/{transactionId}`. Send the `Idempotency-Key` header on every mutating call and handle each typed error on every money and identity path. ## Links - OpenAPI 3.1 machine contract: `openapi.yaml` here, or `GET /v1/partner/openapi`. - Capabilities for your integration: `GET /v1/partner/capabilities`. - Quickstart and the worked payment walkthrough: the Get started section of the docs site. - Authentication, conventions, and the error reference: the API Reference section of the docs site.