Developers

Errors

Every error is JSON with a stable machine-readable code and an HTTP status you can branch on. Codes never change meaning within v1. New codes may be added over time, so treat a code you do not recognize as a generic failure for its status.

The two envelope shapes

Where an error comes from decides its shape. Errors raised before your request reaches an endpoint (authentication, the plan entitlement gate, the rate limiter) use the transport envelope; errors raised by the endpoint itself use standard RFC 7807 problem details. Both are served as application/problem+json.

Transport errors

Returned by API key checks, the entitlement gate, and the rate limiter. The code is in the error field; detail mirrors message.

{
  "error": "invalid_api_key",
  "title": "Invalid API key",
  "message": "The API key is invalid or has been revoked.",
  "detail": "The API key is invalid or has been revoked.",
  "status": 401
}

Covers missing_api_key, invalid_api_key, api_access_required, and rate_limited.

Endpoint errors

Standard RFC 7807 problem details from the endpoint itself. Where the endpoint sets a code, it is carried in the title field.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "unknown_user",
  "status": 404,
  "detail": "No employee was found for external id 'E-9999'.",
  "traceId": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00"
}

Requests that fail input validation (for example a blank firstName on an employee update) return this same shape with an errors dictionary keyed by field name and no code in title.

Error codes

CodeStatusEnvelopeMeaning
missing_api_key401Transport No key on the request. Send it via Authorization: Bearer or X-Api-Key.
invalid_api_key401Transport The key does not exist or has been revoked.
api_access_required402Transport The key is valid but the plan does not include API access.
invalid_return_url400Endpoint The returnUrl passed to /today is not on the tenant's exact-match allowlist.
unknown_user404Endpoint No employee matches the external id in the URL.
duplicate_external_id409Endpoint POST /employees with an external id that already exists.
seat_cap_reached409Endpoint POST /employees would exceed the plan's employee limit.
rate_limited429Transport Too many requests in the current one minute window. Honor the Retry-After header.

Sample bodies

missing_api_key 401

{
  "error": "missing_api_key",
  "title": "Missing API key",
  "message": "Provide your key via 'Authorization: Bearer <key>' or the 'X-Api-Key' header.",
  "detail": "Provide your key via 'Authorization: Bearer <key>' or the 'X-Api-Key' header.",
  "status": 401
}

invalid_api_key 401

{
  "error": "invalid_api_key",
  "title": "Invalid API key",
  "message": "The API key is invalid or has been revoked.",
  "detail": "The API key is invalid or has been revoked.",
  "status": 401
}

api_access_required 402

{
  "error": "api_access_required",
  "title": "API access required",
  "message": "This tenant's plan does not include API access. Add the API access option or start a trial.",
  "detail": "This tenant's plan does not include API access. Add the API access option or start a trial.",
  "status": 402
}

invalid_return_url 400

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "invalid_return_url",
  "status": 400,
  "detail": "The returnUrl is not in this tenant's allowlist.",
  "traceId": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00"
}

unknown_user 404

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "unknown_user",
  "status": 404,
  "detail": "No employee was found for external id 'E-9999'.",
  "traceId": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00"
}

duplicate_external_id 409

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10",
  "title": "duplicate_external_id",
  "status": 409,
  "detail": "An employee already exists for external id 'E-1001'.",
  "traceId": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00"
}

seat_cap_reached 409

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10",
  "title": "seat_cap_reached",
  "status": 409,
  "detail": "Employee limit reached (25). Add seats in Billing to raise the cap.",
  "traceId": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00"
}

rate_limited 429

# Response headers
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/problem+json

{
  "error": "rate_limited",
  "title": "Too many requests",
  "message": "Rate limit exceeded. Wait for the current one minute window to reset, then retry.",
  "detail": "Rate limit exceeded. Wait for the current one minute window to reset, then retry.",
  "status": 429
}

Rate limits

Limits use fixed one minute windows with no request queueing. When a window is full, every further request in that window gets 429 rate_limited with Retry-After: 60.

CallerLimitCounted per
Sandbox key (tbm_test_)60 requests per minuteCaller IP (the key is shared)
Live keys (tbm_live_)300 requests per minuteKey
No key (health, OpenAPI)30 requests per minuteCaller IP

Handling errors well

  • 401 and 402 are configuration problems, not transient faults. Alert an operator instead of retrying.
  • 404 unknown_user usually means your roster sync has not created the employee yet. Create it, then retry.
  • 409 duplicate_external_id on create means the employee already exists. Treat it as success and switch to PUT if you need to update.
  • 429 resolves itself. Wait the Retry-After seconds, then continue; do not tight-loop.
  • 5xx is ours. Retry with backoff, and remember our stance: your systems own the policy for what happens while we are down.