Errors
Most errors are returned as RFC 7807
application/problem+json:
{
"type": "urn:smartepi:error:invalid_reference",
"title": "Invalid reference",
"status": 422,
"detail": "Unknown or cross-tenant reference: costCenterId"
}
The type is a stable URN identifier for the error class (urn:smartepi:error:<token>),
not a link to fetch — branch on it programmatically; do not expect it to resolve.
Application errors vs. gateway rejections
Two layers can reject a request, and they look different:
- Application errors (
400,404,405,409,422,500) come from the API itself. They are RFC 7807application/problem+json(as above) and carry anX-Request-Idheader — include it when reporting an issue. - Gateway rejections (
401,403,429) are usually produced before the application runs. They are not RFC 7807 and do not carryX-Request-Id, and their body shape differs by case:401and429are a plain lowercase{"message":"…"}, while a gateway403(rejected key or a blocked IP on a cold/cache-miss request) returns the raw AWS authorizer-deny body{"Message":"User is not authorized to access this resource with an explicit deny in an identity-based policy"}— note the capitalMessageand that this exact text is an AWS string that may change. - One exception — a warm-path
403: when a key has an IP allow-list and the authorizer result is cached (a warm request), the application re-checks the source IP itself and, if it is outside the allow-list, returns an RFC 7807{"type":"urn:smartepi:error:forbidden","title":"Forbidden","status":403,"detail":"source IP not allowed"}with anX-Request-Id. So a403can arrive in either shape depending on cache state.
Branch on the HTTP status code, never on the body text. A missing key (401), a
bad/inactive key or blocked IP (403, in either shape above), and a rate-limit (429)
will not reliably look like the problem+json shape above — branch on the HTTP status.
Status codes
| Status | Meaning | What to do |
|---|---|---|
400 | Malformed request (bad JSON, invalid field, an invalid cursor), or a missing If-Match/_version on update/delete. | Fix the request body/params; only ever send back a cursor you received; send If-Match on writes. |
401 | No API key was sent (missing Authorization header). Body: {"message":"Unauthorized"}. | Send the Authorization: Bearer sk_… header. |
403 | A key was sent but was rejected: the key is invalid / unknown / revoked / expired, OR its IP allow-list blocked the source IP, OR the key was just created and has not finished activating (see Authentication). Body is usually the raw AWS authorizer-deny JSON {"Message":"User is not authorized…"} (capital Message); a blocked IP on a warm (cache-hit) request instead returns RFC 7807 {"type":"urn:smartepi:error:forbidden",…} with X-Request-Id. Branch on the status, not the text. | Check the key is valid and active; if it is brand-new, wait ~1 minute and retry; if you use an IP allow-list, call from an allowed IP. |
404 | Not found, or not yours to access. | The id does not exist for you. |
405 | Method not allowed on this resource. | e.g. writing a read-only resource, or get-by-id on a list-only transaction. |
409 | Version conflict (stale _version, type: urn:smartepi:error:conflict); a duplicate entity name within your tenant (type: urn:smartepi:error:duplicate_name); a duplicate employee identification or email (type: urn:smartepi:error:duplicate_identification / urn:smartepi:error:duplicate_email); or a delete blocked because the entity is still referenced (type: urn:smartepi:error:resource_in_use). | Re-read then retry for conflicts (see Concurrency); use a different name/identification/email for duplicates; re-point or remove the dependents before deleting. |
422 | Foreign-key validation failed (type: urn:smartepi:error:invalid_reference). | A referenced id is missing or not yours. |
429 | Rate limit exceeded (metered per organization). Body: {"message":"Too Many Requests"} (gateway-shaped, lowercase message, with x-amzn-errortype: TooManyRequestsException). | Back off and retry with your own exponential backoff. A Retry-After header is not guaranteed (the burst-rate throttle does not send one), so do not depend on it. Branch on the 429 status, not the body text. |
500 | Server error. | Retry with backoff; report the X-Request-Id. |
Notes on 404 vs 403
A 404 is returned both when a record genuinely does not exist and when it
isn't accessible to your API key — the API never reveals the existence of data
outside your organization. Do not treat 404 as "definitely deleted".
422 — foreign-key validation
On a write, every foreign key you set (e.g. costCenterId, sectorId,
jobRoleId, groupId, a group rule's productId) must reference a record that
belongs to your organization. If any does not, the whole write is rejected
with 422 and a detail naming the offending field — nothing is persisted.