Skip to main content

Overview

A top-up credits a customer’s wallet with actual, spendable money. Feddi credits the ledger after the payment settles; Feddi never holds the cash. You can credit a wallet two ways:
  • Confirm an online-gateway top-up with POST /topup/confirm.
  • Report a cashier-sold reload SKU with POST /topup/sku/{skuId}.
Each top-up can accrue a reload bonus. The bonus is a separate, lighter money class: a promotional grant that is locked, expirable, and clawback-able. It never merges into the actual balance. Read Money: actual vs promotional before you build this flow.
Money is always a minor-units integer paired with an explicit ISO-4217 currency (for example amount_minor: 5000, currency: QAR). Call GET /capabilities and GET /openapi at runtime for the authoritative list of operations, currencies, and credential types available to your integration.

How a top-up settles

The customer pays

For an online top-up, the customer pays on the provider’s hosted page and the cash settles to the merchant’s bank. For a cashier-sold reload, the customer pays the reload SKU at the till.

You report the settled payment

Call POST /topup/confirm (online) or POST /topup/sku/{skuId} (SKU). Feddi credits the actual balance and accrues any configured reload bonus as a promotional grant in the same atomic transaction. Both effects apply together or neither does.

Feddi returns both money classes

The response carries credited_minor (actual money) and bonus_minor (promotional grant) as distinct fields, plus the balances after the credit.
The two settlement paths look like this: Online gateway top-up (e.g. Sadad) Cashier-sold reload SKU A standalone top-up entry point that resolves a provider session before payment is not yet available. Today, confirm a top-up against a settled provider payment reference (provider_payment_ref), or report a cashier-sold SKU sale against a finalized POS order reference (pos_order_ref).

Idempotency

Both write endpoints are idempotent on the Idempotency-Key header, a partner-generated UUID with a 24-hour TTL. Send it on every mutating call.
  • Same key, same payload: byte-identical replay. meta.idempotency_replayed is true.
  • Same key, different payload: the call returns IDEMPOTENCY_KEY_REUSED (HTTP 422).
POST /topup/confirm deduplicates a second way: on the provider settlement reference (provider_payment_ref). A duplicate provider notification plus a partner retry credit the wallet exactly once. POST /topup/sku/{skuId} deduplicates a second way: on the POS order reference (pos_order_ref). A duplicate sale report never double-credits.

Confirm an online-gateway top-up

POST /topup/confirm credits the customer’s actual balance and accrues any configured reload bonus, then settles both in one atomic transaction. The credited amount is server-authoritative: Feddi derives it from the finalized provider settlement event and reconciles it against the amount_minor you send. Because the customer is already identified at top-up time, the reload bonus accrues directly to released with no claim gate. Authenticate with a POS terminal JWT.

Request

required
Partner-generated UUID. The dedup key for the call. 24-hour TTL.
required
The request context. merchant_id is required and is enforced against the credential’s authority; a merchant_id outside that authority is rejected. Optional members: branch_id, terminal_id, cashier_id, partner_session_id.
required
The provider that processed the charge. One of SADAD, STRIPE.
required
The provider’s settled payment reference (for example a Stripe PaymentIntent id or a Sadad transaction number). Used as a secondary idempotency anchor.
required
ISO-4217 code, 3 characters. A currency with no active provider returns CURRENCY_NOT_SUPPORTED.
required
Expected paid amount in minor units, minimum 1. The credited amount is derived server-side from the finalized provider event and reconciled against this value. Nullable.
required
A prepared provider session reference, when you have one. Nullable; omit it to confirm against provider_payment_ref alone.
required
Request metadata. partner_request_id (UUID, correlation only, not the dedup key) and api_version are required when meta is present. Optional occurred_at and sent_at timestamps (RFC3339); send both when you have them.

Response

required
The deposit transaction id.
required
One of pending, completed, failed, escrowed. completed means the wallet was credited. escrowed means the credit is held for an unverified customer until they register.
required
The credited wallet id.
required
Actual (cashable) balance credited, in minor units.
required
Promotional bonus accrued, in minor units. 0 when no bonus applied.
required
Actual balance after the credit. Nullable; null when the credit is escrowed.
required
Released promotional balance after the accrual. Nullable.
required
ISO-4217 currency code of the credit.
required
The reload bonus accrued by this top-up, if any. See the fields below.
credited_minor is the actual-money credit. bonus_minor is the separate promotional grant. They are returned as distinct fields and accounted distinctly. Never sum them into one balance.

Errors

Cashier-sold reloads (top-up SKUs)

Some merchants sell a reload as a line item at the till (for example a “QAR 50 wallet load”). When a registered top-up SKU is sold, report it with POST /topup/sku/{skuId}. Feddi credits the resolved wallet by the SKU’s registered wallet_credits_minor and accrues any configured SKU bonus as a promotional grant with source SKU_TOPUP_BONUS. The credited amount and bonus are derived server-side from the SKU registration and the finalized POS sale; POS-submitted money fields are ignored. The bonus accrues released because the customer is identified at point of sale. If the customer is not yet a verified Feddi customer, the sale creates an unverified proto-wallet: the customer keeps the reload (actual balance credits) and the bonus stays locked behind proof-of-phone until they register. In that case status is escrowed and customer_state is pending_proof. See PENDING_PROOF and promo release. Authenticate with a POS terminal JWT.

Request

required
Partner-generated UUID. 24-hour TTL.
required
The registered top-up SKU id (UUID). A SKU that does not exist, is retired, or belongs to another integration resolves to NOT_FOUND.
required
The request context. merchant_id is required; optional branch_id, terminal_id, cashier_id, partner_session_id.
required
The finalized POS order reference for this sale. Used as the server-side derivation source and the secondary idempotency anchor.
required
The identity credential, discriminated by credential_type (one of phone, provider_customer_id, card_fingerprint, short_code, qr). For example a phone credential carries an E.164 phone value. The enabled set is gated by GET /capabilities.
required
Number of SKU units sold, minimum 1, default 1. The credit is wallet_credits_minor multiplied by quantity.
required
Request metadata. partner_request_id and api_version are required when meta is present; optional occurred_at and sent_at.

Response

The response is the same TopupResult shape as POST /topup/confirm, with one additional field surfaced on the SKU path:
required
The resolved customer identity state at credit time. One of verified, pending_proof. Nullable.

Errors

Configure the reload bonus

POST /topup/reload-bonus/config sets the amount-banded bonus a wallet program grants on a top-up (for example “top up QAR 50 or more, get a 10% bonus”). The config is forward-looking only: it governs future top-ups and never re-grants retroactively. The bonus it produces accrues as a promotional grant, distinct from the customer’s actual cashable balance. Authenticate with an API key. This call requires the enterprise-admin integrations permission.

Request

required
Partner-generated UUID. 24-hour TTL.
required
The request context. merchant_id is required.
required
The wallet program these tiers govern.
required
ISO-4217 code, 3 characters.
required
One or more non-overlapping bonus bands. Each tier carries min_topup_minor, optional max_topup_minor, bonus_type, and bonus_value. See the tier fields below.
required
How long an accrued bonus grant lives before it expires, in days, minimum 1. Drives the grant’s expires_at. Nullable.
required
Request metadata. partner_request_id and api_version are required when meta is present.
Tiers must not overlap, and percentage values stay within the 0 to 100 range. An overlapping or out-of-range tier returns VALIDATION_ERROR.

Response

required
The persisted config id (UUID).
required
The wallet program the tiers govern.
required
ISO-4217 currency code.
required
One of active, inactive.
required
The saved bonus bands, each with min_topup_minor, max_topup_minor, bonus_type, and bonus_value.
required
Grant lifetime in days. Nullable.
required
When the config was last saved (RFC3339).

Insufficient-funds recovery

When a wallet pay fails with INSUFFICIENT_FUNDS, top up the wallet and retry the charge. Take the customer’s gateway payment or sell a reload SKU, then call POST /topup/confirm or POST /topup/sku/{skuId} to credit the wallet before re-attempting the payment. See Payments and redemption for the charge retry.

Response metadata

Every response carries a uniform meta block. Two fields you may see:
  • data_completeness_score: an opaque integer Feddi may use internally. No action required.
  • decision_trace_id: an opaque correlation id. Persist it and echo it where a call accepts one.

Where to go next

Money: actual vs promotional

Why actual balance and reload-bonus promo are never one number.

PENDING_PROOF and promo release

How a reload credits while the bonus stays locked until phone proof.

Payments and redemption

The promo-first debit and the insufficient-funds moment that triggers a reload.

Incentives

The other sources of promotional grants beyond the reload bonus.