Customers, KYC & Wallets
Customers are the people and businesses your project serves. Zirzir keeps their profile, verifies them in KYC tiers, and can hold money for them in wallets: stored value booked in the ledger, with per-tier limits enforced on every movement.
All endpoints use your API key. Each environment (test/live) has its own customers.
Customers
Section titled “Customers”curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"full_name": "Abebe Kebede", "phone": "+251 911 22 33 44", "date_of_birth": "1990-05-17", "external_ref": "crm-1"}' \ http://localhost:8080/api/v1/customerskindisindividual(default) orbusiness.- Phone numbers are normalised (
+251911223344) and unique per environment, as isexternal_ref(your own id). GET /api/v1/customers?q=abebe&status=active&kyc_status=verifiedsearches by name, phone, email or external reference.PATCH /api/v1/customers/:idupdates fields. Changing the name or date of birth of a tier 2+ customer sends them back to review at tier 1: their documents no longer prove the new identity.POST .../freezeand.../unfreezestop and restart all wallet movements.POST .../closecloses a customer whose wallets are empty.GET .../eventsis the customer’s audit trail: every change, review, decision and document-number reveal.
KYC tiers
Section titled “KYC tiers”| Tier | Individual | Business | Unlocks |
|---|---|---|---|
| 0 | — | — | Nothing (no wallet) |
| 1 | Name and phone | Name and phone | A wallet, within tier 1 limits |
| 2 | An accepted, unexpired identity document (national ID, kebele ID, passport, driving licence, residence permit) | An accepted business licence | Tier 2 limits |
| 3 | Tier 2 plus an accepted proof of address | Tier 2 plus an accepted TIN certificate | Tier 3 limits |
You review documents and decide tiers; zirzir refuses a tier whose requirements are not met.
# Submit a document (the number is encrypted at rest; only the last 4 are shown)curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"doc_type": "national_id", "number": "ET-0001-2345-6789", "issuing_country": "ET", "expires_on": "2030-01-01", "file_url": "https://files.example.com/id.jpg", "file_sha256": "<sha-256 of the file>"}' \ http://localhost:8080/api/v1/customers/$CUSTOMER/documents
# Accept or reject it (a rejection needs a note)curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"decision": "accept"}' \ http://localhost:8080/api/v1/customers/$CUSTOMER/documents/$DOC/review
# What is missing for tier 2?curl -H "X-API-Key: $KEY" "http://localhost:8080/api/v1/customers/$CUSTOMER/kyc/requirements?tier=2"
# Approve at tier 2 (or {"decision": "reject", "note": "…"})curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"decision": "approve", "tier": 2, "note": "ID checked in branch"}' \ http://localhost:8080/api/v1/customers/$CUSTOMER/kycDocument files stay in your own storage: send their file_url (https) and file_sha256 so the record points at exactly the file you reviewed. Set ZIRZIR_ENCRYPTION_KEY so document numbers are encrypted at rest.
The API never returns a full document number, only its last 4 characters. A reviewer signed in to the dashboard can reveal one with GET /api/projects/$PROJECT/customers/$CUSTOMER/documents/$DOC/number?environment=live (a user session, not an API key); every reveal is recorded with the user in the customer’s events and the audit log.
If a verified customer’s name or date of birth changes, their accepted documents are marked superseded and no longer count: collect and review new ones before granting tier 2 or 3 again.
Limits
Section titled “Limits”Set limits per tier and currency. A limit you do not set does not apply, so configure the limits your regulator requires before going live.
curl -X PUT -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"tier": 1, "currency": "ETB", "max_balance_minor": 500000, "max_single_minor": 200000, "max_daily_out_minor": 500000}' \ http://localhost:8080/api/v1/kyc/limits| Limit | Applies to |
|---|---|
max_balance_minor | A wallet’s balance after any credit (deposit, top-up, incoming transfer) |
max_single_minor | Each withdrawal or outgoing transfer |
max_daily_out_minor | The sum of withdrawals and outgoing transfers per UTC day |
Wallets
Section titled “Wallets”# Open an ETB wallet (tier 1 or above, active customers only)curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"currency": "ETB"}' http://localhost:8080/api/v1/customers/$CUSTOMER/wallets
# Cash-in: from your balance into the walletcurl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"currency": "ETB", "amount": "300.00", "idempotency_key": "cashin-77"}' \ http://localhost:8080/api/v1/customers/$CUSTOMER/wallets/deposit
# Cash-out or purchase: from the wallet into your balance (same body)curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"currency": "ETB", "amount": "50.00", "idempotency_key": "cashout-12"}' \ http://localhost:8080/api/v1/customers/$CUSTOMER/wallets/withdraw
# Wallet to walletcurl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"from_customer_id": "cus_a", "to_customer_id": "cus_b", "currency": "ETB", "amount": "80.00", "idempotency_key": "p2p-9"}' \ http://localhost:8080/api/v1/wallet-transfers- Amounts are
amount(an exact decimal string) oramount_minor. - Every movement is idempotent: the same
idempotency_keyreturns the original transfer with"replayed": true, even if a limit would now refuse a new one. - Movements for one customer are processed one at a time, so concurrent requests cannot together break a limit.
- Wallet money only moves through these endpoints. The generic ledger transfer and hold endpoints refuse wallet accounts, so limits cannot be bypassed.
GET /api/v1/customers/$CUSTOMER/walletsshows each wallet’sbalanceandavailable.
Top-ups
Section titled “Top-ups”Let a customer fund their wallet with any payment provider: create the payment with metadata.wallet_customer_id.
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"provider": "telebirr", "reference": "TOPUP-1", "amount": 50.00, "currency": "ETB", "customer_phone": "0911223344", "metadata": {"wallet_customer_id": "cus_a"}}' \ http://localhost:8080/api/v1/paymentsThe customer is checked when the payment is created (a top-up cannot also split to a subaccount). When the payment completes and is booked, the amount moves from your balance into their wallet, exactly once. If crediting it would break a limit (or the customer was frozen in the meantime), the top-up is held as blocked and recorded in the customer’s events; fix the cause and retry it, or refund the payment.
Refunding a top-up payment:
- If the top-up has not reached the wallet yet (
pendingorblocked), the refund cancels it (statusrefunded). - If it was already credited, the refund is refused until you reverse it with
POST /api/v1/wallet-topups/$PAYMENT/reverse, which takes the amount back out of the wallet (it must still be there). Otherwise the customer would keep the value while the payer gets their money back.
A customer with pending or blocked top-ups cannot be closed.
curl -H "X-API-Key: $KEY" "http://localhost:8080/api/v1/wallet-topups?status=blocked"curl -X POST -H "X-API-Key: $KEY" http://localhost:8080/api/v1/wallet-topups/$PAYMENT/retry