Whish Money
Whish Money
Partner Integration Guide

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.

v1.1.1Platform: WhishJSON over HTTPSAuthor: TecFrac
Authentication
sessionId + token
Production base URL
https://api.whish.money/itel-service/api
Currencies
AED · USD

Integration flow

  1. 1
    Send the top-up. Call Send an airtime top-up with the destination msisdn, countryCode, amount, and a unique externalId.
  2. 2
    Read the result. The response returns the outcome synchronously; there is no callback to wait for.
  3. 3
    Retry safely. Resend the same externalId to retry; an already-processed top-up replays with retrieved: true and never charges twice.

Authentication & headers

Every request must include the HTTP headers below, plus Content-Type: application/json on requests that send a body.

HeaderDescription
sessionIdValue provided by Whish.
tokenValue provided by Whish.
User-AgentIdentifies 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

EnvironmentBase URLPurpose
Sandboxhttps://partner.api.sbx.whish.money/itel-service/apiTesting, QA, integration verification.
Productionhttps://api.whish.money/itel-service/apiLive 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.

FieldTypeDescription
statusBooleantrue when the call was accepted and processed. false when it was not; read code for the reason.
codeStringnull 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.
dialogObjectOptional title / message to display to the user.
dataObjectResponse payload for the operation; shape varies per endpoint (and may be a scalar or null for some operations).
actions / extraObjectReserved; typically null.
retrievedBooleanPresent 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:

statuscodeTreat as
truenullSucceeded. Process data.
false500Pending. The call did not return an outcome, so the result is unknown rather than failed. Reconcile as described below; do not mark it failed.
falseany other codeFailed, 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.

GET/account/balance

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.

Example request · cURL
cURL · Sandbox
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)'
Response · 200 success
application/json
{
  "status": true,
  "code": null,
  "dialog": null,
  "actions": null,
  "extra": null,
  "data": {
    "balanceDetails": {
      "balance": 999890.646,
      "voidBalance": 0.0
    }
  }
}

Response fields (in data.balanceDetails)

FieldTypeDescription
balanceDoubleThe real balance of the account, in the session's currency.
voidBalanceDoubleValue 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.

POST/transfer/airtime
Example request · cURL
cURL · Sandbox
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

FieldTypeRequiredDescription
externalIdStringRequiredUnique per top-up (idempotency key). Reuse the same value to retry the same top-up.
idintRequiredDenomination id of the requested item (provided by the Whish team). Same concept as denominationId in the other Whish APIs.
countryCodeStringRequiredCountry code of the phone number.
msisdnStringRequiredRecipient phone number.
amountDoubleRequiredAmount 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.
Request
Request body
{
  "externalId": "9876543210",
  "id": 40000,
  "countryCode": "971",
  "msisdn": "971522222222",
  "amount": 50.0
}
Response · 200 success
application/json
{
  "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
      }
    ]
  }
}
Replay after timeout (same externalId)
application/json
{
  "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[])

FieldTypeDescription
transactionIdStringWhish transaction identifier for this top-up.
phoneStringThe line that was recharged.
amountStringAmount 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.
originalAmountStringThe 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.
feesStringFees applied, when the carrier reports them; otherwise null.
serviceStringCarrier/service (for example DU).
denominationStringDenomination name, including the allowed amount range.
helpStringCarrier assistance text to show the customer, when the carrier provides one (for example a support number or a USSD string).
date / timeStringDate (YYYY-MM-DD) and time (HH:MM) the top-up was processed.
voidedBooleantrue 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.
fromVoidBooleantrue 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)
FieldTypeDescription
serialStringSerial number, when the product issues one. null for a direct top-up, which delivers no code.
secretStringPIN/secret, when the product issues one. null for a direct top-up.
formattedSecretStringDisplay-formatted variant of secret, when one is issued.
validityStringValidity period, when the carrier reports one.
expiryStringExpiry date, when applicable. null for a direct top-up.
picture / pictureUrlStringCarrier artwork name and its full URL, for display.
printType / viewTypeintPresentation codes assigned by Whish for printing and display.
Response failure
application/json
{
  "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 IDBehavior
40000The variable top-up denomination, DU MORE TIME (5-500). Accepts any amount from 5 to 500 AED.
Test targetExpected outcome
971522222222Success path.
971544444444Failure 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

CodeMeaningRecommended handling
failThe 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.
failedReplay: the original transaction was reversed.Final failure; retry with a new externalId if desired.
literal messageThe X-Event-Timestamp header is stale (older than 20 seconds) or malformed.Regenerate the timestamp and resend.
error.generalThe requested denomination or service id could not be resolved.Verify the id or denomination in the request.
auth.wrong_phone_formatThe target msisdn is invalid for the resolved country/operator pattern.Fix the msisdn.
airtime.invalid_amountFor the affected operator, the amount must be a multiple of 5.Send a valid amount.
sales.denomination_not_activeOperator pattern mismatch, no provider configured, or no denomination matches the amount.Verify amount and operator; retry later; contact Whish if persistent.
purchase.confirmThe price changed versus the quoted cost (new price in extra).Re-quote via /transfer/cost, then resubmit.
itel.unknown_errorInternal error (for example an ambiguous denomination match).Retry with backoff; escalate if persistent.

Common codes (all payment & billing endpoints)

Authentication, session, and request validation:

CodeMeaningRecommended handling
auth.session_not_existMissing or invalid session token.Re-authenticate, then retry.
auth.session_expiredSession token expired.Re-authenticate, then retry.
400Required parameters or headers are missing, or the body failed field-level validation.Fix the request payload; do not retry unchanged.
timestamp.invalidRequest timestamp is outside the allowed clock-skew window.Sync the client clock (NTP), then retry.
request.invalid / device.invalid_osDevice or channel-level validation rejected the request.Verify your integration headers; contact Whish if persistent.
500Unhandled server-side exception.Retry with backoff using the same externalId to avoid double execution; report if persistent.
itel.unknown_errorGeneric fallback for unexpected errors or server misconfiguration.Retry with backoff; escalate if persistent.
emoji.not_supportedPayload contained characters the database rejects (for example emoji).Strip unsupported characters, then retry.
deprecationThe endpoint version is deprecated.Migrate to the current endpoint version.

Account, balance, and limits (any debiting endpoint):

CodeMeaningRecommended handling
sales.account_balance_insufficientAccount balance cannot cover the amount.Top up the account, then retry with a new externalId.
sales.account_balance_insufficient_no_balanceSame insufficient-balance condition; variant returned on third-party sale routes.Top up, then retry with a new externalId.
account.terminatedThe calling account has been terminated.Contact Whish (not recoverable client-side).
account.dealer.terminatedThe parent dealer account is terminated.Contact Whish.
sales.denomination_not_activeThe requested service/denomination is disabled.Verify the denomination id; retry later or contact Whish.
sales.schema_not_activePricing schema not configured/active for this account and service.Contact Whish (configuration issue).
sales.exceeded_daily_limitDaily amount/count limit reached.Wait for the next day, or request a limit increase.
sales.exceeded_monthly_limitMonthly limit reached.Wait, or request a limit increase.
sales.lost_transactions_existEarlier 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_currencyThe currency is not enabled or recognized for this route (typically only LBP and USD).Send a supported currency.
fatalInternal 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.

VersionDateChange
v1.1.12026-07-15Documentation: error-code reference added; externalId documented as a String; sandbox base URL updated.
v1.12026-05-14URL changes; success and failure scenarios added.
v1.02017-01-24Initial 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.