Whish Money
Whish Money
Partner Integration Guide

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.

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

Integration flow

  1. 1
    Call List top-up options with the customer target.
  2. 2
    Display data.options to the customer.
  3. 3
    Call Pay for a top-up with the selected optionId, the amount, and a new externalId.
  4. 4
    Store the returned transactionId or sale response details.
  5. 5
    On retry, reuse the same externalId for 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.

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.

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.

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 real and void 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.

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 'language: en' \
  -H 'User-Agent: AcmeStore/2.1 (https://acme.example; dev@acme.example)'
Response · 200 success
application/json
{
  "status": true,
  "code": null,
  "dialog": null,
  "extra": null,
  "data": {
    "balanceDetails": {
      "balance": 940.0,
      "voidBalance": 0.0
    }
  }
}

Response fields

FieldTypeDescription
balanceDoubleThe real balance of the account.
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.

List top-up options

Retrieves the available options for a customer target, such as a phone number, account number, or bill reference.

POST/v2/{provider}/fetch
Example request · cURL
cURL · Sandbox
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

ParameterTypeRequiredDescription
providerStringRequiredProvider code supplied by Whish (for example etisalatNba).

Request body

FieldTypeRequiredDescription
targetStringRequiredCustomer identifier for the provider. For etisalatNba, send the destination mobile number.
Request
Request body
{
  "target": "+971561234567"
}
Response · 200 success
application/json
{
  "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

FieldTypeDescription
data.targetStringNormalized target.
data.optionsArrayAvailable options for the target.
data.options[].idStringOption id to send as optionId in the Pay request.
data.options[].displayNameStringDisplay name for the customer. It also mentions the price, but do not parse it: use amount.
data.options[].amountDoubleAmount to pay for this option. Send this value back, unchanged, as amount in the Pay request.
data.options[].priceDoublePrice of the option, in currency.
data.options[].currencyStringCurrency the option is priced in (for example AED).
data.options[].metadataObjectProvider-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.

POST/v2/{provider}/pay
Example request · cURL
cURL · Sandbox
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

ParameterTypeRequiredDescription
providerStringRequiredSame provider code used for List top-up options.

Request body

FieldTypeRequiredDescription
targetStringRequiredSame customer identifier used for fetch, unless Whish instructs otherwise for your provider.
optionIdStringRequiredThe id of the option you selected from List top-up options.
amountDoubleRequiredThat option's amount from the fetch response, sent back unchanged. A different value is rejected with external.amount.mismatch.
externalIdStringRequiredUnique per payment (idempotency key).
Request
Request body
{
  "target": "+971561234567",
  "optionId": "341504052",
  "amount": 10.0,
  "externalId": "681958946"
}
Response · 200 success
application/json
{
  "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[])

FieldTypeDescription
transactionIdStringWhish transaction identifier for this payment.
phoneStringThe target that was topped up.
amountStringAmount charged, as a formatted string including the currency (for example 10 USD).
originalAmountStringAmount before any adjustment, same format as amount.
feesStringFees charged, when the provider reports them; otherwise null.
serviceStringProvider service the option belongs to.
denominationStringDenomination/option name sold.
serialStringSerial to deliver, when the provider issues one. null for bundle products such as etisalatNba.
secretStringPIN/secret to deliver, when the provider issues one. null for bundle products.
helpStringProvider assistance text to show the customer, when supplied.
expiryStringExpiry date of the sold item (YYYY-MM-DD), when applicable.
date / timeStringDate (YYYY-MM-DD) and time (HH:mm) of the sale.
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 (5)
FieldTypeDescription
formattedSecretStringDisplay-formatted variant of secret, when one is issued.
validityStringValidity period of the sold item, when the provider reports one.
picture / pictureUrlStringProduct artwork, when the provider supplies it; empty otherwise.
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.
printType / viewTypeintPresentation codes assigned by Whish for printing and display.

Error codes

Failure sample
{
  "status": false,
  "code": "external.invalid.request",
  "dialog": {
    "title": "Error",
    "message": "Invalid Request"
  }
}
CodeDescription
external.invalid.requestRequired field is missing or invalid.
external.unknown.providerProvider code is unknown or not active.
external.error.no.offersOffers could not be retrieved for the target.
external.amount.mismatchThe submitted amount does not match the provider-confirmed amount.
external.failed.paymentPayment 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.

VersionDateChange
v1.02026-07-15Preview 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.