Merchant API reference
Accept Crypto 4 Naira vouchers from any site or app. All endpoints are JSON over HTTPS, authenticated with your merchant API key. Base URL: https://crypto4naira.com/api/public/merchant/v1
Quick start
- Register your business on the merchant dashboard. Live and sandbox keys are issued instantly.
- Integrate against the sandbox key (
c4n_test_) using the built-in test codes — nothing real is touched. - Swap in your live key (
c4n_live_) and call/vouchers/redeemat checkout with a uniquereference. - Add a webhook URL in your settings and verify the
x-c4n-signatureheader before trusting the payload.
Base URL & versioning
Every endpoint lives under the versioned base path below. Send and expect application/json; amounts are whole naira (NGN) and currency is always "NGN". Breaking changes ship under a new /v2 path — /v1 keeps working.
https://crypto4naira.com/api/public/merchant/v1 POST /vouchers/validate POST /vouchers/redeem GET /transactions
Authentication
Send your key as a bearer token (or in the x-api-key header). Live keys start with c4n_live_, sandbox keys with c4n_test_. Keys are shown once — rotate or revoke them from your merchant dashboard. Never expose a key in browser code: call the API from your server, or use hosted checkout.
Authorization: Bearer c4n_live_xxxxxxxxxxxxxxxx Content-Type: application/json
A missing key returns 401 missing_api_key, a wrong or revoked key 401 invalid_api_key, and a suspended merchant 403 merchant_suspended.
Sandbox mode
Requests made with a sandbox key are fully simulated. They never read or spend a real voucher, never move money, and never appear in your live payments list. Every sandbox response includes "livemode": false (live responses include "livemode": true), and sandbox webhooks are still delivered and signed so you can test your handler end to end.
C4N-TEST-0000-0001 -> valid voucher, NGN 15,000 C4N-TEST-0000-0002 -> valid voucher, NGN 100,000 C4N-TEST-0000-0009 -> already_used C4N-TEST-0000-0008 -> not_found Any other code with a sandbox key returns 404 not_found. GET /transactions with a sandbox key always returns an empty list.
POST /vouchers/validate
Check a code is real and unused, and read its value, without spending it. Validation never changes state.
| Field | Type | Required | Notes |
|---|---|---|---|
| code | string | yes | 6–40 chars. Dashes and case are ignored. |
POST https://crypto4naira.com/api/public/merchant/v1/vouchers/validate
{ "code": "C4N-XXXX-XXXX-XXXX" }
200 { "ok": true, "valid": true, "livemode": true, "currency": "NGN", "amount": 20000 }
200 { "ok": false, "error": "already_used", "valid": false, "livemode": true, "currency": "NGN" }
404 { "ok": false, "error": "not_found", "message": "No voucher found with that code." }A code that exists but is not spendable returns HTTP 200 with valid: false and an error of already_used, listed_for_sale or cancelled. Always branch on ok, not on the status code alone.
POST /vouchers/redeem
Spends the voucher and credits your merchant balance, net of the platform fee. The whole operation is atomic: either the voucher is spent and you are credited, or nothing happens.
| Field | Type | Required | Notes |
|---|---|---|---|
| code | string | yes | The customer's voucher code. |
| reference | string | yes | Your unique payment reference (1–120 chars). Idempotency key. |
| amount | number | no | Naira to charge. Omit to take the voucher's full value. |
| metadata | object | no | Free-form key/value pairs echoed to your webhook. |
Idempotency. Retrying the same reference returns the original result with "idempotent": trueinstead of charging twice — safe to retry on timeouts.
Partial spend and change. If the voucher is worth more than amount and partial spend is enabled in your settings, the balance comes back as a brand-new voucher in change_code / change_amount — show it to the customer, it is theirs. With partial spend off, an over-valued voucher returns partial_not_allowed.
POST https://crypto4naira.com/api/public/merchant/v1/vouchers/redeem
{ "code": "C4N-XXXX-XXXX-XXXX", "amount": 5000, "reference": "order_123" }
200 {
"ok": true,
"livemode": true,
"transaction_id": "…",
"reference": "order_123",
"currency": "NGN",
"amount": 5000,
"fee": 50,
"net": 4950,
"change_code": "C4N-YYYY-YYYY-YYYY",
"change_amount": 15000,
"idempotent": false
}
400 { "ok": false, "error": "insufficient_value", "available": 3000,
"message": "The voucher's value is less than the amount charged." }GET /transactions
Most recent payments first. Sandbox keys always get an empty list.
| Query param | Type | Default | Notes |
|---|---|---|---|
| limit | number | 50 | 1–200. Values outside the range are clamped. |
GET https://crypto4naira.com/api/public/merchant/v1/transactions?limit=50
200 {
"ok": true,
"livemode": true,
"currency": "NGN",
"transactions": [
{
"id": "…",
"reference": "order_123",
"amount": 5000,
"fee": 50,
"net": 4950,
"change_amount": 15000,
"voucher": "C4N-••••-••••-1234",
"status": "paid",
"created_at": "2026-01-01T00:00:00.000Z"
}
]
}CORS & preflight
Every endpoint answers OPTIONS with 204 and permissive CORS headers, so browser calls are technically possible — but do not ship a live key to the browser. Use your server, or hosted checkout.
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: authorization, content-type, x-api-key
Error reference
Failures return { "ok": false, "error": "…", "message": "…" }. Match on error, show message to staff, not to the customer verbatim.
| error | HTTP | Meaning | What to do |
|---|---|---|---|
| missing_api_key | 401 | No key sent. | Add the Authorization header. |
| invalid_api_key | 401 | Unknown or revoked key. | Rotate a key in the dashboard. |
| merchant_suspended | 403 | Account is not active. | Contact support. |
| invalid_request | 400 | Body failed validation. | Check field names and types. |
| not_found | 404 | No voucher with that code. | Ask the customer to re-check the code. |
| already_used | 400 | Voucher was spent. | Ask for another voucher. |
| listed_for_sale | 400 | Voucher is escrowed on the marketplace. | Customer must delist it first. |
| cancelled | 400 | Voucher was cancelled. | Ask for another voucher. |
| insufficient_value | 400 | Voucher is worth less than the charge. | Read `available` and charge less, or split payment. |
| partial_not_allowed | 400 | You only accept exact-value vouchers. | Enable partial spend in settings. |
| throttled | 429 | Too many failed attempts. | Back off a few minutes before retrying. |
| server_error | 500 | Unexpected failure. | Retry with the same reference — it is idempotent. |
Webhooks
Set a webhook URL in your merchant settings and we POST a voucher.paid event on every payment (including sandbox ones). The raw body is signed with HMAC-SHA256 using your webhook secret and sent as the x-c4n-signature header (hex). Compare it against the raw body before parsing. Respond 2xx quickly; treat the event as informational — the redeem response is the source of truth, and duplicate deliveries should be de-duplicated on transaction_id.
POST https://your-site.example/c4n-webhook
x-c4n-signature: 9f2c…
{ "event": "voucher.paid",
"created_at": "2026-01-01T00:00:00.000Z",
"data": {
"livemode": true,
"transaction_id": "…",
"reference": "order_123",
"currency": "NGN",
"amount": 5000,
"fee": 50,
"net": 4950,
"change_code": "C4N-YYYY-YYYY-YYYY",
"change_amount": 15000
} }Verify in Node
import crypto from "node:crypto";
app.post("/c4n-webhook", express.raw({ type: "*/*" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.C4N_WEBHOOK_SECRET)
.update(req.body) // raw Buffer, not the parsed object
.digest("hex");
const got = req.header("x-c4n-signature") ?? "";
const ok = got.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString());
// fulfil order for event.data.reference
res.sendStatus(200);
});Verify in PHP
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, getenv('C4N_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_C4N_SIGNATURE'] ?? '')) {
http_response_code(401); exit;
}
$event = json_decode($raw, true);Hosted checkout
No code at all: send customers to your checkout link. We collect the voucher, take payment, show any change voucher and return them to your success URL (set once in merchant settings).
https://crypto4naira.com/pay/your-merchant-slug?amount=5000&reference=order_123
| Param | Type | Required | Notes |
|---|---|---|---|
| amount | number | no | Naira due. Omit to let the customer spend the full voucher. |
| reference | string | no | Your order reference; echoed back and used for idempotency. |
After a successful payment the page shows the reference and any change code, then offers a return button to your configured success URL. Confirm the payment server-side via the webhook or GET /transactions before releasing goods.
Fees & settlement
The platform fee is charged in basis points on each redemption (default 100 bps = 1%). net = amount − fee, and only net reaches your merchant balance. Settle that balance whenever you like: to your Crypto 4 Naira NGN wallet, or into USDT, BTC, ETH or BNB at the live rate — choose the destination under Settings in the merchant dashboard.
amount 5,000 fee (1%) 50 net 4,950
Code samples
cURL
curl -X POST https://crypto4naira.com/api/public/merchant/v1/vouchers/redeem \
-H "Authorization: Bearer c4n_live_xxx" \
-H "Content-Type: application/json" \
-d '{"code":"C4N-XXXX-XXXX-XXXX","amount":5000,"reference":"order_123"}'Node (fetch)
const res = await fetch("https://crypto4naira.com/api/public/merchant/v1/vouchers/redeem", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.C4N_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ code, amount: 5000, reference: "order_123" }),
});
const data = await res.json();
if (!data.ok) throw new Error(data.error);PHP
$ch = curl_init('https://crypto4naira.com/api/public/merchant/v1/vouchers/redeem');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('C4N_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'code' => $code, 'amount' => 5000, 'reference' => 'order_123',
]),
]);
$data = json_decode(curl_exec($ch), true);Python
import os, requests
r = requests.post(
"https://crypto4naira.com/api/public/merchant/v1/vouchers/redeem",
headers={"Authorization": f"Bearer {os.environ['C4N_API_KEY']}"},
json={"code": code, "amount": 5000, "reference": "order_123"},
timeout=20,
)
data = r.json()
if not data["ok"]:
raise RuntimeError(data["error"])Need keys or a sandbox? Head to the merchant dashboard.