Skip to main content

Three ways to redeem

A wallet debit is one POST /payments call. What changes per method is the credential the customer presents; the debit that follows is identical. There are two authentication modes: a typed phone plus OTP, or a dynamic credential the wallet shows as a linked QR and short code. 1. Phone + OTP (two-leg) 2. Dynamic credential: QR or short code (no OTP) The wallet shows a rotating QR and a short code that are two forms of the same single-use nonce. They refresh together on the same clock, so the customer can scan the QR or, if the scan fails, read the short code out loud. Both are dynamic, so neither needs an OTP. Short code (the QR’s typed twin) The short_code type returns CREDENTIAL_TYPE_UNSUPPORTED unless enabled for your integration; check GET /capabilities.

Debit the wallet with one call

You debit a Feddi wallet with POST /payments. One call collects a customer credential, debits the balance atomically, and returns a PaymentResult that splits the debit into promotional and actual money. The debit is atomic and promo-first. Feddi spends released promotional credit first (FIFO by expiry), then actual balance, in a single transaction. The two parts always sum to the amount you asked for. If the combined balance cannot cover the amount, nothing is debited and the credential is not consumed, so you can route to recovery and retry. The base URL is https://api.feddi.io/v1/partner. Paths below are relative to it.

Operations

POST /payments and POST /qr/mint and POST /payments/{id}/capture authenticate with the terminal-scoped JWT (PosTerminalJWT). GET /payments/{id} authenticates with the integration x-api-key. Refresh the terminal JWT before a money call: it has a 600 second TTL. See Authentication.
required
On GET /payments/{id} and POST /payments/{id}/capture, the payment_id returned by POST /payments.

Debit the wallet

POST /payments collects the credential, debits the balance, and returns the result in one round trip. Send the full envelope, an Idempotency-Key header, and a credential block discriminated on credential.type.
required
Bearer <terminal-jwt>. The POS terminal JWT you exchanged your x-api-key for at POST /auth/token. 600 second TTL.
required
A partner-generated UUID, stable for this one logical payment and unique across different ones. A replay returns the original result byte-identical with meta.idempotency_replayed: true. Do not re-debit on a replay.
required
Request metadata. Carries partner_request_id (UUID, required, a correlation id only, not the dedup basis), api_version (string, required), and the optional occurred_at and sent_at timestamps (RFC3339). Send both timestamps.
required
Request context. Carries merchant_id (UUID, required, key-enforced to a merchant your credential is authorized for) plus optional branch_id, terminal_id, cashier_id, partner_session_id, and feddi_session_id.
required
The customer credential, discriminated on credential.type. One credential block per call. See the credential shapes below.
required
Amount to debit in the currency’s minor unit (fils for QAR, halalas for SAR). Server-authoritative: the debit is exactly this value. Minimum 1.
required
ISO-4217 currency code, for example QAR.
required
Your order or receipt reference, stored for reconciliation. Maps to context.partner_session_id if absent.
required
Item lines for the order. Optional. Line totals are captured for reconciliation, never the debit basis: amount_minor is the debit. We recommend sending the lines your POS has. See the basket line fields below.

Credential types

credential is a discriminated union on credential.type. The otp and qr types are callable today. The short_code and pass_tap types are accepted by the schema but return CREDENTIAL_TYPE_UNSUPPORTED unless enabled for your integration. Call GET /capabilities and read supported_payment_credential_types for the set enabled for you. Adding a new credential type extends the enum and adds a validator, never a new route.

Basket line fields

Each entry in basket.lines carries these fields. We recommend sending the ones your POS has.
required
Your stock-keeping identifier for the line.
required
Units of this line.
required
Per-unit price in minor units.
required
Line total in minor units (quantity times unit price, less line discount).
required
Merchant category for the line, for example beverage or bakery.
required
Discount already applied to this line in minor units.

The PaymentResult

A successful debit returns a PaymentResult inside the standard envelope. The result splits the debit into debited_promo_minor and debited_actual_minor, and the two always sum to amount_minor.
required
Feddi-assigned payment id. Pass it to GET /payments/{id} for reconciliation.
required
One of completed (debit landed), voided (reversed), or pending (async edge case only).
required
The wallet that was debited.
required
Total amount debited (promo plus actual), in minor units.
required
Portion drawn from released promo grants, spent first (FIFO by expiry). Zero if no released promo was available.
required
Portion drawn from the actual (real-money) balance, in minor units.
required
ISO-4217 currency code of the debit.
required
Echoes the order_ref you sent.
required
The full balance after the debit: actual_minor, promo_available_minor, promo_locked_minor, pending_topups_minor, currency, and a promo_grants array.
required
RFC3339 timestamp when the debit completed.
debited_promo_minor + debited_actual_minor == amount_minor, always. Promotional credit is spent first, so the customer’s real money stays intact and promo is use-it-or-lose-it. The two money classes are never one bucket. See Money: actual vs promotional.
The meta.data_completeness_score field is an opaque integer Feddi may use internally. No action required.

A v1 payment is a single atomic debit

In v1 a wallet payment is one atomic debit. There is no reservation, no held-money state, no authorized balance you capture later. The ledger debits immediately and returns the result. The POST /payments/{id}/capture endpoint has one callable role today: the OTP second leg described below.

The OTP second leg (capture)

The OTP path can run in two legs when your POS separates credential collection from debit authorization. The first leg mints and sends the OTP. The customer enters it on the PIN-pad. Then POST /payments/{id}/capture carries the OTP and authorizes the debit.
required
Request metadata, same shape as on POST /payments.
required
Request context, same shape as on POST /payments.
required
One of capture (debit the wallet and complete the payment) or close (release without debiting).
required
Amount to capture. Must be ≤ the authorized amount. Captures the full authorized amount if absent.
required
ISO-4217 currency code. Must match the authorization.
A capture returns the same PaymentResult with status: completed. A close returns status: voided. The call is idempotent on the Idempotency-Key header.

Mint a QR or short-code

When the customer pays from their wallet app, mint a rotating credential with POST /qr/mint, render the qr_payload as a QR code, and optionally display the short_code for manual entry. The POS then scans the QR and passes it back as credential.type: qr in POST /payments.
required
Request metadata, same shape as on POST /payments.
required
Request context, same shape as on POST /payments.
required
The customer to resolve the wallet for. Carries credential_type (one of phone or provider_customer_id) plus the matching phone or provider_customer_id value.
required
If true, also generate a 5-character alphanumeric short-code alongside the QR payload. Defaults to false.
required
Disambiguate when the customer maps to multiple wallet programs.
The nonce is bound to the customer’s wallet with a 5 minute TTL. Re-minting before the TTL expires returns a new nonce and invalidates the previous one. The QR session is single-use: a replayed nonce in POST /payments returns CREDENTIAL_EXPIRED_OR_REPLAYED (400). Mint is idempotent on the Idempotency-Key header: a retried mint with the same key returns the same nonce while it is within its TTL.
required
The QR session id.
required
The full QR payload string. Encode this as a QR code in the wallet app.
required
5-character alphanumeric short-code, present only if include_short_code was true. Single-use, same TTL as the QR nonce.
required
Nonce expiry time. After this, presenting the QR at the POS returns CREDENTIAL_EXPIRED_OR_REPLAYED.
required
Seconds until the nonce expires.

Insufficient funds is a recovery path

When the wallet balance (actual plus released promo) cannot cover the amount, POST /payments returns INSUFFICIENT_FUNDS (402). Nothing is debited. The OTP or QR credential is not consumed, so the customer can retry after a reload.
On INSUFFICIENT_FUNDS, read details.shortfall_minor to size a reload prompt, credit the wallet, then retry the debit. To credit the wallet, confirm a top-up against a provider settlement reference with POST /topup/confirm. A standalone top-up entry point is not yet available, so today you confirm a top-up against a finalized provider settlement reference. For the mechanics, see Top-up and reload bonus.

A PENDING_PROOF wallet may not pay

A PENDING_PROOF wallet holds reloads but gates promo release and payment behind phone proof. A payment against one returns FORBIDDEN (403). Redirect the customer to enrollment to verify their phone, then retry. See Enrollment and signup and PENDING_PROOF and promo release.

Errors

Errors are typed string codes in error.code, never a bare HTTP number. Branch on the code and read error.details for the actionable fields. The full set is in Errors.

Idempotency on the debit

Every POST /payments call carries an Idempotency-Key header. A retried call with the same key and same payload replays the original PaymentResult byte-identical with meta.idempotency_replayed: true: it does not re-debit. A reused key with a different payload returns IDEMPOTENCY_KEY_REUSED (422). The debit defends in depth beyond the key. The OTP is consumed atomically with the debit, so a failed debit such as INSUFFICIENT_FUNDS leaves the OTP retryable. QR nonces are single-use. See Idempotency and errors.

Where to go next

Money: actual vs promotional

The two-class balance model behind the promo-first debit split.

Top-up and reload bonus

Crediting actual balance, the recovery path after insufficient funds.

PENDING_PROOF and promo release

Why an unverified wallet cannot pay, and how to release it.

Idempotency and errors

The dedup key and typed error model that govern the debit.