Skip to main content

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

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.
The raw key is shown exactly once when it is created. Store it. A lost key is regenerated, never recovered. See Authentication for the full key lifecycle.

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.

Probe health, no key required

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.

Read your capabilities

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.

Exchange your key for a terminal JWT

POST /auth/token exchanges your x-api-key for a short-lived terminal-scoped bearer token. Refresh it before any money call.

Identify a customer

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 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.
required
Always true on HTTP 200. The platform is reachable.
required
One of up (all subsystems nominal) or degraded (one or more subsystems impaired, platform still serving).
required
Named subsystems currently impaired, for example ['promo_engine', 'webhook_fanout']. Empty when status is up.
Health carries no tenant data by design. It is mounted ahead of the auth layer, so it is the cheapest liveness probe.

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.
required
Your platform- or merchant-scoped API key (fddi_...). The integration is resolved from this key.
required
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.
required
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.
required
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.
required
Named feature flags keyed by operation. A missing key or false means the feature is not available for your integration.
required
Per-operation rate limit descriptors with a per_minute integer. These match the X-RateLimit-* headers enforced on each endpoint.
Cache this response, honor its Cache-Control header, and re-fetch on a CREDENTIAL_TYPE_UNSUPPORTED or CURRENCY_NOT_SUPPORTED typed error.

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.
required
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).
required
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.
required
Request context. Requires merchant_id (key-enforced, never free-text). branch_id, terminal_id, and cashier_id are optional.
required
Optional cashier identifier to sign into the JWT scope. Server-validated: 64 characters or fewer, no control characters.
required
The compact JWT string. Send it as Authorization: Bearer <token> on subsequent terminal-scoped calls.
required
Always Bearer.
required
Seconds until expiry (600).
required
Absolute expiry timestamp (RFC 3339).
required
Scope claims derived from the key: integration_id, enterprise_id, and the nullable brand_id, branch_id, cashier_id.
required
Mirrors the originating key’s sandbox flag, carried as a JWT claim.
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.

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.
required
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.
required
Request metadata. Requires partner_request_id and api_version.
required
Request context. Requires merchant_id.
The data.resolution_state field returns one of three states:
required
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).
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.
Key response fields:
required
The POS-facing identity id. Null when resolution_state is not_found.
required
The wallet program binding. Null when resolution_state is not_found. An ambiguous match returns WALLET_PROGRAM_AMBIGUOUS (HTTP 422) instead of this field.
required
The ledger id. Null when resolution_state is not_found or the wallet has no credits yet.
required
Opaque correlation id for this identity resolution. Persist and echo it where a call accepts one.
required
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).
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.
The credential types follow a discriminated-union pattern on credential_type. Read Identity and credentials for the full set, and confirm what your integration accepts via GET /capabilities.

Where to go next

Make your first payment

Identify, charge promo-first, and recover from insufficient funds, worked end to end.

Authentication

The two API credentials, the key lifecycle, and JWT scope claims.

Build with an AI agent

Read capabilities and the OpenAPI spec at runtime, validate bodies, and handle every typed error.

Idempotency and errors

The deduplication contract, typed error codes, and recovery actions.