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

  1. Register your business on the merchant dashboard. Live and sandbox keys are issued instantly.
  2. Integrate against the sandbox key (c4n_test_) using the built-in test codes — nothing real is touched.
  3. Swap in your live key (c4n_live_) and call /vouchers/redeem at checkout with a unique reference.
  4. Add a webhook URL in your settings and verify the x-c4n-signature header 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.

FieldTypeRequiredNotes
codestringyes6–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.

FieldTypeRequiredNotes
codestringyesThe customer's voucher code.
referencestringyesYour unique payment reference (1–120 chars). Idempotency key.
amountnumbernoNaira to charge. Omit to take the voucher's full value.
metadataobjectnoFree-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 paramTypeDefaultNotes
limitnumber501–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.

errorHTTPMeaningWhat to do
missing_api_key401No key sent.Add the Authorization header.
invalid_api_key401Unknown or revoked key.Rotate a key in the dashboard.
merchant_suspended403Account is not active.Contact support.
invalid_request400Body failed validation.Check field names and types.
not_found404No voucher with that code.Ask the customer to re-check the code.
already_used400Voucher was spent.Ask for another voucher.
listed_for_sale400Voucher is escrowed on the marketplace.Customer must delist it first.
cancelled400Voucher was cancelled.Ask for another voucher.
insufficient_value400Voucher is worth less than the charge.Read `available` and charge less, or split payment.
partial_not_allowed400You only accept exact-value vouchers.Enable partial spend in settings.
throttled429Too many failed attempts.Back off a few minutes before retrying.
server_error500Unexpected 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
ParamTypeRequiredNotes
amountnumbernoNaira due. Omit to let the customer spend the full voucher.
referencestringnoYour 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.