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).
No-code widget
Paste two lines of HTML onto your site. Best if you don't have a developer.
Jump to the widget βREST API
Full control from your own backend, with code samples. Best for developers.
Jump to the API β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.
{ "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.
fixed voucher: amount_kobo Γ total_uses percentage voucher: max_amount_kobo Γ total_uses // the cap is the worst case per use
| 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. |
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
<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"anddata-title="Have a voucher?". - If your cart total changes live, tell the widget:
EssentialVoucher.updateCart(el, 1999000).
Full attribute list
data-publishable-keyβ required. Yourepk_β¦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
- Types the voucher code and the phone number on their wallet.
- Receives a 6-digit code by SMS and types it in.
- 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:
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)
$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
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.
# 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_..."
| Event | When |
|---|---|
| reservation.created | Discount reserved β payment still pending |
| reservation.confirmed | Settled β your wallet was credited |
| reservation.released | Hold released before settlement |
| reservation.expired | Hold lapsed past the 30-minute window |
Each delivery is signed. Verify it before trusting the body, then return any 2xx to acknowledge:
$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.
{ "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 |
|---|---|
| 401 | Missing, malformed, or revoked key. |
| 404 | Voucher/redemption not found, or it belongs to another merchant. |
| 422 | The request was understood but refused β invalid/expired voucher, wrong category, bad code, already used, etc. |
| 429 | Too many attempts (per phone or per key). Back off and retry later. |
| 500 / 502 | Unexpected 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
- Create a small fixed voucher at My Vouchers with a merchant email you control and a category.
- Look it up with
GET /merchant/vouchers/{code}. - Run the four-step flow with the shopper's phone number.
- 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.