Error codes
This page covers the public DPP API (GET /api/dpp/* and the GS1 Digital Link resolver at /01/...). Error responses include a code field; use code, not the HTTP status, as the primary basis for error handling. The one exception is the per-IP edge limiter, described below.
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 browser flow with tokenised links and two same-origin browser endpoints, none intended for programmatic use: see PoLI access.
Public DPP API error reference
| HTTP Status | Code | Meaning | Recommended action |
|---|---|---|---|
300 | MULTIPLE_PRODUCTS | More than one published product carries the GTIN on /01/... | Add /21/{serial} or /10/{batch}, or follow a link in choices |
400 | INVALID_VERSION | version in /api/dpp/{slug}/v/{version} is not a positive integer | Send an integer of 1 or more |
404 | PRODUCT_NOT_FOUND | No publicly served product found for the given slug | Verify the slug. Drafts, submitted products, regulator-revoked products, test products and products that were never published return 404. Published passports stay online even after withdrawal, end of production or a manufacturer suspension; see Archived passports. |
404 | VERSION_NOT_FOUND | The product is served but has no snapshot at that version | Do not retry. |
404 | CONFIG_NOT_FOUND | The product has no usable category configuration | Contact support. |
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 | Wait at least 30 seconds before retrying, and contact support if it persists, quoting the time of the request and the slug. |
500 | MAPPING_FAILED | The JSON-LD document could not be built (/api/dpp/{slug}/jsonld) | Retry later and contact support if it persists. |
503 | SNAPSHOT_INTEGRITY_FAILURE | The stored snapshot could not be verified, so nothing is served | Retry after the Retry-After interval (300 seconds). The platform team is alerted automatically. |
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."
}
}
The per-IP edge limiter answers 429 with {"error":"Too many requests"} and no code. Branch on the status for that case.
A withdrawn, deprecated or suspended-manufacturer passport is not an error. It answers 200 with an availability object and the header X-DPP-Availability: archived:
{
"availability": {
"status": "archived",
"reasons": ["withdrawn"],
"withdrawal": {
"reason": "Recall",
"withdrawnAt": "2026-05-10T09:00:00.000Z",
"successorUrl": "/dpp/replacement-product-slug"
}
}
}
(Abbreviated; the passport fields sit beside it, and the full object is in Archived passports.)
Error handling best practices
Use code, not HTTP status
const res = await fetch(`https://dpp.traceable.digital/api/dpp/${slug}`);
const data = await res.json().catch(() => ({}));
if (!res.ok && data.error?.code === 'RATE_LIMITED') {
// wait for Retry-After seconds, then retry
}
if (res.headers.get('X-DPP-Availability') === 'archived') {
// show data.availability.notice and any successor link
}
Only retry transient errors
const RETRYABLE = new Set(['RATE_LIMITED', 'SNAPSHOT_INTEGRITY_FAILURE', 'INTERNAL_ERROR']);
// 404 PRODUCT_NOT_FOUND is deterministic, so do not retry.
Check for archived passports
From platform release v0.99.20 a withdrawn DPP returns 200 with an availability block, not 410. Earlier releases returned 410 PRODUCT_WITHDRAWN, and a client written for that must change: branch on the X-DPP-Availability: archived header or the availability object and surface the reason. See DPP endpoints.
Log what identifies the request
The public DPP routes return no request identifier. Log the time of the request, the slug and the status when you contact support. For /api/v1 requests, log the X-Traceable-Request-Id response header, which is also returned as requestId in the error body.
if (response.status >= 500) {
logger.error('Traceable API server error', {
slug,
status: response.status,
at: new Date().toISOString(),
requestId: response.headers.get('X-Traceable-Request-Id'), // /api/v1 only
});
}