Skip to main content

DPP endpoints

Traceable exposes DPP data through two complementary systems, each serving a different purpose.

Two access methods​

MethodURL patternResponseUse case
GS1 Digital Link resolver/01/{GTIN}[/21/{serial}][/10/{batch}]HTML (DPP viewer), or the passport JSON-LD when the request prefers JSONQR codes on physical batteries; machines that resolve the same address
JSON data API/api/dpp/{slug}JSON / JSON-LDMachine-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.


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​

SegmentApplication IdentifierDescription
/01/{GTIN}AI 01GTIN (required). 8, 12, 13, or 14 digits with a valid GS1 check digit.
/21/{serial}AI 21Individual serial number (optional)
/10/{batch}AI 10Batch 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:

FormatDigitsExample
GTIN-8801234565
GTIN-1212012345678905
GTIN-13130123456789012
GTIN-141401234567890128

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 headerResponse
Prefers application/json, application/ld+json or any application/*+json type over HTML200 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 preferenceThe HTML passport viewer
application/linkset+json named exactly and preferred over HTML and every JSON type, or ?linkType=linkset or ?linkType=all on any Accept200 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:passportAvailability node; 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 in choices. The body has error.code MULTIPLE_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.

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​

ParameterTypeRequiredDescription
slugstringYesThe 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​

ParameterTypeRequiredDescription
slugstringYesThe 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 combining https://schema.org and a Traceable term map that binds the dpp:, gs1: and bpv1_3: prefixes. The document is not a Verifiable Credential, so the W3C credentials context is not included
  • @type: ["Product", "DigitalProductPassport"]. Product is the schema.org type; DigitalProductPassport is defined in the context as dpp:DigitalProductPassport
  • hasPart: where the product has declared components with publicly resolvable links, the JSON-LD includes schema.org hasPart entries linking to the child components' own passports (child links are access-gated; only https resolution 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​

ParameterTypeRequiredDescription
slugstringYesThe URL-safe product identifier
versionintegerYesThe 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:

HeaderDescription
X-DPP-VersionThe version number served
X-Snapshot-Sha256SHA-256 over the bytes served
X-Snapshot-BytesSize in bytes of the served snapshot
X-Snapshot-Published-AtISO 8601 timestamp when this version was published

Other responses​

StatusCodeCondition
400INVALID_VERSIONversion is not a positive integer
404PRODUCT_NOT_FOUNDThe product is not publicly served, or the slug is unknown
404VERSION_NOT_FOUNDThe product is publicly served but no snapshot exists for that version
429RATE_LIMITEDRate limit exceeded; honour the Retry-After header
503SNAPSHOT_INTEGRITY_FAILUREThe 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
500INTERNAL_ERRORUnexpected 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.)

FieldMeaning
statusAlways archived
reasonsOne or more of withdrawn, end_of_production, company_inactive
noticeThe same text a visitor reads on the passport page
withdrawalReason, 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
endOfProductionAtWhen the model went out of production, or null
manufacturerContactThe 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 the dpp:passportAvailability node.
  • Safety withdrawals. For the reasons Recall, SafetyDefect and NonCompliance the passport is still returned, but no certificate or document carries a Verified status.

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 slug404 PRODUCT_NOT_FOUND404
Draft or submitted (not yet published)404 PRODUCT_NOT_FOUND404
Revoked by a regulator404 PRODUCT_NOT_FOUND404
Published200 (DPP data)200 (DPP viewer)
Published, manufacturer suspended or inactive200 with availability (company_inactive)200 (notice)
Withdrawn200 with availability (withdrawn)200 (withdrawal notice, noindex)
Deprecated, and published before200 with availability (end_of_production)200 (notice, noindex)
Deprecated, never published404 PRODUCT_NOT_FOUND404
Was this page helpful?