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), or the passport JSON-LD when the request prefers JSON | QR codes on physical batteries; machines that resolve the same address |
| 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, because 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. A browser gets the battery passport viewer rendered inline; a machine that asks for JSON gets the passport JSON-LD from the same URI (see Content negotiation). The GS1 URI remains in the browser URL bar, and 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://dpp.traceable.digital/01/09506000134352
https://dpp.traceable.digital/01/09506000134352/21/ABC123
https://dpp.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, for a browser or an
Accept: */*request. 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.
If a GTIN matches more than one published product, the resolver renders a chooser page listing the matches instead of silently serving the oldest one. Serial (/21/) and batch (/10/) path segments must be 1 to 20 characters, the first a letter or digit and the rest letters, digits, ., _ or -. A segment breaking that rule returns 404 by design, and the same rule is enforced when operators enter the identifiers, so published item-level links always resolve.
Content negotiation
The same address answers people and machines. What you get depends on the request's Accept header:
Accept header | Response |
|---|---|
Prefers application/json, application/ld+json or any application/*+json type over HTML | 200 with the passport JSON-LD on the requested URI. There is no redirect. Content-Type is application/json when application/json matched, and application/ld+json for application/ld+json and the other +json types |
A browser, */*, or no JSON preference | The HTML passport viewer |
application/linkset+json named exactly and preferred over HTML and every JSON type, or ?linkType=linkset or ?linkType=all on any Accept | 200 with a GS1 link set (RFC 9264). Content-Type: application/linkset+json, Vary: Accept. It lists the passport under https://gs1.org/voc/dpp as an HTML link and a JSON-LD link, and the HTML link also under https://gs1.org/voc/defaultLink |
Preference follows the q values in the header. A negotiated response carries Vary: Accept. The body is the same document as GET /api/dpp/:slug/jsonld and passes the same public-field gates, so it holds public fields only.
The paths /gs1-linkset/... and /gs1-jsonld/... are internal and cannot be opened directly: a direct request returns 404. Always use /01/{gtin}.
Passport responses on /01/... (HTML) carry a Link header of the form <path?linkType=linkset>; rel="linkset"; type="application/linkset+json". Any other linkType value (gs1:dpp, dpp, gs1:sustainabilityInfo, or an unknown one) returns the passport, HTML or JSON-LD by Accept.
A request that prefers JSON can return:
- 200 OK: the passport JSON-LD. For an archived passport (withdrawn, no longer produced, or its manufacturer no longer active) the JSON-LD carries a
dpp:passportAvailabilitynode; see Archived passports. - 300 Multiple Choices: more than one published product carries the GTIN. Add the serial (
/21/) or batch (/10/) qualifier, or follow one of the links inchoices. The body haserror.codeMULTIPLE_PRODUCTS. - 404 Not Found: always the same body,
{"error":{"code":"PRODUCT_NOT_FOUND","message":"No passport found for this identifier"}}. It is returned whether the GTIN is unknown, the check digit is invalid, the path is malformed, or the product is not publicly available. - 429 Too Many Requests: the per-IP rate limit was exceeded. The response carries
Retry-After: 60.
Earlier versions redirected a JSON request to /api/dpp/{slug}/jsonld with a 307. That redirect is gone: the JSON-LD is served on the GS1 URI itself, so a client needs no slug.
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://dpp.traceable.digital/01/{GTIN}/21/{serialNumber}
If no GTIN has been assigned, the platform falls back to the slug-based URL:
https://dpp.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.
On production the passport host is dpp.traceable.digital. When the passport host is active, app.traceable.digital answers /dpp/..., /01/..., /api/dpp/... and /i/... with a 307 redirect to the same path on the passport host. Follow redirects (curl -L) or call the passport host directly, as the examples below do.
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
The body is a configuration-driven object, not a fixed schema. Which sections and fields are present depends on the product's category and on what the operator has populated. Common top-level keys are productId, versionId (a string such as "3.0"), productName, productModel, brandName, categoryId, productCategory, productImage, dppId, productDetails, manufacturerInfo, complianceAndCertificates, materialAndCircularity, technicalSpecifications, batteryInformation, batteryPerformance, environmentalData, productSafety, dpp_metadata, formData (gated by access tier), dataFreshness, dynamicDataUpdatedAt, lastUpdatedAt and lastUpdated. carbonFootprint and recycledContent, when present, are single numbers. There is no id, slug, status, batteryCategory or updatedAt key. The current version is in the X-DPP-Version response header. See the DPP data model page for the full list and the meaning of each key.
An archived passport also carries an availability object and the header X-DPP-Availability: archived; see Archived passports.
Response: 404 Not Found
Returned when the slug does not match any product, or the product's DPP is not published.
{
"error": {
"code": "PRODUCT_NOT_FOUND",
"message": "No DPP found for 'swiftvolt-48v-100ah-ev-pack'"
}
}
A 404 with code CONFIG_NOT_FOUND means the product has no usable category configuration.
Response: 429 Too Many Requests
The wait time is carried by the Retry-After response header (Retry-After: 60, in seconds). There is no retryAfter field in the body.
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Try again later."
}
}
If the per-IP edge limiter answers first, the body is {"error":"Too many requests"} with X-RateLimit-* headers instead. See Rate limiting.
Response: 503 Service Unavailable
{
"error": {
"code": "SNAPSHOT_INTEGRITY_FAILURE",
"message": "The published snapshot for this DPP cannot be verified. The platform team has been notified."
}
}
The stored snapshot could not be verified, so nothing is served. The response carries Retry-After: 300.
Response: 500 Internal Server Error
An unexpected failure returns {"error":{"code":"INTERNAL_ERROR","message":"Failed to fetch DPP"}}.
Examples
curl
curl -sL https://dpp.traceable.digital/api/dpp/swiftvolt-48v-100ah-ev-pack | jq .
JavaScript (Node.js / browser fetch)
// The body is configuration-driven, so type only the keys you rely on.
type DppObject = Record<string, unknown> & {
productId: string;
versionId: string;
productName: string;
};
async function fetchDpp(slug: string): Promise<DppObject> {
const response = await fetch(
`https://dpp.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?.message ?? response.statusText}`
);
}
return response.json();
}
Python (requests)
import requests
def fetch_dpp(slug: str) -> dict:
url = f"https://dpp.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 passport's GS1 Digital Link URI on the host that served the request:
- If the product has a valid GTIN:
https://<host>/01/{GTIN}, followed by/10/{batch}and/21/{serial}(in that order) when the request carried them - If no valid GTIN:
https://<host>/dpp/{slug}
The @id never has a query string. The document carries no decentralised identifier.
The document uses:
@context: An array combininghttps://schema.organd a Traceable term map that binds thedpp:,gs1:andbpv1_3:prefixes. The document is not a Verifiable Credential, so the W3C credentials context is not included@type:["Product", "DigitalProductPassport"].Productis the schema.org type;DigitalProductPassportis defined in the context asdpp:DigitalProductPassporthasPart: where the product has declared components with publicly resolvable links, the JSON-LD includes schema.orghasPartentries linking to the child components' own passports (child links are access-gated; onlyhttpsresolution targets are emitted)
Response: 404 Not Found
{
"error": {
"code": "PRODUCT_NOT_FOUND",
"message": "No DPP found for 'swiftvolt-48v-100ah-ev-pack'"
}
}
The other error bodies use the same nested envelope: CONFIG_NOT_FOUND (404), MAPPING_FAILED (500), SNAPSHOT_INTEGRITY_FAILURE (503, with Retry-After: 300), INTERNAL_ERROR (500) and RATE_LIMITED (429, with Retry-After: 60). This route also answers OPTIONS with 204 and sets Access-Control-Allow-Origin: *.
Example JSON-LD response (product with GTIN)
The @context binds three prefixes: dpp: (Traceable's DPP terms), gs1: (GS1 vocabulary) and bpv1_3: (Traceable's mapping of the BatteryPass v1.3 attributes). 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",
{
"dpp": "https://schema.traceable.digital/dpp/v1/",
"gs1": "https://gs1.org/voc/",
"bpv1_3": "https://schema.traceable.digital/batterypass/v1.3/",
"DigitalProductPassport": "dpp:DigitalProductPassport"
}
],
"@type": ["Product", "DigitalProductPassport"],
"@id": "https://dpp.traceable.digital/01/09506000134352",
"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"
},
"dpp:circularity": {
"dpp: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. The stored snapshot is verified against its recorded SHA-256, then re-gated for the public tier at read time, so the served body is a public-tier derivative of the stored snapshot. 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 for any passport that is publicly served, live or archived (see Archived passports). For an archived passport the response is the gated snapshot (see below) and carries the header X-DPP-Availability: archived; the notice itself is on the live routes. A passport withdrawn as counterfeit answers with its notice only, never a snapshot. If a product was never published, or is unpublished, revoked or a test product, this endpoint returns 404 even though historical snapshots may exist. The raw stored bytes are never served. The body is the snapshot re-serialised after the current public-tier field gate, the current template visibility, removal of the score keys complianceScore, sustainabilityScore and traceabilityScore, and the document scan filter. It carries "visibility_policy_applied_at": "serve". X-Snapshot-Sha256 and X-Snapshot-Bytes describe the served bytes, not the stored bytes. Responses carry Cache-Control: no-store and X-Robots-Tag: noindex, nosnippet.
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 publicly served, or the slug is unknown |
| 404 | VERSION_NOT_FOUND | The product is publicly served 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 (300 seconds). The same code is returned when the stored content is not valid JSON |
| 500 | INTERNAL_ERROR | Unexpected server error |
Error bodies use the nested envelope:
{
"error": {
"code": "VERSION_NOT_FOUND",
"message": "Version 7 of 'swiftvolt-48v-100ah-ev-pack' has no published snapshot"
}
}
Archived passports
A published passport stays online after the product is withdrawn, after the model goes out of production, and after its manufacturer is suspended or stops operating. Traceable keeps these passports available, read-only. For a manufacturer that has ceased its activity, Regulation (EU) 2023/1542, Article 78(e) requires the passport to remain available. From platform release v0.99.20 these passports answer 200, not 410 or 404, on every public route: /api/dpp/:slug, /api/dpp/:slug/jsonld, a GS1 Digital Link request that prefers JSON, and the browser pages.
A passport is archived when it was published and one of these is true:
- Its product was withdrawn.
- Its model is deprecated (end of production).
- Its company is suspended or inactive.
Machine API: 200 with an availability block
GET /api/dpp/:slug returns the normal passport JSON plus an availability object, and the response header X-DPP-Availability: archived. A live passport has neither.
{
"availability": {
"status": "archived",
"reasons": ["withdrawn"],
"notice": "Product withdrawn Withdrawal date: 10 May 2026. Reason: Recall. ...",
"withdrawal": {
"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"
},
"endOfProductionAt": null,
"manufacturerContact": "compliance@example.com"
}
}
(The example shows only the availability object; the passport fields sit beside it.)
| Field | Meaning |
|---|---|
status | Always archived |
reasons | One or more of withdrawn, end_of_production, company_inactive |
notice | The same text a visitor reads on the passport page |
withdrawal | Reason, date, the operator's details and any successor (successorUrl is a path on the passport host). null unless the product was withdrawn. Inside it, operatorDetails, successorUrl and successorName may be null |
endOfProductionAt | When the model went out of production, or null |
manufacturerContact | The company contact email, present only when the passport is archived because it was withdrawn, and not after a sole-member erasure. Otherwise null |
GET /api/dpp/:slug/jsonld and the negotiated GS1 response carry the same information as a dpp:passportAvailability node (@type dpp:PassportAvailability) with dpp:availabilityStatus, dpp:archiveReason, dpp:notice, and where they apply dpp:withdrawalReason, dpp:withdrawnAt and dpp:endOfProductionAt. The JSON-LD responses do not carry the X-DPP-Availability header.
Two cases are narrower:
- Counterfeit. A passport withdrawn because it was found to be counterfeit answers with the notice only, never a passport value:
{ "availability": { ... } }on the JSON route, and a JSON-LD document holding only thedpp:passportAvailabilitynode. - Safety withdrawals. For the reasons
Recall,SafetyDefectandNonCompliancethe passport is still returned, but no certificate or document carries aVerifiedstatus.
Update clients that branch on 410. Earlier releases returned 410 PRODUCT_WITHDRAWN for a withdrawn product. That response is gone from the public passport routes. To detect an archived passport, check for the X-DPP-Availability: archived header or the availability object (in JSON-LD, the dpp:passportAvailability node), and surface the notice, the withdrawal reason and any successor link to your user. An integration that treats every response with passport data as current will hide the recall.
Human page: 200 with a notice
The browser pages (/dpp/{slug} and the GS1 resolver /01/{gtin}) return HTTP 200 and render a notice with the reason, date, manufacturer statement, successor link and contact, marked noindex so search engines drop them. Archived passports are also left out of the sitemap.
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 |
| Revoked by a regulator | 404 PRODUCT_NOT_FOUND | 404 |
| Published | 200 (DPP data) | 200 (DPP viewer) |
| Published, manufacturer suspended or inactive | 200 with availability (company_inactive) | 200 (notice) |
| Withdrawn | 200 with availability (withdrawn) | 200 (withdrawal notice, noindex) |
| Deprecated, and published before | 200 with availability (end_of_production) | 200 (notice, noindex) |
| Deprecated, never published | 404 PRODUCT_NOT_FOUND | 404 |