Skip to content
Recite Labs

Zirzir.One API for mobile money.

An open-source, self-hosted payment orchestrator for African mobile money. One REST API in front of Telebirr, Chapa, M-Pesa and more, with a ledger and signed webhooks. Written in Rust, MIT licensed.

  1. 1  Your app creates a payment
  2. 2  Zirzir books it
  3. 3  The provider charges
  4. 4  The customer confirms
Early releaseSelf-hostedOpen source, MIT licence
zirzir.africa

What works today.

Zirzir is an early, self-hosted engine. This is what it does now.

  • 01

    Native drivers

    Telebirr, Chapa, SantimPay, Arifpay and LakiPay, written in Rust over real HTTP. They fail closed on errors.

  • 02

    Plugins

    Bun plugins for M-Pesa (Daraja STK), Airtel Money, Telegram invoices and a CBE Birr USSD helper.

  • 03

    Double-entry ledger

    Integer minor units. Completed payments and split shares are booked atomically and idempotently, in SQLite or a TigerBeetle cluster.

  • 04

    Signed webhooks

    payment.completed, payment.failed and payment.cancelled events, signed with an HMAC and retried after 5m, 15m, 1h and 24h.

  • 05

    Dashboard and API

    A REST API and an embedded dashboard, with bcrypt auth, rate-limited login and idempotency keys.

  • 06

    Encrypted credentials

    Provider credentials are encrypted at rest with AES-256-GCM when you set an encryption key.

  • 07

    Receipts

    HTML receipts, plus a PDF receipt for every payment at /api/v1/payments/:id/receipt.pdf.

Providers.

Nine today. Telebirr and CBE Birr have no status API, so you reconcile those payments in the dashboard.

TelebirrPath: Native RustAutomatic settlement: No, manual reconciliationNotes: RSA payload, TLS verified
ChapaPath: Native RustAutomatic settlement: Yes, status APINotes: Checkout initialize
SantimPayPath: Native Rust + pluginAutomatic settlement: Yes, status APINotes: Requires merchant ID and token
ArifpayPath: Native Rust + pluginAutomatic settlement: Yes, status APINotes: Requires API key and settlement account
LakiPayPath: Native Rust + pluginAutomatic settlement: Yes, signature or status APINotes: Direct STK or hosted checkout
M-PesaPath: Bun pluginAutomatic settlement: Yes, status APINotes: Daraja OAuth and STK Push
CBE BirrPath: Bun pluginAutomatic settlement: No, manual reconciliationNotes: Returns a USSD string; there is no CBE charge API
Airtel MoneyPath: Bun pluginAutomatic settlement: Yes, status APINotes: Airtel Africa collection API
TelegramPath: Bun pluginAutomatic settlement: Yes, webhook secret signatureNotes: createInvoiceLink

How it fits together.

Your app talks to one server you run. That server talks to the providers and keeps the books.

Merchant app

TypeScript or Python SDK

/api/v1

Zirzir

Rust server: Axum, SQLite and an embedded dashboard

  • Ledger

    SQLite by default, or a TigerBeetle cluster

  • Native drivers

    Telebirr, Chapa, SantimPay, Arifpay, LakiPay

  • Bun plugins over stdio JSON-RPC

    M-Pesa, Airtel Money, Telegram, CBE Birr USSD

Run it yourself.

Build the dashboard first, then the server. It runs on your own machine or with Docker.

# 1. Build the dashboard
cd server/web
bun install && bun run build

# 2. Build and run the server
cd ..
cargo build --release --locked
export ZIRZIR_ADMIN_API_KEY="$(openssl rand -hex 32)"
./target/release/zirzir-server --port 8080

Then open

Dashboard
localhost:8080/
API
localhost:8080/api/v1
Health
localhost:8080/health

Prefer Docker? One command with docker compose runs the same server from server/docker-compose.yml.

For production, also set

ZIRZIR_PUBLIC_URL
Your server’s public URL. Providers then call back to Zirzir, and each payment’s callback_url becomes your notification URL.
ZIRZIR_WEBHOOK_SECRET
Signs the events sent to your app with X-Zirzir-Signature. No events are sent while it is unset.
ZIRZIR_ENCRYPTION_KEY
Encrypts provider credentials at rest. If you lose the key, you re-enter the credentials.
ZIRZIR_TIGERBEETLE_ADDRESSES
Optional. Books the ledger in a TigerBeetle cluster instead of SQLite. Switching backends starts empty books.

SDKs and CLI.

Call Zirzir from your backend, or try a charge from the terminal.

TypeScript and Python

Server mode
{ baseUrl, apiKey }
Standalone mode, for Chapa, Telebirr and LakiPay
{ provider, credentials }

Flutter and React Native packages are thin checkout wrappers around POST /api/v1/payments. Confirm the final status on your backend.

CLI

zirzir login --server http://localhost:8080

zirzir provider set lakipay \
  --public-key "$LAKIPAY_PUBLIC_KEY" \
  --secret-key "$LAKIPAY_SECRET_KEY"

zirzir charge --amount 1 --currency ETB \
  --provider lakipay

zirzir listen \
  --forward-to http://localhost:3000/api/webhooks

What is not included.

Zirzir is early. This is what it does not do yet, so you can plan around it.

  • A hosted cloud or an SLA

    This is a self-hosted engine, not a multi-tenant cloud. There is no published SLA, and the hosted control plane is not in this repository.

  • PostgreSQL

    SQLite only.

  • Live FX quotes

    Static reference rates. Unknown currencies are rejected.

  • Filed tax invoices

    Receipts are merchant copies, not ERCA or KRA statutory filings.

  • Plugin worker pools

    One Bun process per plugin, with sequential JSON-RPC.

Run payments on your own server.