code field. Build your error handling logic around the code, not the message.
Error Envelope
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": { ... },
"requestId": "a1b2c3d4-..."
}
}
Complete Error Reference
Authentication & Authorization Errors
| HTTP Status | Code | Cause | How to Fix |
|---|---|---|---|
401 | UNAUTHORIZED | API key is missing, invalid, expired, or the associated partner account is suspended | Verify your x-api-key header is set correctly; contact support if the key is valid but still rejected |
403 | FORBIDDEN | Valid API key but missing the required permission for this endpoint | Request the missing permission from the CleanLife team |
403 | INVALID_PARTNER_DOMAIN | The Origin or Referer header on your request does not match any registered domain for this partner | Register your domain with CleanLife or remove the Origin header for server-to-server calls |
403 | SANDBOX_DOMAIN_NOT_ALLOWED | Your request came from a domain registered as a sandbox domain; sandbox access is currently disabled | Contact CleanLife to register a production domain |
Validation Errors
| HTTP Status | Code | Cause | How to Fix |
|---|---|---|---|
400 | VALIDATION_ERROR | One or more request fields failed validation (wrong type, missing required field, out-of-range value, invalid UUID, etc.) | Read the details field for a breakdown of which fields failed and why |
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "serviceId must be a UUID",
"details": {
"message": ["serviceId must be a UUID"],
"statusCode": 400,
"error": "Bad Request"
},
"requestId": "..."
}
}
| Field | Validation Rule | Error |
|---|---|---|
serviceId | Must be a valid UUID v4; required | serviceId must be a UUID |
addressId | Required on GET /partners/services; must be a valid UUID v4 | addressId must be a UUID |
contactId | Must be a valid UUID v4; required on bookings | contactId must be a UUID |
phone | Must be a valid Saudi phone number (contacts endpoint) | phone must be a valid phone number |
date | Must be an ISO 8601 date string | date must be a valid ISO 8601 date string |
timeslot.startAt | Must be a string | timeslot.startAt must be a string |
limit (pagination) | Must be integer 1–100 | limit must not be greater than 100 |
Resource Errors
| HTTP Status | Code | Cause | How to Fix |
|---|---|---|---|
404 | RESOURCE_NOT_FOUND | Booking, contact, address, or service not found — either does not exist or belongs to a different partner | Verify the ID and that the resource was created by your partner account |
404 | SERVICE_NOT_ALLOWED | The serviceId is not enabled for your partner account or not available at the address | Use a serviceId from GET /partners/services?addressId=... |
Booking Business Rule Errors
| HTTP Status | Code | Cause | How to Fix |
|---|---|---|---|
409 | DUPLICATE_EXTERNAL_REFERENCE | You attempted to create a booking with an externalReference that already exists for your partner account | Use a unique externalReference value per booking |
422 | BOOKING_CREATION_FAILED | The booking could not be completed — typically a payment or validation failure | Review your booking request fields; retry once; if persistent, contact support |
422 | BOOKING_ALREADY_CANCELLED | Attempted to cancel a booking that is already cancelled | No action needed; the booking is already cancelled |
422 | BOOKING_NOT_PAYABLE | Attempted to confirm payment for a cancelled or failed booking | Payment can only be confirmed for active bookings |
422 | PAYMENT_RESPONSIBILITY_MISMATCH | Attempted to call confirm-payment but your partner account has paymentResponsibility = CLEANOS | Only PARTNER responsibility accounts can confirm payments |
422 | PAYMENT_ALREADY_CONFIRMED | Attempted to confirm payment for a booking whose payment was already confirmed | Payment confirmation is idempotent in intent but rejected as a duplicate; no action needed |
Rate Limit Errors
| HTTP Status | Code | Cause | How to Fix |
|---|---|---|---|
429 | RATE_LIMIT_EXCEEDED | Exceeded your API key’s request limit within the current 60-second sliding window | Wait for the window to reset and retry; implement exponential backoff |
Server Errors
| HTTP Status | Code | Cause | How to Fix |
|---|---|---|---|
500 | INTERNAL_ERROR | Unexpected server-side error | Retry with exponential backoff; if persistent, contact support with the requestId from the response |
Using requestId for Support
Every response (including errors) includes a requestId in the error envelope and an X-Request-Id response header. If you contact CleanLife support about an unexpected error, always include this value — it allows the engineering team to trace the exact request through server logs.
# See the request ID in the response header
curl -i -X GET "https://apiv3.thecleanlife.dev/v1/partners/pricing/tiers" \
-H "x-api-key: YOUR_KEY"
# → X-Request-Id: a1b2c3d4-0000-0000-0000-000000000001
Retry Strategy
| Error Code | Retryable? | Notes |
|---|---|---|
VALIDATION_ERROR | No | Fix the request payload before retrying |
UNAUTHORIZED | No | Fix the API key before retrying |
FORBIDDEN | No | Request the missing permission |
RESOURCE_NOT_FOUND | No | The resource does not exist |
DUPLICATE_EXTERNAL_REFERENCE | No | Use a different externalReference |
BOOKING_CREATION_FAILED | Yes | Retry once with the same or corrected payload |
RATE_LIMIT_EXCEEDED | Yes | Back off and retry after the 60-second window |
INTERNAL_ERROR | Yes | Retry with exponential backoff (see Best Practices) |