Jotalee Gateway API

The Jotalee Gateway gives your product a secure, rate-limited connection for sending mobile money transfers in XOF. Every account gets its own isolated balance and credential — you never see or handle anyone else's funds or authentication details.

Learn more at jootal-gateway.netlify.app.

Environments

EnvironmentBase URLUse for
Staginghttps://dev-gateway.jootal.comIntegration testing
Productionhttps://gateway.jootal.comLive transfers

The two environments are fully isolated — separate accounts, credentials, and balances. A token issued on one will not authenticate on the other, so make sure you're pointed at the right base URL before sending requests.

Need staging credentials to start integrating? Request access here — Jootal will provision a staging account and forward your login separately.

All endpoints below are relative to whichever base URL you're integrating against. All requests and responses use application/json.

Response conventions

Transfer-related endpoints under /v1 wrap successful responses in the same envelope used by our underlying payment provider, so the shape is stable across the transfer lifecycle:

{
  "return_code": 200,
  "return_message": "Success",
  "output": { ... }
}

Gateway-specific errors (auth, balance, rate limiting, validation) use a simpler shape instead:

{ "error": "ERROR_CODE", "message": "Human-readable description" }

Account and transaction endpoints under /partner return plain JSON objects directly (no envelope) — see the examples in each section below.

Errors & rate limits

StatusMeaning
400Malformed or missing request fields
401Missing, invalid, or expired token
402Insufficient balance — transfer was not attempted
403Account suspended, or accessing a resource you don't own
404Resource not found
422Transfer was rejected by the payment provider — balance already refunded
429Rate limit exceeded — see below
502Could not reach the payment provider — balance already refunded

Every account has a per-minute request limit set by Jootal when your account was created. If you exceed it, you'll get:

HTTP/1.1 429 Too Many Requests
Retry-After: 37

{ "error": "RATE_LIMIT_EXCEEDED", "retry_after": 37 }

retry_after is in seconds. Back off until then before retrying.


Authentication

Every request (other than login and the health check) requires a JWT in the Authorization header:

Authorization: Bearer <token>

Tokens expire after 1 hour. Refresh before expiry rather than logging in again on every request.

Log in

Exchange your account credentials for a JWT.

POST/auth/login

Request body

{
  "username": "masskode",
  "password": "your-password"
}

Response 200

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_in": 3600
}
StatusMeaning
400Missing username or password
401Invalid credentials
403Account suspended — contact Jootal

Refresh token

Exchange a still-valid token for a fresh one, without re-sending credentials.

POST/auth/refreshAuth required

Response 200

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_in": 3600
}

Transfers

All endpoints below require Authorization: Bearer <token>.

Get your balance

Returns your account's allocated balance — the amount available to you, not the gateway's total balance with its payment provider.

GET/v1/balanceAuth required

Response 200

{
  "balance": 1500000,
  "currency": "XOF",
  "account": "Masskode"
}

Send a transfer

Sends a mobile money transfer. The gateway checks your balance, debits it, sends the transfer, and automatically refunds you if the transfer fails downstream.

POST/v1/transfers/sendAuth required

Request body

{
  "external_id": "your-internal-ref-123",
  "sender_first_name": "John",
  "sender_last_name": "Doe",
  "sender_phone_number": "1212991882",
  "receiver_first_name": "Jane",
  "receiver_last_name": "Doe",
  "receiver_phone_number": "771111111",
  "relationship": "SON",
  "sending_country_code": "USA",
  "sending_currency": "USD",
  "sending_amount": 10,
  "receiving_amount": 6000,
  "receiving_currency": "XOF",
  "receiving_country_code": "SEN",
  "receiving_option": "MOBILE_WALLET",
  "provider": "WAVESN",
  "account_number": "771111111",
  "purpose": "FAMILY_ASSISTANCE",
  "source_of_funds": "BUSINESS"
}

Valid values for relationship, purpose, source_of_funds, and receiving_option come from GET /v1/enumerations. Fetch that once and cache it — don't hardcode values.

Receiving options

receiving_optionprovideraccount_number
MOBILE_WALLETWAVESN — Wave SénégalPhone number
OFMS — Orange Finances Mobiles Sénégal
BANK_DEPOSITomitBank account number
CASH_PICKUPomitomit

Shown: verified XOF examples. See Countries & currencies for the full current list.

Balance is checked and debited against receiving_amount before the transfer is attempted. If the downstream transfer fails or can't be reached, your balance is credited back automatically — no reconciliation needed on your end.

Response 200

{
  "return_code": 200,
  "return_message": "Success",
  "output": {
    "reference": "1695412083916",
    "pin": "35791796"
  }
}

Errors

StatusErrorMeaning
400Missing or invalid receiving_amount
402INSUFFICIENT_BALANCEBalance too low — nothing was debited
422Rejected downstream — balance credited back
429RATE_LIMIT_EXCEEDEDToo many requests — see Retry-After header
502UPSTREAM_ERRORPayment provider unreachable — balance credited back

Response 402

{
  "error": "INSUFFICIENT_BALANCE",
  "balance": 5000,
  "required": 6000
}

Get transfer status

Get the current status of a transfer you sent, including live delivery status.

GET/v1/transfers/:idAuth required

:id is the gateway transaction ID returned to you — not the provider's reference value from the send response.

Response 200

{
  "id": "3f2e1a...",
  "account_user_id": "uuid",
  "versus_tx_id": "1695412083916",
  "amount": 6000,
  "currency": "XOF",
  "status": "sent",
  "balance_before": 1506000,
  "balance_after": 1500000,
  "created_at": "2026-06-15T10:30:00Z",
  "versus_details": {
    "status": "PAID",
    "statuses": [
      { "status": "CREATED", "date": "2026-06-15 10:30:00" },
      { "status": "TRANSMITTED", "date": "2026-06-15 10:30:01" },
      { "status": "PAID", "date": "2026-06-15 10:30:05" }
    ]
  }
}

You can only retrieve your own transactions — requesting someone else's returns 403.


Reference

Lookup endpoints for values you need before sending a transfer: supported countries/currencies/payout options, valid enum values, fee previews, and your funding exchange rate.

Countries & currencies

The table below is the source of truth for supported countries, currencies, and payout options — GET /v1/enumerations has no country or currency data, so don't infer coverage from it.

Only send transfers that settle in XOF. The gateway's balance ledger is XOF-only, and receiving_amount is debited as a raw number — receiving_currency isn't checked against it. Every country below settles in XOF today; check each entry's currencies field via the API rather than assuming that stays true.
CountryCurrencyMobile moneyBank payout
🇸🇳 SénégalXOFWave, Orange (2 providers)25 banks
🇨🇮 Côte d'IvoireXOFOrange, Wave, MTN (3 providers)30 banks
🇲🇱 MaliXOFOrange (1 provider)13 banks
🇹🇬 TogoXOF— not available14 banks
🇧🇯 BéninXOF— not available14 banks
🇧🇫 Burkina FasoXOF— not available17 banks
🇬🇼 Guinée-BissauXOF— not available5 banks
🇳🇪 NigerXOF— not available12 banks

Counts are exact as of 2026-07-12. Call GET /v1/countries for the individual provider and bank codes.

List countries

Every country the gateway supports, with its currency, mobile money providers (with the provider code to use on a transfer), and covered banks. Hand-maintained by Jootal, not a live mirror of any single upstream provider.

GET/v1/countriesAuth required

Response 200

{
  "country": "Sénégal",
  "iso_code": "SN",
  "flag": "🇸🇳",
  "currencies": ["XOF"],
  "mobile_money": [
    { "code": "OFMS", "name": "Orange Finances Mobiles Sénégal" },
    { "code": "WAVESN", "name": "Wave Sénégal" }
  ],
  "banks": [
    { "code": "SN 0137", "name": "BANQUE ATLANTIQUE-SENEGAL (BAS)" },
    { "code": "SN 0191", "name": "BANQUE DE DAKAR (BDK)" }
    /* ...25 banks total for Sénégal */
  ],
  "cash_pickup": false
}

Shown for Sénégal only — the actual response is an array covering every country in the table above. Use mobile_money[].code directly as the provider field in POST /v1/transfers/send.

banks[].code identifies the destination bank for a BANK_DEPOSIT transfer, but no confirmed request field accepts it yet — the transfer body above only shows account_number. Confirm the correct field with your Jootal contact before building against bank deposit.

cash_pickup is false for every country today — coverage hasn't been confirmed, so it's left unset rather than guessed.

List enumerations

Valid values for relationship, purpose, source_of_funds, occupation, gender, marital_status, id_type, and receiving_option (with its provider list, grouped under MOBILE_WALLET). These change rarely — fetch once and cache the result.

GET/v1/enumerationsAuth required
No country or currency data here — see Countries & currencies above for that.

Calculate fees

Preview the fee for a transfer before sending it.

POST/v1/feesAuth required

Request body

{
  "amount": 6000,
  "currency": "XOF",
  "receiving_option": "MOBILE_WALLET"
}

Response 200

{
  "return_code": 200,
  "return_message": "Fees",
  "output": { "fees": 120 }
}

Get your exchange rate

The USD → XOF rate your balance was funded at — the weighted average across all USD deposits to your account. Useful for estimating how much XOF a USD amount converts to before sending.

GET/api/exchange_rateAuth required

Response 200

{
  "return_code": 200,
  "return_message": "Success",
  "output": {
    "rate": 656,
    "base_currency": "USD",
    "quote_currency": "XOF",
    "usd_loaded": 305.00
  }
}

Fixed at deposit time, not a live market rate — it only changes when your account receives a new deposit. quote_currency is XOF for every account today.

Response 404

{ "return_code": 404, "return_message": "No FX rate configured for this account" }

Account

Read-only endpoints for your own balance, top-off history, and transaction history. All require Authorization: Bearer <token>.

Prefer a UI over raw API calls? Log into /dashboard with the same credentials to view your balance, top-offs, and transactions in the browser — it calls these same endpoints under the hood.

Get balance

GET/partner/balanceAuth required

Response 200

{ "balance": 1500000, "currency": "XOF", "account": "Masskode" }

List top-offs

Paginated list of top-offs credited to your account.

GET/partner/allocationsAuth required

Query parameters

ParamDefaultNotes
page1
limit20Max 100

Response 200

{
  "allocations": [ ... ],
  "total": 4,
  "page": 1,
  "limit": 20
}

List transactions

Paginated list of your own past transactions.

GET/partner/transactionsAuth required

Query parameters

ParamDefaultNotes
page1
limit20Max 100
statusFilter by status: pending, sent, failed, rejected

Response 200

{
  "transactions": [ ... ],
  "total": 127,
  "page": 1,
  "limit": 20
}

Account summary

GET/partner/summaryAuth required

Response 200

{
  "balance": 1500000,
  "currency": "XOF",
  "total_sent": 12400000,
  "tx_count": 87,
  "last_tx_at": "2026-06-15T09:00:00Z"
}

Other

Health check

No authentication required. Useful for uptime monitoring.

GET/health

Response 200

{ "status": "ok", "ts": "2026-07-12T10:00:00.000Z" }