Core Concepts
Errors
Every error response shares one shape, regardless of endpoint.
{
"error": {
"message": "Human-readable description",
"code": "MACHINE_READABLE_CODE",
"details": { }
}
}
details is only present on some validation errors (for example, OTP verification includes attemptsRemaining). Success responses are always shaped as { "data": ... }, optionally with a sibling meta key.
Status codes
| HTTP status | Code | Meaning |
|---|---|---|
| 401 | UNAUTHENTICATED | Missing, malformed, or invalid credentials — bearer token or application credential triple |
| 403 | FORBIDDEN | Credentials are valid but not permitted for this action, e.g. a suspended application |
| 404 | NOT_FOUND | Resource doesn't exist, or exists but isn't owned by the caller |
| 409 | CONFLICT | The request can't be completed given current state — e.g. deleting a project that still has applications |
| 422 | VALIDATION_ERROR | Request body or parameters failed validation |
| 429 | RATE_LIMITED | See Rate Limits |
| 500 | INTERNAL_ERROR | Unhandled server-side failure — safe to retry, worth reporting if persistent |
| 501 | NOT_IMPLEMENTED | Reserved for endpoints not yet built |
Why 404 instead of 403 for ownership
Every console-scoped resource lookup (a project, application, webhook, or OTP request that belongs to someone else) returns 404 rather than 403. This is deliberate: a 403 confirms the resource exists, which is itself information an attacker enumerating IDs shouldn't get for free.
Common causes
| Symptom | Likely cause |
|---|---|
401 on /v1/otp/send | Client ID, Client Secret, and API Key must all belong to the same application — check for a copy-paste mismatch across environments (test vs live) |
422 on /v1/otp/send | Phone number doesn't match the expected virtual format, e.g. +999 482 918 102 |
422 on /v1/otp/verify | OTP has expired (5-minute TTL) or the code is wrong — check attemptsRemaining in the response |
409 on deleting a project | The project still has applications — delete or move them first |