Variable Topup
Directly top up a destination phone line with a variable amount. Supported for du and Etisalat topups.
REST over HTTPS with JSON requests and responses. Intended for authorized distributors.
Integration flow
- 1Send the top-up. Call Send an airtime top-up with the destination
msisdn,countryCode, amount, and a uniqueexternalId. - 2Read the result. The response returns the outcome synchronously; there is no callback to wait for.
- 3Retry safely. Resend the same
externalIdto retry; an already-processed top-up replays withretrieved: trueand never charges twice.
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.
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 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. To work in both AED and USD, use a separate session for each.
curl 'https://partner.api.sbx.whish.money/itel-service/api/account/balance' \
-H 'sessionId: YOUR_SESSION_ID' \
-H 'token: YOUR_TOKEN' \
-H 'User-Agent: AcmeStore/2.1 (https://acme.example; dev@acme.example)'{
"status": true,
"code": null,
"dialog": null,
"actions": null,
"extra": null,
"data": {
"balanceDetails": {
"balance": 999890.646,
"voidBalance": 0.0
}
}
}Response fields (in data.balanceDetails)
| Field | Type | Description |
|---|---|---|
balance | Double | The real balance of the account, in the session's currency. |
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. |
Send an airtime top-up
Tops up a destination phone line directly. Supported for du and Etisalat topups.
curl -X POST 'https://partner.api.sbx.whish.money/itel-service/api/transfer/airtime' \
-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 '{
"externalId": "9876543210",
"id": 40000,
"countryCode": "971",
"msisdn": "971522222222",
"amount": 50.0
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
externalId | String | Required | Unique per top-up (idempotency key). Reuse the same value to retry the same top-up. |
id | int | Required | Denomination id of the requested item (provided by the Whish team). Same concept as denominationId in the other Whish APIs. |
countryCode | String | Required | Country code of the phone number. |
msisdn | String | Required | Recipient phone number. |
amount | Double | Required | Amount to top up, in AED. Must fall within the denomination's range: 5 to 500 for DU MORE TIME (5-500). The range is part of the denomination name, and out-of-range values are rejected with airtime.invalid_amount. |
{
"externalId": "9876543210",
"id": 40000,
"countryCode": "971",
"msisdn": "971522222222",
"amount": 50.0
}{
"status": true,
"code": null,
"dialog": {
"title": "Direct Topup",
"message": "The line +971522222222 will be recharged in a bit. Thank you.",
"yesButton": null,
"noButton": null
},
"actions": null,
"extra": null,
"data": {
"listOfSoldItems": [
{
"transactionId": "20242196",
"service": "DU",
"picture": "icondu",
"pictureUrl": "https://datry0d5u8raf.cloudfront.net/icondu.png",
"denomination": "DU MORE TIME (5-500)",
"serial": null,
"secret": null,
"formattedSecret": null,
"phone": "+971522222222",
"amount": "49.5",
"originalAmount": "50 AED",
"fees": null,
"validity": null,
"expiry": null,
"help": "For assistance, please contact 155",
"time": "16:27",
"date": "2026-07-20",
"voided": false,
"fromVoid": false,
"printType": 21,
"viewType": 21
}
]
}
}{
"status": true,
"code": null,
"dialog": {
"title": "Direct Topup",
"message": "The line +971522222222 will be recharged in a bit. Thank you.",
"yesButton": null,
"noButton": null
},
"actions": null,
"extra": null,
"retrieved": true,
"data": {
"listOfSoldItems": [
{
"transactionId": "20242196",
"service": "DU",
"picture": "icondu",
"pictureUrl": "https://datry0d5u8raf.cloudfront.net/icondu.png",
"denomination": "DU MORE TIME (5-500)",
"serial": null,
"secret": null,
"formattedSecret": null,
"phone": "+971522222222",
"amount": "49.5",
"originalAmount": "50 AED",
"fees": null,
"validity": null,
"expiry": null,
"help": "For assistance, please contact 155",
"time": "16:27",
"date": "2026-07-20",
"voided": false,
"fromVoid": false,
"printType": 21,
"viewType": 21
}
]
}
}Response fields (listOfSoldItems[])
| Field | Type | Description |
|---|---|---|
transactionId | String | Whish transaction identifier for this top-up. |
phone | String | The line that was recharged. |
amount | String | Amount actually debited from your account. Where a discount applies to your account this is less than what you requested, so a 50 request can debit 49.5. |
originalAmount | String | The amount delivered to the subscriber, with its currency, for example 50 AED. This is what you requested, and the subscriber receives it in full regardless of what you were debited. Compare it with amount to see your discount. |
fees | String | Fees applied, when the carrier reports them; otherwise null. |
service | String | Carrier/service (for example DU). |
denomination | String | Denomination name, including the allowed amount range. |
help | String | Carrier assistance text to show the customer, when the carrier provides one (for example a support number or a USSD string). |
date / time | String | Date (YYYY-MM-DD) and time (HH:MM) the top-up was processed. |
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. |
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 (7)
| Field | Type | Description |
|---|---|---|
serial | String | Serial number, when the product issues one. null for a direct top-up, which delivers no code. |
secret | String | PIN/secret, when the product issues one. null for a direct top-up. |
formattedSecret | String | Display-formatted variant of secret, when one is issued. |
validity | String | Validity period, when the carrier reports one. |
expiry | String | Expiry date, when applicable. null for a direct top-up. |
picture / pictureUrl | String | Carrier artwork name and its full URL, for display. |
printType / viewType | int | Presentation codes assigned by Whish for printing and display. |
{
"status": false,
"code": "fail",
"dialog": {
"title": "Error",
"message": "Failed to direct recharge."
},
"actions": null,
"extra": null,
"data": null
}Sandbox has fixed target numbers that force each outcome. See Sandbox scenarios.
Sandbox scenarios
| Denomination ID | Behavior |
|---|---|
40000 | The variable top-up denomination, DU MORE TIME (5-500). Accepts any amount from 5 to 500 AED. |
| Test target | Expected outcome |
|---|---|
971522222222 | Success path. |
971544444444 | Failure path (code fail). |
For these test targets, set countryCode to 971 and send an amount between 5 and 500 AED. 40000 is the only seeded denomination, and it covers the whole range, so one id serves every test.
Error codes
Every code below is returned with status: false. Match the string exactly and branch on it, never on the dialog message text, which is written for display and can change.
Re-sending an already-used externalId never re-executes the transaction: the recorded result is replayed verbatim, including the original error code (safe retry, no double charge).
Variable Topup codes
| Code | Meaning | Recommended handling |
|---|---|---|
fail | The top-up could not be completed (for example invalid amount, invalid number, insufficient balance, or carrier unavailable). | Read the dialog message; fix the input or top up the account, then retry with a new externalId. |
failed | Replay: the original transaction was reversed. | Final failure; retry with a new externalId if desired. |
| literal message | The X-Event-Timestamp header is stale (older than 20 seconds) or malformed. | Regenerate the timestamp and resend. |
error.general | The requested denomination or service id could not be resolved. | Verify the id or denomination in the request. |
auth.wrong_phone_format | The target msisdn is invalid for the resolved country/operator pattern. | Fix the msisdn. |
airtime.invalid_amount | For the affected operator, the amount must be a multiple of 5. | Send a valid amount. |
sales.denomination_not_active | Operator pattern mismatch, no provider configured, or no denomination matches the amount. | Verify amount and operator; retry later; contact Whish if persistent. |
purchase.confirm | The price changed versus the quoted cost (new price in extra). | Re-quote via /transfer/cost, then resubmit. |
itel.unknown_error | Internal error (for example an ambiguous denomination match). | Retry with backoff; escalate if persistent. |
Common codes (all payment & billing endpoints)
Authentication, session, and request validation:
| Code | Meaning | Recommended handling |
|---|---|---|
auth.session_not_exist | Missing or invalid session token. | Re-authenticate, then retry. |
auth.session_expired | Session token expired. | Re-authenticate, then retry. |
400 | Required parameters or headers are missing, or the body failed field-level validation. | Fix the request payload; do not retry unchanged. |
timestamp.invalid | Request timestamp is outside the allowed clock-skew window. | Sync the client clock (NTP), then retry. |
request.invalid / device.invalid_os | Device or channel-level validation rejected the request. | Verify your integration headers; contact Whish if persistent. |
500 | Unhandled server-side exception. | Retry with backoff using the same externalId to avoid double execution; report if persistent. |
itel.unknown_error | Generic fallback for unexpected errors or server misconfiguration. | Retry with backoff; escalate if persistent. |
emoji.not_supported | Payload contained characters the database rejects (for example emoji). | Strip unsupported characters, then retry. |
deprecation | The endpoint version is deprecated. | Migrate to the current endpoint version. |
Account, balance, and limits (any debiting endpoint):
| Code | Meaning | Recommended handling |
|---|---|---|
sales.account_balance_insufficient | Account balance cannot cover the amount. | Top up the account, then retry with a new externalId. |
sales.account_balance_insufficient_no_balance | Same insufficient-balance condition; variant returned on third-party sale routes. | Top up, then retry with a new externalId. |
account.terminated | The calling account has been terminated. | Contact Whish (not recoverable client-side). |
account.dealer.terminated | The parent dealer account is terminated. | Contact Whish. |
sales.denomination_not_active | The requested service/denomination is disabled. | Verify the denomination id; retry later or contact Whish. |
sales.schema_not_active | Pricing schema not configured/active for this account and service. | Contact Whish (configuration issue). |
sales.exceeded_daily_limit | Daily amount/count limit reached. | Wait for the next day, or request a limit increase. |
sales.exceeded_monthly_limit | Monthly limit reached. | Wait, or request a limit increase. |
sales.lost_transactions_exist | Earlier transaction results were not acknowledged. | Query the status of prior transactions (replay their externalIds) before submitting new ones. |
account.topup.check.condition.fail (surfaced as transfer.failed.title) | The receiving account would exceed its maximum allowed wallet balance. | Final failure; the receiver must reduce balance or verify their account. |
currency.not_supported / invalid_currency | The currency is not enabled or recognized for this route (typically only LBP and USD). | Send a supported currency. |
fatal | Internal failure while registering the externalId (defensive path; not expected in practice). | Retry with the same externalId; escalate if persistent. |
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.1.1 | 2026-07-15 | Documentation: error-code reference added; externalId documented as a String; sandbox base URL updated. |
v1.1 | 2026-05-14 | URL changes; success and failure scenarios added. |
v1.0 | 2017-01-24 | Initial release. |
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.