Overview
Enrollment turns a phone number the cashier typed into a verified Feddi customer who can spend, in two calls.POST /enroll/initiate creates a wallet identity in the pending_proof state and sends an SMS verification link. POST /enroll/verify proves phone ownership and atomically resolves every deferred side-effect, including releasing locked promotional grants.
The single rule that governs this page: a LOCKED promotional grant releases only when the customer reaches verified and the claim-gate is satisfied. Proof of phone is the one deliberate exception in the money model.
A customer’s balance is scoped per merchant. The same phone number at a different merchant is a different customer with a different balance.
Read PENDING_PROOF and promo release before you wire any cashback or signup path. This page assumes you know it.
Operations
Four operations make up the callable signup surface. All are stable and callable today.Create the
pending_proof wallet identity and send the verification SMS.Prove phone ownership, flip to
verified, and resolve side-effects atomically.Resend the verification SMS. Rate-limited and attempt-capped.
Drive the claim release-engine directly. Used for ops re-drive;
verify runs the same engine inline.The signup flow
A promo grant landsLOCKED on the proto-wallet, the customer proves their phone by SMS, and verify releases the held promo in one transaction.
POST /enroll/initiate find-or-creates a wallet identity in the pending_proof state for the merchant and normalized phone, mints a single-use signed verification link (a short-TTL JWT), and sends it by SMS. No money moves at this step and no provider-customer link is created yet. The verification SMS is transactional and needs no marketing consent opt-in.
POST /enroll/verify decodes the signed token, guards that it is not expired, not consumed, and that the wallet identity is still pending_proof, then in one transaction flips pending_proof to verified and resolves every deferred side-effect atomically.
1
Bind the POS customer id
Create the provider-customer link so the POS provider’s customer id resolves to this wallet identity on every future order.
2
Bridge to the canonical wallet
Find-or-create the canonical wallet for the verified identity so a money ledger exists.
3
Release the locked promo
Run the claim-gate against every
LOCKED cashback-class grant escrowed on the proto-wallet, flipping eligible grants to RELEASED and crediting spendable promo balance.4
Arm offers eligibility
Mark the customer eligible for marketing and offers, subject to recorded consent.
Verify is idempotent. A re-submit of an already-consumed token replays the original verified result with
meta.idempotency_replayed: true. It does not re-release grants. A genuinely already-consumed token returns HTTP 200 with the replay flag, not a 422.Initiate the signup
POST /enroll/initiate opens the flow. Supply the phone the cashier entered. Pass feddi_session_id in context to anchor the enrollment to a live checkout session, or omit it for a standalone signup.
This call is idempotent on the Idempotency-Key header. A retry with the same key replays the original wallet identity and link without sending a new SMS and without resetting the resend rate-limit window.
required
A partner-generated UUID. Required on every mutating call.
required
Request context.
merchant_id is required; branch_id, terminal_id, cashier_id, partner_session_id, and feddi_session_id are optional. See Conventions.required
The customer phone number. Feddi normalizes it to E.164. An invalid or unsupported country code returns
VALIDATION_ERROR.required
The POS provider’s customer id. Bound to the wallet identity at verify, not here.
required
Disambiguate when the phone resolves to more than one program. Omitting it when the phone is ambiguous returns
WALLET_PROGRAM_AMBIGUOUS with the candidate program ids in error.details.candidates.required
Preferred SMS language for the verification link, for example
en or ar. Defaults to the program or branch default.required
The id of the wallet identity. Persist it to address the same customer on verify, resend, and claims.
required
pending_proof for a new or unverified enrollment. verified if the phone was already a verified customer (an idempotent no-op enroll).The normalized E.164 phone.
The resolved program id.
required
Whether a verification SMS was dispatched.
false on an idempotent replay that did not re-send.One of
sms, whatsapp, or null.When the verification link expires. After this, the customer needs a resend.
Always
false at initiate. The provider-customer link is created at verify.true if this call created the proto-wallet, false if it already existed.The checkout session this enrollment anchored to, when
feddi_session_id was supplied.Verify the phone
POST /enroll/verify proves phone ownership. The customer clicks the SMS link, or the cashier keys the code. Supply verification_token or code. This call releases promo, so treat it as a money path. It is idempotent on the Idempotency-Key header.
required
A partner-generated UUID.
required
Request context.
merchant_id is required.required
The signed single-use token from the SMS link. Supply this or
code.required
The numeric code the cashier keyed, as an alternative to the link token.
required
The id of the now-verified wallet identity.
required
Always
verified on success or an idempotent replay of a prior verify.When verification completed.
The canonical wallet bridged at verify.
The resolved program id.
Whether a new provider-customer link was created.
false on a replay.The verified customer’s actual (real-money) balance in minor units. Pair with
currency.Released, spendable promotional balance in minor units, after the claim-gate ran.
ISO-4217 currency for the balance fields.
The grants this verify released, empty if none were escrowed. Each carries
promo_grant_id, released_minor, and source (one of CASHBACK, RELOAD_BONUS, SKU_TOPUP_BONUS, GATEWAY_BONUS, SIGNUP_BONUS).Money is always minor units with an explicit currency.
balance_minor: 5000 with currency: 'QAR' means 50.00 QAR. The server is authoritative. Never compute money client-side.Resend the verification SMS
POST /enroll/resend mints a fresh single-use token and invalidates the prior one. Resolve the target by wallet_user_id or phone. The target must be in pending_proof; a verified customer needs no link and returns VALIDATION_ERROR. This is a transactional message and needs no consent gate.
Send limits protect the customer from SMS bombing: a maximum of 3 sends per rolling 24-hour window, with a minimum interval of 60 seconds between sends. A send that exceeds the cap is rejected with RATE_LIMITED (HTTP 429) and error.details.retry_after_seconds. A retry within the 60-second floor replays the prior dispatch rather than counting as a new send.
required
A partner-generated UUID.
required
Request context.
merchant_id is required.required
The
pending_proof wallet identity to resend to. Supply this or phone.required
E.164 phone, normalized identically to initiate. An alternative to
wallet_user_id.required
The wallet identity the SMS was sent to.
required
Whether the SMS was dispatched.
When the new verification link expires.
Sends used in the rolling 24-hour window, including the initiate send.
Remaining sends before the 3-per-24h cap.
The earliest time the 60-second floor allows another send.
Drive claims directly
POST /claims is the explicit, idempotent driver for the claim release-engine: it converts a verified customer’s LOCKED cashback-class grants into spendable promo balance. The common signup path does not need it, because POST /enroll/verify invokes the same engine inline. Reach for POST /claims for ops re-drive, and for the top-up-before-register ordering where verification already happened but a later accrual still needs releasing.
For the top-up-before-register ordering, verify already ran with nothing to release, a later accrual escrows a fresh LOCKED grant, and POST /claims releases it.
The engine converges across event orderings. Register-before-accrual and accrual-before-register both converge to the same released balance. A claim after clawback releases 0. A duplicate claim is an idempotent no-op replay. Concurrent claims collapse to exactly one creditor.
Resolve the customer by wallet_user_id or an identity credential in customer. At least one is required. The call is idempotent on the Idempotency-Key header.
required
A partner-generated UUID.
required
Request context.
merchant_id is required.required
The verified wallet identity whose
LOCKED grants to release. Supply this or customer.required
An identity credential resolving the customer, as an alternative to
wallet_user_id.required
The id of the claim resource.
required
The resolved wallet identity.
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).One of
verified, pending_proof, or null.The canonical wallet credited.
Total promo released by this claim in minor units.
0 when nothing was claimable.The spendable promo balance after the claim, in minor units.
ISO-4217 currency for the money fields.
The grants released by this claim. Each carries
promo_grant_id, released_minor, source, state, and expires_at.Grants not released, each with
promo_grant_id and a reason. The reason is one of already_clawed_back, expired, gate_not_met, or already_released.meta.decision_trace_id is an opaque correlation id. Persist it and echo it where a call accepts one.Errors
Every error is a typed string code inerror.code, never a bare HTTP status number. Branch on error.code, not the HTTP status. See Idempotency and errors and the Error reference.
Malformed body, invalid or unsupported phone, expired token, or a wallet identity not in the required state.
details carries the field errors.Missing or invalid
x-api-key or terminal JWT (expired, malformed, or wrong audience).Authorized key but wrong merchant or branch scope, a token belonging to another integration, or a claim against an unverified customer (the claim-gate denies).
Unknown customer, or a customer in another tenant. Cross-tenant requests resolve to
NOT_FOUND, never a leak.The phone resolves to multiple programs and none was specified. Candidates are listed in
error.details.candidates.Same
Idempotency-Key, different payload.Resend exceeded the 60-second floor or the 3-per-24h cap.
error.details.retry_after_seconds tells you when to retry.Where to go next
PENDING_PROOF and promo release
Why locked promo releases only on verified plus the claim-gate, and how grants escrow.
Money: actual vs promotional
The two-class balance model that enrollment unlocks.
Top-up and reload bonus
Where the locked promo grants that signup releases come from.
Identity and credentials
How a customer is resolved before you enroll them.