Skip to main content

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

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.

Read GET /capabilities for the tenant-scoped enabled set

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.

Read GET /openapi for the served contract

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

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.

Fetch the served spec

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.

Validate every request body against its schema

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.

Reconcile drift toward the spec

If your types and the served document disagree, regenerate from the served document. The spec is the source of truth.

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.

Exchange the API key for a terminal JWT

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.

Refresh before the TTL expires

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

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.
required
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.
required
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.
required
Your own correlation id, echoed in logs and responses. Not the idempotency basis.
An opaque integer Feddi may use internally. No action required.
An opaque correlation id. Persist it and echo it where a call accepts one.
See Conventions and Idempotency & 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.
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.
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.
See 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.

Identify the customer

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.

Charge the wallet

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.
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 and Identity & credentials.

Where to go next

Make your first payment

The callable identify, pay, recover loop worked end to end.

Authentication

The API-key lifecycle and the terminal JWT exchange.

Conventions

The response envelope, money-as-minor-integers, idempotency, versioning, and pagination.

Errors

Every typed code, its HTTP status, and its recovery action.