The one rule
ALOCKED promotional grant becomes spendable only when the customer is verified and the merchant’s claim-gate condition is met. Nothing else releases it.
Internalize this before you wire any cashback or enrollment path. It is the boundary that lets promotional credit accrue for an unverified walk-up customer without putting that credit at risk.
What PENDING_PROOF is
When a customer is not yet verified, Feddi creates a wallet in thepending_proof state. It is a real wallet with a deliberate split:
Actual credits flow normally
A reload (top-up) credits the actual cashable balance immediately. Money the customer paid in is theirs right away, verified or not.
Promo grants stay LOCKED
Cashback and reload-bonus grants accrue as
LOCKED promotional grants. They sit in escrow against the wallet, gated behind proof of phone ownership.A
pending_proof wallet is not a stub. It holds a real reload balance and a real ledger of locked grants. The only thing it withholds is the ability to spend promotional credit.The cashier signup flow
Two calls. The first creates the wallet and sends a verification link. The second proves the phone and releases everything in one transaction. The full lifecycle: a reload credits actual balance while the bonus staysLOCKED, a claim before verify is denied, and verify releases the grant.
1
Initiate enrollment
Call
POST /enroll/initiate with the customer’s phone. Feddi normalizes it to E.164, creates a pending_proof wallet, and sends an SMS verification link. No money moves and no POS customer id is bound yet. Both happen at verify.2
Verify the phone
Call
POST /enroll/verify with the signed verification_token from the SMS link, or the keyed code. Feddi flips the wallet to verified and resolves all deferred side-effects in one transaction.Step 1: initiate
POST /enroll/initiate returns the wallet id and confirms the SMS dispatch.
required
The customer’s phone. Normalized to E.164 server-side.
required
The POS provider’s customer id. Bound to the wallet at verify, not at initiate.
required
Explicit program when the customer could map to several. Omit or send
null to use the integration’s current assignment. Ambiguity returns WALLET_PROGRAM_AMBIGUOUS with candidate ids in error.details.candidates.required
Preferred SMS language, for example
en or ar. Defaults to the program or branch default.Initiate response fields
required
The wallet identity id.
required
One of
pending_proof (new or unverified enrollment) or verified (the phone was already a verified customer, an idempotent no-op).required
Whether a verification SMS was dispatched.
false on an idempotent replay that did not re-send.required
One of
sms, whatsapp, or null.required
When the verification link expires (date-time), or
null.required
Always
false at initiate. The POS customer id is bound at verify.required
true if this call created the wallet, false if it already existed.Resends are rate-limited: a 60-second floor between sends and a cap of 3 per rolling 24h window. Use
POST /enroll/resend, which returns sends_remaining_24h and next_send_allowed_at. If the phone is already a verified customer, initiate is an idempotent no-op that returns customer_state: "verified" and does not re-send.Step 2: verify resolves everything atomically
POST /enroll/verify decodes the signed token, guards that it is not expired, not consumed, and that its wallet is still pending_proof, then runs one transaction that resolves four side-effects together.
required
The signed single-use token from the SMS link. Provide exactly one of this or
code, never both.required
The numeric code the cashier keyed, an alternative to the link token. Exactly 6 ASCII digits, for example
482913.1
Bind the POS customer id
Binds the POS provider customer id to the wallet so future orders auto-identify.
2
Bridge to the canonical wallet
Find-or-creates the canonical wallet, so the customer keeps their reload balance plus the now-released bonus.
3
Release the LOCKED grants
Runs the
LOCKED grants through the claim-gate. With verified now satisfied, they flip LOCKED to RELEASED and become spendable promo balance.4
Arm offer eligibility
Turns on marketing and offer eligibility for the now-verified customer.
Verify response fields
required
The wallet identity id.
required
Always
verified on success, including an idempotent replay of a prior verify.required
When verification completed (date-time).
required
The canonical wallet bridged at verify, or
null.required
Whether a new POS customer binding was created.
false on a replay.required
Actual cashable balance after any escrowed credit was applied, in minor units.
required
Released promo balance after any escrowed grants were released, in minor units.
required
ISO-4217 currency code, for example
QAR.required
The
LOCKED grants released by this verify, empty if none were escrowed. Each item carries promo_grant_id, released_minor, source (one of CASHBACK, RELOAD_BONUS, SKU_TOPUP_BONUS, GATEWAY_BONUS, SIGNUP_BONUS), state, and expires_at.Atomicity: all or nothing
None of the four side-effects resolve partially. If any step fails, the whole flip rolls back and the verification link stays consumable. You never see a half-verified customer with a bound POS id but unreleased grants, or a released grant with no canonical wallet behind it.Replay never re-releases
Verify is idempotent. A re-submit of an already-consumed token replays the original verified result withmeta.idempotency_replayed: true. It does not release the grants a second time. Bind your logic to the response, not to the fact that you made the call. See Idempotency & errors.
What does NOT release a grant
Two paths look like they should unlock promo and deliberately do not. Both exist so that softer identity signals never move promotional credit on their own.An anonymous-to-identity merge
Merging an anonymous session into a resolved customer carries grants over, but a merge never releases a
LOCKED grant on its own. Release stays gated on verified plus the claim-gate.A card-fingerprint identify
Identifying by card fingerprint links the session, but the link is probationary (reversible) until the phone is verified. Masked card numbers can collide, so a fingerprint match never auto-acts on money.
The promo-grant state machine
A promotional grant moves through a small, one-way set of states.required
Accrued but gated. Cashback and reload-bonus grants land here while the customer is unverified. Not spendable.
required
Spendable promo balance. Reached only through the claim-gate at verify, or an ops re-drive via
POST /claims. Spent promo-first, FIFO by expiry.required
Reversed, for example reversing a top-up claws back the reload bonus it earned. Terminal.
required
Past its own expiry clock, on a merchant-configured schedule independent of actual balance. Terminal.
Re-driving a release with POST /claims
POST /enroll/verify invokes the claim engine inline for the common path. POST /claims is the explicit re-drive: use it when verification already happened but a later accrual still needs releasing (a top-up bonus that landed after the customer registered).
Resolve the customer by wallet_user_id or by an identity credential. The call is idempotent on the Idempotency-Key header and converges regardless of the order in which register, accrual, and claim arrive: a duplicate claim is a no-op replay, a claim after clawback releases 0, and concurrent claims collapse to exactly one creditor.
required
The claim-state-machine value. One of
pending (awaiting verify or gate), released (grants credited), expired (grants lapsed before claim), or clawed_back (grants reversed before claim, releases 0).required
Total promo released by this claim,
0 when nothing was claimable, in minor units.required
Grants released by this claim. Each carries
promo_grant_id, released_minor, source, state, and expires_at.required
Grants not released, each with a
promo_grant_id and a reason (one of already_clawed_back, expired, gate_not_met, already_released).A clawback never touches the customer’s cash
When a clawback hits promo that is already spent, Feddi books the spent portion as a merchant marketing loss. It does not, and cannot, pull from the customer’s actual cash balance. Promotional credit is a contingent merchant cost. Actual balance is the customer’s real money. The clawback respects that wall. Clawback isPOST /grants/{id}/clawback. It is operator-gated (API-key auth), not a cashier action. A LOCKED grant claws back its full amount. A RELEASED grant claws back only the remaining unspent amount; any already-spent portion is recorded in spent_loss_logged. The call is idempotent on the Idempotency-Key header: clawing back an already-CLAWED_BACK grant replays as a no-op.
required
Why the grant is being clawed back. One of
SOURCE_ORDER_VOIDED, SOURCE_REFUNDED, FRAUD, DUPLICATE, OTHER.required
Free-text note for the audit trail.
required
The promotional grant id.
required
Always
CLAWED_BACK on success.required
The amount clawed back, as
amount_minor plus currency.required
Any already-spent promotional portion booked as a merchant loss, as
amount_minor plus currency. Never pulled from the customer’s actual cash.Inspecting a customer’s grants
Read the full promotional-grant ledger withGET /customers/{customerId}/grants. The path parameter customerId is the wallet identity id. Results are cursor-paginated and tenant-scoped.
required
Filter by grant state. One of
LOCKED, RELEASED, CLAWED_BACK, EXPIRED.required
Opaque pagination cursor from a prior response’s
next_cursor.required
Max grants per page. Default 50, max 100.
Grant fields
required
The grant ledger row id.
required
What funded the grant. One of
CASHBACK, RELOAD_BONUS, SKU_TOPUP_BONUS, GATEWAY_BONUS, SIGNUP_BONUS.required
One of
LOCKED, RELEASED, CLAWED_BACK, EXPIRED.required
The original grant amount, as
amount_minor plus currency.required
The unspent remainder, as
amount_minor plus currency.required
When the grant accrued (date-time), or
null.required
When the grant expires unclaimed or unspent (date-time), or
null.required
Server-derived reference to the finalized source event, for example
pos:order:5512.The response
meta includes data_completeness_score, an opaque integer Feddi may use internally. No action required.Where to go next
Money: actual vs promotional
The two-class model and why locked promo is never a real-money liability.
Enrollment & signup
The initiate, verify, resend, and claims endpoints in detail.
Identity & credentials
Why a card-fingerprint match is probationary until the phone is verified.
Incentives
Reading the grant ledger and managing clawback.