Errors
Ugrat uses conventional HTTP response codes to indicate the success or failure of a request. Error responses share the same envelope:
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "rate limit exceeded",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
request_id identifies the request in our logs — include it when contacting support. Validation errors additionally carry a fields array with a stable machine-readable code per field (and optional meta, e.g. the maximum length):
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "request validation failed",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"fields": [
{ "field": "recipient", "code": "invalid_phone" },
{ "field": "body", "code": "too_long", "meta": { "max": 800 } }
]
}
}
| Code | Meaning |
|---|---|
| 400 BAD_REQUEST | Malformed JSON, unknown field, or an invalid path parameter |
| 400 VALIDATION_FAILED | One or more fields failed validation — see fields |
| 401 UNAUTHORIZED | API key missing, invalid, or deactivated |
| 403 ACCOUNT_PENDING | The account has not been approved yet |
| 403 ACCOUNT_BLOCKED | The account is blocked |
| 403 MESSAGING_NOT_ENABLED | Free-text messaging is not enabled for this account (OTP still works) |
| 403 FORBIDDEN | The key is valid but lacks permission for this operation |
| 404 NOT_FOUND | Resource does not exist or is not visible to the caller |
| 409 CONFLICT | The request collides with existing data — e.g. reusing a reference_id you already used |
| 422 UNPROCESSABLE_ENTITY | The request is well-formed but cannot be completed — e.g. an OTP code that is wrong, expired, or out of attempts |
| 429 RATE_LIMITED | Rate limit exceeded — retry after the Retry-After seconds |
| 500 INTERNAL_ERROR | Unexpected error on our side — retry, and report the request_id if it persists |
| 503 SERVICE_UNAVAILABLE | No delivery route is available right now, or the service is briefly unavailable — retry after the Retry-After seconds when present |