Skip to content

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.

The shape
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 fields, or one JSON object
# 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, false and null as 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=25000 is the number 25000, while iban=00123 and a 19-digit order number stay text;
  • anything else as text.

Listing

Every list command carries the same nine flags, but four of them are sent under a name the API does not read.

FlagEffect
--pageA specific page
--allFollow pagination until the end, 25 items per request
--limitWith --all, caps the total. On its own it changes nothing, the page stays at 25 items
-q, --searchFull text search
-f, --filterkey=value, repeatable, sent as a query parameter as is
--statusFilter by status
--since, --untilSent as since and until, which the API ignores
--expandSent 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.

Narrowing a list
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.json

date_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.

get, post and delete
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_CSUGajfv9xh0XQ5wu2lx

The 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.

Retrying safely, and acting for a connected merchant
# 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_7Yh2MpL4tRb3nP8sZcXv

Output

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[]'.

Composing with jq
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.

CommandWhen it asks
transfers createWith a live key: sends money
refunds createWith a live key: sends money back
payments processWith a live key: charges the payer
webhooks rotate-secretAlways: invalidates the secret your server is using
accounts tokenAlways: replaces a connected account's invitation link
Every deleteAlways: removes the record

The read only commands

Four commands need no arguments and answer questions you would otherwise look up.

Reference data and your own balance
wajub balance      # your balance, per currency
wajub channels     # every channel slug Wajub supports
wajub countries    # supported countries
wajub currencies   # supported currencies

wajub 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.

Operator lookup, IBAN check and fraud controls
# 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-2d5a7b9e1f40

Both 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.

What did you think of this content?