Three ways to redeem
A wallet debit is onePOST /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 withPOST /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 inbasket.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 aPaymentResult 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.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. ThePOST /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. ThenPOST /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.
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 withPOST /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.
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.
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
APENDING_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 inerror.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
EveryPOST /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.