Guide
Errors & rate limits
The tables below are generated from the handlers and the Nginx configuration, so they list what the running system does.
Error format#
{
"success": false,
"error": "INSUFFICIENT_SCOPE",
"message": "This endpoint requires the 'webhooks:manage' OAuth scope."
} erroris a stable, upper-case, machine-readable code: branch on it, not onmessage.- The device API adds a
requestId; quote it when you contact support. - Validation failures on the public forms return
422 VALIDATION_FAILEDwith adetailsarray of human-readable field messages. - The OAuth token endpoint follows RFC 6749 instead:
{ "error": "invalid_client", "error_description": "…" }.
Rate limits#
Limits are applied per client IP address in two layers. Many users behind one office, school or mobile-carrier network share a single address and a single budget; plan retries accordingly.
1. Edge (Nginx), before the request reaches the application
| Applies to | Rate | Burst |
|---|---|---|
/api/v1/mobile/(activation/(start|complete)|auth/refresh) | 10 requests / minute | 10 |
/api/v1/public/(leads|newsletter/subscribe) | 30 requests / minute | 10 |
/api/v1/verify/ | 120 requests / minute | 60 |
Burst is the number of extra requests accepted instantly before the steady rate applies. Everything not listed is unaffected at the edge.
2. Application
| Endpoint | Limit |
|---|---|
POST /api/v1/mobile/activation/complete | 10 per 1 minute |
POST /api/v1/mobile/activation/start | 10 per 1 minute |
POST /api/v1/public/leads | 5 per 10 minutes |
POST /api/v1/public/newsletter/subscribe | 3 per 10 minutes |
GET /api/v1/public/platform | 60 per 1 minute |
GET /api/v1/public/status | 60 per 1 minute |
GET /api/v1/verify/:hash | 60 per 1 minute |
Reading a 429
A 429 from the application is JSON (RATE_LIMITED) and carries
Retry-After, plus X-RateLimit-Limit, -Remaining and
-Reset on the endpoints that report them. A 429 from the edge
is Nginx's default HTML page with no Retry-After. Treat the status code
as authoritative, and back off with jitter (start at a few seconds, double up to a minute) when no
Retry-After is present.
API error codes#
| Status | Code | Meaning |
|---|---|---|
| 401 | AUTHENTICATION_REQUIRED | A valid RemoteRegister session or bearer token is required. |
| 403 | AUDITOR_READ_ONLY | External auditor (R8) access is strictly read-only. |
| 403 | CROSS_TENANT_ACCESS_DENIED | This session is not authorized for the requested tenant. |
| 403 | CSRF_PROTECTION | Cookie-authenticated mutations must be sent as application/json. |
| 403 | FORBIDDEN_ROLE | This action requires one of: …. |
| 403 | INSUFFICIENT_SCOPE | This endpoint requires the '…' OAuth scope. |
| 403 | LEARNER_SELF_SCOPE_VIOLATION | Learner accounts may only access their own self-service records and notifications. |
| 403 | MACHINE_SCOPE_VIOLATION | Machine tokens may only access integration, webhook and edge-sync endpoints. |
| 429 | RATE_LIMITED | Too many requests. Please try again shortly. |
Device API error codes#
| Status | Code | Meaning |
|---|---|---|
| 400 | CONSENT_REQUIRED | consentRef is required and must reference the recorded consent. |
| 400 | PAYLOAD_TOO_LARGE | A batch must contain 1-500 events. |
| 400 | SIGNATURE_INVALID | The exact request body was not preserved; the signature cannot be verified. |
| 400 | TEMPLATE_FORMAT_UNSUPPORTED | position is not a supported finger. |
| 400 | TEMPLATE_TOO_LARGE | Template must be valid base64 between 64 and 4096 bytes. |
| 400 | VALIDATION_ERROR | clientRequestId is required for this operation. |
| 401 | AUTHENTICATION_REQUIRED | The refresh token is invalid or expired. |
| 401 | NONCE_REPLAYED | X-RR-Nonce has already been used. |
| 401 | SIGNATURE_INVALID | The activation challenge signature did not verify. |
| 401 | TIMESTAMP_SKEW | X-RR-Timestamp must be within 120 seconds of server time (ISO 8601 or epoch seconds/milliseconds). |
| 403 | ASSIGNMENT_REQUIRED | The requested cohort is outside this device assignment. |
| 403 | CONSENT_REQUIRED | The learner has not given POPIA consent for biometric processing. |
| 403 | LEARNER_NOT_ASSIGNED | The learner is not assigned to this device. |
| 403 | PLAY_INTEGRITY_FAILED | Device attestation did not pass. |
| 404 | DEVICE_NOT_FOUND | Activation was not found. |
| 404 | MATCH_NOT_FOUND | No enrolled templates are available in this scope. |
| 404 | PAIRING_CODE_INVALID | The pairing code is not recognised. |
| 409 | ACTIVATION_ALREADY_COMPLETED | This activation is no longer pending. |
| 409 | BATCH_HASH_MISMATCH | previousBatchHash does not match the last accepted batch. |
| 409 | BATCH_OUT_OF_ORDER | deviceSeq must be greater than the last accepted sequence (…). |
| 409 | DUPLICATE_BIOMETRIC | This template is already enrolled to another learner. |
| 409 | PAIRING_CODE_USED | The pairing code has already been used. |
| 410 | ACTIVATION_CHALLENGE_EXPIRED | The activation challenge has expired. Start again with a new pairing code. |
| 410 | PAIRING_CODE_EXPIRED | The pairing code has expired. |
| 410 | PAIRING_CODE_REVOKED | The pairing code has been revoked. |
| 422 | QUALITY_TOO_LOW | NFIQ2 score must be at least …. |
| 501 | EVIDENCE_UPLOAD_NOT_CONFIGURED | Evidence upload is not configured on this deployment. Contact the tenant administrator. |
| 503 | PAIRING_NOT_CONFIGURED | Pairing is not configured on this deployment. |
Some codes appear with more than one status or message depending on which check failed (for example
VALIDATION_ERROR); the message identifies the field.