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.
| Method | Path | Idempotent? |
|---|---|---|
POST | /v1/purchases/validate | Safe — no state change |
POST | /v1/purchases/confirm | No — issues tokens / debits funds |
POST | /v1/purchases/retry | Safe — 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 -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
}'const res = await fetch("https://api.scratchpower.com/v1/purchases/validate", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
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,
}),
});
const quote = await res.json();resp = requests.post(
"https://api.scratchpower.com/v1/purchases/validate",
headers={"Authorization": f"Bearer {token}"},
json={
"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,
},
)
quote = resp.json()Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
source_channel | string | yes | WEB, SMART_APP, USSD, BATCH, POS |
language | string | yes | Locale tag, e.g. en-BW — used for error message i18n + msisdn region |
product_type | string | yes | ELECTRICITY / AIRTIME / WATER |
provider_code | string | yes | From the providers[].code on the matching VAS product |
debited_account_id | integer | yes | The account ID returned from GET /v1/accounts. /v1/auth/me does not return accounts — /v1/accounts is the single source of truth |
beneficiary_phone_number | string | yes | E.164 (e.g. 26771436390) — the consumer who'll receive the SMS / token |
subscriber_identifier | string | yes | Meter number, phone number, account number — depends on product |
amount | number | yes | Decimal amount in currency units |
currency | string | yes | ISO 4217 (e.g. BWP) |
charges_on_top_flag | boolean | optional | true (default): operator pays charges on top of amount. false: charges deducted from amount |
comment | string | optional | Free text stored on the operation record |
external_reference | string | optional | Your 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).
{
"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
| Field | Type | Notes |
|---|---|---|
id | integer | Operation id — store it on success |
reference | string | Human-readable reference, e.g. TOP/2026/05/1871610006 |
status | string | ON_GOING (validate), SUCCESSFUL / FAILED / PENDING (confirm) |
type | string | Always TOPUP for purchases |
subscriber_identifier | string | Echoed back from the request — never electricity-specific |
subscriber_details | string | Provider-verified consumer name when available |
total / net / charge / commission / entry | money | Currency-prefixed money strings |
product | object | Product-specific block, see next section |
metadata | object | Provider-side cost breakdown — populated on confirm |
product shape per product_type
product.type | provider | token | units |
|---|---|---|---|
ELECTRICITY | yes | issued 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" |
AIRTIME | yes | — (credit lands directly on the SIM) | — |
WATER | yes | issued 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 -X POST https://api.scratchpower.com/v1/purchases/confirm \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @request.jsonResponse
{
"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"
}
}{
"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"
}
}{
"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:
"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):
"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:
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() }];
}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.
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 -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.