Appearance
Coming from the Xash vend API
This page is for you if you call app.xash.co.zw/api/v1 today with a server token (svt_…). That is the API documented at docs.xash.co.zw. It shows what is different in the Partners API and which new call replaces each old one.
The Partners API is a separate product. It has its own sign-up, keys, balance and base URL: https://partners.xash.co.zw/api/v1. Moving over does not change your server token or your app.xash.co.zw wallet.
What changes
A separate partner account and balance
You sign up at partners.xash.co.zw as a business. You get a partner account with its own prepaid balance.
- The balance is in USD only. There is no ZWG wallet. A ZWG product is priced in USD, and the quote tells you the USD amount.
- It is not your app.xash.co.zw wallet. Money in your old wallet stays there.
- You fund it with a top-up (mobile money, USDT or bank transfer), or with money you collect from customers.
Keys instead of server tokens
You create API keys in the portal, not with POST /auth/server-tokens. There is no login, PIN or password call in the API.
xash_test_…keys use the sandbox. They never move real money, and you get one as soon as you sign up.xash_live_…keys sell for real. They work after you finish go-live in the portal.- Each key has abilities, such as
transactions:writeorbalance:read. Give each system only what it needs.
The old API had no sandbox, so every test sold real airtime. Build and test against the sandbox first.
Quote, then sell
The old API sold in one call. Every sale is now two calls:
POST /quoteschecks the recipient and returns the price, the fee and thetotal_debit. For ZESA and bills it also returns the customer name. A quote lasts 5 minutes.POST /transactionsspends the quote.
This replaces the separate "check account" calls. electricity/check-account and dstv/lookup are now just the quote.
A reference and an Idempotency-Key on every sale
Every POST /transactions needs:
- a
referencein the body: your own unique id for the sale, such as your order number; - an
Idempotency-Keyheader: a UUID you make for that sale.
If your request times out, send the same request again with the same body and the same key. You get the original sale back with 200. You never pay twice.
The old API had no such protection. Sending airtime/direct again after a timeout could sell twice. With the new API, retrying is the right thing to do.
202 means pending, not failed
201with statuscompletedmeans it was delivered.202with statuspendingmeans the outcome is not final yet. The money is reserved. Wait for the webhook, orGET /transactions/{id}. Never buy again.- Only status
failedmeans nothing was delivered. The money is then back in your balance.
The API never answers 502 or 504.
One error shape
The old API mostly answered 400 with only a message. Now every error looks like this:
json
{
"success": false,
"message": "Your balance is too low for this.",
"error": {
"code": "INSUFFICIENT_BALANCE",
"hint": "Top up your balance, then make a new quote.",
"docs_url": "https://docs.partners.xash.co.zw/errors#insufficient_balance"
}
}Branch on error.code, never on message. The codes never change. The HTTP status also means something now: 402 is a low balance, 409 is a conflict and 422 is bad input. See Errors for every code.
Webhooks instead of polling
The old API made you poll. Now you set a webhook URL in the portal, and we send you an event when a sale, collection or top-up finishes, such as transaction.completed or collection.completed.
- Each event is signed. Check the
X-Xash-Signatureheader: it is the hex HMAC-SHA256 of{X-Xash-Timestamp}.{raw body}, made with your webhook secret. - Reject events with a timestamp older than 5 minutes.
- Use
X-Xash-Event-Idto ignore an event you have already handled. - Events are short.
GETthe record for the full details.
POST /webhooks/test sends you a signed test event.
Collections are spend-only
You can still take EcoCash, OMari and InnBucks payments from your customers with POST /collections. The money goes into your partner balance.
That money can only buy Xash products. It is never paid out to you, and there is no refund call. Collections are free.
To top up your own balance, use POST /fundings instead.
Old endpoint to new call
Old paths are under https://app.xash.co.zw/api/v1. New paths are under https://partners.xash.co.zw/api/v1.
Product codes are dotted and lowercase, such as econet.airtime or zetdc.prepaid. Get the full list, with your price and the fields each product needs, from GET /products.
Account and keys
| Old | New |
|---|---|
POST /auth/register, set-password, login, PIN calls, change-password, create-business | Sign up and manage users in the portal. No API call. |
GET/POST/DELETE /auth/server-tokens | Create and revoke API keys in the portal. No API call. |
GET /auth/profile, GET /profile | GET /me |
GET /wallet, GET /wallet/{currency} | GET /balances (USD only) |
GET /services | GET /products |
GET /pos/branches/{identifier} | GET /branches |
POST /pos/branches | Create branches in the portal. Tag a sale with branch_id. |
Selling
| Old | New |
|---|---|
GET /airtime/carriers | GET /products?category=airtime, or POST /lookups/mobile to find the operator for a number |
POST /airtime/direct | POST /quotes with e.g. econet.airtime, then POST /transactions |
GET /airtime/direct/{carrier}/values, GET /airtime/voucher/{carrier}/values | GET /products?category=voucher |
POST /airtime/direct/voucher/{carrier}, POST /airtime/voucher/{carrier}/buy | POST /quotes, then POST /transactions. The PIN is in fulfilment. One voucher per sale: there is no quantity. |
GET /bundles | GET /products?category=data |
POST /bundles/buy/{bundle} | POST /quotes with the bundle's code, e.g. netone.bundle.whatsapp_1gb, then POST /transactions |
POST /bundles/voucher/buy/{bundle} | POST /quotes, then POST /transactions. One voucher per sale. |
POST /electricity/check-account | POST /quotes with zetdc.prepaid. The quote returns the customer name. |
POST /electricity/buy-tokens | POST /transactions with that quote. The token is in fulfilment. |
GET /dstv/packages | GET /products?category=bill |
POST /dstv/lookup | POST /quotes |
POST /dstv/purchase | POST /transactions |
GET /wifi/vouchers | GET /products?category=voucher |
POST /wifi/vouchers/buy | POST /quotes, then POST /transactions. One voucher per sale. |
GET /wifi/vouchers/purchases/{reference} | GET /transactions?reference=… or GET /transactions/{id} |
Only products in your GET /products list are on sale to you.
Money in
| Old | New |
|---|---|
POST /ecocash/pay, /innbucks/pay, /omari/pay, /payments/receive to take a customer's payment | POST /collections with method ecocash, innbucks or omari. Send currency: "ZWG" for a ZWG payment. |
| The same calls, to load your own wallet | POST /fundings |
POST /crypto/pay | POST /fundings (USDT) |
POST /omari/confirm | POST /collections/{id}/confirm |
GET /{method}/poll/{payment} | The collection.completed or funding.credited webhook, or GET /collections/{id} and GET /fundings/{id} |
POST /ecocash/direct/charge, GET /ecocash/direct/transaction | POST /collections, GET /collections/{id} |
POST /card/pay | No equivalent. There are no card payments yet. |
POST /ecocash/direct/refund | No equivalent. Collected money is spend-only. |
GET /ecocash/lookup, POST /{method}/poll/{payment}/report-debit | No equivalent yet. |
Reports
| Old | New |
|---|---|
GET /reports/mini/{currency}/{range}, GET /reports/history/{currency} | GET /statement for balance changes. GET /transactions and GET /collections to list or find records. |
GET /reports/transaction/{transaction} | GET /transactions/{id} |
GET /reports/commission-overview/{currency}, GET /reports/commission/{currency}/{range} | No equivalent. There is no commission: your discount is already in the price you pay. |
Everything else
| Old | New |
|---|---|
POST /transfer, POST /transfer/confirm/{transfer} | No equivalent. You cannot move balance to another account. |
POST /notifications/sms | No equivalent. |
/users/…, /admin/…, /providers-balance | No equivalent. These were for Xash's own systems. |
Example: an airtime sale
Both examples use a sandbox number in the 263000000… range. The amounts, ids and messages are examples. Do not run the old example: the old API has no sandbox.
Before
One call. A timeout leaves you guessing, and retrying could sell twice.
bash
curl https://app.xash.co.zw/api/v1/airtime/direct \
-H "Authorization: Bearer $XASH_SERVER_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"mobile_phone": "263000000001", "amount": 1, "currency": "USD"}'json
{
"success": true,
"message": "Recharge to 263000000001 was successful",
"data": {
"name": "Econet",
"amount": 1,
"currency": "USD",
"mobile_phone": "263000000001",
"status": "success",
"balance": { "balance": 99, "currency": "USD", "profit_on_hold": 0 },
"commission": "USD0.05"
}
}After
1. Quote. Amounts are strings with two decimal places.
bash
curl https://partners.xash.co.zw/api/v1/quotes \
-H "Authorization: Bearer $XASH_KEY" \
-H "Content-Type: application/json" \
-d '{"product": "econet.airtime", "amount": "1.00", "recipient": {"phone": "263000000001"}}'json
{
"success": true,
"message": "Quote created.",
"data": {
"id": "qt_01JB4Y3KQ2N8F7",
"product": "econet.airtime",
"customer": null,
"face_value": "1.00",
"face_currency": "USD",
"fee": "0.00",
"total_debit": "0.95",
"rate": null,
"expires_at": "2026-10-17T14:10:00+02:00"
}
}2. Sell. Use your own order number as the reference, and a new UUID as the Idempotency-Key. Save both before you send.
bash
curl https://partners.xash.co.zw/api/v1/transactions \
-H "Authorization: Bearer $XASH_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f1c9a52-6d0e-4c7b-9a51-2e8d4b7f0c13" \
-d '{"quote_id": "qt_01JB4Y3KQ2N8F7", "reference": "order-10045"}'json
{
"success": true,
"message": "Sale completed.",
"data": {
"id": "tx_01JB4Y5R9M2C6D",
"reference": "order-10045",
"status": "completed",
"product": "econet.airtime",
"total_debit": "0.95",
"balance_after": "99.05",
"created_at": "2026-10-17T14:05:09+02:00",
"completed_at": "2026-10-17T14:05:11+02:00"
}
}The full record has more fields, such as fulfilment (the token or PIN) and receipt.
What to do with each answer:
201, statuscompleted: done. Show your customer the receipt.202, statuspending: wait for thetransaction.completedortransaction.failedwebhook. Do not sell again.- A timeout or a network error: send step 2 again with the same body and the same
Idempotency-Key. - An error: read
error.codeand followerror.hint. For example,QUOTE_EXPIREDmeans make a new quote.
Next steps
- Sign up at partners.xash.co.zw and copy your
xash_test_key. - Read the endpoints and errors reference. AI agents can read llms.txt.
- Move one product at a time, and test each in the sandbox.
- Go live in the portal, top up, and switch to your
xash_live_key.