Skip to main content

Errors and conventions

Availability

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 β€” never 403. 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​

HTTPCodesNotes
400INVALID_JSON, MALFORMED_BODY, VALIDATION_ERROR, IDEMPOTENCY_KEY_REQUIRED, FILE_REQUIRED, INVALID_ID, MODE_HEADER_MISMATCHMalformed 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
401UNAUTHENTICATED, INVALID_API_KEY, INVALID_SIGNATURE, SIGNING_NOT_CONFIGUREDUNAUTHENTICATED β€” no bearer key presented. INVALID_API_KEY β€” the bearer key is unknown, revoked, or expired. INVALID_SIGNATURE β€” the HMAC signature check failed
403FORBIDDEN_SCOPE, FORBIDDEN_MODULE, FORBIDDEN_COMPANY, CROSS_MODE_DENIEDThe 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)
404NOT_FOUND, NO_FILE, SUPPLIER_NOT_ACCESSIBLEUnknown, cross-tenant, or inaccessible resource. Default-deny returns 404 (never a 403 "exists but forbidden" oracle) for unknown or cross-tenant resources
409LOCK_CONFLICT, PRODUCT_FROZEN, INVALID_STATE, GTIN_IN_USE, NAME_IN_USEA conflict with current state; do not blind-retry
415UNSUPPORTED_MEDIA_TYPEDocument file type not allowed
422FIELD_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_PRODUCTDeterministic rejection; fix the input
429RATE_LIMITEDPer-key limit (reads and writes); back off
502UPLOAD_FAILED, DOWNLOAD_FAILEDTransient storage error; safe to retry
500INTERNAL_ERRORAn unexpected server error; retry after a short delay and contact support with the requestId if it persists
503FEATURE_DISABLEDThe 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 fresh lockVersion.
  • 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; error lists 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, other 5xx): retry with exponential backoff, reusing the same Idempotency-Key.
  • Deterministic (4xx with a specific code): the result will not change on retry. Fix the input.
  • LOCK_CONFLICT: re-fetch, re-apply, retry with the new lockVersion.

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.