Essential Nigeria
Wallet API v1.0
Live
πŸ“–

API Documentation

Essential Nigeria Wallet β€” Backend REST API

Base URL

https://api-ewallet.eroot.ng/api

Version

v1.0.0

Format

application/json

About this API

This is the backend API powering the Essential Nigeria e-wallet platform. It handles user authentication, dedicated virtual accounts via Paystack, peer-to-peer transfers, cooperative savings (Ajo), fixed deposits, voucher management, and webhook processing.

πŸ”

Authentication

Protected endpoints require a JWT Bearer token in the Authorization header. Obtain a token via POST /login.

Authorization Header
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGci...
Token Expiry: Tokens expire per the JWT_TTL config value (default 60 minutes). On expiry the API returns 401 Token expired.
⚠️

Error Codes

Code Meaning Common Cause
200 OK Request succeeded
201 Created Resource created successfully
400 Bad Request Missing required field or invalid parameter
401 Unauthorized Missing, expired, or invalid JWT token
403 Forbidden Action not permitted for this user
404 Not Found Resource does not exist
422 Unprocessable Entity Validation failed β€” check errors object
500 Internal Server Error Unexpected server-side exception
502 Bad Gateway Upstream Paystack API call failed
Error Response Shape
{
    "error": "Invalid credentials",        // single error string
    "errors": {                             // validation error map (422)
        "email": ["The email field is required."],
        "amount": ["The amount must be at least 1."]
    },
    "message": "Human-readable description"
}
πŸ‘€

Auth Endpoints

🏦

Customers

πŸ’³

Dedicated Virtual Accounts

Dedicated Virtual Accounts (DVAs) are unique bank account numbers assigned to each user via Paystack. Incoming transfers to the DVA are detected and credited to the user's wallet.
βž•

Fund Wallet

A second way to fund a wallet, on top of a bank transfer into the dedicated account above β€” card or bank payment via Paystack's hosted checkout, initiated on demand from the dashboard. Two things credit the wallet for the same payment, both idempotent on the same Paystack reference: the verify call below (the fast path, when the browser returns from checkout) and the Paystack webhook (the reliable path, in case it never does). Whichever lands first wins β€” the other is a no-op.

Walkthrough: a ₦2,000 top-up

  1. User taps Fund Wallet on the dashboard and enters ₦2,000.
  2. The app calls POST /wallet/fund/initialize with amount_kobo: 200000. Essential Wallet asks Paystack to open a checkout session tagged with metadata: { purpose: "wallet_funding", user_id } and returns an authorization_url.
  3. The user is redirected to that URL and pays by card or bank on Paystack's own page β€” Essential Wallet never sees the card details.
  4. Paystack redirects back to the app's callback URL with ?reference=....
  5. The app calls GET /wallet/fund/verify/{reference}, which checks the payment with Paystack directly and β€” only if it actually succeeded β€” credits ₦2,000 to the wallet.
  6. Independently, Paystack's own webhook delivers charge.success for the same reference. If it arrives before step 5, step 5 just reports credited: false for a reference already credited β€” the wallet is still only credited once.
πŸ’Έ

Transfers

Unverified accounts are capped

Users who haven't completed KYC verification can transfer up to config('kyc.unverified_daily_transfer_limit_kobo') (default ₦50,000) cumulatively per day. Exceeding it returns 422 with code: "KYC_REQUIRED".

πŸ”§

Transfer Control

Balance sync, OTP management, and transfer finalization.

Admin only β€” not a per-user setting

The three endpoints below toggle Paystack's account-wide OTP requirement β€” it applies to every transfer on the platform for every user, not just the caller's own. All three require users.is_admin and return 403 for regular users.

πŸ“‹

Beneficiaries

🐷

Savings

All savings endpoints are prefixed with /savings and require authentication.

πŸ“ˆ

Fixed Deposits

Prefix: /fixed-deposits. All endpoints require authentication.

🀝

Ajo (Cooperative Savings)

Ajo is a traditional Nigerian rotating savings system. A group of members each contribute a fixed amount every cycle; one member receives the entire pot per cycle. Prefix: /ajo. All endpoints require authentication.

🎟️

Vouchers

Vouchers are redeemable codes with two redemption modes. Wallet mode (the default) credits the redeemer's own Essential Wallet balance β€” a personal reward, unconnected to any purchase. Checkout mode settles the voucher's value to a merchant instead, designed for a third-party storefront (an "eStore") that wants a voucher to actually discount a real order. Voucher types: fixed (flat NGN amount) or percentage (% of cart value) β€” both work in either mode. Prefix: /vouchers. All endpoints require authentication.

Who can create what

Fixed vouchers can be created by any user β€” creating one debits the creator's own wallet for the full possible payout (amount_kobo Γ— total_uses), the same way buying a gift card would. This is what makes it safe to let any user mint one β€” nothing is created from nothing. Percentage vouchers have no fixed amount to back at creation time, so those remain admin/staff-only (users.is_admin). A creator can never redeem their own voucher.

Every voucher has a category, and it's enforced β€” not just a label

Every voucher is minted with a category: general, food, medical, transport, education, utilities, or shopping.

Example: a ₦5,000 voucher is created with category: "food". At checkout, an eStore selling medical supplies calls POST /vouchers/redeem with mode: "checkout" and category: "medical" for its cart. The API rejects it β€” 422 "This voucher is only valid for food purchases, not medical." β€” and no discount is reserved. The same call with category: "food" against a grocery eStore succeeds.

This is only enforced in checkout mode, where the store declares what the order is actually for. In wallet mode the credit lands in the redeemer's general wallet balance and becomes fungible with everything else in there β€” there's no purchase to check the category against, so it's informational only there (surfaced in the redemption message and in the wallet app's voucher list).

How a voucher affects a real purchase (checkout mode)

Walk through the exact scenario of a ₦25,000 product and a ₦5,000 voucher, using checkout mode end-to-end. This is the merchant-key + OTP flow β€” the one a third-party eStore should use. It authenticates every single call with only the eStore's own esk_... merchant key; the shopper never leaves the eStore's own checkout page, never sees a wallet login screen, and no wallet JWT is ever issued to or handled by the eStore. See Merchant / eStore Integration for the full endpoint reference.

  1. Customer has a ₦25,000 product in their cart on the eStore, and enters a voucher code at checkout. The eStore's backend calls GET /merchant/vouchers/{code} with its esk_... merchant key to fetch the voucher's value, category, and validity up front, and shows the shopper a discount preview.
  2. The checkout modal asks for the phone number linked to the shopper's Essential Wallet account. The eStore's backend calls POST /merchant/vouchers/{code}/redeem/request-otp with that phone number β€” still just the merchant key, nothing from the shopper. Essential Wallet resolves the phone number to a wallet account, texts it a 6-digit code, and responds with an opaque otp_reference (never the code itself).
  3. The shopper types the code they received by SMS into the same modal β€” no redirect, no popup. The eStore's backend calls POST /merchant/vouchers/redeem with otp_reference, otp, cart_kobo: 2500000, category: "food" (what this cart actually is β€” must match the voucher's own category), and the eStore's own order_reference. Essential Wallet verifies the code, resolves which wallet account it belongs to, and reserves the ₦5,000 discount β€” no money moves yet β€” responding with amount_to_charge_kobo: 2000000 and a redemption_reference.
  4. The eStore charges Paystack for ₦20,000 β€” the pre-computed remainder, not the original cart total.
  5. Paystack payment succeeds. The eStore calls POST /merchant/vouchers/redeem/{reference}/confirm (merchant key). This is the moment ₦5,000 actually moves: it's credited to the merchant's own Essential Wallet balance (the account registered as merchant_email when the voucher was created) β€” never the customer's wallet.
  6. Between the merchant's Paystack settlement (₦20,000, minus Paystack's fees) and the Essential Wallet credit (₦5,000), the merchant ends up holding the full ₦25,000 of value β€” just split across two accounts instead of one.
  7. If the Paystack charge fails or the customer abandons checkout, the eStore calls POST /merchant/vouchers/redeem/{reference}/release (merchant key) to free the voucher slot back up β€” or does nothing and lets the 30-minute hold expire on its own.

There's a second, JWT-based way to reach the same mode: "checkout" reservation

POST /vouchers/redeem (documented below, in this section) also accepts mode: "checkout" when called with a shopper's own wallet JWT instead of a merchant key β€” that's what powers checkout redemption inside the Essential Wallet app itself. A genuine third-party eStore has no way to obtain a shopper's wallet JWT (it's a different party on a different server), so it should always use the merchant-key + OTP flow above instead β€” never try to get a shopper's JWT into your own backend.

eStore integration checklist

  1. The merchant creates an Essential Wallet account once (same signup any user goes through) and shares that email with whoever creates the voucher, so it can be set as merchant_email at creation time.
  2. Get a merchant API key (esk_...) issued for the store β€” see Merchant / eStore Integration. This one key authenticates every call below; you never need a wallet JWT for any of it.
  3. When the shopper enters a code, use that merchant key to call GET /merchant/vouchers/{code} β€” returns value, category, validity, remaining uses, merchant name, and an optional discount preview via ?cart_kobo=. Use it to show the shopper what the code is worth and reject an obvious category mismatch before going any further.
  4. Ask the shopper for the phone number on their Essential Wallet account (a field in your own checkout modal β€” no redirect), and call POST /merchant/vouchers/{code}/redeem/request-otp with it, still just your merchant key. A 6-digit code is texted to that number.
  5. Ask the shopper for the code they just received (another field in the same modal), and call POST /merchant/vouchers/redeem with otp_reference, otp, the real cart_kobo, the cart's category (rejected with 422 if it doesn't match the voucher's own), and your own order_reference β€” this is what reserves the discount.
  6. Charge Paystack for the returned amount_to_charge_kobo β€” not the original cart total.
  7. On Paystack success, call POST /merchant/vouchers/redeem/{reference}/confirm (merchant key). This is what actually pays the merchant β€” nothing is settled before this call.
  8. On Paystack failure or an abandoned cart, call POST /merchant/vouchers/redeem/{reference}/release (optional β€” an unconfirmed reservation also expires on its own after 30 minutes).
  9. For reconciliation afterwards, use the merchant key again against GET /merchant/redemptions/{reference} to check settlement status without needing the shopper's session.

The trust boundary that remains

Essential Wallet has no way to independently verify that the eStore's Paystack charge actually succeeded β€” confirm is an attested call from the eStore's own server, not something checked against Paystack directly. A compromised or buggy eStore integration could call confirm without ever having charged the customer, paying the merchant for nothing. This is the same category of trust assumption already noted for merchant_name/merchant_url being informational β€” treat the eStore's own server as the thing responsible for only calling confirm after a real, verified Paystack success.

🏬

Merchant / eStore Integration

The server-to-server surface for a third-party storefront ("eStore"): looking up a voucher, redeeming one at checkout, and checking settlement status β€” all authenticated with a single business-level credential, your merchant API key. No wallet login, redirect, or JWT is ever needed on the eStore's side for any of this.

One credential for eStores: your merchant key. Never a wallet JWT.

The wallet's own frontend authenticates its calls with a JWT issued to a logged-in Essential Wallet user. An eStore is a different party, on a different server, and is never issued one of those JWTs β€” there is no supported way for a third-party eStore to obtain a shopper's wallet JWT, and it should not try to.

Credential Identifies Used for Lives where
esk_... (merchant API key) The store, as a business Every endpoint on this page, including checkout redemption Store's server env/secrets only β€” never sent to a browser

So how does Essential Wallet know which shopper is redeeming, if the eStore never holds a shopper credential? By phone number + a one-time code texted to it β€” see POST /merchant/vouchers/{code}/redeem/request-otp below. The shopper types both into the eStore's own checkout modal; nothing about it involves the wallet's own login screen.

Getting a key

There's no self-serve merchant portal β€” an operator issues a key on the api-wallet backend. But this command does not create anything for you β€” it only looks up an already-existing users row by email and attaches a key to it. Run it against an email nobody has registered yet, and it fails.

The email argument must belong to a real, already-registered Essential Wallet user

you@yourstore.com below is a placeholder β€” swap it for the merchant's actual email, and that person/business must sign up for an Essential Wallet account first (the normal /register flow, same as any user β€” via the wallet app's sign-up page, or POST /register directly). This is the same account that gets set as merchant_email when creating vouchers β€” see Vouchers. Run the command before that signup exists, and it fails immediately:

$ php artisan merchant:create-key you@yourstore.com --label="my store β€” production"
No Essential Wallet account found for that email.

Register that email with the wallet first, then re-run the exact same command β€” no other change needed.

Terminal β€” on api-wallet, after the merchant has an Essential Wallet account
php artisan merchant:create-key you@yourstore.com --label="my store β€” production"

# Merchant API key created. Copy it now β€” it will not be shown again:
#
#   esk_OriC12b7AKrhQJtu7J6pcQN32YVJjkFWWo6PSShE
#
# +----+------------------+-----------------------+--------------+
# | ID | Merchant         | Label                 | Prefix       |
# +----+------------------+-----------------------+--------------+
# | 3  | you@yourstore.com| my store β€” production | esk_OriC12b7 |
# +----+------------------+-----------------------+--------------+

Only the key's SHA-256 hash is stored β€” if it's lost, issue a new one, there's no way to recover the plaintext.

What this endpoint deliberately leaves out

Who created the voucher and who has redeemed it are not in this response, even though the merchant-key requirement means only a registered integrator can call it at all. Those are wallet users' own account activity, not the voucher's business-facing data β€” handing a customer's identity to a third-party store isn't something this endpoint does, regardless of which merchant is asking. used_count/uses_remaining tell you how many times it's been used without saying by whom.

Checkout redemption (merchant key + OTP)

The four endpoints below are how a third-party eStore actually redeems a voucher at checkout β€” the merchant-key equivalent of /vouchers/redeem and its confirm/release friends, but authenticated with your merchant key throughout instead of a shopper's wallet JWT. See the walkthrough and checklist in Vouchers for the end-to-end scenario these four calls fit into.

Worked example: rejecting a category mismatch

A shopper on a medical-supplies eStore tries to apply a food voucher. The store's own backend catches this cheaply, and the wallet API enforces it independently even if the store didn't bother checking:

  1. Store calls GET /merchant/vouchers/FOOD2026?cart_kobo=800000 with its esk_... key. Response: { "valid": true, "category": "food", "preview_value_kobo": 500000 }.
  2. Store compares "food" against its own cart category, "medical" β€” mismatch. It shows the shopper "This voucher can't be used here" without ever calling redeem. No customer JWT was needed for this rejection.
  3. Even if the store's own check had a bug and it went ahead with the OTP flow anyway β€” POST /merchant/vouchers/redeem with category: "medical" and a valid, verified OTP β€” the wallet API rejects it independently: 422 "This voucher is only valid for food purchases, not medical." β€” no discount is ever reserved.
πŸͺͺ

KYC Verification

Optional identity verification (NIN or BVN), backed by VerifyNG, a separately-deployed identity-verification microservice. Requires VERIFYNG_BASE_URL, VERIFYNG_CLIENT_KEY, and VERIFYNG_API_SECRET to be configured β€” without them, /kyc/verify returns a 502 with a clear "not configured" message rather than failing silently.

πŸ””

Webhooks

Important β€” Webhook Security

All Paystack webhook requests include an x-paystack-signature header containing an HMAC-SHA512 signature. The webhook handler verifies this against your PAYSTACK_SECRET_KEY before processing.

Webhook Behaviour Summary

Event Action
charge.success (DVA) Credits the wallet immediately, keyed to the Paystack transaction reference β€” safe against duplicate webhook delivery
charge.success (wallet_funding) Credits the wallet for the user_id in metadata β€” same idempotency guard, and a no-op if GET /wallet/fund/verify already credited this reference first
transfer.success Updates WalletTransaction status β†’ success (wallet was already debited when the transfer was initiated)
transfer.failed Updates WalletTransaction status β†’ failed and credits the wallet back β€” guarded against double-refunding on retries
transfer.reversed Updates WalletTransaction status β†’ reversed and credits the wallet back β€” same guard as transfer.failed
Essential Nigeria

Essential Nigeria Wallet API β€” v1.0.0 β€” Β© 2026

Powered by Paystack Β· Built with Laravel