Submit a Recharge
Place a real-money mobile recharge — the wallet is debited and the operator is called immediately
Submit a Recharge
Places a real recharge on a real phone. Your wallet is debited and the request goes to the operator immediately.
This endpoint moves real money and is irreversible once the operator confirms. There is no cancel endpoint. Send an Idempotency-Key on every call, and never retry a request whose outcome you have not checked with GET /api/v1/recharges/:id.
Endpoint
POST /api/v1/rechargesAuthentication: API Key + Secret required Idempotency-Key: required
Request headers
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your API key |
X-API-Secret | Yes | Your API secret |
Content-Type | Yes | Must be application/json |
Idempotency-Key | Yes | Unique key, 8–256 characters. Records live 24 hours. Learn more → |
A missing key returns 400 IDEMPOTENCY_KEY_REQUIRED; a key under 8 characters returns 400 IDEMPOTENCY_KEY_TOO_SHORT.
Request
curl -X POST https://api.starshiprewards.com/api/v1/recharges \
-H "X-API-Key: <your-api-key>" \
-H "X-API-Secret: <your-api-secret>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rch_9f2c1d84_1755500000" \
-d '{
"mobile_number": "9876543210",
"operator_id": 1,
"amount": 199,
"client_reference": "TOPUP-2026-0042"
}'Body
| Field | Type | Required | Description |
|---|---|---|---|
mobile_number | string | Yes | Digits only, 10 digits for India. No +91, spaces or dashes |
operator_id | number | Yes | From GET /api/v1/recharge/operators |
amount | number | string | Yes | Face value in whole rupees |
circle_id | number | Conditional | Required only when the operator's mapping demands a circle. See Circles |
is_postpaid | boolean | No | false (default) = prepaid top-up. true = pay a known postpaid bill amount |
is_special | boolean | No | Special recharge. BSNL / MTNL only |
country_code | string | No | ISO-2. Defaults to IN |
nickname | string | No | Optional label passed to the vendor. Alphanumeric, max 40 characters |
client_reference | string | No | Your own reference, echoed back on every read |
force | boolean | No | Bypass the duplicate guard. Default false |
amount must be a whole rupee. 199.50 is rejected with a 400 — never rounded, never truncated.
Response
The status code carries meaning. Read it before the body.
| HTTP | status | Meaning |
|---|---|---|
200 | SUCCESS | The operator confirmed. Final. Wallet debited |
200 | FAILED | The recharge did not happen. Final. Wallet already credited back |
202 | PROCESSING | Accepted, not settled. Wallet debited. Poll for the outcome |
202 Accepted — the case to design for
{
"id": 84213,
"reference_number": "RCH-84213-2X9F",
"status": "PROCESSING",
"sub_status": "VENDOR_SUBMITTED",
"mobile_number": "9876543210",
"operator_id": 1,
"face": 199.00,
"discount": 4.98,
"payable": 194.02,
"vendor_recharge_id": "JRI-77120043",
"client_reference": "TOPUP-2026-0042",
"created_at": "2026-08-19T09:14:22Z"
}202 is a receipt, not a failure. We hold the recharge and settle it from the vendor's push or our own poll. The wallet stays debited until the vendor tells us otherwise. Poll GET /api/v1/recharges/:id until status leaves PROCESSING. Do not resubmit — a resubmit is a second real recharge.
200 OK — settled immediately
{
"id": 84214,
"reference_number": "RCH-84214-B7KQ",
"status": "SUCCESS",
"sub_status": "OPERATOR_CONFIRMED",
"mobile_number": "9876543210",
"operator_id": 1,
"face": 199.00,
"discount": 4.98,
"payable": 194.02,
"vendor_recharge_id": "JRI-77120044",
"operator_txn_id": "AIRTEL8891203344",
"client_reference": "TOPUP-2026-0042",
"created_at": "2026-08-19T09:15:02Z"
}A FAILED response has the same shape with status: "FAILED" and a failure_reason. A FAILED recharge is still HTTP 200 — the API request succeeded; the recharge did not.
Response fields
| Field | Type | Description |
|---|---|---|
id | number | Starship recharge id. Use it for reads |
reference_number | string | Our reference, and the idempotency key we give the vendor. Store it for reconciliation |
status | string | PROCESSING | SUCCESS | FAILED |
sub_status | string | Finer detail. Diagnostic — branch on status, not this |
mobile_number | string | Echoed back |
operator_id | number | Echoed back |
face | number | The amount that reaches the operator |
discount | number | Your discount applied |
payable | number | What your wallet was debited. Authoritative for reconciliation |
vendor_recharge_id | string | The vendor's own reference. Omitted when not yet assigned |
operator_txn_id | string | The operator's transaction id. Set on success; omitted otherwise |
failure_reason | string | The raw vendor message, verbatim. Omitted when there is none |
client_reference | string | Your reference, echoed back. Omitted when you sent none |
created_at | string | ISO 8601 timestamp |
vendor_recharge_id, operator_txn_id, failure_reason and client_reference are omitted from the JSON entirely when unset — they are not null. A PROCESSING recharge legitimately has no operator_txn_id yet. Code defensively for absent keys.
Duplicates
A repeat of the same (mobile_number, amount) from your account inside a Starship-configured window is refused with 409:
{
"error": {
"message": "a matching recharge was submitted recently; resend with force=true if the repeat is intended"
}
}This guard exists so a retry storm cannot turn one intent into two irreversible recharges.
When a 409 is correct behaviour: your first call actually landed and you are re-sending after a timeout. Do not force it — call GET /api/v1/recharges and find the existing recharge.
When to use force: the repeat is genuinely intended — a user deliberately topping the same number up twice.
{
"mobile_number": "9876543210",
"operator_id": 1,
"amount": 199,
"force": true
}force: true disables the last automatic protection against a double recharge. Set it only from a deliberate, explicit user action — never as a blanket retry setting in your HTTP client.
force vs Idempotency-Key
They solve different problems and you need both.
Idempotency-Key | Duplicate guard | |
|---|---|---|
| Catches | The same request sent twice (retry, crash, double-click) | Two different requests with the same effect |
| Scope | One key, 24 hours | Client + number + amount, configured window |
| Bypass | Send a new key | force: true |
A retry with the same Idempotency-Key replays the original response. A retry with a new key is a new request and will hit the duplicate guard — which is exactly what should happen.
Circles
Some operator/vendor mappings require a telecom circle, because the vendor validates operator and location together. Where that applies, omitting circle_id returns a 400.
There is no public circles endpoint in v1 — request the circle id list from your account manager for the operators you cover. BSNL and MTNL are the usual cases.
Errors
Each failure maps to a distinct status so you can tell "retry later" from "this will never work".
| Status | Message | Cause | Retry? |
|---|---|---|---|
400 | insufficient wallet balance | Wallet cannot cover payable | After topping up |
400 | invalid mobile number: … | Wrong format — must be digits only | No, fix the input |
400 | amount must be a whole rupee greater than zero | Fractional, zero or negative amount | No, fix the input |
400 | operator_id is required | Missing or zero operator_id | No |
400 | unknown operator / unknown circle | No such operator or circle | No |
400 | circle_id is required for this operator | The mapping needs circle_id | No, add circle_id |
400 | operator does not support prepaid/postpaid/special recharge | The operator does not support this kind | No |
400 | operator is not mapped to the recharge vendor | The operator is not currently routable | No — contact support |
400 | IDEMPOTENCY_KEY_REQUIRED | Missing Idempotency-Key header | No, add the header |
403 | recharge is not enabled for this client | Your account is not enabled | No — contact your account manager |
403 | recharge over the API is not enabled for this client | The API channel is off for your account | No — contact your account manager |
403 | this recharge type is not enabled for this client | Prepaid or postpaid is off for your account | No |
403 | this operator is not enabled for this client | A per-client overlay blocks this operator | No |
404 | no INR wallet found for this client | No INR wallet exists | No — contact support |
409 | duplicate | Inside the duplicate window | Only with force, deliberately |
503 | recharge is temporarily unavailable | Global kill-switch, or recharge is unconfigured | Yes, with backoff |
503 | recharge is temporarily paused for this client | Your account is paused for maintenance | Yes, with backoff |
500 | — | Our fault | Yes, with backoff — then check for a created recharge before re-sending |
On a 500 or a network timeout, do not blindly re-send. The recharge may already exist. Retry with the same Idempotency-Key to replay the original response, or search GET /api/v1/recharges by your client_reference before deciding.
Integration checklist
- Generate a unique
Idempotency-Keyper recharge intent — and reuse it for retries of that same intent - Send
client_referenceso you can find the recharge later without our id - Branch on the HTTP status first, then on
status - Treat
202as "poll", never as "failed" - Store
idandreference_numberfrom every response, including failures - Reconcile against
payable, not againstface - Back off on
503— it is transient by design
Next
List and get recharges — poll a PROCESSING recharge and reconcile.