Troubleshooting
401 INVALID_API_KEY immediately after generating or rotating a key
Cause: Auth results are cached in-process inside each Lambda container with a hard TTL of 120 seconds. After you generate a new key or the previous key is revoked, in-flight Lambda containers may still hold the old cached result.
Fix: Wait up to 2 minutes, then retry. The cache is per-container; under load you may hit multiple containers, so a brief burst of 401s followed by successful requests is normal during key rotation.
401 after a valid key is revoked — requests still succeeding
Cause: Same in-process cache. Soft revalidation happens every 30 seconds; hard expiry at 120 seconds.
Fix: A revoked key will stop working within 120 seconds across all containers. If you need immediate revocation (security incident), contact Apploye support to force a Lambda redeploy, which clears all container caches.
429 RATE_LIMIT_EXCEEDED — what to do
- Read the
Retry-Afterheader. Do not retry before that many seconds have elapsed. - The window is fixed (60 seconds by default), so
Retry-Aftertells you exactly when the counter resets. - If you are consistently hitting the limit, consider batching requests or reducing polling frequency. Contact Apploye to request a per-org rate limit increase.
429 AUTH_FAILURE_THROTTLE — blocked despite sending a valid key
Cause: Your IP address sent 10 or more failed auth requests within 60 seconds. The per-IP throttle blocks all requests from that IP, including ones with valid keys, until the failure window resets.
Fix: Wait for the Retry-After seconds to elapse, then retry with a valid key. A successful auth resets the failure counter.
Prevention: Don't loop rapidly with a wrong key (e.g., during debugging with a placeholder value).
503 SERVICE_UNAVAILABLE — retrying safely
Cause: The key was authenticated, but Redis quota accounting did not complete. The API fails closed rather than accepting an uncounted request.
Fix: Honor Retry-After: 5, add jitter, and retry. If the response persists, contact Apploye with request_id; do not rotate a valid key.
Empty results when data exists in the Apploye dashboard
Check 1 — Date range and timezone. The API uses the organization timezone by default. If your org is in Asia/Dhaka (+06:00) and you query start_date=2025-01-15&end_date=2025-01-15 without specifying timezone, the range is 2025-01-15 00:00:00 +06:00 to 2025-01-15 23:59:59 +06:00. Data recorded in UTC may fall in a different calendar day.
To align exactly with what Apploye shows, pass timezone=<org-timezone> explicitly, or call GET /v1/organizations/ first to retrieve it.
Check 2 — Filter combinations. On time_activity_report and manual_timesheet_reports, passing both user_id and team_id returns 400. On other report endpoints, the filters combine with AND semantics — passing a project_id the user is not assigned to returns empty results.
Check 3 — Member status. GET /v1/members/ returns only active members. If you used a member list to populate a user_id filter and the user was deactivated between calls, the report will be empty.
Check 4 — Zero UUID sentinel. If your code constructs the UUID from a nullable field and accidentally sends 00000000-0000-0000-0000-000000000000, it is treated as "no filter" — not as a specific user.
project_id filter returns 400 on report endpoints
Cause: Report endpoints validate project_id against your organization before issuing the Timetrack query. If the UUID belongs to a different org or does not exist, the API rejects it with VALIDATION_ERROR instead of returning empty results.
Fix: Use project IDs obtained from GET /v1/projects/ under the same API key.
team_id returns 404 on some endpoints but 200 on clockinout
This is intentional — see Filters guide. The clockinout endpoint builds a full member-eligibility roster before filtering, so an empty team produces an empty (but valid) result set. The other report endpoints resolve team membership at a different stage where "team not found" is an error condition.
Timesheet cap error — too many timesheets for this user
GET /v1/timesheet_idle_times/ with user_id returns 400 VALIDATION_ERROR if the user has more than 1,000 timesheets in the requested date range.
Fix: Narrow the date range. Instead of 31 days, try 7-day windows.
Without user_id (fetching all users), the cap is not applied per-page — but each user's timesheets are counted individually, so a very active user on a team page could still trigger it.
GET /v1/screenshots/ returns 404 for a valid user
Check 1 — Lookback limit. The maximum lookback is 179 days. If date is older than 179 days ago, the API returns 404.
Check 2 — User membership. The user must belong to your organization. If the user was removed or their membership was revoked, 404 is returned.
Check 3 — Date format. The date parameter must be YYYY-MM-DD. Other formats return 400.
Screenshot images are null
Cause: The screenshot was captured but not successfully uploaded to S3 (network error at capture time, or the user's desktop client discarded it due to privacy settings).
Cause: AWS_S3_CUSTOM_DOMAIN environment variable is not set on the Lambda, so the URL cannot be constructed.
The screen_count field reflects how many screenshots were captured; null image and thumbnail mean the file is not available.
payment_settings returns a bare array, not a paginated envelope
This is by design. Payment settings are always returned as a complete list — every historical rate period. There is no pagination. The response is a JSON array, not the { count, next, previous, results } object returned by other endpoints.
Pagination links point to the wrong host
Cause: The PUBLIC_API_BASE_URL environment variable is not set or is set to an incorrect value on the Lambda.
Fix: Set PUBLIC_API_BASE_URL=https://api.apploye.com in the Lambda environment. The default is the same value but the variable may be overridden to a wrong URL in some deployments.
cursor pagination — next page returns duplicates or misses records
Cause: The cursor encodes the user position at page time. If records are inserted or deleted between page requests, the cursor may not include or may re-include records at the boundary.
Recommendation: For full syncs, complete the entire cursor walk in one pass. For incremental syncs, use start_date/end_date windows (e.g. the previous 7 days) and deduplicate by id rather than relying on cursor-based continuity.
Partner sync returns empty after updated_since
Check 1 — Watermark timezone. updated_since is compared against updated_at stored in UTC. Pass an ISO 8601 UTC timestamp (e.g. 2026-01-15T10:00:00Z). Local offsets are accepted but must be unambiguous.
Check 2 — Page drift. Offset pagination with updated_since can skip or duplicate rows if data changes while you walk pages. Upsert by id and advance your watermark only after a full pass completes.
Check 3 — Nested vs flat routes. GET /v1/members/{user_id}/projects/ and GET /v1/projects/{project_id}/tasks/ apply membership scoping and may return empty even when org-wide flat lists (GET /v1/tasks/, GET /v1/project_memberships/) have rows. Use flat lists for polling; use nested routes for drill-down.
Support correlation. Every response includes request_id in error bodies. Successful requests log the API Gateway request_id on request_complete. In CloudWatch (/aws/lambda/public-api-project), search:
fields @timestamp, event, route_name, result_count, has_updated_since, page, duration_ms
| filter request_id = "<id-from-error-or-api-gateway>"
| sort @timestamp ascIf result_count is 0 with has_updated_since = true, the query ran successfully but nothing matched the watermark — not a permissions error. result_count is the envelope count (total matches), not the number of rows on the current page.
How to get the organization timezone
GET /v1/organizations/The response includes "timezone": "America/New_York" (IANA string). Pass this value to timezone on all subsequent date-range queries to ensure alignment with what Apploye displays.
