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
| Environment | Base URL | Use for |
|---|---|---|
| Staging | https://dev-gateway.jootal.com | Integration testing |
| Production | https://gateway.jootal.com | Live 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.
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
| Status | Meaning |
|---|---|
| 400 | Malformed or missing request fields |
| 401 | Missing, invalid, or expired token |
| 402 | Insufficient balance — transfer was not attempted |
| 403 | Account suspended, or accessing a resource you don't own |
| 404 | Resource not found |
| 422 | Transfer was rejected by the payment provider — balance already refunded |
| 429 | Rate limit exceeded — see below |
| 502 | Could 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.
Request body
{
"username": "masskode",
"password": "your-password"
}
Response 200
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 3600
}
| Status | Meaning |
|---|---|
| 400 | Missing username or password |
| 401 | Invalid credentials |
| 403 | Account suspended — contact Jootal |
Refresh token
Exchange a still-valid token for a fresh one, without re-sending credentials.
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.
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.
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_option | provider | account_number |
|---|---|---|
MOBILE_WALLET | WAVESN — Wave Sénégal | Phone number |
OFMS — Orange Finances Mobiles Sénégal | ||
BANK_DEPOSIT | omit | Bank account number |
CASH_PICKUP | omit | omit |
Shown: verified XOF examples. See Countries & currencies for the full current list.
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
| Status | Error | Meaning |
|---|---|---|
| 400 | — | Missing or invalid receiving_amount |
| 402 | INSUFFICIENT_BALANCE | Balance too low — nothing was debited |
| 422 | — | Rejected downstream — balance credited back |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests — see Retry-After header |
| 502 | UPSTREAM_ERROR | Payment 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.
: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.
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.
| Country | Currency | Mobile money | Bank payout |
|---|---|---|---|
| 🇸🇳 Sénégal | XOF | Wave, Orange (2 providers) | 25 banks |
| 🇨🇮 Côte d'Ivoire | XOF | Orange, Wave, MTN (3 providers) | 30 banks |
| 🇲🇱 Mali | XOF | Orange (1 provider) | 13 banks |
| 🇹🇬 Togo | XOF | — not available | 14 banks |
| 🇧🇯 Bénin | XOF | — not available | 14 banks |
| 🇧🇫 Burkina Faso | XOF | — not available | 17 banks |
| 🇬🇼 Guinée-Bissau | XOF | — not available | 5 banks |
| 🇳🇪 Niger | XOF | — not available | 12 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.
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.
Calculate fees
Preview the fee for a transfer before sending it.
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.
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>.
Get balance
Response 200
{ "balance": 1500000, "currency": "XOF", "account": "Masskode" }
List top-offs
Paginated list of top-offs credited to your account.
Query parameters
| Param | Default | Notes |
|---|---|---|
page | 1 | |
limit | 20 | Max 100 |
Response 200
{
"allocations": [ ... ],
"total": 4,
"page": 1,
"limit": 20
}
List transactions
Paginated list of your own past transactions.
Query parameters
| Param | Default | Notes |
|---|---|---|
page | 1 | |
limit | 20 | Max 100 |
status | — | Filter by status: pending, sent, failed, rejected |
Response 200
{
"transactions": [ ... ],
"total": 127,
"page": 1,
"limit": 20
}
Account summary
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.
Response 200
{ "status": "ok", "ts": "2026-07-12T10:00:00.000Z" }