API and resource commands
One grammar for every resource, plus a raw request when you need it.
Most commands are the same five verbs applied to eleven resources. Learn the grammar once and you can drive the whole API without opening the reference.
wajub <resource> list
wajub <resource> retrieve <id>
wajub <resource> create -d field=value
wajub <resource> update <id> -d field=value
wajub <resource> delete <id>payments, customers, refunds, transfers, beneficiaries, links, invoices, events,
disputes, accounts and webhooks all answer to it, minus the verbs that make no sense for
them. There is no refunds update, because a refund cannot be edited. shield, identity and
skills have their own shape, covered at the end of this page.
links and invoices work with a live key only. With a sandbox key the CLI stops before sending
anything and exits with status 1: invoices is available in live mode only — use a live key (sk.).
Sending a body
Two ways. -d is for the common case, --data-raw for a body you already have or types you want
to set yourself. They combine: --data-raw is read first, then each -d is applied on top of it,
so a -d field wins over the same field in the JSON.
# Repeatable key=value. Dots build nested objects.
wajub payments create \
-d amount=25000 \
-d currency=XAF \
-d channel=cm.mtn \
-d customer.email=amina@example.com \
-d customer.phone=+237670000000
# An array, written as inline JSON
wajub webhooks create -d url=https://example.com/webhooks/wajub -d 'events=["payment.succeeded","payment.failed"]'
# Or a body you already have
wajub webhooks create --data-raw '{
"url": "https://example.com/webhooks/wajub",
"events": ["payment.succeeded", "payment.failed"]
}'A field named phone, ending in _phone (customer.phone, payout_phone), account_number or
reference is always sent as text, exactly as typed, at any depth. Any other value -d reads as:
true,falseandnullas those JSON values;- a value that starts with
[and ends with], or{and}, as inline JSON, when it parses; - a number when it has no leading zero and at most 15 digits, so
amount=25000is the number25000, whileiban=00123and a 19-digit order number stay text; - anything else as text.
Brackets are not arrays
events[0]=payment.succeeded does not build a list: brackets work like dots, and the CLI sends
"events": {"0": "payment.succeeded"}, an object the API refuses. Write the array as inline JSON,
as above. Send the body with --data-raw when a value must keep a type these rules do not give
it.
Listing
Every list command carries the same nine flags, but four of them are sent under a name the API
does not read.
| Flag | Effect |
|---|---|
--page | A specific page |
--all | Follow pagination until the end, 25 items per request |
--limit | With --all, caps the total. On its own it changes nothing, the page stays at 25 items |
-q, --search | Full text search |
-f, --filter | key=value, repeatable, sent as a query parameter as is |
--status | Filter by status |
--since, --until | Sent as since and until, which the API ignores |
--expand | Sent as expand, which the API ignores |
Use -f for what the API actually reads: per_page, from 1 to 100, and on payments, refunds and
disputes date_from and date_to.
wajub payments list -f per_page=5
wajub payments list --status failed -f date_from=2026-01-01
wajub payments list -q amina@example.com
wajub transfers list --all --limit 500 --json > transfers.jsondate_from and date_to are whole days. The API filters on the calendar date, so there is no way
to ask for the last two hours from here. For a narrower window, list a day and filter the JSON
yourself.
Raw requests
When a route has no dedicated command, or you want to see exactly what the API returns, three commands take any path.
wajub get /balance
wajub get "/payments?per_page=5&status=succeeded"
wajub get /tax/settings
wajub post /customers -d email=amina@example.com -d name="Amina Nkem"
wajub post /transfers --data-raw '{"amount":100000,"currency":"XAF","beneficiary":"ben_…"}'
wajub delete /payments/trx_CSUGajfv9xh0XQ5wu2lxThe path is relative to the API base, and the key comes from your profile, so there is no header
to remember and no key in your history. Quote any path containing &, or your shell eats it.
Two flags that change what the request means
--idempotency-key sets the Idempotency-Key header. It exists on payments create,
payments process, transfers create, refunds create and wajub post, the calls the API makes
idempotent. On the first four the CLI generates a fresh key for every call anyway, so you only pass
it explicitly when you want two invocations to be treated as the same attempt. Other create
commands send none.
--sync sets the X-Sync header, making the call act on behalf of a connected account. It is
available on every create and update, on the action commands such as payments process, and
on wajub post. list, retrieve and delete do not take it.
# Run this twice: the second call returns the first result, it does not charge twice
wajub payments create -d amount=25000 -d currency=XAF --idempotency-key order-4172
# Create the payment on a connected merchant's account
wajub payments create -d amount=25000 -d currency=XAF --sync acc_7Yh2MpL4tRb3nP8sZcXvOutput
Human output is a table. --json prints the API response envelope unchanged, so lists come back
under items and the balance under balance. That is what you pipe. One list differs:
wajub events list --json puts the events under events, as GET /events does, so the filter is
jq '.events[]'.
wajub payments list --status failed --json \
| jq -r '.items[] | [.id, .amount, .failure_reason] | @tsv'
wajub balance --json | jq '.balance.available'
# Every failed payout of the month, as CSV
wajub transfers list --status failed --all --json \
| jq -r '.items[] | [.id, .amount, .currency, .failure_reason] | @csv'Commands that ask before acting
A command that moves money asks for confirmation when it runs with a live key; with a sandbox key
it runs at once. A command that destroys or replaces something always asks. --yes (or -y, or
the alias --force) skips the prompt, which is what you want in a script and nowhere else.
| Command | When it asks |
|---|---|
transfers create | With a live key: sends money |
refunds create | With a live key: sends money back |
payments process | With a live key: charges the payer |
webhooks rotate-secret | Always: invalidates the secret your server is using |
accounts token | Always: replaces a connected account's invitation link |
Every delete | Always: removes the record |
The read only commands
Four commands need no arguments and answer questions you would otherwise look up.
wajub balance # your balance, per currency
wajub channels # every channel slug Wajub supports
wajub countries # supported countries
wajub currencies # supported currencieswajub channels is the one to reach for when a payment fails with an unknown channel: it lists
every slug Wajub supports, the same catalogue for every account. What your own account may use
depends on its providers, and wajub get /providers answers that.
In sandbox, wajub balance returns a simplified object where available equals total and the
other figures are zero. The full breakdown exists in live only.
Identity and Shield
Two topics do not follow the CRUD grammar because the underlying products do not.
# Which operator a number belongs to (sandbox key only)
wajub identity resolve -d account_number=+237670000000 -d channel=cm.mtn -d country=CM -d type=mobile
# Whether an IBAN is well formed
wajub identity validate -d iban=FR7630006000011234567890189
# Shield (live key only)
wajub shield stats
wajub shield settings
wajub shield update-settings --enabled --block-threshold 70 --review-threshold 40
wajub shield blocklist
wajub shield block +237670000000 --type phone --reason "chargeback ring"
# The id is the entry's, a UUID, as `wajub shield blocklist` shows it
wajub shield unblock 9b1f6c2e-4a7d-4f0b-8c3e-2d5a7b9e1f40Both identity commands need the Growth, Scale or Enterprise plan. identity resolve answers from sandbox
data only: with a live key it returns 501. identity validate checks the format and checksum of
an IBAN, not who owns it.
Every shield command needs a live key. With a sandbox key the CLI stops before sending anything,
exits with status 1 and says Shield is available in live mode only — use a live key (sk.). --type accepts email,
phone, ip, country and card_bin. update-settings takes --[no-]enabled,
--review-threshold and --block-threshold on every plan, and --[no-]auto-3ds and
--[no-]blocklist, which need Shield Advanced.