Skip to main content

Fetch a public DPP

Traceable DPPs are identified by two different identifiers depending on context:

  • GTIN (Global Trade Item Number): the GS1 identifier encoded in the QR code on the physical battery, accessed via https://dpp.traceable.digital/01/{GTIN}/21/{serial}.
  • Slug: the URL-safe identifier used by the JSON data API (/api/dpp/{slug}).

Both resolve to the same passport. The examples on this page use the passport host, dpp.traceable.digital. When the passport host is active, app.traceable.digital answers the same paths with a 307 redirect, so a client that follows redirects works against either host.

Starting from a GTIN (QR code scan)​

When your integration begins with a scanned QR code, you have a GTIN and optionally a serial number, not a slug. You do not need the slug. The GS1 Digital Link resolver at /01/{GTIN} returns the passport JSON-LD from the same address when the request prefers JSON. A browser gets the HTML viewer from the same address. See Content negotiation.

JavaScript (Node.js)

async function fetchPassportByGtin(
gtin: string,
serial?: string
): Promise<Record<string, unknown>> {
const path = serial
? `/01/${encodeURIComponent(gtin)}/21/${encodeURIComponent(serial)}`
: `/01/${encodeURIComponent(gtin)}`;

const response = await fetch(`https://dpp.traceable.digital${path}`, {
headers: { Accept: 'application/ld+json' },
signal: AbortSignal.timeout(10_000),
});

if (response.status === 404) {
throw new Error(`GTIN ${gtin} not found or the DPP is not publicly available`);
}
if (response.status === 300) {
// More than one published product carries this GTIN. Add a serial or batch.
const body = await response.json();
throw new Error(`Several products match: ${JSON.stringify(body.choices)}`);
}
if (response.status === 429) {
const retryAfter = Number(response.headers.get('Retry-After')) || 60;
throw new Error(`Rate limited. Retry after ${retryAfter} seconds.`);
}
if (!response.ok) {
throw new Error(`GS1 resolver error: HTTP ${response.status}`);
}

return response.json();
}

// Usage: after scanning a QR code that encodes the GS1 Digital Link URI
const passport = await fetchPassportByGtin('09506000134352', 'SN-2026-00421');
console.log(passport['name']);

Python

import requests


def fetch_passport_by_gtin(gtin: str, serial: str | None = None) -> dict:
path = f"/01/{gtin}/21/{serial}" if serial else f"/01/{gtin}"
response = requests.get(
f"https://dpp.traceable.digital{path}",
headers={"Accept": "application/ld+json"},
timeout=10,
)

if response.status_code == 404:
raise ValueError(f"GTIN {gtin} not found or DPP is not publicly available")
if response.status_code == 300:
raise ValueError(f"Several products match: {response.json()['choices']}")

response.raise_for_status()
return response.json()


# Usage
passport = fetch_passport_by_gtin("09506000134352", "SN-2026-00421")
print(passport["name"])

The result is the JSON-LD document described in JSON-LD format. It holds public fields only.


Fetching by slug​

The examples below show how to fetch DPP data by slug using the JSON API directly. The body is a configuration-driven object; see DPP data model for its keys.

curl​

One-liner with pretty-printed output (-L follows the redirect if you call app.traceable.digital):

curl -sfL https://dpp.traceable.digital/api/dpp/swiftvolt-48v-100ah-ev-pack | jq .

With error handling and status code checking:

#!/bin/bash

SLUG="${1:-swiftvolt-48v-100ah-ev-pack}"
URL="https://dpp.traceable.digital/api/dpp/${SLUG}"

HTTP_STATUS=$(curl -sL -D /tmp/dpp-headers.txt -o /tmp/dpp-response.json -w "%{http_code}" "$URL")

case $HTTP_STATUS in
200)
echo "DPP fetched successfully:"
jq '.productName, .versionId, .manufacturerInfo.name' /tmp/dpp-response.json
;;
404)
echo "Error: DPP not found for slug '${SLUG}'" >&2
exit 1
;;
429)
# Wait time is carried in the Retry-After response header, not the body.
RETRY=$(grep -i '^Retry-After:' /tmp/dpp-headers.txt | tr -d '\r' | awk '{print $2}')
echo "Error: Rate limited. Retry after ${RETRY:-60} seconds." >&2
exit 1
;;
*)
echo "Error: Unexpected HTTP ${HTTP_STATUS}" >&2
cat /tmp/dpp-response.json >&2
exit 1
;;
esac

JavaScript (Node.js)​

Complete async function with TypeScript type annotations. The body is configuration-driven, so the type names only the keys that are always present and leaves the rest open:

interface DppObject {
productId: string;
versionId: string; // "N.0"
productName: string;
productModel?: string;
brandName?: string;
categoryId?: string;
manufacturerInfo?: { name?: string; website?: string; contact?: string };
carbonFootprint?: number; // a single number, when present
recycledContent?: number; // a single number, when present
dpp_metadata?: { version?: number; last_updated?: string };
lastUpdatedAt: string;
// Present only on an archived passport (withdrawn, end of production, or inactive company)
availability?: { status: 'archived'; reasons: string[]; notice: string };
[key: string]: unknown;
}

class DppNotFoundError extends Error {
constructor(slug: string) {
super(`DPP not found or not published for slug: ${slug}`);
this.name = 'DppNotFoundError';
}
}

class DppRateLimitError extends Error {
constructor(public readonly retryAfter: number) {
super(`Rate limited. Retry after ${retryAfter} seconds.`);
this.name = 'DppRateLimitError';
}
}

/**
* Fetch a published DPP by slug.
*
* @param slug - The URL-safe product identifier (e.g. "swiftvolt-48v-100ah-ev-pack")
* @returns The DPP object, with `availability` set when the passport is archived
* @throws DppNotFoundError if the DPP does not exist or is not published
* @throws DppRateLimitError if the rate limit is exceeded
* @throws Error for other unexpected failures
*/
async function fetchDpp(slug: string): Promise<DppObject> {
const url = `https://dpp.traceable.digital/api/dpp/${encodeURIComponent(slug)}`;

let response: Response;
try {
response = await fetch(url, {
headers: { 'Accept': 'application/json' },
// Use AbortSignal for timeout in Node.js 18+
signal: AbortSignal.timeout(10_000),
});
} catch (err) {
if (err instanceof Error && err.name === 'TimeoutError') {
throw new Error('Traceable API request timed out after 10 seconds');
}
throw new Error(`Network error fetching DPP: ${err}`);
}

if (response.status === 404) {
throw new DppNotFoundError(slug);
}

if (response.status === 429) {
// Wait time is in the Retry-After response header, not the body.
const retryAfter = Number(response.headers.get('Retry-After')) || 60;
throw new DppRateLimitError(retryAfter);
}

if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(
`Failed to fetch DPP (HTTP ${response.status}): ${body.error?.message ?? response.statusText}`
);
}

return response.json() as Promise<DppObject>;
}

// Usage with all error cases handled
async function displayDppSummary(slug: string): Promise<void> {
try {
const dpp = await fetchDpp(slug);

console.log(`Product: ${dpp.productName}`);
console.log(`Brand: ${dpp.brandName ?? 'n/a'}`);
console.log(`Manufacturer: ${dpp.manufacturerInfo?.name ?? 'n/a'}`);
console.log(`Version: ${dpp.versionId}`);
console.log(`Updated: ${dpp.lastUpdatedAt}`);

if (typeof dpp.carbonFootprint === 'number') {
console.log(`Carbon: ${dpp.carbonFootprint}`);
}

if (dpp.availability?.status === 'archived') {
console.log(`Archived: ${dpp.availability.notice}`);
}

} catch (err) {
if (err instanceof DppNotFoundError) {
console.error(`Product not found: ${err.message}`);
} else if (err instanceof DppRateLimitError) {
console.error(`Rate limited. Try again in ${err.retryAfter}s.`);
} else {
console.error('Unexpected error:', err);
throw err; // re-throw unexpected errors
}
}
}

await displayDppSummary('swiftvolt-48v-100ah-ev-pack');

Python (requests)​

import requests


class DppNotFoundError(Exception):
"""Raised when a DPP does not exist or is not published."""
pass


class DppRateLimitError(Exception):
"""Raised when the Traceable API rate limit is exceeded."""
def __init__(self, retry_after: int):
super().__init__(f"Rate limited. Retry after {retry_after} seconds.")
self.retry_after = retry_after


def fetch_dpp(slug: str) -> dict:
"""
Fetch a published DPP from the Traceable API by slug.

Args:
slug: The URL-safe product identifier.

Returns:
The DPP data as a dictionary.

Raises:
DppNotFoundError: If the DPP does not exist or is not published.
DppRateLimitError: If the rate limit is exceeded.
requests.exceptions.Timeout: If the request times out.
requests.exceptions.ConnectionError: If the API is unreachable.
RuntimeError: For other unexpected API errors.
"""
url = f"https://dpp.traceable.digital/api/dpp/{slug}"

try:
response = requests.get(
url,
headers={"Accept": "application/json"},
timeout=10,
)
except requests.exceptions.ConnectionError as exc:
raise requests.exceptions.ConnectionError(
f"Cannot reach Traceable API: {exc}"
) from exc
except requests.exceptions.Timeout:
raise requests.exceptions.Timeout(
"Traceable API request timed out after 10 seconds"
)

if response.status_code == 404:
raise DppNotFoundError(
f"DPP not found or not published for slug: {slug}"
)

if response.status_code == 429:
# Wait time is in the Retry-After response header, not the body.
retry_after = int(response.headers.get("Retry-After", 60))
raise DppRateLimitError(retry_after)

if not response.ok:
try:
body = response.json()
error_msg = body.get("error", {}).get("message", response.reason)
except (ValueError, AttributeError):
error_msg = response.reason
raise RuntimeError(
f"Traceable API error (HTTP {response.status_code}): {error_msg}"
)

return response.json()


def display_dpp_summary(slug: str) -> None:
"""Fetch and print a summary of a DPP."""
try:
dpp = fetch_dpp(slug)

print(f"Product: {dpp['productName']}")
print(f"Brand: {dpp.get('brandName', 'n/a')}")
print(f"Manufacturer: {dpp.get('manufacturerInfo', {}).get('name', 'n/a')}")
print(f"Version: {dpp['versionId']}")
print(f"Updated: {dpp['lastUpdatedAt']}")

carbon = dpp.get("carbonFootprint")
if isinstance(carbon, (int, float)):
print(f"Carbon: {carbon}")

availability = dpp.get("availability")
if availability and availability.get("status") == "archived":
print(f"Archived: {availability['notice']}")

except DppNotFoundError as e:
print(f"Not found: {e}")
except DppRateLimitError as e:
print(f"Rate limited: {e}")
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
print(f"Network error: {e}")
except RuntimeError as e:
print(f"API error: {e}")
raise


if __name__ == "__main__":
display_dpp_summary("swiftvolt-48v-100ah-ev-pack")

Notes on caching​

DPP data does not change frequently. Manufacturers typically update a DPP only when a regulatory audit, updated certification, or new carbon footprint calculation is available. A 5-minute cache TTL is a reasonable default for most use cases, and matches the s-maxage=300 the platform sets on public responses.

To cache efficiently and invalidate correctly:

  1. Store the lastUpdatedAt timestamp (or the X-DPP-Version response header) alongside your cached DPP data
  2. Periodically fetch the DPP and compare it to your cached value
  3. Only update your cache if it has changed
interface CachedDpp {
data: DppObject;
cachedAt: number; // Date.now()
lastUpdatedAt: string; // from DPP response
}

const cache = new Map<string, CachedDpp>();
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

async function fetchDppCached(slug: string): Promise<DppObject> {
const cached = cache.get(slug);
const now = Date.now();

if (cached && now - cached.cachedAt < CACHE_TTL_MS) {
return cached.data; // cache hit, within TTL
}

const dpp = await fetchDpp(slug);

// Only update cache if data has changed
if (!cached || cached.lastUpdatedAt !== dpp.lastUpdatedAt) {
cache.set(slug, { data: dpp, cachedAt: now, lastUpdatedAt: dpp.lastUpdatedAt });
}

return dpp;
}

Checking for an archived passport before displaying​

The public API returns published DPPs, and keeps a published passport online, read-only, after the product is withdrawn, goes out of production, or its manufacturer becomes inactive. Such a response is 200 with an availability object and the header X-DPP-Availability: archived. Always check for it before rendering a passport as current:

async function getDppForDisplay(slug: string): Promise<DppObject | null> {
try {
const dpp = await fetchDppCached(slug);

if (dpp.availability?.status === 'archived') {
// Show the notice, the reason and any successor link to your user.
console.warn(`DPP ${slug} is archived: ${dpp.availability.notice}`);
}

return dpp;
} catch (err) {
if (err instanceof DppNotFoundError) {
return null; // never published, not yet published, or revoked: show nothing
}
throw err;
}
}

See Archived passports for the full availability object.

Was this page helpful?