Whish Money
Whish Money
Partner Integration Guide

Bills

Fetch and pay real-time bills (utilities, telecom, tuition, and more) with an OTP-verified fetch flow.

REST over HTTPS with JSON requests and responses. Intended for authorized distributors.

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

Integration flow

  1. 1
    Fetch the bill. Call Fetch a bill with the customer account. Some billers require a provider OTP first. You receive the billId and the exact baseAmount due.
  2. 2
    Pay the bill. Call Pay a bill with the billId and baseAmount returned by fetch, plus a unique externalId.
  3. 3
    Reconcile if needed. On a timeout, resend the same externalId; the original result replays with retrieved: true and never pays 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.
languageAlways set to en.
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 real and void balance of the account. The void balance is the value of items you purchased that have since been voided, meaning set aside rather than cancelled: that value stays with your account and is drawn on the next time you buy the same product.

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 USD and LBP, 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 '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": -217.718,
      "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.

Fetch a bill

Fetches a bill. For most denominations this is a two-step, OTP-verified flow; selected denominations skip OTP.

POST/bill/fetch
Example request · cURL
cURL · Sandbox
curl -X POST 'https://partner.api.sbx.whish.money/itel-service/api/bill/fetch' \
  -H 'sessionId: YOUR_SESSION_ID' \
  -H 'token: YOUR_TOKEN' \
  -H 'language: en' \
  -H 'User-Agent: AcmeStore/2.1 (https://acme.example; dev@acme.example)' \
  -H 'Content-Type: application/json' \
  -d '{ "denominationId": 53001, "target": "971555555555" }'

Request body

FieldTypeRequiredDescription
denominationIdintRequiredIdentifier of the product/biller.
targetStringRequiredBill target (e.g. subscriber phone number).
verificationIdintOptionalOTP session ID from Step 1; include only in Step 2, together with otp.
otpintOptionalOne-time password sent by the carrier; required only in Step 2.

Standard flow: OTP first

Treat code = bill.requires_provider_otp as an expected challenge, not an error. Persist verificationId = data and prompt the user for the OTP. Sandbox fixed values: verificationId = 6789, otp = 1234.

Step 1: request (denominationId 53001)
Request body
{
  "denominationId": 53001,
  "target": "971555555555"
}
Step 1: response challenge
application/json
{
  "status": false,
  "code": "bill.requires_provider_otp",
  "dialog": {
    "title": "Verification required",
    "message": "Please enter the OTP sent by the provider."
  },
  "actions": null,
  "extra": null,
  "data": 6789
}
Step 2: submit OTP
Request body
{
  "denominationId": 53001,
  "target": "971555555555",
  "verificationId": 6789,
  "otp": 1234
}
Step 2: response success
application/json
{
  "status": true,
  "code": null,
  "dialog": null,
  "actions": null,
  "extra": null,
  "data": {
    "billId": 1220,
    "target": "0555555555",
    "baseAmount": 19.0,
    "additionalAmount": 0.0,
    "totalAmount": 19.0,
    "allowPayAll": true,
    "minAmount": 19.0,
    "maxAmount": 19.0,
    "amountIncrement": 0.0
  }
}

Alternative flow: no OTP (selected denominations)

Request (denominationId 54001)
Request body
{
  "denominationId": 54001,
  "target": "971555555555"
}
Response success
application/json
{
  "status": true,
  "code": null,
  "dialog": null,
  "extra": null,
  "data": {
    "billId": 1234,
    "target": "971555555555",
    "baseAmount": 30.40,
    "additionalAmount": 1.60,
    "totalAmount": 32.00,
    "allowPayAll": true,
    "minAmount": null,
    "maxAmount": null,
    "amountIncrement": null
  }
}

Response fields (fetched bill)

FieldTypeDescription
billIdLongIdentifier of the fetched bill. Pass it to Pay a bill.
targetStringThe bill target, normalized by the biller (may differ from what you sent).
baseAmountDoubleThe amount to settle the bill. Send this value as amount on Pay a bill.
additionalAmountDoubleInformational only; you do not need to act on it. Always pay baseAmount.
totalAmountDoubleInformational only. Pay baseAmount, not this value.
allowPayAllBooleanWhether the biller accepts paying the full outstanding amount in one call.
minAmount / maxAmountDoubleFor variable-amount billers, the accepted range; null for fixed-amount bills.
amountIncrementDoubleFor variable-amount billers, the step the amount must fall on; null or 0 for fixed-amount bills.

Pay a bill

Pays the bill retrieved by Fetch Bill. Always pass the exact billId and baseAmount (as amount) returned by fetch.

POST/bill/payment
Example request · cURL
cURL · Sandbox
curl -X POST 'https://partner.api.sbx.whish.money/itel-service/api/bill/payment' \
  -H 'sessionId: YOUR_SESSION_ID' \
  -H 'token: YOUR_TOKEN' \
  -H 'language: en' \
  -H 'User-Agent: AcmeStore/2.1 (https://acme.example; dev@acme.example)' \
  -H 'Content-Type: application/json' \
  -d '{ "externalId": "9876543210", "billId": 1220, "amount": 19.0 }'

Request body

FieldTypeRequiredDescription
externalIdStringRequiredUnique per payment (idempotency key). Reuse the same value to retry the same payment.
billIdLongRequiredThe billId returned by /bill/fetch.
amountDoubleRequiredMust equal baseAmount from /bill/fetch.
Request
Request body
{
  "externalId": "9876543210",
  "billId": 1220,
  "amount": 19.0
}
Response · 200 success
application/json
{
  "status": true,
  "code": null,
  "dialog": null,
  "extra": null
}
Replay after timeout (same externalId)
application/json
{
  "status": true,
  "code": null,
  "dialog": null,
  "extra": null,
  "retrieved": true
}
Response failure
application/json
{
  "status": false,
  "code": "payment.failed",
  "dialog": {
    "title": "Error",
    "message": "Payment failed"
  },
  "extra": null
}

Sandbox scenarios

Denomination IDBehavior
53001Standard fetch & payment (requires provider OTP).
54001Fetch & payment (no OTP).
Test targetExpected outcome
971555555555Success path (fetch and pay).
971555555554Fetch fails (bill.fetch_failed).
971555555553Fetch succeeds; payment fails (payment.failed).

Fixed sandbox OTP values: verificationId = 6789, otp = 1234.

Best practices

  • Always include sessionId, token, language headers.
  • Treat bill.requires_provider_otp as a two-step flow, not an error; persist verificationId.
  • Use the exact billId and baseAmount from fetch when paying.
  • Use one externalId per payment (client-side idempotency). To retry the same payment after a timeout, resend the same externalId so it is not charged twice; only mint a new one for a genuinely new payment.
  • Normalize amounts to two decimals for display; preserve full precision internally.
  • On a timeout, treat the outcome as unknown: reconcile before you retry, and never assume a non-response means failure.

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).

Bill fetch codes

CodeMeaningRecommended handling
bill.requires_provider_otpFlow control, not an error: the provider requires an OTP. The response includes a verificationId.Prompt the customer for the OTP and resubmit with verificationId + otp. Detect via code, not the message.
bill_payment.wrong_target_formatThe target does not match the operator's expected pattern.Fix the target (phone, account, or reference number).
bill.retrieval.fail.genericThe provider returned no bill (none open, or the lookup failed).Verify the target; the subscriber may have no open bill.
bill.operator_not_supportedThe operator is not available through this integration.Final failure for this operator.
bill.user_not_foundThe subscriber or reference was not found at the provider.Verify target details.
bill.fetch_user_failedProvider-side lookup failure.Retry; verify the target.
bill.fetch_failedThe bill fetch did not complete.Retry; verify the target and denomination.
bill.payment.dsl.not.existThe DSL number does not exist.Fix the DSL number.
bill.payment.invalid.referenceThe reference number is invalid.Fix the reference.
bill.payment.otp_invalid / bill.payment.otp_expiredThe OTP submitted for a provider-OTP flow is wrong or expired.Re-prompt the customer; re-trigger the OTP flow if expired.
bill.payment.daily_attempt_limit_reachedToo many fetch or OTP attempts today.Wait 24 hours before retrying.
bill.payment.otp_attempt_limit_reachedToo many wrong OTP entries; the OTP was invalidated.Re-trigger the OTP flow and try again later.
bill.currency_invalidCurrency mismatch for this biller.Send the biller's currency.
sales.denomination_not_active / sales.schema_not_activeThe biller or denomination is not enabled for this account.Verify the denomination id; contact Whish.

Other provider-specific texts may surface under provider error codes, for example bill.payment.contact_touch, bill.payment.pay_at_touch_agent, bill.payment.pay_at_alfa_agent, bill.ajman.voucher.expired, bill.payment.wrong.amount.currency.usd, bill.payment.not_allowed_postpaid, bill.payment.not_available, and bill.skiptick_invalid_payment_status.

Bill payment codes

CodeMeaningRecommended handling
bill.payment.unpayableThe fetched bill was flagged non-payable (provider restriction or outage).Final failure; direct the customer to the biller.
itel.amountMismatchPartialA partial-payment amount is outside the bill's min/max/increment bounds.Send an amount within the bounds returned by bill fetch.
itel.amountMismatchMessageThe amount must equal the exact bill amount for this biller.Send exactly totalAmount from the fetch response.
bill.payment.bill_paidThe bill was already settled.Treat as terminal; re-fetch to confirm.
bill.payment.bill_retrieve_againThe fetched bill snapshot is stale.Re-run /bill/fetch, then pay.
bill.payment.no_open_invoiceNo outstanding invoice at the provider.Nothing to pay.
bill.payment.not_availableThe provider reports this bill cannot currently be paid.Retry later; if persistent, direct the customer to the biller.
bill.payment.not_allowedThe provider forbids paying this subscriber's invoice via this channel.Final failure.
bill.ogero.requires.contactOgero payments require a contact number for the receipt.Include a valid contact field.
application.invalid.phone.formatThe optional contact field is not a valid Lebanese number.Fix the contact number.
payment.failedThe bill payment did not go through.Read the dialog message; re-fetch the bill and retry with a new externalId.
failedReplay: the payment transaction was reversed.Final failure; retry with a new externalId if appropriate.

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.2.12026-07-15Documentation: error-code reference added; externalId documented as a String; sandbox base URL updated.
v1.22026-03-19User-Agent header requirement added.
v1.12025-10-27OTP fetch and payment flow, updated URLs, redesigned documentation, test cases, sandbox scenarios, and best practices.
v1.02023-03-15New Bills API.

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.