Errors and conventions
The Integration API is GA on production (app.traceable.digital) as of v0.99.6.1. The API returns 503 FEATURE_DISABLED on any environment where it is not enabled. See Availability.
This page covers the behaviour shared by every endpoint: default-deny auth, the error envelope, the full code catalogue, field-level validation, idempotency, rate limits, optimistic locking, and request correlation. For per-endpoint request and response schemas, see the API reference.
Default-denyβ
- A missing or invalid bearer key returns
401. - An unknown resource, or a resource owned by another tenant, returns
404β never403. The API never confirms that another tenant's data exists (no existence oracle). - The tenant (
companyId) is derived from the key only. It is never read from the request body or path; sending it is rejected.
Error envelopeβ
Every 4xx and 5xx response uses the same shape:
{
"error": {
"code": "MACHINE_READABLE_CODE",
"message": "Human-readable description",
"requestId": "...",
"details": {}
}
}
Always branch on code, never on message. Some errors add details.
Field-level validation (and its privacy guarantee)β
Batch field writes (PUT /api/v1/products/{id}/form-data) are fully validated and fail closed: on any failure the whole batch is rejected with 422 FIELD_VALIDATION_ERROR, and details.fieldErrors[] lists each problem as {field_id, label, code, message}. Unknown fields are rejected, not silently dropped.
fieldErrors[] references the field, never the value you submitted. The API does not echo your data back in errors. This is a deliberate privacy guarantee: an error report or log line cannot leak the content an ERP sent.
Each fieldErrors[] entry carries a stable code, one of: REQUIRED_MISSING, WRONG_TYPE, INVALID_OPTION, OUT_OF_RANGE, PATTERN_MISMATCH, UNKNOWN_FIELD.
{
"error": {
"code": "FIELD_VALIDATION_ERROR",
"message": "One or more formData fields failed validation.",
"requestId": "β¦",
"reason": "FIELD_VALIDATION_ERROR",
"fieldErrors": [
{ "field_id": "nominal_voltage", "label": "Nominal Voltage",
"code": "WRONG_TYPE", "message": "'Nominal Voltage' must be a valid number." }
]
}
}
Complete error-code catalogueβ
| HTTP | Codes | Notes |
|---|---|---|
| 400 | INVALID_JSON, MALFORMED_BODY, VALIDATION_ERROR, IDEMPOTENCY_KEY_REQUIRED, FILE_REQUIRED, INVALID_ID, MODE_HEADER_MISMATCH | Malformed or incomplete request. INVALID_ID β a path id is not a well-formed identifier. MODE_HEADER_MISMATCH β a sandbox (pk_test_) key was used without the correct X-Traceable-Mode: test header |
| 401 | UNAUTHENTICATED, INVALID_API_KEY, INVALID_SIGNATURE, SIGNING_NOT_CONFIGURED | UNAUTHENTICATED β no bearer key presented. INVALID_API_KEY β the bearer key is unknown, revoked, or expired. INVALID_SIGNATURE β the HMAC signature check failed |
| 403 | FORBIDDEN_SCOPE, FORBIDDEN_MODULE, FORBIDDEN_COMPANY, CROSS_MODE_DENIED | The key lacks the required scope or module, targets a company other than its own, or is the wrong mode for the endpoint (for example a sandbox key on a live-only GDPR endpoint) |
| 404 | NOT_FOUND, NO_FILE, SUPPLIER_NOT_ACCESSIBLE | Unknown, cross-tenant, or inaccessible resource. Default-deny returns 404 (never a 403 "exists but forbidden" oracle) for unknown or cross-tenant resources |
| 409 | LOCK_CONFLICT, PRODUCT_FROZEN, INVALID_STATE, GTIN_IN_USE, NAME_IN_USE | A conflict with current state; do not blind-retry |
| 415 | UNSUPPORTED_MEDIA_TYPE | Document file type not allowed |
| 422 | FIELD_VALIDATION_ERROR, SUBMIT_BLOCKED, SUBMIT_REJECTED, FILE_INVALID, INVALID_METADATA, STORAGE_LIMIT, PLAN_LIMIT, SELF_REFERENCE, CIRCULAR_REFERENCE, MAX_COMPONENTS, GS1_PREFIX_INVALID, SUCCESSOR_INVALID, DEMO_PRODUCT | Deterministic rejection; fix the input |
| 429 | RATE_LIMITED | Per-key limit (reads and writes); back off |
| 502 | UPLOAD_FAILED, DOWNLOAD_FAILED | Transient storage error; safe to retry |
| 500 | INTERNAL_ERROR | An unexpected server error; retry after a short delay and contact support with the requestId if it persists |
| 503 | FEATURE_DISABLED | The API is not enabled on this environment |
A few worth calling out:
LOCK_CONFLICT(409) β an optimistic-lock clash on update. Re-GET, re-apply, retry with the freshlockVersion.PRODUCT_FROZEN(409) β the product is no longer editable (for example, already submitted).INVALID_STATE(409) β for example, withdrawing a product that is not in a withdrawable state.SELF_REFERENCE/CIRCULAR_REFERENCE/MAX_COMPONENTS(422) β component-graph violations.SUBMIT_BLOCKED(422) β mandated fields are missing;errorlists them. Surface to the user; do not retry-loop.
Error response examplesβ
404 cross-tenant or not-owned supplier (for example POST .../traceability referencing a supplier you do not own):
{ "error": { "code": "SUPPLIER_NOT_ACCESSIBLE", "message": "Supplier not availableβ¦",
"requestId": "β¦", "reason": "SUPPLIER_NOT_ACCESSIBLE" } }
409 optimistic lock (PUT .../form-data, PATCH .../products/{id}) β carries the current lockVersion so you can re-read and retry:
{ "error": { "code": "LOCK_CONFLICT", "message": "β¦re-read and retryβ¦",
"requestId": "β¦", "currentLockVersion": 5 } }
409 frozen product (a write against an Approved or Deprecated product):
{ "error": { "code": "PRODUCT_FROZEN", "message": "β¦", "requestId": "β¦", "reason": "PRODUCT_FROZEN" } }
409 invalid state (for example withdrawing a product that is not Approved):
{ "error": { "code": "INVALID_STATE", "message": "β¦", "requestId": "β¦", "reason": "INVALID_TRANSITION" } }
The remaining classes follow the same envelope: 401 for a missing or invalid key or a bad signature, 403 FORBIDDEN_SCOPE for a missing scope, 429 RATE_LIMITED (fail-closed) when rate-limited, and 503 FEATURE_DISABLED where the API is not enabled.
Retry guidanceβ
- Transient (
429,502, other5xx): retry with exponential backoff, reusing the sameIdempotency-Key. - Deterministic (
4xxwith a specific code): the result will not change on retry. Fix the input. LOCK_CONFLICT: re-fetch, re-apply, retry with the newlockVersion.
Idempotencyβ
Every write must send an Idempotency-Key header (a fresh value per operation). The server caches the response, scoped to the key plus the path id plus a hash of the body, for 24 hours, so retries on transient failures are safe.
Rate limitsβ
The Integration API is rate-limited per API key at 100 requests / minute, counting both reads and writes. Live (pk_live_) and sandbox (pk_test_) keys have separate quotas. The limiter fails closed: if the backend is unavailable, requests are rejected with 429, not allowed through unchecked. Back off and retry, honouring the Retry-After response header. See Rate limiting for the full three-layer picture.
Optimistic lockingβ
PATCH /api/v1/products/{id} is optimistic-locked. Send the lockVersion from your last read; a stale value returns 409 LOCK_CONFLICT.
Request correlationβ
Every response includes an X-Traceable-Request-Id header, mirrored as requestId in any error envelope. Persist it: it links your request to the entry on the Traceable tamper-evident audit trail.