Starship Rewards API
Recharge

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/recharges

Authentication: API Key + Secret required Idempotency-Key: required

Request headers

HeaderRequiredDescription
X-API-KeyYesYour API key
X-API-SecretYesYour API secret
Content-TypeYesMust be application/json
Idempotency-KeyYesUnique 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

FieldTypeRequiredDescription
mobile_numberstringYesDigits only, 10 digits for India. No +91, spaces or dashes
operator_idnumberYesFrom GET /api/v1/recharge/operators
amountnumber | stringYesFace value in whole rupees
circle_idnumberConditionalRequired only when the operator's mapping demands a circle. See Circles
is_postpaidbooleanNofalse (default) = prepaid top-up. true = pay a known postpaid bill amount
is_specialbooleanNoSpecial recharge. BSNL / MTNL only
country_codestringNoISO-2. Defaults to IN
nicknamestringNoOptional label passed to the vendor. Alphanumeric, max 40 characters
client_referencestringNoYour own reference, echoed back on every read
forcebooleanNoBypass 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.

HTTPstatusMeaning
200SUCCESSThe operator confirmed. Final. Wallet debited
200FAILEDThe recharge did not happen. Final. Wallet already credited back
202PROCESSINGAccepted, 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

FieldTypeDescription
idnumberStarship recharge id. Use it for reads
reference_numberstringOur reference, and the idempotency key we give the vendor. Store it for reconciliation
statusstringPROCESSING | SUCCESS | FAILED
sub_statusstringFiner detail. Diagnostic — branch on status, not this
mobile_numberstringEchoed back
operator_idnumberEchoed back
facenumberThe amount that reaches the operator
discountnumberYour discount applied
payablenumberWhat your wallet was debited. Authoritative for reconciliation
vendor_recharge_idstringThe vendor's own reference. Omitted when not yet assigned
operator_txn_idstringThe operator's transaction id. Set on success; omitted otherwise
failure_reasonstringThe raw vendor message, verbatim. Omitted when there is none
client_referencestringYour reference, echoed back. Omitted when you sent none
created_atstringISO 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-KeyDuplicate guard
CatchesThe same request sent twice (retry, crash, double-click)Two different requests with the same effect
ScopeOne key, 24 hoursClient + number + amount, configured window
BypassSend a new keyforce: 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".

StatusMessageCauseRetry?
400insufficient wallet balanceWallet cannot cover payableAfter topping up
400invalid mobile number: …Wrong format — must be digits onlyNo, fix the input
400amount must be a whole rupee greater than zeroFractional, zero or negative amountNo, fix the input
400operator_id is requiredMissing or zero operator_idNo
400unknown operator / unknown circleNo such operator or circleNo
400circle_id is required for this operatorThe mapping needs circle_idNo, add circle_id
400operator does not support prepaid/postpaid/special rechargeThe operator does not support this kindNo
400operator is not mapped to the recharge vendorThe operator is not currently routableNo — contact support
400IDEMPOTENCY_KEY_REQUIREDMissing Idempotency-Key headerNo, add the header
403recharge is not enabled for this clientYour account is not enabledNo — contact your account manager
403recharge over the API is not enabled for this clientThe API channel is off for your accountNo — contact your account manager
403this recharge type is not enabled for this clientPrepaid or postpaid is off for your accountNo
403this operator is not enabled for this clientA per-client overlay blocks this operatorNo
404no INR wallet found for this clientNo INR wallet existsNo — contact support
409duplicateInside the duplicate windowOnly with force, deliberately
503recharge is temporarily unavailableGlobal kill-switch, or recharge is unconfiguredYes, with backoff
503recharge is temporarily paused for this clientYour account is paused for maintenanceYes, with backoff
500Our faultYes, 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

  1. Generate a unique Idempotency-Key per recharge intent — and reuse it for retries of that same intent
  2. Send client_reference so you can find the recharge later without our id
  3. Branch on the HTTP status first, then on status
  4. Treat 202 as "poll", never as "failed"
  5. Store id and reference_number from every response, including failures
  6. Reconcile against payable, not against face
  7. Back off on 503 — it is transient by design

Next

List and get recharges — poll a PROCESSING recharge and reconcile.