Error Response Format
All errors return a JSON body alongside the HTTP status code:
{
"error": "A human-readable description of what went wrong",
"issues": [
{
"code": "too_small",
"message": "Number must be greater than 0",
"path": ["amount"]
}
],
"details": "Additional context string (optional)"
}
error- Always present; a description stringissues- Present on validation errors (400); array of Zod issue objectsdetails- Present on some errors; supplementary context
HTTP Status Codes
| Code | Name | Common Cause |
|---|---|---|
200 | OK | Successful GET or PATCH |
201 | Created | Successful POST (resource created) |
400 | Bad Request | Missing required field; invalid value; failed Zod schema |
401 | Unauthenticated | No Authorization header; token expired; token invalid |
403 | Forbidden | Valid token but insufficient role or out-of-scope request |
404 | Not Found | Resource with the given ID does not exist |
405 | Method Not Allowed | Endpoint exists but does not support this HTTP method |
500 | Internal Server Error | Unhandled exception; see API logs |
503 | Service Unavailable | Firestore or BigQuery temporarily unavailable |
Validation Errors (400)
Validation is performed using Zod schemas. A validation error returns HTTP 400 with an issues array:
{
"error": "Validation failed",
"issues": [
{
"code": "invalid_type",
"expected": "number",
"received": "string",
"path": ["amount"],
"message": "Expected number, received string"
},
{
"code": "too_small",
"minimum": 1,
"type": "number",
"path": ["value"],
"message": "Number must be greater than or equal to 1"
}
]
}
Common Zod error codes:
| Code | Meaning |
|---|---|
invalid_type | Wrong data type (e.g., string instead of number) |
too_small | Value or string below minimum |
too_big | Value or string above maximum |
invalid_enum_value | Value not in the allowed enum |
invalid_string | String format invalid (email, URL, regex) |
Common Error Scenarios
401 - Token Expired
{ "error": "Token has expired" }
Fix: Call user.getIdToken(true) to force a refresh and retry with the new token.
403 - Wrong Scope
{ "error": "Access denied. Your account does not have access to this property." }
Fix: Verify the authenticated user has the required role (Manager or PropertyAdmin) at the requested propertyId or organizationId.
404 - Employee Not Found
{ "error": "Employee not found", "details": "No employee record with ID emp_xxxxxxx" }
Fix: Check the employeeId is correct and the environment parameter matches where the employee was created.
400 - Tip Pool Percentages Invalid
{
"error": "Tip pool distribution percentages must sum to 100",
"issues": [{ "path": ["tipPoolConfig", "distribution"], "message": "Percentages sum to 85, not 100" }]
}
Fix: Ensure all percentage values in tipPoolConfig.distribution sum to exactly 100 (±0.01 tolerance).
503 - Analytics Unavailable
{ "error": "Analytics service temporarily unavailable", "details": "BigQuery query failed; falling back to Firestore" }
Fix: Retry in a few seconds. The API automatically falls back to Firestore for most analytics queries.
Rate Limiting
The Aplauso API does not implement application-level rate limiting. Requests are subject to Google Cloud Functions default quotas:
- Up to 540 concurrent function executions
- Standard GCP request rate limits apply at high volume
If you are making very high-volume requests (e.g., bulk data import), contact Aplauso to discuss options.
Retry Strategy
For transient errors (500, 503), use exponential backoff:
async function fetchWithRetry(url, options, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url, options);
if (res.status === 503 && i < retries - 1) {
await new Promise(r => setTimeout(r, Math.pow(2, i) * 500));
continue;
}
return res;
} catch (e) {
if (i === retries - 1) throw e;
await new Promise(r => setTimeout(r, Math.pow(2, i) * 500));
}
}
}
Do not retry 400, 401, 403, or 404 errors - these require a code or data fix, not a retry.