Sandbox & live

Anakalice API

Everything you need to make your first sandbox payment.

Introduction

Anakalice is an integration and orchestration layer for African payment infrastructure. You integrate once, and Anakalice routes payments to underlying providers and rails. The platform supports two modes. In sandbox, payments are simulated through a mock provider and no real money moves. In live, payments route to the provider you connect (such as Paystack) and settle to your bank. Start in sandbox, then switch to live once your business is verified and a provider is connected.

Quick Start

The shortest path from zero to a successful sandbox payment:

  1. Create an account and a business.
  2. Generate a test API key in the dashboard and copy the secret key.
  3. Create a payment with the API (below).
  4. Open the returned checkout_url.
  5. Click Simulate successful payment.
  6. Watch the transaction become SUCCESS in the dashboard.
  7. Receive the payment.success webhook.

Authentication

Authenticate every API request with your secret key in the Authorization header. Test keys look like sk_test_.... Never put a secret key in browser or mobile app code.

Authorization: Bearer sk_test_your_secret_key

Creating a Payment

Amounts are integers in the currency's smallest unit (minor units). NGN 1,500.00 is 150000 kobo. The response includes a checkout_url to send your customer to.

curl -X POST http://localhost:3000/v1/payments \
  -H "Authorization: Bearer sk_test_your_secret_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_1042" \
  -d '{
    "amount": 150000,
    "currency": "NGN",
    "customer_email": "buyer@example.com",
    "merchant_reference": "ORDER-1042"
  }'
{
  "payment": {
    "id": "akl_9f2c...",
    "status": "CREATED",
    "amount": 150000,
    "currency": "NGN",
    "provider": "mock",
    "checkout_url": "http://localhost:3000/checkout/akl_9f2c...",
    "created_at": "2026-08-26T10:00:00.000Z"
  }
}

Retrieving a Payment

Use the payment id (the akl_ reference). List all payments with GET /v1/payments.

curl http://localhost:3000/v1/payments/akl_9f2c... \
  -H "Authorization: Bearer sk_test_your_secret_key"

Idempotency

Send an Idempotency-Key header when creating a payment. If the same business retries with the same key, Anakalice returns the original payment instead of creating a duplicate. Safe to retry on network errors.

# Both requests return the SAME payment; only one is created.
-H "Idempotency-Key: order_1042"

Refunds

Refund a successful payment in full or part. The provider's confirmation — not the API response — makes a refund final; you receive a refund.succeeded (or refund.failed) and payment.refunded webhook. Send an Idempotency-Key to make retries safe. Omit amount to refund the full remaining amount.

curl -X POST https://api.anakalice.com/v1/payments/akl_xxx/refunds \
  -H "Authorization: Bearer sk_test_your_secret_key" \
  -H "Idempotency-Key: refund_akl_xxx_1" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 5000, "reason": "customer request" }'

Webhooks

Add an endpoint on the Webhooks page (must be an https URL). Anakalice POSTs a signed JSON event to it whenever a payment changes state. Current events: payment.created, payment.success, payment.failed, payment.refunded.

Headers on every delivery

X-Anakalice-Event-IDUnique, immutable id for this event (evt_…). Use it for idempotency.
X-Anakalice-EventThe event type, e.g. payment.success.
X-Anakalice-SignatureHMAC-SHA256(endpoint_secret, rawBody), hex.

Verify the signature

Compute the HMAC over the exact raw request body (do not re-serialize the parsed JSON — key order or spacing may differ). Compare in constant time.

import crypto from "node:crypto";

export function verify(rawBody, signature, secret) {
  const expected = crypto.createHmac("sha256", secret)
    .update(rawBody).digest("hex");        // sign the EXACT bytes received
  return signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

Delivery semantics

  • At-least-once. Respond with any 2xx and we mark the event delivered. Anything else (or a timeout past 8s) is a failure and will be retried.
  • Duplicates are possible. A retry after your server already processed the event (e.g. you replied slowly, or crashed after processing) will deliver the same event again. This is expected — design for it.
  • Idempotency. Deduplicate on X-Anakalice-Event-ID: record processed event ids and ignore repeats. Do not use the payment reference as the key — one payment emits several different events.
  • Retries & backoff. Failed deliveries retry automatically with exponential backoff (10s, 20s, 40s, 80s, … capped at 1 hour).
  • Max attempts & dead-letter. After 8 failed attempts the event stops retrying and is marked FAILED (dead-lettered). It is kept in your Webhook deliveries log.
  • If your endpoint is down. No action needed for transient outages — the backoff schedule keeps retrying. If an event has already dead-lettered, fix your endpoint and press Retry on the Webhook deliveries page; the original event id and signed body are preserved and re-sent through the same pipeline.

Handling an event

app.post("/anakalice/webhook", async (req, res) => {
  const raw = req.rawBody;                       // exact bytes, not re-serialized
  const sig = req.header("X-Anakalice-Signature");
  if (!verify(raw, sig, process.env.ANAKALICE_WEBHOOK_SECRET)) {
    return res.sendStatus(400);                  // bad signature -> we will retry
  }

  const id = req.header("X-Anakalice-Event-ID"); // idempotency key
  if (await alreadyProcessed(id)) return res.sendStatus(200); // duplicate -> ack

  const event = JSON.parse(raw);
  try {
    await handle(event.event, event.data);       // e.g. "payment.success"
    await markProcessed(id);
    res.sendStatus(200);                          // success -> delivered
  } catch (e) {
    res.sendStatus(500);                          // failure -> we retry with backoff
  }
});

Testing

Open a payment's checkout_url and choose Simulate successful payment or Simulate failed payment. The simulation sends a signed provider webhook through the same engine a real provider will use, so the transaction status and your merchant webhooks behave exactly as they will in production.

Errors

Errors use a consistent envelope:

{ "error": { "type": "authentication_error", "message": "Invalid or revoked API key" } }
TypeHTTPMeaning
authentication_error401Missing, invalid, revoked, or non-secret key
validation_error422Request body failed validation
invalid_amount422Amount is not a positive integer
unsupported_currency422Provider does not support the currency
not_found404Resource does not exist for your business
rate_limited429Too many requests; retry after a short wait
webhook_verification_failed400Inbound provider signature invalid
server_error500Unexpected error

Ready to try it?

Generate a test key in the dashboard and make your first sandbox payment.