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://app.traceable.digital/01/{GTIN}/21/{serial}. This is the EU Battery Regulation-compliant access method for end users and supply chain systems.
  • Slug — the URL-safe identifier used by the JSON data API (/api/dpp/{slug}). This is what programmatic integrations use to fetch machine-readable DPP data.

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. The GS1 Digital Link resolver at /01/{GTIN} returns an HTML page, not JSON. To get machine-readable DPP data, extract the slug from the embedded JSON-LD in the HTML response, then call the JSON data API.

The HTML response from the GS1 resolver includes a <script type="application/ld+json"> tag in the <head> containing the full DPP data. The slug is available as the traceable:slug property in that document.

JavaScript (Node.js)

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

const response = await fetch(`https://app.traceable.digital${path}`, {
headers: { 'Accept': 'text/html' },
signal: AbortSignal.timeout(10_000),
});

if (response.status === 404) {
throw new Error(`GTIN ${gtin} not found or the DPP is not published`);
}
if (!response.ok) {
throw new Error(`GS1 resolver error: HTTP ${response.status}`);
}

const html = await response.text();

// Extract the JSON-LD block from the <head>
const match = html.match(/<script[^>]+type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/);
if (!match?.[1]) {
throw new Error('JSON-LD not found in GS1 resolver response');
}

let jsonLd: Record<string, unknown>;
try {
jsonLd = JSON.parse(match[1]);
} catch {
throw new Error('Failed to parse JSON-LD from GS1 resolver response');
}

const slug = jsonLd['traceable:slug'] as string | undefined;
if (!slug) {
throw new Error('traceable:slug not found in JSON-LD response');
}

return slug;
}

// Full flow: GTIN → slug → DPP data
async function fetchDppByGtin(gtin: string, serial?: string): Promise<DppObject> {
const slug = await resolveGtinToSlug(gtin, serial);
return fetchDpp(slug);
}

// Usage — e.g. after scanning a QR code that encodes the GS1 Digital Link URI
const dpp = await fetchDppByGtin('09506000134352', 'SN-2026-00421');
console.log(dpp.productName);

Python

import re
import json
import requests


def resolve_gtin_to_slug(gtin: str, serial: str | None = None) -> str:
path = f"/01/{gtin}/21/{serial}" if serial else f"/01/{gtin}"
url = f"https://app.traceable.digital{path}"

response = requests.get(
url,
headers={"Accept": "text/html"},
timeout=10,
)

if response.status_code == 404:
raise ValueError(f"GTIN {gtin} not found or DPP is not published")

response.raise_for_status()

# Extract the JSON-LD block from the HTML <head>
match = re.search(
r'<script[^>]+type="application/ld\+json"[^>]*>(.*?)</script>',
response.text,
re.DOTALL,
)
if not match:
raise ValueError("JSON-LD not found in GS1 resolver response")

jsonld = json.loads(match.group(1))
slug = jsonld.get("traceable:slug")

if not slug:
raise ValueError("traceable:slug not found in JSON-LD response")

return slug


def fetch_dpp_by_gtin(gtin: str, serial: str | None = None) -> dict:
slug = resolve_gtin_to_slug(gtin, serial)
return fetch_dpp(slug)


# Usage
dpp = fetch_dpp_by_gtin("09506000134352", "SN-2026-00421")
print(dpp["productName"])
note

If traceable:slug is absent from the JSON-LD in your environment, inspect the raw <script type="application/ld+json"> content in the HTML response to identify the correct property name for your platform version.


Fetching by slug

The examples below show how to fetch DPP data by slug using the JSON API directly.

curl

One-liner with pretty-printed output:

curl -sf https://app.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://app.traceable.digital/api/dpp/${SLUG}"

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

case $HTTP_STATUS in
200)
echo "DPP fetched successfully:"
jq '.productName, .batteryCategory, .manufacturer.name' /tmp/dpp-response.json
;;
404)
echo "Error: DPP not found for slug '${SLUG}'" >&2
exit 1
;;
429)
RETRY=$(jq -r '.retryAfter' /tmp/dpp-response.json)
echo "Error: Rate limited. Retry after ${RETRY} 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:

interface DppManufacturer {
name: string;
country: string;
registrationNumber: string;
contactEmail: string;
address: string;
}

interface DppCarbonFootprint {
totalKgCO2ePerKwh: number;
lifecycle: {
rawMaterialExtraction: number;
manufacturing: number;
distribution: number;
usePhase: number;
endOfLife: number;
};
calculationMethodology: string;
verifiedBy: string | null;
verifiedAt: string | null;
}

interface DppRecycledContent {
cobaltPercent: number;
lithiumPercent: number;
nickelPercent: number;
leadPercent: number;
}

interface DppPerformance {
nominalCapacityKwh: number;
ratedVoltageV: number;
cycleLifeAtEightyPercent: number;
roundTripEfficiencyPercent: number;
operatingTempMinC: number;
operatingTempMaxC: number;
}

type BatteryCategory =
| 'EV_BATTERY'
| 'LMT_BATTERY'
| 'INDUSTRIAL_BATTERY'
| 'SLI_BATTERY'
| 'PORTABLE_BATTERY';

interface DppObject {
id: string;
slug: string;
productName: string;
batteryCategory: BatteryCategory;
status: 'published';
version: number;
manufacturer: DppManufacturer;
carbonFootprint: DppCarbonFootprint | null;
recycledContent: DppRecycledContent;
performance: DppPerformance;
hazardousSubstances: Array<{
name: string;
casNumber: string;
concentrationPercent: number;
}>;
compliance: {
euBatteryRegulation: boolean;
reachCompliant: boolean;
rohsCompliant: boolean;
certifications: string[];
};
publishedAt: string;
updatedAt: string;
createdAt: string;
}

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 complete DPP object
* @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://app.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) {
const body = await response.json().catch(() => ({}));
throw new DppRateLimitError(body.retryAfter ?? 60);
}

if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(
`Failed to fetch DPP (HTTP ${response.status}): ${body.error ?? 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(`Category: ${dpp.batteryCategory}`);
console.log(`Manufacturer: ${dpp.manufacturer.name} (${dpp.manufacturer.country})`);
console.log(`Version: ${dpp.version}`);
console.log(`Updated: ${dpp.updatedAt}`);

if (dpp.carbonFootprint) {
console.log(`Carbon: ${dpp.carbonFootprint.totalKgCO2ePerKwh} kgCO₂e/kWh`);
}

console.log(`Recycled cobalt: ${dpp.recycledContent.cobaltPercent}%`);
console.log(`Certifications: ${dpp.compliance.certifications.join(', ')}`);

} 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
from dataclasses import dataclass
from typing import Optional


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://app.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:
try:
body = response.json()
retry_after = body.get("retryAfter", 60)
except ValueError:
retry_after = 60
raise DppRateLimitError(retry_after)

if not response.ok:
try:
body = response.json()
error_msg = body.get("error", response.reason)
except ValueError:
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"Category: {dpp['batteryCategory']}")
print(f"Manufacturer: {dpp['manufacturer']['name']} ({dpp['manufacturer']['country']})")
print(f"Version: {dpp['version']}")
print(f"Updated: {dpp['updatedAt']}")

cf = dpp.get("carbonFootprint")
if cf:
print(f"Carbon: {cf['totalKgCO2ePerKwh']} kgCO\u2082e/kWh")

cobalt = dpp["recycledContent"]["cobaltPercent"]
print(f"Recycled cobalt: {cobalt}%")

certs = dpp["compliance"]["certifications"]
print(f"Certifications: {', '.join(certs)}")

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.

To cache efficiently and invalidate correctly:

  1. Store the updatedAt timestamp alongside your cached DPP data
  2. Periodically fetch the DPP and compare updatedAt to your cached value
  3. Only update your cache if updatedAt has changed
interface CachedDpp {
data: DppObject;
cachedAt: number; // Date.now()
updatedAt: 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.updatedAt !== dpp.updatedAt) {
cache.set(slug, { data: dpp, cachedAt: now, updatedAt: dpp.updatedAt });
}

return dpp;
}

Checking DPP status before displaying

The public API only returns published DPPs (drafts return 404). However, a DPP could be unpublished by the manufacturer after you cache it. Always check the status field before rendering:

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

// The API only returns published DPPs, but be defensive
if (dpp.status !== 'published') {
console.warn(`DPP ${slug} has unexpected status: ${dpp.status}`);
return null;
}

return dpp;
} catch (err) {
if (err instanceof DppNotFoundError) {
return null; // product unpublished or removed — show nothing
}
throw err;
}
}