DPP endpoints
Traceable exposes DPP data through two complementary systems, each serving a different purpose.
Two access methods
| Method | URL pattern | Response | Use case |
|---|---|---|---|
| GS1 Digital Link resolver | /01/{GTIN}[/21/{serial}][/10/{batch}] | HTML (DPP viewer) | QR codes on physical batteries |
| JSON data API | /api/dpp/{slug} | JSON / JSON-LD | Machine-readable integration |
The GS1 Digital Link format is what EU Battery Regulation 2023/1542 requires to be encoded in the data carrier (QR code) on the physical battery. The JSON API is for programmatic access to the underlying data.
Both methods resolve to the same underlying DPP. A battery without a GTIN assigned in Traceable will only have a slug-based URL — the GS1 resolver requires a GTIN.
GS1 Digital Link resolver
GET /01/:gtin
GET /01/:gtin/21/:serial
GET /01/:gtin/10/:batch
Resolves a GTIN to the corresponding DPP and renders the battery passport viewer inline. The GS1 URI remains in the browser URL bar — there is no redirect to a slug-based URL.
This is the endpoint that QR codes on physical batteries should encode. It implements the GS1 Digital Link standard (ISO/IEC 18975).
Path structure
| Segment | Application Identifier | Description |
|---|---|---|
/01/{GTIN} | AI 01 | GTIN (required). 8, 12, 13, or 14 digits with a valid GS1 check digit. |
/21/{serial} | AI 21 | Individual serial number (optional) |
/10/{batch} | AI 10 | Batch or lot number (optional) |
Examples
https://app.traceable.digital/01/09506000134352
https://app.traceable.digital/01/09506000134352/21/ABC123
https://app.traceable.digital/01/09506000134352/21/ABC123/10/BATCH-2026-04
Validation
The GTIN is validated using the standard GS1 modulo-10 check digit algorithm before the database lookup. Requests with an invalid check digit return a 404 immediately — no database query is made.
Supported formats:
| Format | Digits | Example |
|---|---|---|
| GTIN-8 | 8 | 01234565 |
| GTIN-12 | 12 | 012345678905 |
| GTIN-13 | 13 | 0123456789012 |
| GTIN-14 | 14 | 01234567890128 |
Response
- 200 OK — HTML page rendering the DPP viewer for the resolved product. The DPP data is embedded in the page and in the HTML
<head>as JSON-LD. - 404 Not Found — GTIN not found in the Traceable registry, GTIN check digit is invalid, or the matched product's DPP is not published.
This endpoint returns HTML, not JSON. To retrieve machine-readable DPP data for a product you know by GTIN, resolve the GTIN first to get the slug, then use the JSON API.
Constructing a GS1 Digital Link URI
When generating the QR code URI for a battery product in Traceable, the platform uses this format when a GTIN is assigned:
https://app.traceable.digital/01/{GTIN}/21/{serialNumber}
If no GTIN has been assigned, the platform falls back to the slug-based URL:
https://app.traceable.digital/dpp/{slug}
The platform's QR code generator in the Operator Portal handles this automatically and displays a "GS1 Compliant" badge when the GTIN-based format is used.
JSON data API
GET /api/dpp/:slug
Retrieve the complete public DPP data for a battery product as JSON.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | The URL-safe product identifier assigned at DPP creation. Example: swiftvolt-48v-100ah-ev-pack |
Authentication
None. This endpoint returns public fields only and takes no credentials.
Restricted (PoLI-tier) fields are not available through this JSON API. There is no accessToken parameter and no restrictedFields in the response. Restricted fields are shown only on the rendered DPP web page, and only to a browser session that has completed the PoLI access flow.
Response — 200 OK
Returns the full DPP object for the requested product.
| Field | Type | Description |
|---|---|---|
id | string | Internal DPP record identifier |
slug | string | URL-safe product identifier. Immutable after publish. |
productName | string | Full commercial product name |
batteryCategory | string | One of: EV_BATTERY, LMT_BATTERY, INDUSTRIAL_BATTERY, SLI_BATTERY, PORTABLE_BATTERY |
status | string | published — only published DPPs are returned by this endpoint |
version | integer | Version number, incremented each time the DPP is updated and republished |
manufacturer | object | Manufacturer name, country, registration number, and contact details |
carbonFootprint | object | null | Carbon footprint data (present for EV and Industrial batteries; null for others) |
recycledContent | object | Recycled content percentages for cobalt, lithium, nickel, lead |
performance | object | Capacity, energy density, cycle life, round-trip efficiency |
hazardousSubstances | array | List of hazardous substances above threshold, with CAS numbers and concentrations |
supplyChain | object | Supply chain due diligence summary and responsible sourcing information |
compliance | object | Regulatory compliance declarations and certification references |
publishedAt | string | ISO 8601 timestamp when the DPP was first published |
updatedAt | string | ISO 8601 timestamp of the most recent update |
createdAt | string | ISO 8601 timestamp when the DPP record was created |
Response — 404 Not Found
Returned when the slug does not match any product, or the product's DPP is not published.
{
"error": "DPP not found or not published",
"code": "PRODUCT_NOT_FOUND",
"details": {}
}
Response — 429 Too Many Requests
The wait time is carried by the Retry-After response header (seconds). There is no retryAfter field in the body.
{
"error": "Rate limit exceeded",
"code": "RATE_LIMITED"
}
Examples
curl
curl -s https://app.traceable.digital/api/dpp/swiftvolt-48v-100ah-ev-pack | jq .
JavaScript (Node.js / browser fetch)
async function fetchDpp(slug: string): Promise<DppObject> {
const response = await fetch(
`https://app.traceable.digital/api/dpp/${encodeURIComponent(slug)}`
);
if (response.status === 404) {
throw new Error(`DPP not found for slug: ${slug}`);
}
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
throw new Error(`Rate limited. Retry after ${retryAfter} seconds.`);
}
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(`Failed to fetch DPP: ${data.error ?? response.statusText}`);
}
return response.json();
}
Python (requests)
import requests
def fetch_dpp(slug: str) -> dict:
url = f"https://app.traceable.digital/api/dpp/{slug}"
response = requests.get(url, timeout=10)
if response.status_code == 404:
raise ValueError(f"DPP not found or not published for slug: {slug}")
if response.status_code == 429:
retry_after = response.headers.get("Retry-After", "60")
raise RuntimeError(f"Rate limited. Retry after {retry_after}s.")
response.raise_for_status()
return response.json()
GET /api/dpp/:slug/jsonld
Retrieve a DPP in JSON-LD format for semantic web interoperability.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | The URL-safe product identifier |
Authentication
None required.
Response — 200 OK
Returns a JSON-LD document representing the DPP. The Content-Type is application/ld+json.
The @id of the returned document is the canonical URI for the product:
- If the product has a GTIN assigned:
https://app.traceable.digital/01/{GTIN}/21/{serial}(GS1 Digital Link) - If no GTIN:
https://app.traceable.digital/dpp/{slug}
The document uses:
@context: An array combininghttps://schema.org, the W3C Verifiable Credentials v1 context, and a Traceable term map that binds thedpp:,espr:, andgs1:prefixes@type:["Product", "DigitalProductPassport"](unprefixed schema.org terms)
Response — 404 Not Found
{
"error": "DPP not found or not published",
"code": "PRODUCT_NOT_FOUND",
"details": {}
}
Example JSON-LD response (product with GTIN)
The @context binds three prefixes — dpp: (Traceable's DPP terms), espr: (EU ESPR terms), and gs1: (GS1 vocabulary). Standard product and organisation fields use unprefixed schema.org terms. Battery-specific values such as the carbon footprint and recycled content are emitted as flat summary objects, not deep lifecycle breakdowns. The example below is trimmed to the shape of the response — a live passport carries more fields.
{
"@context": [
"https://schema.org",
"https://www.w3.org/2018/credentials/v1",
{
"dpp": "https://schema.traceable.digital/dpp/v1/",
"espr": "https://data.europa.eu/espr/",
"gs1": "https://gs1.org/voc/"
}
],
"@type": ["Product", "DigitalProductPassport"],
"@id": "https://app.traceable.digital/01/09506000134352/21/SN-2026-00421",
"name": "SwiftVolt 48V 100Ah EV Pack",
"sku": "SV-48100-EV",
"gtin": "09506000134352",
"brand": { "@type": "Brand", "name": "SwiftVolt" },
"manufacturer": {
"@type": "Organization",
"name": "SwiftVolt Energy Systems GmbH",
"url": "https://swiftvolt.de"
},
"dpp:dppId": "dpp_swiftvolt_48v_100ah",
"dpp:version": 3,
"dpp:carbonFootprint": {
"dpp:totalKgCO2e": 294.7,
"dpp:kgCO2ePerKwh": 61.4,
"dpp:methodology": "PEFCR for batteries"
},
"espr:circularity": {
"espr:recycledContentPercentage": 16
}
}
For the full JSON-LD shape, every emitted field, and the vocabulary behind each prefix, see the JSON-LD format data model page and the Parse JSON-LD output integration example.
GET /api/dpp/:slug/v/:version
Retrieve a specific historical version of a published DPP, exactly as it was published at that version. This is intended for regulators and auditors who hold a copy of a passport and need to retrieve and compare a prior published state.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | The URL-safe product identifier |
version | integer | Yes | The published version number to retrieve. Must be a positive integer (1 or greater) |
Authentication
None required. The endpoint serves only publicly visible data, with restricted fields stripped from the historical snapshot the same way they are stripped from the live response.
Availability
Version history is retrievable only for products that are currently published. If a product is unpublished, withdrawn, or deprecated at the time of your request, this endpoint returns 404 even though historical snapshots exist. The endpoint serves the recorded byte content of the snapshot, with public-field gating applied at read time.
Response — 200 OK
Returns the gated DPP snapshot for the requested version as JSON. The response also includes integrity headers describing the snapshot:
| Header | Description |
|---|---|
X-DPP-Version | The version number served |
X-Snapshot-Sha256 | SHA-256 over the bytes served |
X-Snapshot-Bytes | Size in bytes of the served snapshot |
X-Snapshot-Published-At | ISO 8601 timestamp when this version was published |
Other responses
| Status | Code | Condition |
|---|---|---|
| 400 | INVALID_VERSION | version is not a positive integer |
| 404 | PRODUCT_NOT_FOUND | The product is not currently published, or the slug is unknown |
| 404 | VERSION_NOT_FOUND | The product is published but no snapshot exists for that version |
| 429 | RATE_LIMITED | Rate limit exceeded; honour the Retry-After header |
| 503 | SNAPSHOT_INTEGRITY_FAILURE | The snapshot index exists but the stored content could not be served intact. This fails closed rather than serve a partial record. Retry after the interval in Retry-After |
Error bodies use the nested envelope:
{
"error": {
"code": "VERSION_NOT_FOUND",
"message": "No snapshot exists for the requested version"
}
}
Withdrawn products
When an operator withdraws a published battery passport (for example after a recall), the public surfaces respond differently depending on whether you are calling the machine API or loading the human-facing page. This distinction matters for integrators.
Machine API: HTTP 410 Gone
GET /api/dpp/:slug and GET /api/dpp/:slug/jsonld return 410 Gone for a withdrawn product, with a body describing the withdrawal:
{
"error": {
"code": "PRODUCT_WITHDRAWN",
"message": "This battery passport has been withdrawn by the manufacturer",
"reason": "Recall",
"withdrawnAt": "2026-05-10T09:00:00.000Z",
"operatorDetails": "Voluntary recall of affected production batch",
"successorUrl": "/dpp/replacement-product-slug",
"successorName": "Replacement product name",
"manufacturerContact": "compliance@example.com"
}
}
successorUrl, successorName, operatorDetails, and manufacturerContact may be null if not provided. The response carries Cache-Control: public, max-age=0, must-revalidate so a later reinstatement is not served stale.
Handle 410 explicitly. A QR scanner or integration that treats every non-200 as "not found" will hide the recall information. Branch on 410 and surface the withdrawal reason and any successor link to your user.
Human page: 200 with a withdrawal notice
The browser-facing pages (/dpp/{slug} and the GS1 resolver /01/{gtin}) return HTTP 200 and render a styled withdrawal notice (reason, date, manufacturer statement, successor link, contact), marked noindex so search engines drop it. They do not return 410, because a page route serves a human-readable notice rather than a machine error.
Status summary
| Product state | /api/dpp/:slug and /jsonld | /dpp/:slug and /01/:gtin (browser) |
|---|---|---|
| Unknown slug | 404 PRODUCT_NOT_FOUND | 404 |
| Draft or submitted (not yet published) | 404 PRODUCT_NOT_FOUND | 404 |
| Published | 200 (DPP data) | 200 (DPP viewer) |
| Withdrawn | 410 PRODUCT_WITHDRAWN | 200 (withdrawal notice, noindex) |
| Deprecated | 404 PRODUCT_NOT_FOUND | 404 |