Start
Sell airtime, data and ZESA
Put every network and every meter on your till, POS or app, in USD and ZiG: check, sell, tell the cashier, print the receipt, close the day.
Put Econet, NetOne and Telecel airtime, data bundles and ZESA tokens on your till, in your POS software or in your app. Your customer pays you; Jusa sends the airtime, bundle or token in seconds and debits your prepaid Jusa Credit. Every network and every meter, in USD and ZiG, through one API, so you never hold airtime stock or a stack of provider accounts.
Margin
Buy below faceSell at face value. On the Dealer tier your Jusa Credit is debited less than face, per network, at the moment of the sale: the margin is yours on every sale, with nothing to claim later.Safety
Never sell twiceKey each sale on your till's own sale number. A dropped connection, a retry or a double tap is the same sale, never a second one.Books
Ties to the cash drawerEvery sale carries its till and its cost. The day's sales per till, and a monthly statement of face sold, credit drawn and discount earned.What POS vendors ask us
| Is there an API? | Yes: REST and JSON at https://jusa.localhost.co.zw/dev/v1, signed webhooks, a
sandbox, and SDKs for Python, Node, PHP, Go, Java and Kotlin, .NET,
Dart and Ruby. |
| What do we earn? | A business we have verified buys below face on the Dealer tier: a percentage off face per network and currency, set when you are approved, taken as a lower debit when each sale is made. Without it you buy at face value, with no fees, which suits apps that sell at cost or reward users. |
| What are the terms? | Apply from the dashboard or with
POST /dealer/application and upload your company documents (incorporation certificate, directors, a
director's ID, proof of address, bank letter, tax clearance). Jusa staff review them and set your rates.
How the Dealer tier works. |
| How is a sale's status reported? | Every sale has one status, a webhook for each change, and a promise for what happens to its money. The table below says what the cashier does for each. |
| USD and ZiG? | Both. Products come in each currency, and USD and ZWG Jusa Credit are separate balances with no exchange between them. |
| How do we fund it? | Prepaid Jusa Credit, topped up any time by card or bank transfer; dealers can have cash collected by Jusa staff. Set a low-balance alert so a till never runs dry. |
A sale, step by step
Everything below runs in test mode first with a test key: test numbers act out every outcome, and nothing real moves until you switch to a live key. The samples use the client you make once:
Python
import logging
import os
import jusa
logger = logging.getLogger(__name__)
jusa.api_key = os.environ["JUSA_SECRET_KEY"] # jusa_sk_test_… while you build
Node
import Jusa from '@jusa/node';
const jusa = new Jusa(process.env.JUSA_SECRET_KEY); // jusa_sk_test_… while you build
PHP
<?php
require __DIR__ . '/vendor/autoload.php';
$jusa = new \Jusa\JusaClient(getenv('JUSA_SECRET_KEY')); // jusa_sk_test_… while you build
Go
package main
import (
"context"
"fmt"
"log"
"os"
jusa "jusa.localhost.co.zw/sdk/go"
)
func main() {
apiKey, found := os.LookupEnv("JUSA_SECRET_KEY") // jusa_sk_test_… while you build
if !found {
log.Fatal("set JUSA_SECRET_KEY")
}
jusaClient := jusa.NewClient(apiKey)
if err := run(context.Background(), jusaClient); err != nil {
log.Fatal(err)
}
}
// run is where the samples go: each call returns its error, wrapped with what it was doing.
func run(ctx context.Context, jusaClient *jusa.Client) error {
fmt.Println("live mode:", jusaClient.IsLiveMode())
return nil
}
Java
import java.util.logging.Logger;
import zw.co.localhost.jusa.Jusa;
import zw.co.localhost.jusa.Params;
import zw.co.localhost.jusa.RequestOptions;
import zw.co.localhost.jusa.exception.InsufficientCreditException;
import zw.co.localhost.jusa.exception.JusaException;
// The samples' methods go in a class like this one. Calls throw a checked JusaException.
public class SurveyRewards {
private static final Logger LOGGER = Logger.getLogger(SurveyRewards.class.getName());
private final Jusa jusa = new Jusa(System.getenv("JUSA_SECRET_KEY")); // jusa_sk_test_… while you build
}
C#
using Jusa;
var app = WebApplication.CreateBuilder().Build();
var logger = app.Logger;
// jusa_sk_test_… while you build. Make one client and share it.
var jusa = new JusaClient(Environment.GetEnvironmentVariable("JUSA_SECRET_KEY"));
Dart
import 'dart:io';
import 'package:jusa/jusa.dart';
// jusa_sk_test_… while you build. On your server only, never in a Flutter app.
final jusa = Jusa(Platform.environment['JUSA_SECRET_KEY']);
Ruby
require "jusa"
require "logger"
LOGGER = Logger.new($stderr)
Jusa.api_key = ENV.fetch("JUSA_SECRET_KEY") # jusa_sk_test_… while you build
curl
export JUSA_SECRET_KEY=jusa_sk_test_… # a test key: nothing real moves
-
Give each till its own key
A restricted key per till (or per branch) holds only what a till needs, with spend caps so a lost device cannot empty your balance. Its id also tags every sale it makes, which is how you close each till's day. Add
allowed_ipsif your tills sit behind fixed addresses.Once per tillPython
def make_a_key_for_one_till(till_id: str) -> None: try: till_key = jusa.ApiKey.create( kind="rk", label=f"till-{till_id}", scopes=[ "catalog:read", "lookups:read", "sends:read", "sends:write", "tokens:read", ], spend_caps={"USD": {"per_send": "50.00", "daily": "1500.00"}}, ) # Shown once: put it straight into the till's configuration. print(till_key.id, till_key.redacted) except jusa.JusaError as jusa_error: logger.error("Jusa refused: %s [%s]", jusa_error.code, jusa_error.request_id) raiseNode
import { JusaError } from '@jusa/node'; async function makeAKeyForOneTill(tillId) { try { const tillKey = await jusa.keys.create({ kind: 'rk', label: `till-${tillId}`, scopes: [ 'catalog:read', 'lookups:read', 'sends:read', 'sends:write', 'tokens:read', ], spend_caps: { USD: { per_send: '50.00', daily: '1500.00' } }, }); // Shown once: put it straight into the till's configuration. console.log(tillKey.id, tillKey.redacted); } catch (error) { if (error instanceof JusaError) { console.error(`Jusa refused: ${error.code} [${error.requestId}]`); } throw error; } }PHP
function makeAKeyForOneTill(\Jusa\JusaClient $jusa, string $tillId): void { try { $tillKey = $jusa->keys->create([ 'kind' => 'rk', 'label' => "till-{$tillId}", 'scopes' => [ 'catalog:read', 'lookups:read', 'sends:read', 'sends:write', 'tokens:read', ], 'spend_caps' => ['USD' => ['per_send' => '50.00', 'daily' => '1500.00']], ]); // Shown once: put it straight into the till's configuration. echo $tillKey->id, ' ', $tillKey->redacted, "\n"; } catch (\Jusa\Exception\JusaException $jusaError) { error_log("Jusa refused: {$jusaError->getErrorCode()} [{$jusaError->getRequestId()}]"); throw $jusaError; } }Go
func makeAKeyForOneTill(ctx context.Context, tillID string) error { tillKey, err := jusaClient.Keys.Create(ctx, jusa.Params{ "kind": "rk", "label": "till-" + tillID, "scopes": []string{ "catalog:read", "lookups:read", "sends:read", "sends:write", "tokens:read", }, "spend_caps": jusa.Params{ "USD": jusa.Params{"per_send": "50.00", "daily": "1500.00"}, }, }) if err != nil { return fmt.Errorf("keys.create: %w", err) } // Shown once: put it straight into the till's configuration. fmt.Println(tillKey.ID, tillKey.Redacted) return nil }Java
void makeAKeyForOneTill(String tillId) throws JusaException { try { var tillKey = jusa.keys().create(Params.of( "kind", "rk", "label", "till-" + tillId, "scopes", List.of( "catalog:read", "lookups:read", "sends:read", "sends:write", "tokens:read" ), "spend_caps", Map.of( "USD", Map.of("per_send", "50.00", "daily", "1500.00") ) )); // Shown once: put it straight into the till's configuration. System.out.println(tillKey.getId() + " " + tillKey.getRedacted()); } catch (JusaException jusaError) { LOGGER.severe("Jusa refused: " + jusaError.getCode() + " [" + jusaError.getRequestId() + "]"); throw jusaError; } }C#
async Task MakeAKeyForOneTillAsync(string tillId) { try { var tillKey = await jusa.Keys.CreateAsync(new { kind = "rk", label = $"till-{tillId}", scopes = new[] { "catalog:read", "lookups:read", "sends:read", "sends:write", "tokens:read", }, spend_caps = new { USD = new { per_send = "50.00", daily = "1500.00" } }, }); // Shown once: put it straight into the till's configuration. Console.WriteLine($"{tillKey.Id} {tillKey.Redacted}"); } catch (JusaException jusaError) { logger.LogError(jusaError, "Jusa refused: {Code} [{RequestId}]", jusaError.Code, jusaError.RequestId); throw; } }Dart
Future<void> makeAKeyForOneTill(String tillId) async { try { final tillKey = await jusa.keys.create({ 'kind': 'rk', 'label': 'till-$tillId', 'scopes': [ 'catalog:read', 'lookups:read', 'sends:read', 'sends:write', 'tokens:read', ], 'spend_caps': {'USD': {'per_send': '50.00', 'daily': '1500.00'}}, }); // Shown once: put it straight into the till's configuration. print('${tillKey.id} ${tillKey.redacted}'); } on JusaException catch (jusaError) { stderr.writeln('Jusa refused: ${jusaError.code} [${jusaError.requestId}]'); rethrow; } }Ruby
def make_a_key_for_one_till(till_id) till_key = Jusa::ApiKey.create( kind: "rk", label: "till-#{till_id}", scopes: [ "catalog:read", "lookups:read", "sends:read", "sends:write", "tokens:read", ], spend_caps: { USD: { per_send: "50.00", daily: "1500.00" } }, ) # Shown once: put it straight into the till's configuration. puts "#{till_key.id} #{till_key.redacted}" rescue Jusa::JusaError => jusa_error LOGGER.error("Jusa refused: #{jusa_error.code} [#{jusa_error.request_id}]") raise endcurl
curl https://jusa.localhost.co.zw/dev/v1/keys \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "rk", "label": "till-4", "scopes": [ "catalog:read", "lookups:read", "sends:read", "sends:write", "tokens:read" ], "spend_caps": {"USD": {"per_send": "50.00", "daily": "1500.00"}} }' # Shown once: put it straight into the till's configuration. -
Load the catalogue
Products, their networks, currency, and the smallest and largest amount each takes. Keep it on the till and refresh it daily;
availablegoes false when a product is paused.DailyPython
def load_the_catalogue() -> None: try: # Once a day, or when a sale is refused as product_unavailable: products = jusa.Product.list(currency="USD") print(products.data) except jusa.JusaError as jusa_error: logger.error("Jusa refused: %s [%s]", jusa_error.code, jusa_error.request_id) raiseNode
import { JusaError } from '@jusa/node'; async function loadTheCatalogue() { try { // Once a day, or when a sale is refused as product_unavailable: const products = await jusa.products.list({ currency: 'USD' }); console.log(products.data); } catch (error) { if (error instanceof JusaError) { console.error(`Jusa refused: ${error.code} [${error.requestId}]`); } throw error; } }PHP
function loadTheCatalogue(\Jusa\JusaClient $jusa): void { try { // Once a day, or when a sale is refused as product_unavailable: $products = $jusa->products->list(['currency' => 'USD']); echo $products->data, "\n"; } catch (\Jusa\Exception\JusaException $jusaError) { error_log("Jusa refused: {$jusaError->getErrorCode()} [{$jusaError->getRequestId()}]"); throw $jusaError; } }Go
func loadTheCatalogue(ctx context.Context) error { // Once a day, or when a sale is refused as product_unavailable: products, err := jusaClient.Products.List(ctx, jusa.Params{"currency": "USD"}) if err != nil { return fmt.Errorf("products.list: %w", err) } fmt.Println(products.Data) return nil }Java
void loadTheCatalogue() throws JusaException { try { // Once a day, or when a sale is refused as product_unavailable: var products = jusa.products().list(Params.of("currency", "USD")); System.out.println(products.getData()); } catch (JusaException jusaError) { LOGGER.severe("Jusa refused: " + jusaError.getCode() + " [" + jusaError.getRequestId() + "]"); throw jusaError; } }C#
async Task LoadTheCatalogueAsync() { try { // Once a day, or when a sale is refused as product_unavailable: var products = await jusa.Products.ListAsync(new { currency = "USD" }); Console.WriteLine($"{products.Data}"); } catch (JusaException jusaError) { logger.LogError(jusaError, "Jusa refused: {Code} [{RequestId}]", jusaError.Code, jusaError.RequestId); throw; } }Dart
Future<void> loadTheCatalogue() async { try { // Once a day, or when a sale is refused as product_unavailable: final products = await jusa.products.list({'currency': 'USD'}); print('${products.data}'); } on JusaException catch (jusaError) { stderr.writeln('Jusa refused: ${jusaError.code} [${jusaError.requestId}]'); rethrow; } }Ruby
def load_the_catalogue # Once a day, or when a sale is refused as product_unavailable: products = Jusa::Product.list(currency: "USD") puts "#{products.data}" rescue Jusa::JusaError => jusa_error LOGGER.error("Jusa refused: #{jusa_error.code} [#{jusa_error.request_id}]") raise endcurl
# Once a day, or when a sale is refused as product_unavailable: curl -G https://jusa.localhost.co.zw/dev/v1/products \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY" \ -d currency=USD -
Check before the customer pays
For airtime and data, a phone lookup says which network the number is on and writes it properly; it asks no provider, so it is instant. For ZESA, a quote confirms the meter with ZETDC and gives the name and address to read back to the customer, the units their money buys, and your cost.
Airtime and dataPython
def check_the_number(phone: str) -> None: try: # Before the customer pays: the network, and the number written properly. lookup = jusa.Lookup.phone(phone=phone) print(lookup.valid, lookup.network, lookup.e164, lookup.suggested_product) except jusa.JusaError as jusa_error: logger.error("Jusa refused: %s [%s]", jusa_error.code, jusa_error.request_id) raiseNode
import { JusaError } from '@jusa/node'; async function checkTheNumber(phone) { try { // Before the customer pays: the network, and the number written properly. const lookup = await jusa.lookups.phone({ phone }); console.log(lookup.valid, lookup.network, lookup.e164, lookup.suggested_product); } catch (error) { if (error instanceof JusaError) { console.error(`Jusa refused: ${error.code} [${error.requestId}]`); } throw error; } }PHP
function checkTheNumber(\Jusa\JusaClient $jusa, string $phone): void { try { // Before the customer pays: the network, and the number written properly. $lookup = $jusa->lookups->phone(['phone' => $phone]); echo $lookup->valid, ' ', $lookup->network, ' ', $lookup->e164, ' ', $lookup->suggested_product, "\n"; } catch (\Jusa\Exception\JusaException $jusaError) { error_log("Jusa refused: {$jusaError->getErrorCode()} [{$jusaError->getRequestId()}]"); throw $jusaError; } }Go
func checkTheNumber(ctx context.Context, phone string) error { // Before the customer pays: the network, and the number written properly. lookup, err := jusaClient.Lookups.Phone(ctx, jusa.Params{"phone": phone}) if err != nil { return fmt.Errorf("lookups.phone: %w", err) } fmt.Println(lookup.Valid, lookup.Network, lookup.E164, lookup.SuggestedProduct) return nil }Java
void checkTheNumber(String phone) throws JusaException { try { // Before the customer pays: the network, and the number written properly. var lookup = jusa.lookups().phone(Params.of("phone", phone)); System.out.println(lookup.getValid() + " " + lookup.getNetwork() + " " + lookup.getE164() + " " + lookup.getSuggestedProduct()); } catch (JusaException jusaError) { LOGGER.severe("Jusa refused: " + jusaError.getCode() + " [" + jusaError.getRequestId() + "]"); throw jusaError; } }C#
async Task CheckTheNumberAsync(string phone) { try { // Before the customer pays: the network, and the number written properly. var lookup = await jusa.Lookups.PhoneAsync(new { phone = phone }); Console.WriteLine($"{lookup.Valid} {lookup.Network} {lookup.E164} {lookup.SuggestedProduct}"); } catch (JusaException jusaError) { logger.LogError(jusaError, "Jusa refused: {Code} [{RequestId}]", jusaError.Code, jusaError.RequestId); throw; } }Dart
Future<void> checkTheNumber(String phone) async { try { // Before the customer pays: the network, and the number written properly. final lookup = await jusa.lookups.phone({'phone': phone}); print('${lookup.valid} ${lookup.network} ${lookup.e164} ${lookup.suggestedProduct}'); } on JusaException catch (jusaError) { stderr.writeln('Jusa refused: ${jusaError.code} [${jusaError.requestId}]'); rethrow; } }Ruby
def check_the_number(phone) # Before the customer pays: the network, and the number written properly. lookup = Jusa::Lookup.phone(phone: phone) puts "#{lookup.valid} #{lookup.network} #{lookup.e164} #{lookup.suggested_product}" rescue Jusa::JusaError => jusa_error LOGGER.error("Jusa refused: #{jusa_error.code} [#{jusa_error.request_id}]") raise endcurl
# Before the customer pays: the network, and the number written properly. curl -G https://jusa.localhost.co.zw/dev/v1/lookups/phone \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY" \ -d phone=0771230000ZESAPython
def check_the_meter(meter_number: str) -> None: try: # Read the name back to the customer before they pay: quote = jusa.Quote.create( product="zesa-usd", target=meter_number, amount="20.00", ) print(quote.meter.customer_name, quote.meter.address, quote.zesa.units_now, quote.cost) except jusa.JusaError as jusa_error: logger.error("Jusa refused: %s [%s]", jusa_error.code, jusa_error.request_id) raiseNode
import { JusaError } from '@jusa/node'; async function checkTheMeter(meterNumber) { try { // Read the name back to the customer before they pay: const quote = await jusa.quotes.create({ product: 'zesa-usd', target: meterNumber, amount: '20.00', }); console.log(quote.meter.customer_name, quote.meter.address, quote.zesa.units_now, quote.cost); } catch (error) { if (error instanceof JusaError) { console.error(`Jusa refused: ${error.code} [${error.requestId}]`); } throw error; } }PHP
function checkTheMeter(\Jusa\JusaClient $jusa, string $meterNumber): void { try { // Read the name back to the customer before they pay: $quote = $jusa->quotes->create([ 'product' => 'zesa-usd', 'target' => $meterNumber, 'amount' => '20.00', ]); echo $quote->meter->customer_name, ' ', $quote->meter->address, ' ', $quote->zesa->units_now, ' ', $quote->cost, "\n"; } catch (\Jusa\Exception\JusaException $jusaError) { error_log("Jusa refused: {$jusaError->getErrorCode()} [{$jusaError->getRequestId()}]"); throw $jusaError; } }Go
func checkTheMeter(ctx context.Context, meterNumber string) error { // Read the name back to the customer before they pay: quote, err := jusaClient.Quotes.Create(ctx, jusa.Params{ "product": "zesa-usd", "target": meterNumber, "amount": "20.00", }) if err != nil { return fmt.Errorf("quotes.create: %w", err) } fmt.Println(quote.Meter.CustomerName, quote.Meter.Address, quote.Zesa.UnitsNow, quote.Cost) return nil }Java
void checkTheMeter(String meterNumber) throws JusaException { try { // Read the name back to the customer before they pay: var quote = jusa.quotes().create(Params.of( "product", "zesa-usd", "target", meterNumber, "amount", "20.00" )); System.out.println(quote.getMeter().getCustomerName() + " " + quote.getMeter().getAddress() + " " + quote.getZesa().getUnitsNow() + " " + quote.getCost()); } catch (JusaException jusaError) { LOGGER.severe("Jusa refused: " + jusaError.getCode() + " [" + jusaError.getRequestId() + "]"); throw jusaError; } }C#
async Task CheckTheMeterAsync(string meterNumber) { try { // Read the name back to the customer before they pay: var quote = await jusa.Quotes.CreateAsync(new { product = "zesa-usd", target = meterNumber, amount = "20.00", }); Console.WriteLine($"{quote.Meter?.CustomerName} {quote.Meter?.Address} {quote.Zesa?.UnitsNow} {quote.Cost}"); } catch (JusaException jusaError) { logger.LogError(jusaError, "Jusa refused: {Code} [{RequestId}]", jusaError.Code, jusaError.RequestId); throw; } }Dart
Future<void> checkTheMeter(String meterNumber) async { try { // Read the name back to the customer before they pay: final quote = await jusa.quotes.create({ 'product': 'zesa-usd', 'target': meterNumber, 'amount': '20.00', }); print('${quote.meter?.customerName} ${quote.meter?.address} ${quote.zesa?.unitsNow} ${quote.cost}'); } on JusaException catch (jusaError) { stderr.writeln('Jusa refused: ${jusaError.code} [${jusaError.requestId}]'); rethrow; } }Ruby
def check_the_meter(meter_number) # Read the name back to the customer before they pay: quote = Jusa::Quote.create(product: "zesa-usd", target: meter_number, amount: "20.00") puts "#{quote.meter.customer_name} #{quote.meter.address} #{quote.zesa.units_now} #{quote.cost}" rescue Jusa::JusaError => jusa_error LOGGER.error("Jusa refused: #{jusa_error.code} [#{jusa_error.request_id}]") raise endcurl
# Read the name back to the customer before they pay: curl https://jusa.localhost.co.zw/dev/v1/quotes \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"product": "zesa-usd", "target": "37261502217", "amount": "20.00"}' -
Make the sale
Use your till's sale number as both
client_referenceandIdempotency-Key. If the connection drops, send the same request again: you get the first answer back, and the customer is charged once.metadatacarries your own fields (till, cashier, branch) onto the sale, its webhooks and your exports.AirtimePython
def sell_airtime_at_the_till(phone: str, sale_id: str, till_id: str) -> None: try: # The till's sale number keys it: sent twice, it is one sale. send = jusa.Send.create( product="airtime-usd", target=phone, amount="2.00", client_reference=f"sale-{sale_id}", metadata={"till": till_id}, idempotency_key=f"sale-{sale_id}", ) print(send.id, send.status, send.cost) except jusa.InsufficientCreditError: top_up_and_try_later() # nothing was charged: retry with the same idempotency key except jusa.JusaError as jusa_error: logger.error("Jusa refused: %s [%s]", jusa_error.code, jusa_error.request_id) raiseNode
import { InsufficientCreditError, JusaError } from '@jusa/node'; async function sellAirtimeAtTheTill(phone, saleId, tillId) { try { // The till's sale number keys it: sent twice, it is one sale. const send = await jusa.sends.create({ product: 'airtime-usd', target: phone, amount: '2.00', client_reference: `sale-${saleId}`, metadata: { till: tillId }, }, { idempotencyKey: `sale-${saleId}` }); console.log(send.id, send.status, send.cost); } catch (error) { if (error instanceof InsufficientCreditError) { await topUpAndTryLater(); // nothing was charged: retry with the same idempotency key return; } if (error instanceof JusaError) { console.error(`Jusa refused: ${error.code} [${error.requestId}]`); } throw error; } }PHP
function sellAirtimeAtTheTill(\Jusa\JusaClient $jusa, string $phone, string $saleId, string $tillId): void { try { // The till's sale number keys it: sent twice, it is one sale. $send = $jusa->sends->create([ 'product' => 'airtime-usd', 'target' => $phone, 'amount' => '2.00', 'client_reference' => "sale-{$saleId}", 'metadata' => ['till' => $tillId], ], ['idempotency_key' => "sale-{$saleId}"]); echo $send->id, ' ', $send->status, ' ', $send->cost, "\n"; } catch (\Jusa\Exception\InsufficientCreditException) { topUpAndTryLater(); // nothing was charged: retry with the same idempotency key } catch (\Jusa\Exception\JusaException $jusaError) { error_log("Jusa refused: {$jusaError->getErrorCode()} [{$jusaError->getRequestId()}]"); throw $jusaError; } }Go
func sellAirtimeAtTheTill(ctx context.Context, phone, saleID, tillID string) error { // The till's sale number keys it: sent twice, it is one sale. sendResult, err := jusaClient.Sends.Create(ctx, jusa.Params{ "product": "airtime-usd", "target": phone, "amount": "2.00", "client_reference": "sale-" + saleID, "metadata": jusa.Params{"till": tillID}, }, jusa.WithIdempotencyKey("sale-"+saleID)) if err != nil { var insufficientCredit *jusa.InsufficientCreditError if errors.As(err, &insufficientCredit) { topUpAndTryLater() // nothing was charged: retry with the same idempotency key return nil } return fmt.Errorf("sends.create: %w", err) } send := sendResult.Send fmt.Println(send.ID, send.Status, send.Cost) return nil }Java
void sellAirtimeAtTheTill(String phone, String saleId, String tillId) throws JusaException { try { // The till's sale number keys it: sent twice, it is one sale. var send = jusa.sends().create(Params.of( "product", "airtime-usd", "target", phone, "amount", "2.00", "client_reference", "sale-" + saleId, "metadata", Map.of("till", tillId) ), RequestOptions.idempotencyKey("sale-" + saleId)); System.out.println(send.getId() + " " + send.getStatus() + " " + send.getCost()); } catch (InsufficientCreditException insufficientCredit) { topUpAndTryLater(); // nothing was charged: retry with the same idempotency key } catch (JusaException jusaError) { LOGGER.severe("Jusa refused: " + jusaError.getCode() + " [" + jusaError.getRequestId() + "]"); throw jusaError; } }C#
async Task SellAirtimeAtTheTillAsync(string phone, string saleId, string tillId) { try { // The till's sale number keys it: sent twice, it is one sale. var sendResult = await jusa.Sends.CreateAsync(new { product = "airtime-usd", target = phone, amount = "2.00", client_reference = $"sale-{saleId}", metadata = new { till = tillId }, }, new RequestOptions { IdempotencyKey = $"sale-{saleId}" }); var send = sendResult.As<Send>(); Console.WriteLine($"{send.Id} {send.Status} {send.Cost}"); } catch (InsufficientCreditException) { await TopUpAndTryLater(); // nothing was charged: retry with the same idempotency key } catch (JusaException jusaError) { logger.LogError(jusaError, "Jusa refused: {Code} [{RequestId}]", jusaError.Code, jusaError.RequestId); throw; } }Dart
Future<void> sellAirtimeAtTheTill(String phone, String saleId, String tillId) async { try { // The till's sale number keys it: sent twice, it is one sale. final send = await jusa.sends.create({ 'product': 'airtime-usd', 'target': phone, 'amount': '2.00', 'client_reference': 'sale-$saleId', 'metadata': {'till': tillId}, }, RequestOptions(idempotencyKey: 'sale-$saleId')) as Send; print('${send.id} ${send.status} ${send.cost}'); } on InsufficientCreditException { await topUpAndTryLater(); // nothing was charged: retry with the same idempotency key } on JusaException catch (jusaError) { stderr.writeln('Jusa refused: ${jusaError.code} [${jusaError.requestId}]'); rethrow; } }Ruby
def sell_airtime_at_the_till(phone, sale_id, till_id) # The till's sale number keys it: sent twice, it is one sale. send = Jusa::Send.create({ product: "airtime-usd", target: phone, amount: "2.00", client_reference: "sale-#{sale_id}", metadata: { till: till_id }, }, idempotency_key: "sale-#{sale_id}") puts "#{send.id} #{send.status} #{send.cost}" rescue Jusa::InsufficientCreditError top_up_and_try_later # nothing was charged: retry with the same idempotency key rescue Jusa::JusaError => jusa_error LOGGER.error("Jusa refused: #{jusa_error.code} [#{jusa_error.request_id}]") raise endcurl
# The till's sale number keys it: sent twice, it is one sale. curl https://jusa.localhost.co.zw/dev/v1/sends \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY" \ -H "Idempotency-Key: sale-T4-000187" \ -H "Content-Type: application/json" \ -d '{ "product": "airtime-usd", "target": "0771230000", "amount": "2.00", "client_reference": "sale-T4-000187", "metadata": {"till": "4"} }'ZESAPython
def sell_a_zesa_token(quote_id: str, meter_number: str, token_phone: str, sale_id: str, till_id: str) -> None: try: # Passing the quote carries its meter confirmation; the token is texted to token_phone too. send = jusa.Send.create( quote=quote_id, target=meter_number, amount="20.00", currency="USD", token_phone=token_phone, client_reference=f"sale-{sale_id}", metadata={"till": till_id}, idempotency_key=f"sale-{sale_id}", ) print(send.id, send.status) except jusa.InsufficientCreditError: top_up_and_try_later() # nothing was charged: retry with the same idempotency key except jusa.JusaError as jusa_error: logger.error("Jusa refused: %s [%s]", jusa_error.code, jusa_error.request_id) raiseNode
import { InsufficientCreditError, JusaError } from '@jusa/node'; async function sellAZesaToken(quoteId, meterNumber, tokenPhone, saleId, tillId) { try { // Passing the quote carries its meter confirmation; the token is texted to token_phone too. const send = await jusa.sends.create({ quote: quoteId, target: meterNumber, amount: '20.00', currency: 'USD', token_phone: tokenPhone, client_reference: `sale-${saleId}`, metadata: { till: tillId }, }, { idempotencyKey: `sale-${saleId}` }); console.log(send.id, send.status); } catch (error) { if (error instanceof InsufficientCreditError) { await topUpAndTryLater(); // nothing was charged: retry with the same idempotency key return; } if (error instanceof JusaError) { console.error(`Jusa refused: ${error.code} [${error.requestId}]`); } throw error; } }PHP
function sellAZesaToken(\Jusa\JusaClient $jusa, string $quoteId, string $meterNumber, string $tokenPhone, string $saleId, string $tillId): void { try { // Passing the quote carries its meter confirmation; the token is texted to token_phone too. $send = $jusa->sends->create([ 'quote' => $quoteId, 'target' => $meterNumber, 'amount' => '20.00', 'currency' => 'USD', 'token_phone' => $tokenPhone, 'client_reference' => "sale-{$saleId}", 'metadata' => ['till' => $tillId], ], ['idempotency_key' => "sale-{$saleId}"]); echo $send->id, ' ', $send->status, "\n"; } catch (\Jusa\Exception\InsufficientCreditException) { topUpAndTryLater(); // nothing was charged: retry with the same idempotency key } catch (\Jusa\Exception\JusaException $jusaError) { error_log("Jusa refused: {$jusaError->getErrorCode()} [{$jusaError->getRequestId()}]"); throw $jusaError; } }Go
func sellAZesaToken(ctx context.Context, quoteID, meterNumber, tokenPhone, saleID, tillID string) error { // Passing the quote carries its meter confirmation; the token is texted to token_phone too. sendResult, err := jusaClient.Sends.Create(ctx, jusa.Params{ "quote": quoteID, "target": meterNumber, "amount": "20.00", "currency": "USD", "token_phone": tokenPhone, "client_reference": "sale-" + saleID, "metadata": jusa.Params{"till": tillID}, }, jusa.WithIdempotencyKey("sale-"+saleID)) if err != nil { var insufficientCredit *jusa.InsufficientCreditError if errors.As(err, &insufficientCredit) { topUpAndTryLater() // nothing was charged: retry with the same idempotency key return nil } return fmt.Errorf("sends.create: %w", err) } send := sendResult.Send fmt.Println(send.ID, send.Status) return nil }Java
void sellAZesaToken(String quoteId, String meterNumber, String tokenPhone, String saleId, String tillId) throws JusaException { try { // Passing the quote carries its meter confirmation; the token is texted to token_phone too. var send = jusa.sends().create(Params.of( "quote", quoteId, "target", meterNumber, "amount", "20.00", "currency", "USD", "token_phone", tokenPhone, "client_reference", "sale-" + saleId, "metadata", Map.of("till", tillId) ), RequestOptions.idempotencyKey("sale-" + saleId)); System.out.println(send.getId() + " " + send.getStatus()); } catch (InsufficientCreditException insufficientCredit) { topUpAndTryLater(); // nothing was charged: retry with the same idempotency key } catch (JusaException jusaError) { LOGGER.severe("Jusa refused: " + jusaError.getCode() + " [" + jusaError.getRequestId() + "]"); throw jusaError; } }C#
async Task SellAZesaTokenAsync(string quoteId, string meterNumber, string tokenPhone, string saleId, string tillId) { try { // Passing the quote carries its meter confirmation; the token is texted to token_phone too. var sendResult = await jusa.Sends.CreateAsync(new { quote = quoteId, target = meterNumber, amount = "20.00", currency = "USD", token_phone = tokenPhone, client_reference = $"sale-{saleId}", metadata = new { till = tillId }, }, new RequestOptions { IdempotencyKey = $"sale-{saleId}" }); var send = sendResult.As<Send>(); Console.WriteLine($"{send.Id} {send.Status}"); } catch (InsufficientCreditException) { await TopUpAndTryLater(); // nothing was charged: retry with the same idempotency key } catch (JusaException jusaError) { logger.LogError(jusaError, "Jusa refused: {Code} [{RequestId}]", jusaError.Code, jusaError.RequestId); throw; } }Dart
Future<void> sellAZesaToken(String quoteId, String meterNumber, String tokenPhone, String saleId, String tillId) async { try { // Passing the quote carries its meter confirmation; the token is texted to token_phone too. final send = await jusa.sends.create({ 'quote': quoteId, 'target': meterNumber, 'amount': '20.00', 'currency': 'USD', 'token_phone': tokenPhone, 'client_reference': 'sale-$saleId', 'metadata': {'till': tillId}, }, RequestOptions(idempotencyKey: 'sale-$saleId')) as Send; print('${send.id} ${send.status}'); } on InsufficientCreditException { await topUpAndTryLater(); // nothing was charged: retry with the same idempotency key } on JusaException catch (jusaError) { stderr.writeln('Jusa refused: ${jusaError.code} [${jusaError.requestId}]'); rethrow; } }Ruby
def sell_a_zesa_token(quote_id, meter_number, token_phone, sale_id, till_id) # Passing the quote carries its meter confirmation; the token is texted to token_phone too. send = Jusa::Send.create({ quote: quote_id, target: meter_number, amount: "20.00", currency: "USD", token_phone: token_phone, client_reference: "sale-#{sale_id}", metadata: { till: till_id }, }, idempotency_key: "sale-#{sale_id}") puts "#{send.id} #{send.status}" rescue Jusa::InsufficientCreditError top_up_and_try_later # nothing was charged: retry with the same idempotency key rescue Jusa::JusaError => jusa_error LOGGER.error("Jusa refused: #{jusa_error.code} [#{jusa_error.request_id}]") raise endcurl
# Passing the quote carries its meter confirmation; the token is texted to token_phone too. curl https://jusa.localhost.co.zw/dev/v1/sends \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY" \ -H "Idempotency-Key: sale-T4-000187" \ -H "Content-Type: application/json" \ -d '{ "quote": "quo_…", "target": "37261502217", "amount": "20.00", "currency": "USD", "token_phone": "0772123456", "client_reference": "sale-T4-000187", "metadata": {"till": "4"} }'The answer is
202with"status": "pending": the sale is written and your Jusa Credit debited by itscost(below face on the Dealer tier). The customer still receives the full face value. -
Tell the cashier
Most sales are
deliveredwithin seconds. Your webhook hearssend.deliveredorsend.failed; while the customer is at the counter, you can also ask for the sale every couple of seconds.While the customer waitsPython
def check_the_sale(send_id: str) -> None: try: # While the customer waits; your webhook hears send.delivered as well. send = jusa.Send.retrieve(send_id) print(send.status, send.failure_code, send.token, send.units) except jusa.JusaError as jusa_error: logger.error("Jusa refused: %s [%s]", jusa_error.code, jusa_error.request_id) raiseNode
import { JusaError } from '@jusa/node'; async function checkTheSale(sendId) { try { // While the customer waits; your webhook hears send.delivered as well. const send = await jusa.sends.retrieve(sendId); console.log(send.status, send.failure_code, send.token, send.units); } catch (error) { if (error instanceof JusaError) { console.error(`Jusa refused: ${error.code} [${error.requestId}]`); } throw error; } }PHP
function checkTheSale(\Jusa\JusaClient $jusa, string $sendId): void { try { // While the customer waits; your webhook hears send.delivered as well. $send = $jusa->sends->retrieve($sendId); echo $send->status, ' ', $send->failure_code, ' ', $send->token, ' ', $send->units, "\n"; } catch (\Jusa\Exception\JusaException $jusaError) { error_log("Jusa refused: {$jusaError->getErrorCode()} [{$jusaError->getRequestId()}]"); throw $jusaError; } }Go
func checkTheSale(ctx context.Context, sendID string) error { // While the customer waits; your webhook hears send.delivered as well. send, err := jusaClient.Sends.Retrieve(ctx, sendID, nil) if err != nil { return fmt.Errorf("sends.retrieve: %w", err) } fmt.Println(send.Status, send.FailureCode, send.Token, send.Units) return nil }Java
void checkTheSale(String sendId) throws JusaException { try { // While the customer waits; your webhook hears send.delivered as well. var send = jusa.sends().retrieve(sendId); System.out.println(send.getStatus() + " " + send.getFailureCode() + " " + send.getToken() + " " + send.getUnits()); } catch (JusaException jusaError) { LOGGER.severe("Jusa refused: " + jusaError.getCode() + " [" + jusaError.getRequestId() + "]"); throw jusaError; } }C#
async Task CheckTheSaleAsync(string sendId) { try { // While the customer waits; your webhook hears send.delivered as well. var send = await jusa.Sends.RetrieveAsync(sendId); Console.WriteLine($"{send.Status} {send.FailureCode} {send.Token} {send.Units}"); } catch (JusaException jusaError) { logger.LogError(jusaError, "Jusa refused: {Code} [{RequestId}]", jusaError.Code, jusaError.RequestId); throw; } }Dart
Future<void> checkTheSale(String sendId) async { try { // While the customer waits; your webhook hears send.delivered as well. final send = await jusa.sends.retrieve(sendId); print('${send.status} ${send.failureCode} ${send.token} ${send.units}'); } on JusaException catch (jusaError) { stderr.writeln('Jusa refused: ${jusaError.code} [${jusaError.requestId}]'); rethrow; } }Ruby
def check_the_sale(send_id) # While the customer waits; your webhook hears send.delivered as well. send = Jusa::Send.retrieve(send_id) puts "#{send.status} #{send.failure_code} #{send.token} #{send.units}" rescue Jusa::JusaError => jusa_error LOGGER.error("Jusa refused: #{jusa_error.code} [#{jusa_error.request_id}]") raise endcurl
# While the customer waits; your webhook hears send.delivered as well. curl https://jusa.localhost.co.zw/dev/v1/sends/snd_… \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY"Status At the counter Your Jusa Credit pending,submittedSending. Ask the customer to wait a moment. Debited deliveredDone. Print the receipt; for ZESA, the token and units are on it. Spent queuedZESA is down nationally. The token goes out when ZESA is back and is texted to the customer's phone; they need not wait. Debited retryingThe network is busy. Jusa keeps trying; the customer gets an SMS when it lands and need not wait. Debited unknown,requires_reviewThe network has not answered. Do not sell it again: it may have landed. Jusa settles it, usually within minutes. Debited failed_creditedIt did not go through. Give the customer their money back or sell again; failure_codesays why (a wrong number, a blocked meter).Returned Each state's guarantees and timings are in settlement and money; every
failure_codeis in errors. -
Print the receipt
The receipt has the network, the number or meter, the amount and, for ZESA, the token and units. Read the token in the clear with the
tokens:readscope; without it the token is masked. If the customer loses the SMS,POST /sends/{id}/resend-tokentexts it again.After deliveredPython
def print_the_receipt(send_id: str) -> None: try: # JSON for your own slip, or ?format=pdf for ours: receipt = jusa.Send.receipt(send_id) except jusa.JusaError as jusa_error: logger.error("Jusa refused: %s [%s]", jusa_error.code, jusa_error.request_id) raiseNode
import { JusaError } from '@jusa/node'; async function printTheReceipt(sendId) { try { // JSON for your own slip, or ?format=pdf for ours: const receipt = await jusa.sends.receipt(sendId); } catch (error) { if (error instanceof JusaError) { console.error(`Jusa refused: ${error.code} [${error.requestId}]`); } throw error; } }PHP
function printTheReceipt(\Jusa\JusaClient $jusa, string $sendId): void { try { // JSON for your own slip, or ?format=pdf for ours: $receipt = $jusa->sends->receipt($sendId); } catch (\Jusa\Exception\JusaException $jusaError) { error_log("Jusa refused: {$jusaError->getErrorCode()} [{$jusaError->getRequestId()}]"); throw $jusaError; } }Go
func printTheReceipt(ctx context.Context, sendID string) error { // JSON for your own slip, or ?format=pdf for ours: _, err := jusaClient.Sends.Receipt(ctx, sendID, nil) if err != nil { return fmt.Errorf("sends.receipt: %w", err) } return nil }Java
void printTheReceipt(String sendId) throws JusaException { try { // JSON for your own slip, or ?format=pdf for ours: var receipt = jusa.sends().receipt(sendId); } catch (JusaException jusaError) { LOGGER.severe("Jusa refused: " + jusaError.getCode() + " [" + jusaError.getRequestId() + "]"); throw jusaError; } }C#
async Task PrintTheReceiptAsync(string sendId) { try { // JSON for your own slip, or ?format=pdf for ours: var receipt = await jusa.Sends.ReceiptAsync(sendId); } catch (JusaException jusaError) { logger.LogError(jusaError, "Jusa refused: {Code} [{RequestId}]", jusaError.Code, jusaError.RequestId); throw; } }Dart
Future<void> printTheReceipt(String sendId) async { try { // JSON for your own slip, or ?format=pdf for ours: final receipt = await jusa.sends.receipt(sendId); } on JusaException catch (jusaError) { stderr.writeln('Jusa refused: ${jusaError.code} [${jusaError.requestId}]'); rethrow; } }Ruby
def print_the_receipt(send_id) # JSON for your own slip, or ?format=pdf for ours: receipt = Jusa::Send.receipt(send_id) rescue Jusa::JusaError => jusa_error LOGGER.error("Jusa refused: #{jusa_error.code} [#{jusa_error.request_id}]") raise endcurl
# JSON for your own slip, or ?format=pdf for ours: curl https://jusa.localhost.co.zw/dev/v1/sends/snd_…/receipt \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY" -
Close the day
List a till's sales by its key and the day to tie them to the cash drawer, and pull the monthly statement for your books. On the Dealer tier the statement adds
face_sold,cost_drawnanddealer_discount_earned; it is also a CSV and a PDF.End of day and monthPython
def close_the_day(till_key_id: str) -> None: try: # One till's sales, newest first: page back to the start of the day and tie them to the drawer. jusa.Send.list(api_key=till_key_id, limit=100) # The month: face value sold, credit drawn and the dealer discount earned. statement = jusa.Statement.retrieve(period="2026-10", currency="USD") print(statement.opening, statement.closing, statement.totals) except jusa.JusaError as jusa_error: logger.error("Jusa refused: %s [%s]", jusa_error.code, jusa_error.request_id) raiseNode
import { JusaError } from '@jusa/node'; async function closeTheDay(tillKeyId) { try { // One till's sales, newest first: page back to the start of the day and tie them to the drawer. await jusa.sends.list({ api_key: tillKeyId, limit: 100 }); // The month: face value sold, credit drawn and the dealer discount earned. const statement = await jusa.statements.retrieve({ period: '2026-10', currency: 'USD', }); console.log(statement.opening, statement.closing, statement.totals); } catch (error) { if (error instanceof JusaError) { console.error(`Jusa refused: ${error.code} [${error.requestId}]`); } throw error; } }PHP
function closeTheDay(\Jusa\JusaClient $jusa, string $tillKeyId): void { try { // One till's sales, newest first: page back to the start of the day and tie them to the drawer. $jusa->sends->list(['api_key' => $tillKeyId, 'limit' => 100]); // The month: face value sold, credit drawn and the dealer discount earned. $statement = $jusa->statements->retrieve([ 'period' => '2026-10', 'currency' => 'USD', ]); echo $statement->opening, ' ', $statement->closing, ' ', $statement->totals, "\n"; } catch (\Jusa\Exception\JusaException $jusaError) { error_log("Jusa refused: {$jusaError->getErrorCode()} [{$jusaError->getRequestId()}]"); throw $jusaError; } }Go
func closeTheDay(ctx context.Context, tillKeyID string) error { // One till's sales, newest first: page back to the start of the day and tie them to the drawer. _, err := jusaClient.Sends.List(ctx, jusa.Params{"api_key": tillKeyID, "limit": 100}) if err != nil { return fmt.Errorf("sends.list: %w", err) } // The month: face value sold, credit drawn and the dealer discount earned. statement, err := jusaClient.Statements.Retrieve(ctx, jusa.Params{ "period": "2026-10", "currency": "USD", }) if err != nil { return fmt.Errorf("statements.retrieve: %w", err) } fmt.Println(statement.Opening, statement.Closing, statement.Totals) return nil }Java
void closeTheDay(String tillKeyId) throws JusaException { try { // One till's sales, newest first: page back to the start of the day and tie them to the drawer. jusa.sends().list(Params.of("api_key", tillKeyId, "limit", 100)); // The month: face value sold, credit drawn and the dealer discount earned. var statement = jusa.statements().retrieve(Params.of( "period", "2026-10", "currency", "USD" )); System.out.println(statement.getOpening() + " " + statement.getClosing() + " " + statement.getTotals()); } catch (JusaException jusaError) { LOGGER.severe("Jusa refused: " + jusaError.getCode() + " [" + jusaError.getRequestId() + "]"); throw jusaError; } }C#
async Task CloseTheDayAsync(string tillKeyId) { try { // One till's sales, newest first: page back to the start of the day and tie them to the drawer. await jusa.Sends.ListAsync(new { api_key = tillKeyId, limit = 100 }); // The month: face value sold, credit drawn and the dealer discount earned. var statement = await jusa.Statements.RetrieveAsync(new { period = "2026-10", currency = "USD", }); Console.WriteLine($"{statement.Opening} {statement.Closing} {statement.Totals}"); } catch (JusaException jusaError) { logger.LogError(jusaError, "Jusa refused: {Code} [{RequestId}]", jusaError.Code, jusaError.RequestId); throw; } }Dart
Future<void> closeTheDay(String tillKeyId) async { try { // One till's sales, newest first: page back to the start of the day and tie them to the drawer. await jusa.sends.list({'api_key': tillKeyId, 'limit': 100}); // The month: face value sold, credit drawn and the dealer discount earned. final statement = await jusa.statements.retrieve({ 'period': '2026-10', 'currency': 'USD', }); print('${statement.opening} ${statement.closing} ${statement.totals}'); } on JusaException catch (jusaError) { stderr.writeln('Jusa refused: ${jusaError.code} [${jusaError.requestId}]'); rethrow; } }Ruby
def close_the_day(till_key_id) # One till's sales, newest first: page back to the start of the day and tie them to the drawer. Jusa::Send.list(api_key: till_key_id, limit: 100) # The month: face value sold, credit drawn and the dealer discount earned. statement = Jusa::Statement.retrieve(period: "2026-10", currency: "USD") puts "#{statement.opening} #{statement.closing} #{statement.totals}" rescue Jusa::JusaError => jusa_error LOGGER.error("Jusa refused: #{jusa_error.code} [#{jusa_error.request_id}]") raise endcurl
# One till's sales, newest first: page back to the start of the day and tie them to the drawer. curl -G https://jusa.localhost.co.zw/dev/v1/sends \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY" \ -d api_key=key_… \ -d limit=100 # The month: face value sold, credit drawn and the dealer discount earned. curl -G https://jusa.localhost.co.zw/dev/v1/statements \ --fail-with-body \ -H "Authorization: Bearer $JUSA_SECRET_KEY" \ -d period=2026-10 \ -d currency=USD
Going live
- Rehearse every row of the status table with the test numbers:
…0000delivers,…0002fails and credits back,…0005is unknown, then delivered. - Add a webhook endpoint and verify its signatures.
- Set a low-balance alert (
PUT /credit/alerts) so you top up before a till runs dry. - Apply for the Dealer tier if you resell, and accept the API terms with your first live key.
Questions about volumes or rates: jusa@localhost.co.zw.