Error codes
This page covers the public DPP API (GET /api/dpp/*, GET /api/health). Error responses include a code field; use code, not the HTTP status, as the primary basis for error handling.
The Integration API (/api/v1) has its own, larger error catalogue with a different envelope — see Integration API: Errors and conventions. The BMS endpoint has its own error responses — see BMS Endpoints. PoLI access is a web flow with no JSON API, so it has no error codes — see PoLI access.
Public DPP API error reference
| HTTP Status | Code | Meaning | Recommended action |
|---|---|---|---|
404 | PRODUCT_NOT_FOUND | No published product found for the given slug | Verify the slug. Only published DPPs from active companies are served; drafts, deprecated, and unpublished products return 404. |
410 | PRODUCT_WITHDRAWN | The product's DPP has been withdrawn (EU Battery Regulation Art. 76) | Surface the withdrawal to your user. The response body carries the reason and any successor link. Do not treat as a transient error. |
429 | RATE_LIMITED | Too many requests in the current window | Wait and retry with exponential backoff. Honour the Retry-After header. See Rate Limiting. |
500 | INTERNAL_ERROR | An unexpected server error | Log the X-Request-Id header and contact support if it persists. Wait at least 30 seconds before retrying. |
503 | SERVICE_UNAVAILABLE | The platform or a dependency is temporarily unavailable | Retry with exponential backoff. Check status.traceable.digital. |
Error response structure
Error responses carry a machine-readable code and a human-readable message. Branch on code, never on the message text.
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Try again later."
}
}
A withdrawn product (410) includes the withdrawal context:
{
"error": {
"code": "PRODUCT_WITHDRAWN",
"message": "This battery passport has been withdrawn by the manufacturer",
"reason": "Recall",
"withdrawnAt": "2026-05-10T09:00:00.000Z",
"successorUrl": "/dpp/replacement-product-slug"
}
}
Error handling best practices
Use code, not HTTP status
const res = await fetch(`https://app.traceable.digital/api/dpp/${slug}`);
const data = await res.json().catch(() => ({}));
if (data?.error?.code === 'PRODUCT_WITHDRAWN') {
// show the withdrawal notice and any successor link
}
Only retry transient errors
const RETRYABLE = new Set(['RATE_LIMITED', 'SERVICE_UNAVAILABLE', 'INTERNAL_ERROR']);
// 404 PRODUCT_NOT_FOUND and 410 PRODUCT_WITHDRAWN are deterministic — do not retry.
Handle 410 explicitly
A withdrawn DPP returns 410, not 404. An integration that treats every non-200 as "not found" will hide the recall. Branch on 410 and surface the reason. See DPP endpoints.
Log the X-Request-Id for 5xx errors
const requestId = response.headers.get('X-Request-Id');
if (response.status >= 500) {
logger.error('Traceable API server error', { requestId, status: response.status });
// Include requestId when contacting support
}