Essential Nigeria
Voucher Integration Guide
🎟️

Voucher Integration Guide

Accept Essential Wallet vouchers on your site or store

An Essential Wallet voucher is a code worth money β€” a fixed amount off, or a percentage off β€” that its creator has already funded. This guide is for anyone who wants to accept vouchers: a shop owner with no developer, or an engineering team building a checkout. There are exactly two ways to integrate, and both run the same secure flow underneath (the shopper proves the voucher is theirs with a one-time SMS code).

60-second version

Customer: enters a voucher code + their phone number at your checkout β†’ gets a 6-digit SMS code β†’ types it in β†’ the voucher value comes off the total. You (the merchant) are settled the discount into your Essential Wallet. You either paste the widget, or call four API endpoints from your server.

πŸ“š

Core concepts

Two types

Fixed β€” a set amount off (e.g. ₦5,000). Percentage β€” a percent off, always with a maximum cap (e.g. 20% up to ₦10,000).

A category

What the voucher may be spent on. One of: general, food, medical, transport, education, utilities, shopping. Checkout redemption enforces it.

A number of uses

A voucher can be redeemed up to its total_uses times, then it's exhausted. Each use is one redemption.

Status

active Β· paused Β· exhausted Β· expired. Only active vouchers redeem.

What a voucher looks like
{ "code": "FOOD2026", "type": "fixed", "category": "food", "amount_kobo": 500000, "total_uses": 20, "used_count": 8, "status": "active", "expires_at": "2026-12-31T23:59:59+00:00" }

All amounts are in kobo (₦1 = 100 kobo), as integers β€” never floats.

πŸ‘₯

Who does what

Role What they are What they do
Creator Any Essential Wallet user (or the platform) Mints the voucher and funds it up front from their wallet. Can't redeem their own voucher.
Shopper Any Essential Wallet user who received the code Redeems it at your checkout. Proves ownership with their phone number + a one-time SMS code.
Merchant (you) A store with an Essential Wallet account Accepts the voucher and is settled the discount into that wallet. Owns the merchant API key.
Admin Essential Wallet operator Can reverse any settled redemption, e.g. after a dispute.

A "merchant" is not a separate login β€” it's a wallet account

You accept vouchers using an ordinary Essential Wallet account that the voucher was pointed at (its settlement account). The same account can also sign in to the wallet app like any user. What makes it a merchant is that it holds an API key and vouchers name it as where they settle.

πŸ’°

How the money works

Vouchers are pre-funded. The moment a creator mints one, their wallet is debited for the full worst-case payout and that value sits in escrow until a shopper redeems it. Nothing is created from nothing, and there is no way to redeem a voucher the creator hasn't paid for.

The rule the creator is charged by
fixed voucher:       amount_kobo    Γ— total_uses
percentage voucher:  max_amount_kobo Γ— total_uses      // the cap is the worst case per use
Where the money goes next depends on the mode:
Mode Used by Settlement
Wallet A shopper in the wallet app The discount is credited straight to the shopper's own wallet immediately (a percentage voucher pays its cap).
Checkout A store, via the API or widget The discount is credited to the merchant's wallet when the store confirms the sale.
Checkout lifecycle β€” a reservation holds a use, then settles or is freed
CREATE   creator wallet βˆ’amountΓ—uses   β†’  voucher ACTIVE, use(s) in escrow
   β”‚
REDEEM   shopper enters code + phone, passes the SMS code
   β”‚     β†’  reservation PENDING, one use held (expires in 30 minutes), NO money moves yet
   β”‚
   β”œβ”€β”€ CONFIRM   merchant wallet +discount   β†’  redemption CONFIRMED   (sale honoured)
   β”œβ”€β”€ RELEASE   use handed back             β†’  redemption RELEASED    (no sale; voucher reusable)
   └── (no confirm within 30 min β†’ the hold EXPIRES and the use is handed back automatically)

REVERSE  a CONFIRMED settlement can be reversed:
         merchant wallet βˆ’discount, use handed back  β†’  redemption REVERSED, value back in escrow
🧩

No-code widget

If you can edit your website's HTML, you can accept vouchers. There's nothing to install, build, or run on your server. The widget renders the whole experience β€” code entry, the SMS code, and the confirmation β€” and talks to Essential Wallet directly from the shopper's browser.

You only need one thing: your publishable key

Ask Essential Wallet for a merchant key. You'll get two values β€” a secret key (esk_…) and a publishable key (epk_…). The widget uses the publishable one, which is safe to put in a page because it can only reach the voucher widget β€” never your settlements or your books.

Step 1 β€” paste these two lines where you want the box

HTML
<script src="https://api-ewallet.eroot.ng/voucher-widget.js" async></script>

<div
    data-essential-voucher-widget
    data-publishable-key="epk_YOUR_PUBLISHABLE_KEY"
    data-api-base="https://api-ewallet.eroot.ng"
    data-cart-kobo="2500000"
    data-category="food"
    data-auto-confirm="true"></div>

Step 2 β€” set two values for your store

  • data-cart-kobo β€” the shopper's current order total, in kobo (₦25,000 = 2500000). The discount is worked out from this.
  • data-category β€” the kind of goods you sell, so the right vouchers are accepted (one of the seven category values above).

Step 3 (optional) β€” tidy up

  • Set data-auto-confirm="true" so a verified voucher settles immediately β€” no server needed. Leave it off if your backend will confirm the sale itself.
  • Brand it with data-accent="#7c3aed" and data-title="Have a voucher?".
  • If your cart total changes live, tell the widget: EssentialVoucher.updateCart(el, 1999000).

Full attribute list

  • data-publishable-key β€” required. Your epk_… key.
  • data-api-base β€” the Essential Wallet API origin. Defaults to wherever the script is served from.
  • data-cart-kobo β€” order total in kobo.
  • data-category β€” category to redeem against; defaults to the voucher's own category.
  • data-auto-confirm β€” "true" to settle on verification.
  • data-order-reference β€” your own order id; a unique one is generated if omitted.
  • data-accent β€” hex accent colour. data-title β€” heading text.

What your shopper experiences

  1. Types the voucher code and the phone number on their wallet.
  2. Receives a 6-digit code by SMS and types it in.
  3. Sees "Voucher applied β€” ₦5,000.00 off this order." (You then charge the reduced total.)

Reacting to the widget (optional)

If you want to update an order summary on your page, listen for these DOM events on the container:

JavaScript
document.querySelector('[data-essential-voucher-widget]')
    .addEventListener('essential-voucher:redeemed', function (e) {
        var reservation = e.detail.reservation;   // { discount_kobo, amount_to_charge_kobo, redemption_reference, ... }
        console.log('Discount:', reservation.discount_kobo / 100);
    });
πŸ”‘

Getting your keys

Essential Wallet issues merchant keys to the store's own Essential Wallet account. There's no self-serve portal yet β€” contact support, and an operator issues a key pair. The response is shown once, so store it safely.

Key Prefix Use it for Safe in a browser?
Secret esk_… Every API endpoint, including settlement No β€” server only
Publishable epk_… Only the voucher widget endpoints Yes

Base URL

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

Every API path in this guide is relative to that. Send your secret key as Authorization: Bearer esk_….

πŸ› οΈ

Developer API

The merchant API is server-to-server: your backend holds the secret key and makes these calls. The shopper never sees a key; they only ever type the voucher code, their phone number, and the SMS code. Below is the complete flow with copy-paste samples.

1. Look up the voucher β€” optional but recommended

Confirm the code is real and see the discount for the current cart before you ask the shopper for anything.

2. Send the shopper a code

This texts a 6-digit code to the shopper's phone. It only works for vouchers that settle to your store, and only if the phone belongs to an Essential Wallet account.

3. Redeem β€” reserve the discount

Exchange the SMS code for a reservation. This holds one use for 30 minutes and returns the exact amount to charge. No money moves yet.

4. Charge the shopper, then confirm

Charge your own payment provider for amount_to_charge_kobo (the total minus the discount). Once that succeeds, call confirm and the discount is paid into your wallet.

If the charge fails, free the hold so the voucher can be used again:

Putting it together (PHP)

PHP β€” the whole checkout flow (inside your own controller/service)
$key   = getenv('ESSENTIAL_MERCHANT_KEY');          // esk_…
$base  = 'https://api-ewallet.eroot.ng/api';
$auth  = ['Authorization' => "Bearer {$key}", 'Content-Type' => 'application/json'];

function call($method, $url, $body = null) {
    global $auth;
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_HTTPHEADER     => array_map(fn($k, $v) => "$k: $v", array_keys($auth), $auth),
        CURLOPT_POSTFIELDS     => $body ? json_encode($body) : null,
    ]);
    $res = json_decode(curl_exec($ch), true);
    curl_close($ch);
    if (!empty($res['error'])) { throw new RuntimeException($res['error']); }
    return $res['data'] ?? $res;
}

// 1. shopper enters code + phone
call('POST', "{$base}/merchant/vouchers/{$code}/redeem/request-otp", ['phone' => $phone]);

// 2. shopper enters the SMS code
$reservation = call('POST', "{$base}/merchant/vouchers/redeem", [
    'otp_reference'   => $otpReference,
    'otp'             => $otp,
    'cart_kobo'       => $cartKobo,
    'order_reference' => "ORDER-{$orderId}",
    'category'        => 'food',
]);

// 3. charge Shopper.php for $reservation['amount_to_charge_kobo'] with YOUR gateway…

// 4. payment succeeded β€” settle the discount to your wallet
call('POST', "{$base}/merchant/vouchers/redeem/{$reservation['redemption_reference']}/confirm");

Prefer the widget's endpoints directly?

Browser apps can call the same flow through the public widget endpoints with the X-Publishable-Key: epk_… header: /public/widget/vouchers/{code}, …/redeem/request-otp, …/redeem, and …/redeem/{reference}/confirm. These are CORS-enabled.

πŸ”€

The redemption flow at a glance

Sequence
Shopper                Your server (or the widget)          Essential Wallet
  β”‚                              β”‚                                     β”‚
  β”‚ enters code + phone ───────► β”‚ POST …/request-otp ───────────────► β”‚ SMS a 6-digit code
  β”‚ ◄─────────────────────────── β”‚                                     β”‚
  β”‚ types SMS code ────────────► β”‚ POST …/redeem ────────────────────► β”‚ hold 1 use, 30 min
  β”‚ ◄── "₦5,000 off; pay ₦20,000" β”‚ ◄── reservation reference ─────── β”‚
  β”‚ pays reduced total to YOU ─► β”‚ (your own payment gateway)          β”‚
  β”‚                              β”‚ POST …/confirm ───────────────────► β”‚ discount β†’ your wallet
  β”‚                              β”‚                                     β”‚ βœ… settled

A store's job is steps 1–4: identify the shopper, get the reservation, take payment, confirm. If anything fails before payment, release the reservation (or just let the 30-minute hold lapse) β€” the voucher returns to full availability either way.

πŸ”

Idempotency & retries

Networks flake and webhooks fire twice. Every step is built to be retried safely:

  • Redeem is keyed on your order_reference. Retry the same order and you get the same reservation back β€” never a second use burned. Use a fresh reference only for a genuinely new order.
  • Confirm and release tolerate repeats. Confirming an already-confirmed reservation returns the original success without paying twice; releasing an already-released one is a no-op.
  • Distinct references per order. Reusing one order_reference for two different orders will return the first reservation, not a new one.

Generate order_reference from your own order id

Something like ORDER-{your_order_id} is ideal β€” stable across retries, unique per order.

πŸ—‚οΈ

Managing your vouchers

These endpoints let a store see and control the vouchers that settle to it. Everything is scoped to your own store β€” another merchant's voucher reads back as a plain 404, identical to one that doesn't exist.

↩️

Reversals

If an order is refunded or a settlement was wrong, a confirmed redemption can be reversed. The credited party is debited and the voucher use is handed back β€” the value goes back into escrow (the creator is not paid out), so it can be used again.

Wallet-mode credits (paid into a shopper's wallet) and disputed settlements are reversed by an Essential Wallet admin via POST /vouchers/redemptions/{reference}/reverse β€” the merchant API has no business touching a credit paid into someone's wallet.

πŸ””

Outbound webhooks

Rather than polling GET /merchant/redemptions/{reference}, register one HTTPS endpoint and receive the reservation lifecycle as it happens. Delivery is best-effort and never blocks the money path β€” the redemption settles whether or not your endpoint is up, and failed attempts are retried with backoff and logged. Registering opts you into all four events.

Configure once, then read the delivery log (secret key)
# Set the destination. Returns a signing_secret ONCE β€” store it.
curl -X PUT https://api.ewallet.eroot.ng/api/merchant/webhook \
  -H "Authorization: Bearer esk_..." -H "Content-Type: application/json" \
  -d '{"url":"https://store.example.com/wallet/webhook"}'

# Recent attempts (filter with ?status=failed or ?event=reservation.confirmed)
curl https://api.ewallet.eroot.ng/api/merchant/webhook/deliveries \
  -H "Authorization: Bearer esk_..."
EventWhen
reservation.createdDiscount reserved β€” payment still pending
reservation.confirmedSettled β€” your wallet was credited
reservation.releasedHold released before settlement
reservation.expiredHold lapsed past the 30-minute window

Each delivery is signed. Verify it before trusting the body, then return any 2xx to acknowledge:

Verifying a delivery (PHP)
$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_ESSENTIAL_TIMESTAMP'];   // X-Essential-Timestamp
$expected  = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $body, $signingSecret);  // your whsec_...
$received  = $_SERVER['HTTP_X_ESSENTIAL_SIGNATURE'];   // X-Essential-Signature

if (! hash_equals($expected, $received)) { http_response_code(400); exit; }

$event = json_decode($body, true);   // $event['type'], $event['data']['redemption_reference'], ...
http_response_code(200);             // non-2xx is retried 6x over ~24h

Full field list and error semantics are in the API reference β†’ Webhooks.

⚠️

Errors

Failures return a non-2xx status and one JSON envelope. Branch on the stable code, and show the human-readable message to staff β€” never match on the English text. The legacy error field is kept as a deprecated alias of message.

Failure envelope
{ "status": false, "code": "voucher.category_mismatch",
  "message": "This voucher is only valid for food purchases, not medical.",
  "details": { "expected": "food", "received": "medical" }, "error": "…" }
Status Meaning
401Missing, malformed, or revoked key.
404Voucher/redemption not found, or it belongs to another merchant.
422The request was understood but refused β€” invalid/expired voucher, wrong category, bad code, already used, etc.
429Too many attempts (per phone or per key). Back off and retry later.
500 / 502Unexpected error or an SMS send failure. Retry; escalate if it persists.
πŸ§ͺ

Testing

  • There is no sandbox β€” keys work against live data. Test with a small real voucher and amounts you're comfortable with.
  • The redeeming phone number must belong to an existing Essential Wallet account, or the OTP request returns "No Essential Wallet account found". Register a test wallet first.
  • The creator of a voucher cannot redeem their own voucher β€” use two accounts.
  • A shopper can only use a given voucher once (per active reservation).

Quick end-to-end check

  1. Create a small fixed voucher at My Vouchers with a merchant email you control and a category.
  2. Look it up with GET /merchant/vouchers/{code}.
  3. Run the four-step flow with the shopper's phone number.
  4. Confirm your merchant wallet balance rose by the discount.
❓

FAQ & troubleshooting

"This voucher is invalid or has expired."

The code is wrong, paused/exhausted/expired, or it settles to a different merchant. A mismatch is deliberately indistinguishable from a non-existent code.

Nothing is credited when I redeem just before payment

That's expected β€” redeeming only reserves the discount. Money reaches your wallet when you confirm after taking payment.

My cart total changes after the shopper applies a voucher

Call EssentialVoucher.updateCart(el, newKobo) (widget) or re-run the flow with the new cart_kobo before confirming.

Can I put my secret key in the page?

No. Use the publishable key with the widget. The secret key goes in your server's environment only.

The shopper got no SMS

Check the number belongs to a wallet account and hasn't hit the request limit (three codes per number per 10 minutes). A 502 means the SMS gateway failed β€” retry shortly.