Start here
Authentication
Four kinds of caller reach the API, each with its own mechanism and its own boundary. Pick the row that matches what you are building.
Overview#
| Caller | Mechanism | Reaches | Availability |
|---|---|---|---|
| Anyone | None | Public endpoints | Live |
| Android device | Bearer device token and an Ed25519 signature on every request | Mobile device API | Implemented; activation blocked in production (see availability) |
| Server integration | OAuth 2.0 client credentials → bearer token | Integrations, edge sync, webhooks | Preview (see below) |
| Administrator (human) | Browser session cookie, CSRF-protected, optional TOTP | The web application and device administration | Live — not an integration mechanism |
Machine integrations (OAuth 2.0 client credentials)#
The token endpoint, scope checks and tenant binding below are implemented and covered by automated tests. Production currently authenticates against a single operator-seeded client. Clients created by tenant administrators in the console are held in a separate registry that the token endpoint does not consult yet, so there is no self-service way to obtain client credentials. Do not plan an integration around this surface until provisioning is connected; this page will say so when it is.
Get a token#
POST https://rr-api.medq.co.zw/api/v1/auth/token implements the client-credentials grant of RFC 6749 §4.4. It is
public (it is how you authenticate) and only grant_type=client_credentials is supported.
Send the client credentials as HTTP Basic or in a form body:
curl -s https://rr-api.medq.co.zw/api/v1/auth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials curl -s https://rr-api.medq.co.zw/api/v1/auth/token \
-d grant_type=client_credentials \
-d client_id="$CLIENT_ID" \
--data-urlencode client_secret="$CLIENT_SECRET" A successful response:
{
"access_token": "<jwt>",
"token_type": "Bearer",
"expires_in": 900,
"scope": "attendance:write lms:sync webhooks:manage"
} -
expires_inis in seconds and defaults to 900 (15 minutes). There is no refresh token: request a new access token when it expires. scopelists the scopes granted to the client; ascopeparameter in the request is ignored.- The token is bound to the client's own tenant. Cross-tenant access is refused.
- Each issuance is written to the tamper-evident audit ledger.
Use it as a bearer token:
curl -s https://rr-api.medq.co.zw/api/v1/webhooks \
-H "Authorization: Bearer $ACCESS_TOKEN" POST /api/v1/oauth/token also exists but is restricted to a signed-in super
administrator (it backs the in-app API console) and returns 401 AUTHENTICATION_REQUIRED
to integrations. Material published before 29 October 2026 (an older OpenAPI document or Postman
collection, the earlier marketing docs page) may still point there; use /api/v1/auth/token.
What a token can call#
A machine token is confined to the routes below, plus /auth/session and
/auth/logout. Anything else returns 403 MACHINE_SCOPE_VIOLATION; a route
outside the token's scopes returns 403 INSUFFICIENT_SCOPE naming the missing scope. This
table is generated from the access policy in the running code.
| Method | Path | Scope required |
|---|---|---|
| GET | /api/v1/integrations/lms/connectors | integrations:read |
| PUT | /api/v1/integrations/lms/connectors/:id | lms:sync |
| GET | /api/v1/integrations/lms/modules/:provider | integrations:read |
| POST | /api/v1/integrations/lms/push-attendance | lms:sync |
| POST | /api/v1/integrations/lms/sync-roster | lms:sync |
| POST | /api/v1/integrations/lms/test-connection | lms:sync |
| GET | /api/v1/integrations/status | integrations:read |
| POST | /api/v1/sync/batches | attendance:write |
| GET | /api/v1/webhooks | webhooks:manage |
| POST | /api/v1/webhooks | webhooks:manage |
| DELETE | /api/v1/webhooks/:id | webhooks:manage |
| POST | /api/v1/webhooks/:id/test | webhooks:manage |
| GET | /api/v1/webhooks/dead-letters | webhooks:manage |
| GET | /api/v1/webhooks/deliveries | webhooks:manage |
| POST | /api/v1/webhooks/deliveries/:id/retry | webhooks:manage |
| POST | /api/v1/webhooks/simulate-event | webhooks:manage |
Request and response schemas for these routes are not yet part of the OpenAPI document.
Scopes#
The scope registry lists every scope a client can be offered, and creating a client with any name
outside it is rejected (400). Only some scopes are enforced by a
machine-accessible route today; the rest are reserved names and grant nothing yet.
| Scope | Meaning | Status |
|---|---|---|
attendance:read | Query verified daily clock-in/out records, sealed session manifests, and attendance percentages. | Reserved — no machine route requires it yet |
attendance:write | Submit biometric clock-in batches and offline attendance logs from approved edge hardware. | Enforced on 1 route |
audit:read | Inspect cryptographic SHA-256 ledger entries and verify ECTA Section 15 evidence seals. | Reserved — no machine route requires it yet |
integrations:read | Read integration health and LMS connector configuration and qualification modules. | Enforced on 3 routes |
integrations:write | Reserved: modify integration settings. No machine-accessible route requires it yet. | Reserved — no machine route requires it yet |
learners:read | Query enrolled learners, demographics, South African ID status, and POPIA consent flags. | Reserved — no machine route requires it yet |
learners:write | Register candidates, upload preflight CSVs, and update banking/demographic attributes. | Reserved — no machine route requires it yet |
lms:sync | Ingest class rosters and push verified attendance hours into Yiba, Wemasoft, or Moodle. | Enforced on 4 routes |
payouts:initiate | Trigger instant disbursements through Netcash PayShap, Paystack, or Stitch Open Banking. | Reserved — no machine route requires it yet |
stipends:read | Access SD5/NMW stipend calculations, deduction breakdowns, and locked disbursement runs. | Reserved — no machine route requires it yet |
webhooks:manage | Register, edit, and test HTTPS event callback subscriptions and examine delivery logs. | Enforced on 8 routes |
Token errors#
| Status | error | Cause |
|---|---|---|
| 400 | unsupported_grant_type | grant_type is present but is not client_credentials. |
| 401 | invalid_client | Unknown client, wrong secret, or missing credentials. The response never says which. |
Administrator sessions#
People sign in to the web application and receive an httpOnly session cookie. Mutating
requests made with that cookie must be sent as application/json (or with
X-Requested-With: XMLHttpRequest); otherwise the API answers
403 CSRF_PROTECTION. Sessions are for the console and for the administrator endpoints in the
mobile reference (pairing codes, device revocation); do not use them
for unattended integrations.