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.
Integration flow
- 1Fetch the bill. Call Fetch a bill with the customer account. Some billers require a provider OTP first. You receive the
billIdand the exactbaseAmountdue. - 2Pay the bill. Call Pay a bill with the
billIdandbaseAmountreturned by fetch, plus a uniqueexternalId. - 3Reconcile if needed. On a timeout, resend the same
externalId; the original result replays withretrieved: trueand never pays 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. |
language | Always set to en. |
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 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.
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.
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": -217.718,
"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. |
Fetch a bill
Fetches a bill. For most denominations this is a two-step, OTP-verified flow; selected denominations skip OTP.
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
| Field | Type | Required | Description |
|---|---|---|---|
denominationId | int | Required | Identifier of the product/biller. |
target | String | Required | Bill target (e.g. subscriber phone number). |
verificationId | int | Optional | OTP session ID from Step 1; include only in Step 2, together with otp. |
otp | int | Optional | One-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.
{
"denominationId": 53001,
"target": "971555555555"
}{
"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
}{
"denominationId": 53001,
"target": "971555555555",
"verificationId": 6789,
"otp": 1234
}{
"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)
{
"denominationId": 54001,
"target": "971555555555"
}{
"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)
| Field | Type | Description |
|---|---|---|
billId | Long | Identifier of the fetched bill. Pass it to Pay a bill. |
target | String | The bill target, normalized by the biller (may differ from what you sent). |
baseAmount | Double | The amount to settle the bill. Send this value as amount on Pay a bill. |
additionalAmount | Double | Informational only; you do not need to act on it. Always pay baseAmount. |
totalAmount | Double | Informational only. Pay baseAmount, not this value. |
allowPayAll | Boolean | Whether the biller accepts paying the full outstanding amount in one call. |
minAmount / maxAmount | Double | For variable-amount billers, the accepted range; null for fixed-amount bills. |
amountIncrement | Double | For 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.
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
| Field | Type | Required | Description |
|---|---|---|---|
externalId | String | Required | Unique per payment (idempotency key). Reuse the same value to retry the same payment. |
billId | Long | Required | The billId returned by /bill/fetch. |
amount | Double | Required | Must equal baseAmount from /bill/fetch. |
{
"externalId": "9876543210",
"billId": 1220,
"amount": 19.0
}{
"status": true,
"code": null,
"dialog": null,
"extra": null
}{
"status": true,
"code": null,
"dialog": null,
"extra": null,
"retrieved": true
}{
"status": false,
"code": "payment.failed",
"dialog": {
"title": "Error",
"message": "Payment failed"
},
"extra": null
}Sandbox scenarios
| Denomination ID | Behavior |
|---|---|
53001 | Standard fetch & payment (requires provider OTP). |
54001 | Fetch & payment (no OTP). |
| Test target | Expected outcome |
|---|---|
971555555555 | Success path (fetch and pay). |
971555555554 | Fetch fails (bill.fetch_failed). |
971555555553 | Fetch succeeds; payment fails (payment.failed). |
Fixed sandbox OTP values: verificationId = 6789, otp = 1234.
Best practices
- Always include
sessionId,token,languageheaders. - Treat
bill.requires_provider_otpas a two-step flow, not an error; persistverificationId. - Use the exact
billIdandbaseAmountfrom fetch when paying. - Use one
externalIdper payment (client-side idempotency). To retry the same payment after a timeout, resend the sameexternalIdso 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
| Code | Meaning | Recommended handling |
|---|---|---|
bill.requires_provider_otp | Flow 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_format | The target does not match the operator's expected pattern. | Fix the target (phone, account, or reference number). |
bill.retrieval.fail.generic | The provider returned no bill (none open, or the lookup failed). | Verify the target; the subscriber may have no open bill. |
bill.operator_not_supported | The operator is not available through this integration. | Final failure for this operator. |
bill.user_not_found | The subscriber or reference was not found at the provider. | Verify target details. |
bill.fetch_user_failed | Provider-side lookup failure. | Retry; verify the target. |
bill.fetch_failed | The bill fetch did not complete. | Retry; verify the target and denomination. |
bill.payment.dsl.not.exist | The DSL number does not exist. | Fix the DSL number. |
bill.payment.invalid.reference | The reference number is invalid. | Fix the reference. |
bill.payment.otp_invalid / bill.payment.otp_expired | The 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_reached | Too many fetch or OTP attempts today. | Wait 24 hours before retrying. |
bill.payment.otp_attempt_limit_reached | Too many wrong OTP entries; the OTP was invalidated. | Re-trigger the OTP flow and try again later. |
bill.currency_invalid | Currency mismatch for this biller. | Send the biller's currency. |
sales.denomination_not_active / sales.schema_not_active | The 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
| Code | Meaning | Recommended handling |
|---|---|---|
bill.payment.unpayable | The fetched bill was flagged non-payable (provider restriction or outage). | Final failure; direct the customer to the biller. |
itel.amountMismatchPartial | A partial-payment amount is outside the bill's min/max/increment bounds. | Send an amount within the bounds returned by bill fetch. |
itel.amountMismatchMessage | The amount must equal the exact bill amount for this biller. | Send exactly totalAmount from the fetch response. |
bill.payment.bill_paid | The bill was already settled. | Treat as terminal; re-fetch to confirm. |
bill.payment.bill_retrieve_again | The fetched bill snapshot is stale. | Re-run /bill/fetch, then pay. |
bill.payment.no_open_invoice | No outstanding invoice at the provider. | Nothing to pay. |
bill.payment.not_available | The provider reports this bill cannot currently be paid. | Retry later; if persistent, direct the customer to the biller. |
bill.payment.not_allowed | The provider forbids paying this subscriber's invoice via this channel. | Final failure. |
bill.ogero.requires.contact | Ogero payments require a contact number for the receipt. | Include a valid contact field. |
application.invalid.phone.format | The optional contact field is not a valid Lebanese number. | Fix the contact number. |
payment.failed | The bill payment did not go through. | Read the dialog message; re-fetch the bill and retry with a new externalId. |
failed | Replay: 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:
| 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.2.1 | 2026-07-15 | Documentation: error-code reference added; externalId documented as a String; sandbox base URL updated. |
v1.2 | 2026-03-19 | User-Agent header requirement added. |
v1.1 | 2025-10-27 | OTP fetch and payment flow, updated URLs, redesigned documentation, test cases, sandbox scenarios, and best practices. |
v1.0 | 2023-03-15 | New 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.