Error Codes
All errors return a JSON body with a consistent envelope:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "start_date and end_date are required.",
"request_id": "a3f2c1d0-1234-5678-abcd-ef0123456789"
}
}error.code is a closed enum in the OpenAPI `ErrorBody` (VALIDATION_ERROR, INVALID_API_KEY, RESOURCE_NOT_FOUND, ROUTE_NOT_FOUND, METHOD_NOT_ALLOWED, RATE_LIMIT_EXCEEDED, AUTH_FAILURE_THROTTLE, SERVICE_UNAVAILABLE, INTERNAL_ERROR). Code generators should map these literals; treat any other value as a forward-compatible extension.
For rate-limit errors, retry_after_seconds is also present:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded.",
"request_id": "...",
"retry_after_seconds": 42
}
}For validation errors, details may be present with field-level context. Canonical shape: each field key maps to a string array (even when there is only one message):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "limit cannot exceed 20.",
"request_id": "...",
"details": {
"limit": ["limit cannot exceed 20."]
}
}
}Some cap-exceeded errors add integer metadata keys alongside field errors:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "User has 1001 timesheets in range; maximum is 1000. Narrow start_date/end_date.",
"request_id": "...",
"details": {
"timesheets_in_range": 1001,
"timesheets_cap": 1000
}
}
}Documented metadata keys: timesheets_in_range, timesheets_cap, log_count, log_cap. Most validation errors omit details entirely — read message first.
See validation-details section below.
request_id is always present and is unique per invocation. Include it when reporting issues.
Error Code Reference
INVALID_API_KEY — 401 Unauthorized
The X-APPLOYE-API-KEY header is missing, malformed, or references a revoked API key.
Common causes:
- Header was not sent.
- The API key was rotated or revoked. Keys are cached in-process for up to 120 seconds; wait up to 2 minutes after revocation before testing again.
- The key belongs to a different organization than the one you are querying.
Response headers: None specific to auth errors.
RATE_LIMIT_EXCEEDED — 429 Too Many Requests
Your organization has exceeded the allowed request rate for this endpoint.
Response headers:
| Header | Value |
|---|---|
Retry-After | Seconds until the rate-limit window resets |
RateLimit-Limit | Current organization/resource limit |
RateLimit-Remaining | 0 |
RateLimit-Reset | Unix timestamp when the window resets |
The global fallback is 30 requests per 60 seconds per organization, HTTP method, and canonical resource group. List and detail routes share a bucket. Overrides may apply.
How to handle: Read the Retry-After header and wait before retrying. Do not retry immediately — the counter does not reset until the window expires.
AUTH_FAILURE_THROTTLE — 429 Too Many Requests
Your IP address has exceeded the authentication-failure threshold (10 failures in 60 seconds). All further requests from this IP are blocked until the failure window resets, even if you supply a valid API key.
Response headers:
| Header | Value |
|---|---|
Retry-After | Seconds until the IP block expires |
This error differs from RATE_LIMIT_EXCEEDED in that it is IP-based, not org-based, and fires before API key validation.
SERVICE_UNAVAILABLE — 503 Service Unavailable
The API authenticated the key but could not complete Redis quota accounting, so it failed closed. The response includes Retry-After: 5; wait, add jitter, and retry.
RESOURCE_NOT_FOUND — 404 Not Found
The requested resource does not exist within your organization. The route was recognised and the handler ran; this is not a mistyped URL.
Common causes and affected endpoints:
| Endpoint | Cause |
|---|---|
GET /v1/clients/{client_id}/ | Client ID does not exist in this org |
GET /v1/clients/{client_id}/projects/ | Client is inactive OR does not exist |
GET /v1/projects/{project_id}/ and sub-resources | Project does not exist in this org |
GET /v1/teams/{team_id}/members/ | Team does not exist in this org |
GET /v1/invoices/{invoice_id}/ | Invoice does not exist in this org |
GET /v1/members/{user_id}/payment_settings/ | User ID does not exist |
GET /v1/members/{user_id}/hourly_payment_logs/ | User ID does not exist |
GET /v1/members/{user_id}/one_time_payment_logs/ | User ID does not exist |
GET /v1/screenshots/ | User not in org, or date beyond 179-day lookback |
GET /v1/tasks/{task_id}/members/ | Task does not exist in this org |
GET /v1/time_activity_reports/ (with team_id) | Team does not exist |
GET /v1/manual_timesheet_reports/ (with team_id) | Team does not exist |
GET /v1/app_reports/ (with team_id) | Team does not exist |
GET /v1/url_reports/ (with team_id) | Team does not exist |
Note: Inactive clients and inactive projects do not return 404 from their primary endpoints (only client ID is checked for existence, not is_active status). See Filters guide.
Note: GET /v1/clockinouts/ with an unknown team_id returns 200 with empty results, not 404. This is intentional — see Filters guide.
ROUTE_NOT_FOUND — 404 Not Found
The URL path is not a valid Partner API endpoint for this Lambda (or API Gateway routed to the wrong function). No business logic ran.
{
"error": {
"code": "ROUTE_NOT_FOUND",
"message": "Route not found.",
"request_id": "..."
}
}Common causes: typo in path, trailing slash omitted or added incorrectly, calling an endpoint that lives on a different Lambda integration.
Do not confuse with `RESOURCE_NOT_FOUND`: if you GET /v1/clients/{client_id}/ with a valid path shape but a UUID that does not exist, you receive RESOURCE_NOT_FOUND with a resource-specific message.
METHOD_NOT_ALLOWED — 405 Method Not Allowed
The Partner API is read-only. Only GET (and OPTIONS for CORS preflight) are supported.
{
"error": {
"code": "METHOD_NOT_ALLOWED",
"message": "Method not allowed.",
"request_id": "..."
}
}Common causes: POST, PUT, PATCH, or DELETE to any /v1/... path.
VALIDATION_ERROR — 400 Bad Request
A request parameter failed validation.
Common messages:
| Parameter | Message |
|---|---|
start_date / end_date missing | "start_date and end_date are required." |
| Invalid date format | "Invalid date format. Use YYYY-MM-DD." |
end_date < start_date | "end_date must be on or after start_date." |
| Range > 31 days | "Date range cannot exceed 31 days." |
| Invalid timezone | "Invalid timezone." or "Invalid timezone. Use a valid IANA timezone string." |
| Invalid UUID | "Invalid UUID format." |
user_id required and missing | "user_id is required." (screenshots) |
date required and missing | "date is required." (screenshots) |
limit > max | "limit cannot exceed 20." (or 100, depending on endpoint) |
limit < 1 | "limit must be >= 1." |
page < 1 | "page must be >= 1." |
user_id + team_id together | "Cannot combine user_id and team_id filters." (report endpoints) |
| Timesheet count exceeds cap | "User has N timesheets in range; maximum is 1000. ..." (see details.timesheets_in_range) |
#### VALIDATION_ERROR details shape
| Kind | JSON shape | Example keys |
|---|---|---|
| Field errors (canonical) | "field": ["message"] | limit, page, start_date, client_id, status, id |
| Payload-cap metadata | "key": integer | timesheets_in_range, timesheets_cap, log_count, log_cap |
| Absent | details omitted | Most validation errors (message is sufficient) |
Handlers normalize scalar strings to one-element arrays at the error boundary. Clients should accept field values as arrays of strings.
INTERNAL_ERROR — 500 Internal Server Error
An unexpected error occurred within the Lambda function.
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred.",
"request_id": "..."
}
}request_id is the most useful field here — include it in any support request. Database errors, unhandled exceptions, and infrastructure outages are all surfaced as INTERNAL_ERROR.
Status Code Summary
| HTTP Status | Error code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid or missing query parameters |
| 401 | INVALID_API_KEY | Missing, revoked, or invalid API key |
| 404 | RESOURCE_NOT_FOUND | Valid route; entity doesn't exist in this organization |
| 404 | ROUTE_NOT_FOUND | Unrecognised path (no handler dispatch) |
| 405 | METHOD_NOT_ALLOWED | Non-GET HTTP method (read-only API) |
| 429 | RATE_LIMIT_EXCEEDED | Per-org per-endpoint rate limit hit |
| 429 | AUTH_FAILURE_THROTTLE | Too many auth failures from this IP |
| 500 | INTERNAL_ERROR | Unexpected server-side error |
| 503 | SERVICE_UNAVAILABLE | Organization quota accounting temporarily unavailable |
