Authentication
The ScratchPower API uses OAuth2 password grant. Each integration is provisioned with:
- A
client_id+client_secretpair (the OAuth client). - An integration user (username + password) — the actor whose permissions, accounts, and audit trail your transactions run against.
- An organisation code — your tenant on the platform.
Every request that isn't /v1/auth/login carries the access token in the
Authorization header:
Authorization: Bearer eyJhbGciOi...Endpoints
| Method | Path | What it does |
|---|---|---|
POST | /v1/auth/login | Exchange credentials for an access token |
POST | /v1/auth/refresh | Exchange a refresh token for a fresh access token |
DELETE | /v1/auth/logout | Audit-log a logout event (JWTs are stateless — discard locally) |
GET | /v1/auth/me | Return the authenticated actor, role, organisation, and roles |
GET | /v1/accounts | List the authenticated actor's accounts (with balance) |
Login
curl -X POST https://api.scratchpower.com/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"grant_type": "password",
"identifier": "integration-user",
"secret_pass": "your-password",
"org_code": "YOUR_ORG",
"source_channel": "WEB"
}'const res = await fetch("https://api.scratchpower.com/v1/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: process.env.SP_CLIENT_ID,
client_secret: process.env.SP_CLIENT_SECRET,
grant_type: "password",
identifier: process.env.SP_USERNAME,
secret_pass: process.env.SP_PASSWORD,
org_code: process.env.SP_ORG,
source_channel: "WEB",
}),
});
const { access_token, refresh_token, access_in } = await res.json();import os, requests
resp = requests.post(
"https://api.scratchpower.com/v1/auth/login",
json={
"client_id": os.environ["SP_CLIENT_ID"],
"client_secret": os.environ["SP_CLIENT_SECRET"],
"grant_type": "password",
"identifier": os.environ["SP_USERNAME"],
"secret_pass": os.environ["SP_PASSWORD"],
"org_code": os.environ["SP_ORG"],
"source_channel": "WEB",
},
)
data = resp.json()
access_token = data["access_token"]Response
{
"access_token": "eyJhbGciOi...",
"access_in": 3600,
"token_type": "bearer",
"refresh_token": "eyJhbGciOi...",
"scope": "read write"
}access_in is the lifetime of the access token in seconds. Refresh before
it expires to avoid forcing the user to log in again.
Refresh
curl -X POST https://api.scratchpower.com/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"grant_type": "refresh_token",
"refresh_token": "eyJhbGciOi..."
}'The response shape is identical to /login. Each refresh issues a new
refresh_token — store the new one and discard the old.
Refresh tokens are single-use. If you refresh and then re-use the previous refresh token, the second call fails.
Bootstrap flow
After your first successful login, do these two reads once and cache the result in process memory for the lifetime of the access token:
GET /v1/auth/me— confirms the token is valid and gives you the actor / role / organisation context.GET /v1/accounts— returns the actor's accounts. In typical single-account integrations you'll cacheaccounts[0].idand pass it asdebited_account_idon every/v1/purchases/{validate,confirm}call.
const me = await sp.get("/v1/auth/me").then(r => r.json());
const accounts = await sp.get("/v1/accounts").then(r => r.json());
const debitedAccountId = accounts[0].id; // cache for the session/v1/auth/me returns identity (who am I, what role, what org).
/v1/accounts returns money state (balance, account category) and
is the single source of truth for the debited_account_id you pass
on every purchase. Splitting them lets the balance-bearing endpoint
stay cacheable independent of identity calls.
Get the authenticated user
GET /v1/auth/me
Authorization: Bearer <access_token>{
"actor": {
"id": 1,
"full_name": "Torti Ama-Njoku",
"phone_number": "26773554420",
"username": "torti",
"email": "torti@scratch-power.com",
"status": "ACTIVE"
},
"role": {
"id": 1,
"code": "ADM",
"name": "Administrator",
"permissions": ["ROLE_TOPUP", "ROLE_USER_CARE", "..."]
},
"organisation": {
"id": 1,
"code": "SPW",
"name": "Scratch Power"
},
"roles": [
{ "id": 1, "code": "ADM", "name": "Administrator", "permissions": [...] }
]
}accounts is intentionally not in this payload — call
/v1/accounts instead.
Storing credentials
Treat client_secret and the integration user's password the same as any
other production secret:
- Read them from a secrets manager / environment variable at runtime.
- Never commit them to source control.
- Rotate them via Scratch Power support if leaked.