International Topup
Fetch a provider's available options for a customer target, then pay for the selected option. Used for cross-border, provider-based top-ups such as Etisalat NBA bundles.
REST over HTTPS with JSON requests and responses. Intended for authorized distributors.
Integration flow
- 1Call List top-up options with the customer target.
- 2Display
data.optionsto the customer. - 3
- 4Store the returned
transactionIdor sale response details. - 5On retry, reuse the same
externalIdfor the same intended payment.
Authentication & headers
Every request must include the HTTP headers below, plus Content-Type: application/json on requests that send a body.
| Header | Description |
|---|---|
sessionId | Value provided by Whish. |
token | Value provided by Whish. |
User-Agent | Identifies your app. Use your own details, not Whish's. Format: AppName/version (website; contact-email). Example: AcmeStore/2.1 (https://acme.example; dev@acme.example). |
Keep your credentials secret. They authorize account actions and money movement, so send them only over HTTPS, store them in a secure secrets manager, and never expose them in client-side code, logs, or version control. Rotate them immediately if you suspect a leak.
Environments
| Environment | Base URL | Purpose |
|---|---|---|
| Sandbox | https://partner.api.sbx.whish.money/itel-service/api | Testing, QA, integration verification. |
| Production | https://api.whish.money/itel-service/api | Live environment for real transactions. |
The cURL examples on this page target Sandbox. To go live, swap the host for the Production base URL above.
Endpoint paths are appended to the base URL as /v2/{provider}/..., where {provider} is the provider code Whish gives you (for example etisalatNba).
Response format
All endpoints return HTTP 200 with Content-Type: application/json and a shared envelope. Always branch on the status and code fields in the body, never on the HTTP status code.
| Field | Type | Description |
|---|---|---|
status | Boolean | true when the call was accepted and processed. false when it was not; read code for the reason. |
code | String | null whenever status is true. When status is false, an operation-specific error code; the value 500 is special (see below). This field only ever reports errors, never a transaction's progress. |
dialog | Object | Optional title / message to display to the user. |
data | Object | Response payload for the operation; shape varies per endpoint (and may be a scalar or null for some operations). |
actions / extra | Object | Reserved; typically null. |
retrieved | Boolean | Present on idempotent operations. true means this response replays an already-processed request (the original was not run again); false or absent means it ran now. |
The two fields move together, so read them as a pair. There are only these combinations:
status | code | Treat as |
|---|---|---|
true | null | Succeeded. Process data. |
false | 500 | Pending. The call did not return an outcome, so the result is unknown rather than failed. Reconcile as described below; do not mark it failed. |
false | any other code | Failed, unless that code is a documented challenge for the endpoint you called (for example a required provider OTP). Handle documented challenges as an expected step in the flow, not as a failure; see the relevant endpoint reference. |
A status: true is never accompanied by a code, and a code never reports how far a transaction has progressed. Where an operation has a lifecycle of its own (created, paid, refunded), the endpoint reports it on a separate field inside data; see that endpoint's response fields.
No separate status endpoint. To reconcile after a timeout or a code: 500 pending result, resend the same request with the same externalId. An already-processed request returns its original result with retrieved: true, so you recover the outcome without charging again.
Get account balance
Returns the real and void balance of the account.
No parameters. Each session (sessionId) is tied to a single currency, so the balance is always returned in that session's currency.
curl 'https://partner.api.sbx.whish.money/itel-service/api/account/balance' \
-H 'sessionId: YOUR_SESSION_ID' \
-H 'token: YOUR_TOKEN' \
-H 'language: en' \
-H 'User-Agent: AcmeStore/2.1 (https://acme.example; dev@acme.example)'{
"status": true,
"code": null,
"dialog": null,
"extra": null,
"data": {
"balanceDetails": {
"balance": 940.0,
"voidBalance": 0.0
}
}
}Response fields
| Field | Type | Description |
|---|---|---|
balance | Double | The real balance of the account. |
voidBalance | Double | Value of your voided items, held for reuse on a future purchase of the same product. Not a refundable cash balance, and only meaningful where there is stock to hold, so it stays 0.0 for services such as bills and airtime top-ups. |
List top-up options
Retrieves the available options for a customer target, such as a phone number, account number, or bill reference.
curl -X POST 'https://partner.api.sbx.whish.money/itel-service/api/v2/etisalatNba/fetch' \
-H 'sessionId: YOUR_SESSION_ID' \
-H 'token: YOUR_TOKEN' \
-H 'User-Agent: AcmeStore/2.1 (https://acme.example; dev@acme.example)' \
-H 'Content-Type: application/json' \
-d '{ "target": "+971561234567" }'Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
provider | String | Required | Provider code supplied by Whish (for example etisalatNba). |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
target | String | Required | Customer identifier for the provider. For etisalatNba, send the destination mobile number. |
{
"target": "+971561234567"
}{
"status": true,
"code": null,
"dialog": null,
"actions": null,
"extra": null,
"retrieved": true,
"data": {
"target": "+971561234567",
"options": [
{
"id": "341504052",
"displayName": "100 local mints for 30 days. AED 10",
"amount": 10.0,
"price": 10.0,
"currency": "AED",
"metadata": {}
},
{
"id": "341504048",
"displayName": "1GB local daily for 30 days and free Burger king meal. AED45",
"amount": 44.99,
"price": 44.99,
"currency": "AED",
"metadata": {}
},
{
"id": "341504049",
"displayName": "30 Intl mint daily Ind.Pak.Ban.Egy.Afg. Month AED 30",
"amount": 30.0,
"price": 30.0,
"currency": "AED",
"metadata": {}
},
{
"id": "341504051",
"displayName": "500 MB daily and 200 Int mints to IND BANG PAK AFG EGP for 30 days. AED35",
"amount": 35.0,
"price": 35.0,
"currency": "AED",
"metadata": {}
},
{
"id": "341504050",
"displayName": "500MB daily and 100 Local mints for 30 days. AED35",
"amount": 35.0,
"price": 35.0,
"currency": "AED",
"metadata": {}
}
]
}
}Response fields
| Field | Type | Description |
|---|---|---|
data.target | String | Normalized target. |
data.options | Array | Available options for the target. |
data.options[].id | String | Option id to send as optionId in the Pay request. |
data.options[].displayName | String | Display name for the customer. It also mentions the price, but do not parse it: use amount. |
data.options[].amount | Double | Amount to pay for this option. Send this value back, unchanged, as amount in the Pay request. |
data.options[].price | Double | Price of the option, in currency. |
data.options[].currency | String | Currency the option is priced in (for example AED). |
data.options[].metadata | Object | Provider-specific fields. |
Take optionId and amount straight from this response. Pay the option's amount exactly as returned here, and send its id as optionId. Do not round it, recalculate it, or read the price out of displayName: an amount that does not match the one fetched is rejected with external.amount.mismatch. Note that displayName is not always exact, for example an option shown as AED45 has an amount of 44.99.
Pay for a top-up
Executes payment for a selected option. The objects in listOfSoldItems contain provider-specific fields.
curl -X POST 'https://partner.api.sbx.whish.money/itel-service/api/v2/etisalatNba/pay' \
-H 'sessionId: YOUR_SESSION_ID' \
-H 'token: YOUR_TOKEN' \
-H 'User-Agent: AcmeStore/2.1 (https://acme.example; dev@acme.example)' \
-H 'Content-Type: application/json' \
-d '{ "target": "+971561234567", "optionId": "341504052", "amount": 10.0, "externalId": "681958946" }'Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
provider | String | Required | Same provider code used for List top-up options. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
target | String | Required | Same customer identifier used for fetch, unless Whish instructs otherwise for your provider. |
optionId | String | Required | The id of the option you selected from List top-up options. |
amount | Double | Required | That option's amount from the fetch response, sent back unchanged. A different value is rejected with external.amount.mismatch. |
externalId | String | Required | Unique per payment (idempotency key). |
{
"target": "+971561234567",
"optionId": "341504052",
"amount": 10.0,
"externalId": "681958946"
}{
"status": true,
"code": null,
"dialog": {
"title": "Success",
"message": "You have successfully paid the bill for +971561234567",
"yesButton": null,
"noButton": null
},
"actions": null,
"extra": null,
"data": {
"listOfSoldItems": [
{
"transactionId": "20263096",
"service": "ETISALAT BUNDLES",
"picture": "",
"pictureUrl": "",
"denomination": "Etisalat NBA",
"serial": null,
"secret": null,
"formattedSecret": null,
"phone": "+971561234567",
"amount": "10 USD",
"originalAmount": "10 USD",
"fees": null,
"validity": null,
"expiry": "2026-07-17",
"help": "For assistance, please contact +971 52 108 8999",
"time": "13:07",
"date": "2026-07-17",
"voided": false,
"fromVoid": false,
"printType": 2,
"viewType": 2
}
]
}
}Response fields (listOfSoldItems[])
| Field | Type | Description |
|---|---|---|
transactionId | String | Whish transaction identifier for this payment. |
phone | String | The target that was topped up. |
amount | String | Amount charged, as a formatted string including the currency (for example 10 USD). |
originalAmount | String | Amount before any adjustment, same format as amount. |
fees | String | Fees charged, when the provider reports them; otherwise null. |
service | String | Provider service the option belongs to. |
denomination | String | Denomination/option name sold. |
serial | String | Serial to deliver, when the provider issues one. null for bundle products such as etisalatNba. |
secret | String | PIN/secret to deliver, when the provider issues one. null for bundle products. |
help | String | Provider assistance text to show the customer, when supplied. |
expiry | String | Expiry date of the sold item (YYYY-MM-DD), when applicable. |
date / time | String | Date (YYYY-MM-DD) and time (HH:mm) of the sale. |
fromVoid | Boolean | true if this item came from previously voided stock, so the purchase drew on your void balance instead of being charged as new stock. |
Additional fields returned (5)
| Field | Type | Description |
|---|---|---|
formattedSecret | String | Display-formatted variant of secret, when one is issued. |
validity | String | Validity period of the sold item, when the provider reports one. |
picture / pictureUrl | String | Product artwork, when the provider supplies it; empty otherwise. |
voided | Boolean | true if this item has been voided, meaning set aside for reuse rather than cancelled. Voiding is not a refund. Only meaningful for stock items such as vouchers and recharge PINs. |
printType / viewType | int | Presentation codes assigned by Whish for printing and display. |
Error codes
{
"status": false,
"code": "external.invalid.request",
"dialog": {
"title": "Error",
"message": "Invalid Request"
}
}| Code | Description |
|---|---|
external.invalid.request | Required field is missing or invalid. |
external.unknown.provider | Provider code is unknown or not active. |
external.error.no.offers | Offers could not be retrieved for the target. |
external.amount.mismatch | The submitted amount does not match the provider-confirmed amount. |
external.failed.payment | Payment could not be completed. |
Version history
Notable changes to this API and its reference, newest first. The current version is the one shown at the top of this page.
| Version | Date | Change |
|---|---|---|
v1.0 | 2026-07-15 | Preview reference published: externalId documented as a String; sandbox base URL set. |
Postman & tools
Import the ready-made Postman collection for this API, set your credentials as collection variables (or attach a sandbox / production environment), and call every endpoint without writing code. The OpenAPI 3.1 specification is also available to generate client SDKs or validate requests.
Download collectionOpenAPI specAll downloads & environments
In Postman: Import → select the file → choose the Sandbox or Production environment → fill in your credentials. Requests use {{baseUrl}} and the credential variables provided.