Errors
Every non-2xx response from the TrakRF API returns a JSON body in a consistent error envelope. This page catalogs the error types you'll see and recommends retry behavior for each.
Envelope shape
The envelope follows RFC 7807 (Problem Details for HTTP APIs) with TrakRF-specific additions:
{
"error": {
"type": "validation_error",
"title": "Invalid request",
"status": 400,
"detail": "identifier must be 1-255 characters",
"instance": "/api/v1/assets",
"request_id": "01JXXXXXXXXXXXXXXXXXXXXXXX"
}
}
| Field | Purpose |
|---|---|
type | A machine-readable identifier — your code should branch on this. Extensible enum. |
title | A short human-readable summary safe to log. |
status | The HTTP status code. Always matches the response's status line. |
detail | A longer human-readable explanation. Safe to log; may name the offending field or value. |
instance | The request path that produced the error. Useful when the same error appears across multiple logs. |
request_id | A ULID matching the X-Request-ID response header. Include this when filing support tickets. |
Error type catalog
type | HTTP status | When you'll see it | Retry? |
|---|---|---|---|
validation_error | 400 | Request body failed schema validation (see validation errors below) | No — fix the request |
bad_request | 400 | Malformed request — bad JSON, unknown query param, invalid sort field | No — fix the request |
unauthorized | 401 | Missing, malformed, revoked, or expired API key | No — re-auth |
forbidden | 403 | Valid key but insufficient scope for this endpoint | No — needs a key with the right scope |
not_found | 404 | Natural-key lookup failed | No — check the identifier |
conflict | 409 | Unique-constraint violation (typically a duplicate identifier) | No — reconcile with GET then PUT |
rate_limited | 429 | You've hit the rate limit — see Rate limits | Yes, after Retry-After seconds |
internal_error | 500 | Unhandled server failure | Yes, with exponential backoff |
The type enum is extensible — TrakRF may add new error types in any v1 release. Clients should handle unknown type values gracefully (fall through to a generic error handler based on HTTP status code, which is a closed enum).
Validation errors
When type is validation_error, the envelope carries an additional fields array with one entry per invalid field:
{
"error": {
"type": "validation_error",
"title": "Validation failed",
"status": 400,
"detail": "Request did not pass validation",
"instance": "/api/v1/assets",
"request_id": "01JXXXXXXXXXXXXXXXXXXXXXXX",
"fields": [
{
"field": "identifier",
"code": "too_long",
"message": "identifier must be at most 255 characters"
},
{
"field": "type",
"code": "invalid_value",
"message": "type is not a valid value"
}
]
}
}
Field entries:
| Field | Purpose |
|---|---|
field | The JSON field name of the offending request attribute (e.g. identifier, org_name). Values are the snake_case JSON keys defined by the endpoint's request schema, not Go struct names or JSON-pointer paths. |
code | A machine-readable code — your validation UI can branch on this. Extensible enum. |
message | A human-readable message safe to show the end user. |
Current code values (extensible):
required— the field is missing and mandatoryinvalid_value— the value is not one of the allowed values, fails a format check (email, URL, UUID), or fails a validation TrakRF has not mapped to a more specific codetoo_short— string or collection length below the minimumtoo_long— string or collection length above the maximumtoo_small— numeric value below the minimumtoo_large— numeric value above the maximum
The code enum is extensible — TrakRF may add new validation codes in any v1 release. Treat unknown codes as generic invalid-value errors and surface the message field.
Query-parameter validation errors
List endpoints validate their query string the same way. The field value in the fields array is the query-parameter name, and detail summarizes the first problem. A few you'll see in practice:
{
"error": {
"type": "validation_error",
"title": "Invalid request",
"status": 400,
"detail": "limit must be ≤ 200",
"instance": "/api/v1/assets",
"request_id": "01JXXXXXXXXXXXXXXXXXXXXXXX",
"fields": [
{
"field": "limit",
"code": "too_large",
"message": "limit must be ≤ 200"
}
]
}
}
{
"error": {
"type": "validation_error",
"title": "Invalid request",
"status": 400,
"detail": "unknown sort field: bogus",
"instance": "/api/v1/assets",
"request_id": "01JXXXXXXXXXXXXXXXXXXXXXXX",
"fields": [
{
"field": "sort",
"code": "invalid_value",
"message": "unknown sort field: bogus"
}
]
}
}
{
"error": {
"type": "validation_error",
"title": "Invalid request",
"status": 400,
"detail": "Invalid 'from' timestamp; RFC3339 required",
"instance": "/api/v1/assets/ASSET-0001/history",
"request_id": "01JXXXXXXXXXXXXXXXXXXXXXXX",
"fields": [
{
"field": "from",
"code": "invalid_value",
"message": "Invalid 'from' timestamp; RFC3339 required"
}
]
}
}
The detail and message strings are stable enough to surface to an end user, but for programmatic handling branch on type and fields[].code — those are the contract.
Retry guidance
Use the retry column in the catalog above as the default, but these patterns apply more broadly:
- 4xx other than 429: Never retry blindly. These indicate a problem with the request the server cannot fix by seeing it again. Fix the request and retry once.
- 429: Wait
Retry-Afterseconds, then retry. Exponential backoff with jitter is appropriate if you're hitting the limit repeatedly. See Rate limits. - 500: Retry with exponential backoff, starting at 1 second, doubling to ~30s, with jitter. Surface the failure after 3-5 attempts.
- Network timeouts (no response): Retry with backoff. Treat the first attempt as "unknown state" — for idempotent methods (
GET,PUT,DELETE), retry is safe. ForPOST, retry may create duplicates if you didn't supply anidentifier; see Idempotency.
Idempotency
The TrakRF v1 API does not support the Idempotency-Key header. Retry safety comes from HTTP semantics and natural-key constraints:
POST /assets,POST /locations— retrying with the sameidentifierhits theUNIQUE(org_id, identifier)constraint and returns409 conflict. Detect the 409, then useGETorPUTto reconcile. If you omitidentifieron a POST retry, you may create duplicates — for retry-critical workflows, always supply an identifier.PUT— HTTP-semantically idempotent. Safe to retry.DELETE— idempotent. A second delete returns404 not_found(not204) so you can detect state drift; both outcomes are fine to treat as "deleted."
Explicit Idempotency-Key header support is on the v1.x roadmap if customer pain materializes.
Request IDs
Every response includes an X-Request-ID header with a ULID. The same ULID appears in the request_id field of any error envelope. When filing a support ticket, include this ID — it lets TrakRF staff find the exact request in logs without grepping.
If your client supplies an inbound X-Request-ID header, it is echoed back unchanged — TrakRF does not validate its format. Clients that supply their own IDs are encouraged to use ULIDs so log tooling remains consistent. When no inbound header is supplied, TrakRF generates a ULID server-side.
Deprecation notices
When TrakRF retires an endpoint (typically at a v2 cutover), responses from the deprecated endpoint carry two extra headers per RFC 8594:
Deprecation: true
Sunset: Wed, 11 Nov 2026 23:59:59 GMT
The minimum gap between Deprecation and Sunset is six months. After the sunset date, the endpoint returns 410 Gone. No endpoints are currently deprecated at v1 launch; this mechanism is in place so future use doesn't surprise integrators.