Purchases

The two-phase purchase flow: validate to get a quote and verify the subscriber, then confirm to actually buy. Retry lets you resolve ambiguous timeouts without double-charging.

MethodPathIdempotent?
POST/v1/purchases/validateSafe — no state change
POST/v1/purchases/confirmNo — issues tokens / debits funds
POST/v1/purchases/retrySafe — looks up + reattempts a prior failure

Validate

Pre-flights the purchase: checks the subscriber with the provider, calculates charges, returns a quote. Show the result to the user before calling confirm.

curl
bash
curl -X POST https://api.scratchpower.com/v1/purchases/validate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "source_channel": "WEB",
    "language": "en-BW",
    "product_type": "ELECTRICITY",
    "provider_code": "BPC",
    "debited_account_id": 13209,
    "beneficiary_phone_number": "26771436390",
    "subscriber_identifier": "04040404040",
    "amount": 100,
    "currency": "BWP",
    "charges_on_top_flag": true
  }'

Request fields

FieldTypeRequiredNotes
source_channelstringyesWEB, SMART_APP, USSD, BATCH, POS
languagestringyesLocale tag, e.g. en-BW — used for error message i18n + msisdn region
product_typestringyesELECTRICITY / AIRTIME / WATER
provider_codestringyesFrom the providers[].code on the matching VAS product
debited_account_idintegeryesThe account ID returned from GET /v1/accounts. /v1/auth/me does not return accounts — /v1/accounts is the single source of truth
beneficiary_phone_numberstringyesE.164 (e.g. 26771436390) — the consumer who'll receive the SMS / token
subscriber_identifierstringyesMeter number, phone number, account number — depends on product
amountnumberyesDecimal amount in currency units
currencystringyesISO 4217 (e.g. BWP)
charges_on_top_flagbooleanoptionaltrue (default): operator pays charges on top of amount. false: charges deducted from amount
commentstringoptionalFree text stored on the operation record
external_referencestringoptionalYour own reference — used for retries

Response

The shape is product-agnostic at the top level and morphs only inside product. Branch on product.type before reading product.token / product.units — they're absent for product types that don't issue them (e.g. AIRTIME).

200 OK — ELECTRICITY
json
{
  "id": 26,
  "channel": "WEB",
  "date": "2026-05-09 19:53:30",
  "status": "ON_GOING",
  "type": "TOPUP",
  "reference": "TOP/2026/05/1871610006",
  "operator": { "id": 6, "full_name": "...", "phone_number": "...", "username": "dealer", "status": "ACTIVE" },
  "requester": { "id": 6, "full_name": "...", "phone_number": "...", "username": "dealer", "status": "ACTIVE" },
  "payer": {},
  "beneficiary": { "id": 13, "full_name": "26771436390", "phone_number": "26771436390", "status": "ACTIVE" },
  "total": "BWP 103.00",
  "net": "BWP 100.00",
  "charge": "BWP 3.00",
  "commission": "BWP 0.00",
  "entry": "BWP 100.00",
  "subscriber_identifier": "04040404040",
  "subscriber_details": "John Smith",
  "product": {
    "type": "ELECTRICITY",
    "provider": "BPC"
  },
  "metadata": {}
}

subscriber_details (where applicable) is the verified consumer name returned by the VAS provider — surface it to the user before confirm so they can catch typos in the subscriber identifier.

Top-level fields

FieldTypeNotes
idintegerOperation id — store it on success
referencestringHuman-readable reference, e.g. TOP/2026/05/1871610006
statusstringON_GOING (validate), SUCCESSFUL / FAILED / PENDING (confirm)
typestringAlways TOPUP for purchases
subscriber_identifierstringEchoed back from the request — never electricity-specific
subscriber_detailsstringProvider-verified consumer name when available
total / net / charge / commission / entrymoneyCurrency-prefixed money strings
productobjectProduct-specific block, see next section
metadataobjectProvider-side cost breakdown — populated on confirm

product shape per product_type

product.typeprovidertokenunits
ELECTRICITYyesissued on confirm. Single-token: bare value (e.g. "0404 0404 0400 0000 0081"). Multi-token: pipe-separated Description: Token pairs — see Token format below"8.1 kWh"
AIRTIMEyes— (credit lands directly on the SIM)
WATERyesissued on confirm when the provider returns one"1500 L"

Confirm

Same payload as validate. Returns the same envelope; on a successful electricity / water purchase, product.token and product.units will be populated and metadata will carry the provider-side breakdown.

curl
bash
curl -X POST https://api.scratchpower.com/v1/purchases/confirm \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @request.json

Response

200 OK — ELECTRICITY (success)
json
{
  "id": 27,
  "status": "SUCCESSFUL",
  "type": "TOPUP",
  "reference": "TOP/2026/05/1871610007",
  "total": "BWP 103.00",
  "net": "BWP 100.00",
  "charge": "BWP 3.00",
  "commission": "BWP 0.00",
  "entry": "BWP 100.00",
  "subscriber_identifier": "04040404040",
  "subscriber_details": "John Smith",
  "product": {
    "type": "ELECTRICITY",
    "provider": "BPC",
    "token": "0404 0404 0400 0000 0081",
    "units": "8.1 kWh"
  },
  "metadata": {
    "vat": "BWP 12.28",
    "levy": "BWP 0.00",
    "regulatory_fee": "BWP 0.00",
    "charge": "BWP 0.00",
    "cost": "BWP 87.72",
    "provider_subscriber_details": "SGC: 901047, TI: 1, KRN: 2",
    "provider_transaction_identifier": "51540"
  }
}
200 OK — AIRTIME (success)
json
{
  "id": 28,
  "status": "SUCCESSFUL",
  "type": "TOPUP",
  "reference": "TOP/2026/05/1871610008",
  "total": "BWP 10.00",
  "net": "BWP 10.00",
  "charge": "BWP 0.00",
  "commission": "BWP 0.00",
  "entry": "BWP 10.00",
  "subscriber_identifier": "26771436390",
  "product": {
    "type": "AIRTIME",
    "provider": "MASCOM"
  },
  "metadata": {
    "provider_transaction_identifier": "AT-998877"
  }
}
200 OK — WATER (success)
json
{
  "id": 29,
  "status": "SUCCESSFUL",
  "type": "TOPUP",
  "reference": "TOP/2026/05/1871610009",
  "total": "BWP 200.00",
  "net": "BWP 200.00",
  "charge": "BWP 0.00",
  "commission": "BWP 0.00",
  "entry": "BWP 200.00",
  "subscriber_identifier": "1234567890",
  "subscriber_details": "Mma Itumeleng",
  "product": {
    "type": "WATER",
    "provider": "WUC",
    "token": "9999 8888 7777 6666 5555",
    "units": "1500 L"
  },
  "metadata": {
    "vat": "BWP 24.56",
    "cost": "BWP 175.44",
    "provider_transaction_identifier": "WUC-44021"
  }
}

Token format

product.token carries either a single token or — when the provider issues several at once (e.g. BPC auto-key-change responses that need to refresh the meter's encryption keys before the new credit will load) — multiple tokens.

Single-token responses return just the bare token value. The provider's description (e.g. "Credit Token") is stripped server-side so your client can paste the value straight into a meter or display to a customer:

text
"token": "0404 0404 0400 0000 0081"

Multi-token responses contain a | character. The value is a list of Description: Token pairs separated by |, with the descriptions preserved exactly as the provider issued them. For BPC electricity the typical descriptions are Credit Token (the main purchase token) and KeyChange Token1 / KeyChange Token2 (the two key-update tokens that must be entered first):

text
"token": "KeyChange Token1: 8190 1047 0212 0545 2032|KeyChange Token2: 8190 1047 0112 0545 2032|Credit Token: 0404 0404 1980 0000 1917"

Detect which shape you've got by checking whether token contains |. A simple parser:

Parse product.token
js
function parseTokens(raw) {
  if (!raw) return [];
  if (raw.includes("|")) {
    // Multi-token — split on `|`, then on `:` once per part.
    return raw.split("|").map((part) => {
      const i = part.indexOf(":");
      return i < 0
        ? { description: null, token: part.trim() }
        : { description: part.slice(0, i).trim(), token: part.slice(i + 1).trim() };
    });
  }
  // Single-token — description has already been stripped server-side.
  return [{ description: null, token: raw.trim() }];
}
Order matters on multi-token responses

When BPC issues KeyChange tokens, the meter must accept them before the Credit Token will load. Present them to the customer in the order the API returned them — left-to-right in the |-separated string.

Confirm is not idempotent

Re-sending the same /confirm payload after a successful response issues a brand-new transaction. Always store the returned id / reference on success and avoid re-sending. For ambiguous timeouts use /v1/purchases/retry (see below) — never just re-send confirm.

Retry

When confirm times out (network drop, provider slow), you don't know whether the underlying transaction succeeded. Calling confirm again risks double-charging. Instead, call retry with the same subscriber_identifier + external_reference — the platform looks up your previous attempt, asks the provider whether it actually completed, and either returns the original successful token or surfaces the genuine failure for you to act on.

curl
bash
curl -X POST https://api.scratchpower.com/v1/purchases/retry \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "source_channel": "WEB",
    "language": "en-BW",
    "subscriber_identifier": "04040404040",
    "external_reference": "your-own-ref-123"
  }'

The response is the same envelope as /confirm. A SUCCESSFUL status means the original transaction succeeded — show the returned product.token to the user. A 400-class error response means the transaction genuinely failed; you can issue a fresh /confirm.