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:
- Store the
lastUpdatedAttimestamp (or theX-DPP-Versionresponse header) alongside your cached DPP data - Periodically fetch the DPP and compare it to your cached value
- 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.