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 string
  • issues - Present on validation errors (400); array of Zod issue objects
  • details - Present on some errors; supplementary context

HTTP Status Codes

CodeNameCommon Cause
200OKSuccessful GET or PATCH
201CreatedSuccessful POST (resource created)
400Bad RequestMissing required field; invalid value; failed Zod schema
401UnauthenticatedNo Authorization header; token expired; token invalid
403ForbiddenValid token but insufficient role or out-of-scope request
404Not FoundResource with the given ID does not exist
405Method Not AllowedEndpoint exists but does not support this HTTP method
500Internal Server ErrorUnhandled exception; see API logs
503Service UnavailableFirestore 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:

CodeMeaning
invalid_typeWrong data type (e.g., string instead of number)
too_smallValue or string below minimum
too_bigValue or string above maximum
invalid_enum_valueValue not in the allowed enum
invalid_stringString 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.