Skip to main content

PoLI access flow

This page walks through a complete PoLI (Person of Legal Interest) integration: submitting an access request, polling for the manufacturer's decision, handling approval and rejection, and using the access token to retrieve restricted DPP fields.

Before using these endpoints, your organisation must have a Traceable account with PoLI access enabled. Contact support@traceable.digital to register.

Overview of the flow

1. POST /api/poli/access        → submit request, receive requestId
2. GET /api/poli/verify → poll for status (pending → approved | rejected)
3. Use restrictedFields data → read restricted DPP fields from the verify response

The manufacturer receives a notification when a request is submitted and must approve or reject it. There is no guaranteed SLA — the response time depends on the manufacturer. The estimatedReviewTime field in the submission response is indicative only.


Step 1 — Submit the access request

interface PoliAccessRequest {
productSlug: string;
requestingEntity: string;
legalBasis: string;
contactEmail: string;
jurisdiction: string;
}

interface PoliAccessResponse {
requestId: string;
status: 'pending';
estimatedReviewTime: string;
}

async function submitPoliRequest(
request: PoliAccessRequest
): Promise<PoliAccessResponse> {
const apiKey = process.env.TRACEABLE_API_KEY;
if (!apiKey) throw new Error('TRACEABLE_API_KEY is not set');

const response = await fetch('https://app.traceable.digital/api/poli/access', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});

if (response.status === 409) {
// A request already exists for this product from this entity.
// Retrieve the existing requestId from the error details.
const data = await response.json();
const existingId: string = data.details?.existingRequestId;
console.log(`Existing request found: ${existingId}`);
return { requestId: existingId, status: 'pending', estimatedReviewTime: 'unknown' };
}

if (response.status === 422) {
const data = await response.json();
throw new Error(`Invalid request: ${JSON.stringify(data.details)}`);
}

if (response.status === 401) {
throw new Error('Invalid or missing API key');
}

if (response.status === 404) {
throw new Error(`Product not found: ${request.productSlug}`);
}

if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(`PoLI submission failed (${response.status}): ${data.error ?? response.statusText}`);
}

return response.json();
}

// Example usage
const submission = await submitPoliRequest({
productSlug: 'swiftvolt-48v-100ah-ev-pack',
requestingEntity: 'Netherlands Authority for Consumers and Markets (ACM)',
legalBasis: 'EU Battery Regulation 2023/1542, Article 74(1) — Market Surveillance Authorities',
contactEmail: 'dpp-access@acm.nl',
jurisdiction: 'NL',
});

console.log(`Request submitted. ID: ${submission.requestId}`);
// Persist submission.requestId — you will need it to poll for status
import os
import requests


def submit_poli_request(
product_slug: str,
requesting_entity: str,
legal_basis: str,
contact_email: str,
jurisdiction: str,
) -> dict:
api_key = os.environ["TRACEABLE_API_KEY"]

payload = {
"productSlug": product_slug,
"requestingEntity": requesting_entity,
"legalBasis": legal_basis,
"contactEmail": contact_email,
"jurisdiction": jurisdiction,
}

response = requests.post(
"https://app.traceable.digital/api/poli/access",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json=payload,
timeout=10,
)

if response.status_code == 409:
# Duplicate — return the existing requestId
data = response.json()
existing_id = data["details"]["existingRequestId"]
print(f"Existing request found: {existing_id}")
return {"requestId": existing_id, "status": "pending"}

if response.status_code == 422:
raise ValueError(f"Invalid request: {response.json().get('details')}")

response.raise_for_status()
return response.json()

Step 2 — Poll for the manufacturer's decision

The manufacturer reviews the request and responds out-of-band. Poll GET /api/poli/verify at a reasonable interval — every 30 minutes is appropriate. Do not poll more frequently than once per minute.

type PoliStatus = 'pending' | 'approved' | 'rejected';

interface PoliVerifyResponse {
requestId: string;
status: PoliStatus;
accessToken: string | null;
accessTokenExpiresAt: string | null;
restrictedFields: Record<string, unknown> | null;
}

async function checkPoliStatus(requestId: string): Promise<PoliVerifyResponse> {
const apiKey = process.env.TRACEABLE_API_KEY;
if (!apiKey) throw new Error('TRACEABLE_API_KEY is not set');

const url = new URL('https://app.traceable.digital/api/poli/verify');
url.searchParams.set('requestId', requestId);

const response = await fetch(url.toString(), {
headers: { 'Authorization': `Bearer ${apiKey}` },
});

if (response.status === 404) {
throw new Error(`Request not found: ${requestId}. Ensure you are using the same API key that submitted the request.`);
}

if (response.status === 403) {
throw new Error('This request was submitted by a different API key. Use the original key to check status.');
}

if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(`Status check failed (${response.status}): ${data.error ?? response.statusText}`);
}

return response.json();
}

// Polling loop with 30-minute interval
async function pollUntilDecision(
requestId: string,
pollIntervalMs = 30 * 60 * 1000, // 30 minutes
maxAttempts = 48 // give up after 24 hours
): Promise<PoliVerifyResponse> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await checkPoliStatus(requestId);

if (result.status === 'approved' || result.status === 'rejected') {
return result;
}

console.log(`[attempt ${attempt + 1}/${maxAttempts}] Status: pending. Polling again in ${pollIntervalMs / 60000} minutes.`);
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
}

throw new Error(`Request ${requestId} still pending after ${maxAttempts} polling attempts.`);
}
import time


def check_poli_status(request_id: str) -> dict:
api_key = os.environ["TRACEABLE_API_KEY"]

response = requests.get(
"https://app.traceable.digital/api/poli/verify",
params={"requestId": request_id},
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)

if response.status_code == 404:
raise ValueError(
f"Request not found: {request_id}. "
"Ensure you are using the same API key that submitted the request."
)

if response.status_code == 403:
raise PermissionError(
"This request was submitted by a different API key."
)

response.raise_for_status()
return response.json()


def poll_until_decision(
request_id: str,
poll_interval_seconds: int = 1800, # 30 minutes
max_attempts: int = 48, # give up after 24 hours
) -> dict:
for attempt in range(max_attempts):
result = check_poli_status(request_id)

if result["status"] in ("approved", "rejected"):
return result

print(
f"[attempt {attempt + 1}/{max_attempts}] Status: pending. "
f"Polling again in {poll_interval_seconds // 60} minutes."
)
time.sleep(poll_interval_seconds)

raise TimeoutError(
f"Request {request_id} still pending after {max_attempts} attempts."
)

Step 3 — Handle approved and rejected outcomes

async function handlePoliDecision(requestId: string): Promise<void> {
const result = await pollUntilDecision(requestId);

if (result.status === 'rejected') {
// Rejection reason is sent to the contactEmail specified at submission.
// The API response does not include the rejection reason.
console.warn(`Request ${requestId} was rejected by the manufacturer.`);
console.warn('Check the contactEmail inbox for the rejection reason.');
return;
}

// status === 'approved'
if (!result.restrictedFields) {
throw new Error('Approved response missing restrictedFields — unexpected API state');
}

// The full restricted field data is available directly in the verify response.
// No additional DPP request is needed for most use cases.
console.log('Restricted fields received:', JSON.stringify(result.restrictedFields, null, 2));

// The accessToken is also available for making additional DPP requests.
// It is valid for 24 hours. After expiry, call verify again — a new token
// is issued automatically as long as the approval remains active.
if (result.accessToken) {
console.log(`Access token valid until: ${result.accessTokenExpiresAt}`);
}
}
def handle_poli_decision(request_id: str) -> None:
result = poll_until_decision(request_id)

if result["status"] == "rejected":
# Rejection reason is sent to the contactEmail specified at submission.
print(f"Request {request_id} was rejected by the manufacturer.")
print("Check the contactEmail inbox for the rejection reason.")
return

# status == "approved"
restricted_fields = result.get("restrictedFields")
if not restricted_fields:
raise RuntimeError("Approved response missing restrictedFields.")

print("Restricted fields received:")
import json
print(json.dumps(restricted_fields, indent=2))

access_token = result.get("accessToken")
if access_token:
print(f"Access token valid until: {result.get('accessTokenExpiresAt')}")

Step 4 — Use the access token for additional DPP requests

If you need to make subsequent requests for the same product's restricted fields (for example, after re-running an analysis), use the access token directly rather than polling verify again.

async function fetchRestrictedDpp(slug: string, accessToken: string): Promise<unknown> {
const apiKey = process.env.TRACEABLE_API_KEY;
if (!apiKey) throw new Error('TRACEABLE_API_KEY is not set');

const url = new URL(`https://app.traceable.digital/api/dpp/${encodeURIComponent(slug)}`);
url.searchParams.set('accessToken', accessToken);

const response = await fetch(url.toString(), {
headers: { 'Authorization': `Bearer ${apiKey}` },
});

if (response.status === 401) {
// Token has expired — call checkPoliStatus again to get a fresh token
throw new Error('Access token expired. Call GET /api/poli/verify to obtain a new token.');
}

if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(`Restricted DPP fetch failed (${response.status}): ${data.error ?? response.statusText}`);
}

return response.json();
}

Access tokens expire after 24 hours. When a token expires, call GET /api/poli/verify again with the original requestId — a new token is issued automatically as long as the manufacturer's approval is still active. If the approval has been revoked, the verify response returns status: 'rejected'.


Complete end-to-end example

async function runPoliFlow(productSlug: string): Promise<void> {
// 1. Submit
const submission = await submitPoliRequest({
productSlug,
requestingEntity: 'Netherlands Authority for Consumers and Markets (ACM)',
legalBasis: 'EU Battery Regulation 2023/1542, Article 74(1) — Market Surveillance Authorities',
contactEmail: 'dpp-access@acm.nl',
jurisdiction: 'NL',
});

const requestId = submission.requestId;
console.log(`Submitted. requestId: ${requestId}`);

// Persist requestId to durable storage before polling —
// the polling loop may span hours or days across process restarts.

// 2. Poll
const decision = await pollUntilDecision(requestId);

// 3. Handle decision
if (decision.status === 'rejected') {
console.warn('Access was rejected. Check your contactEmail for the reason.');
return;
}

console.log('Access approved. Restricted fields:');
console.log(JSON.stringify(decision.restrictedFields, null, 2));
}

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

Error reference for this flow

ScenarioError codeResolution
Product slug does not existPRODUCT_NOT_FOUNDVerify the slug is correct and the DPP is published
Jurisdiction is not an EU member stateUNPROCESSABLE_ENTITYUse a valid EU ISO 3166-1 alpha-2 code (e.g., DE, NL, FR)
A request already exists for this productDUPLICATE_REQUESTUse the existingRequestId from details to check status instead
API key does not match submitting keyFORBIDDENUse the same key that submitted the original request
requestId not foundREQUEST_NOT_FOUNDVerify the requestId value; check you are using the correct API key

See Error codes for the full reference.