Security and flow locks
Security is strongest when each layer has a clear job. The SDK makes correct flows easy; the backend enforces them for every caller.
In the SDK#
- Bearer tokens are sent only when configured; replace them with
setAccessTokenas a session changes. - The GraphQL client does not persist credentials in browser storage.
- Cart methods validate basic request shape before making a request.
- Checkout is single-flight per cart in a client instance and uses an idempotency key.
- Self-scoped cart, wallet, and profile helpers let the backend resolve the signed-in actor rather than trusting a caller-provided user ID.
- Secure payment initiation and payment-status reads are scoped to the authenticated user's own flow.
- Ozow pay-in and bank-transfer starts use JWT authentication followed by ownership validation for the target user.
- Request and checkout events help applications respond to success and failure without logging credentials.
- Webhook URLs are checked by the client for HTTPS shape and independently checked by the backend for safe resolution, ownership, and supported event configuration.
On the server#
The server remains responsible for JWT authentication, actor and resource ownership, role permissions, input validation, authoritative prices/inventory, payment sequencing, idempotency records, audit trails, and rate limits. The SDK cannot secure a resolver that does not enforce those rules.
The checkout backend stores idempotency claims under a unique user/key pair. A losing concurrent request is rejected without changing the active claim. The primary and mirror backend projects should keep this behavior in parity.
Wallet transfers require an idempotency key. sdk.wallet.transfer creates one and reuses it for an unchanged retry in the same SDK instance. The backend atomically writes the sender wallet, recipient wallet, and both ledger entries, and replays the cached response for the same user/key/payload. Raw GraphQL callers must supply a stable key themselves.
Transfer transactions require MongoDB transaction support (a replica set or sharded cluster). If the database cannot start a transaction, the backend rejects the transfer before committing wallet or ledger changes; standalone MongoDB deployments must be converted before enabling wallet transfers.
Withdrawals require an idempotency key in their input and the SDK can generate one for you. If a financial request times out, retry the same logical action with the same key rather than generating a new one.
Two-factor authentication#
Login accepts an optional twoFactorCode; include it when the account requires it. Setup, confirmation, verification, and disable operations are JWT-authenticated and ownership-checked by the backend. beginTwoFactorSetup also needs the account password while 2FA is off. Keep setup secrets and authenticator codes out of browser storage, application logs, analytics, and support tickets. Do not interpret a setup secret as proof that 2FA is enabled; require the confirmation operation to succeed.
Sensitive money movements (wallet withdrawals and transfers, bank withdrawals, and admin withdrawals) are step-up gated. The server rejects them with STEP_UP_REQUIRED unless the request carries a fresh, unused 2FA code in X-Step-Up-Code. Sessions created with a device ID are bound to it (X-Device-Id); the server rejects requests from another device or a changed network, and sessions created without one skip that check for older clients. The SDK retries with a code from getStepUpCode and never sends a code that the user has not entered. Do not cache step-up codes, and do not reuse one across operations.
Hosted payment UX#
For Ozow deposits, request modal data from sdk.wallet.startOzowDepositModal(...) and pass the returned paymentUrl and server-generated postData directly to Ozow's official browser integration. The SDK does not construct its own payment form or iframe. Never expose the Ozow private key, trust a return URL as proof of payment, or credit a wallet from a browser callback. Reconcile the transaction through the server and wait for its confirmed state.
For an outbound payment event, verify the timestamped HMAC against the exact raw body and atomically deduplicate the event ID before enqueuing fulfillment. A local sdk.on('checkout:completed') event is not a payment confirmation or a durable server event.
Protect credentials#
- Never commit API keys, wallet secrets, or access tokens.
- Never expose a private API key in browser or mobile application bundles.
- Prefer a server-side proxy for browser integrations when the backend key is private.
- Use HTTPS outside local development.
- Avoid logging request headers, tokens, reset links, or signed download URLs.
Be careful with user data#
The backend must check ownership even when the caller supplies a user or store ID. Do not treat a successful client-side validation, a cart event, or a checkout readiness response as authorization.