Errors
The Odento API uses conventional HTTP status codes to indicate success or failure. Every error response follows a consistent JSON structure with a machine-readable error code and a human-readable message.
Error Response Format
All error responses return success: false and an error object containing a code (stable, machine-readable) and a message (human-readable, may change):
{
"success": false,
"error": {
"code": "INVALID_TOKEN",
"message": "The provided access token is invalid or malformed."
}
}Some errors include additional fields. For example, validation errors include a details array with per-field error messages:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "The request body contains invalid fields.",
"details": [
{ "field": "email", "message": "Email is required." },
{ "field": "phone", "message": "Phone must include country code." }
]
}
}HTTP Status Codes
| Status | Meaning |
|---|---|
| 200 OK | Request succeeded |
| 201 Created | Resource created successfully |
| 400 Bad Request | Malformed request (missing body, invalid JSON) |
| 401 Unauthorized | Missing or invalid authentication credentials |
| 403 Forbidden | Authenticated but not authorized for this resource |
| 404 Not Found | Resource does not exist |
| 409 Conflict | Request conflicts with current state (e.g., double booking) |
| 422 Unprocessable Entity | Validation error — request is well-formed but contains invalid data |
| 429 Too Many Requests | Rate limit exceeded |
| 500 Internal Server Error | Something went wrong on our end |
| 502 Bad Gateway | Upstream service unavailable |
| 503 Service Unavailable | Temporarily offline for maintenance |
Error Codes
Error codes are stable identifiers that will not change between API versions. Always use the code field for programmatic error handling — never parse the message field.
| Code | HTTP | Description |
|---|---|---|
| MISSING_API_KEY | 401 | No API key headers were provided. Include X-API-Key-ID and X-API-Key-Secret. |
| INVALID_API_KEY | 401 | The API key ID or secret is incorrect, revoked, or does not exist. |
| INVALID_TOKEN | 401 | The JWT access token is invalid, malformed, or has been revoked. |
| EXPIRED_TOKEN | 401 | The JWT access token has expired. Use the refresh token to obtain a new one. |
| MODULE_NOT_ALLOWED | 403 | The API key does not have access to the requested module. |
| SCOPE_NOT_APPROVED | 403 | The API key has access to the module but not the required scope (e.g., write scope for a POST request). |
| RATE_LIMITED | 429 | Rate limit exceeded. Check X-RateLimit-Reset header for when to retry. |
| QUOTA_EXCEEDED | 403 | Monthly request quota exceeded. Upgrade your plan or wait for quota reset. |
| VALIDATION_ERROR | 422 | Request body or query parameters failed validation. See details array. |
| NOT_FOUND | 404 | The requested resource does not exist or has been deleted. |
| CONFLICT | 409 | The request conflicts with the current state (e.g., scheduling conflict, duplicate email). |
| INTERNAL_ERROR | 500 | An unexpected error occurred on the server. Our team is notified automatically. |
Handling Errors
We recommend structuring your error handling around the error code, not the HTTP status or message. Here's a pattern in JavaScript:
async function apiRequest(url, options) {
const response = await fetch(url, options)
const body = await response.json()
if (!body.success) {
switch (body.error.code) {
case 'EXPIRED_TOKEN':
// Refresh the token and retry
await refreshToken()
return apiRequest(url, options)
case 'RATE_LIMITED':
// Wait and retry — check X-RateLimit-Reset header
const reset = response.headers.get('X-RateLimit-Reset')
const waitMs = (parseInt(reset) - Date.now() / 1000) * 1000
await new Promise(r => setTimeout(r, Math.max(waitMs, 1000)))
return apiRequest(url, options)
case 'INVALID_API_KEY':
throw new Error('API key is invalid or revoked. Check your credentials.')
case 'QUOTA_EXCEEDED':
throw new Error('Monthly quota exceeded. Upgrade your plan.')
case 'VALIDATION_ERROR':
const details = body.error.details.map(d => d.field + ': ' + d.message)
throw new Error('Validation failed: ' + details.join(', '))
default:
throw new Error(body.error.message)
}
}
return body.data
}Handling 401 (Token Expiry)
When using JWT authentication, access tokens expire every 15 minutes. Catch EXPIRED_TOKEN errors, refresh the token, and retry the original request. Implement this as a wrapper around your API client so it's handled automatically.
Handling 429 (Rate Limiting)
When you receive a RATE_LIMITED error, stop sending requests immediately. Check the X-RateLimit-Reset header for the Unix timestamp when your limit resets, and wait until then. Implement exponential backoff with jitter for retries.
Handling 5xx (Server Errors)
Server errors are rare but can happen. Implement retry logic with exponential backoff (1s, 2s, 4s, 8s) for 500, 502, and 503 status codes. Do not retry on 400 level errors — they will fail the same way every time.
Idempotency
For POST and PUT requests that create or modify resources, include an Idempotency-Key header with a unique value (e.g., a UUID). If a request is retried due to a network error, the API will return the result of the original request instead of creating a duplicate.
curl -X POST https://api.odento.app/api/v1/patients \
-H "X-API-Key-ID: ok_live_abc123def456ghi789jkl012" \
-H "X-API-Key-Secret: os_abc123def456ghi789jkl012mno345pqr678stu901vwx234yzab567" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{ "first_name": "Priya", "last_name": "Sharma", "email": "priya@example.com", "phone": "+919876543210" }'Idempotency keys are cached for 24 hours. After that, the same key can be reused and will create a new resource.
Was this page helpful?
Help us improve our documentation