/api/v1/passports
API key
List passports in your workspace (paginated JSON-LD)
Returns the non-archived passports of every economic operator bound to your workspace, newest first (createdAt DESC). Operator-scoped API keys only see passports of their bound operator.
Permission: passport:read (read-only — no subscription/402 gate).
Filtering: category and originCountry are exact-match filters on the top-level metadata keys of the same name. Known metadata.category values: textiles, batteries, electronics, cosmetics, toys, iron-steel, aluminium, chemicals, construction; originCountry is ISO 3166-1 alpha-2.
Pagination: page (default 1) and limit (default 10) are numeric strings matching ^[0-9]+$ — any other value is rejected with the framework's default 400 validation body (see 400). Parsed values are clamped server-side to page >= 1 and 1 <= limit <= 100. There is no total count; page until you receive fewer than limit items.
Serialization caveats: - The redaction tier of each item depends on the credential's role: only BRAND_OPERATOR credentials receive the unredacted owner-tier document. Every other role — including TENANT_ADMIN — receives the public tier: facilityDetails (and, for batteries, detailedPerformance / lifecycleAndInUse / circularityAndDisassembly) are masked to the literal string "[REDACTED - Privileged Access Required]". - economicOperator.role is absent from list items and manufacturingFacility is always null here — fetch a single passport (GET /api/v1/passports/{id}) for the facility node and operator role. - The response passes through a declared response schema: top-level keys other than success, page, limit, passports are stripped. Passport items allow additional properties, so undeclared item keys (status, archivedAt, retentionUntil, manufacturingFacility, the flattened metadata keys) pass through intact — but two declared item keys are mangled by their subschemas: the @context term-map object (second array element) is always emptied to {}, and proof is emptied to {} on sealed items (null on unsealed) — signatureValue, merkleRoot, redactedLeaves, x5c and rfc3161 are all stripped from list output. Fetch a single passport (GET /api/v1/passports/{id}) or the public resolver for the verifiable proof block.
Rate limits: global limiter, 100 requests/min per IP (600/min for known crawler user agents); 429 carries x-ratelimit-* headers.
Parameters
| Name | In | Type | Description | |
|---|---|---|---|---|
page |
query | integer | optional | 1-based page number (digits only). default1 · min 1 |
limit |
query | integer | optional | Page size. default10 · min 1 · max 100 |
category |
query | string | optional | Exact-match filter on |
originCountry |
query | string | optional | Exact-match filter on |
Example
curl -X GET "https://opendpp-node.eu/api/v1/passports" \
-H "Authorization: Bearer $OPENDPP_API_KEY"
Responses
Paginated list of JSON-LD passport documents. No total count is returned. Note the list-specific mangling: the @context term map is emptied to {} and proof contents are stripped (see operation description).
Example 200 response
{
"success": true,
"page": 1,
"limit": 10,
"passports": [
{
"@context": [
"https://w3id.org/dpp/context/v1",
{}
],
"@type": "DigitalProductPassport",
"@id": "https://opendpp-node.eu/01/09501101530003/21/9b2fa884-3f1e-4c2a-9d4b-5e6f7a8b9c0d",
"id": "9b2fa884-3f1e-4c2a-9d4b-5e6f7a8b9c0d",
"productId": "09501101530003",
"digitalLinkUri": "https://opendpp-node.eu/01/09501101530003/21/9b2fa884-3f1e-4c2a-9d4b-5e6f7a8b9c0d",
"digitalSeal": null,
"signingPublicKey": null,
"status": "ACTIVE",
"archivedAt": null,
"retentionUntil": null,
"proof": null,
"createdAt": "2026-06-12T09:41:00.000Z",
"updatedAt": "2026-06-12T09:41:00.000Z",
"economicOperator": {
"@type": "EconomicOperator",
"id": "f2d6a9c1-4b3e-4d2a-8c1f-0a9b8c7d6e5f",
"name": "Aurora Textiles GmbH",
"regId": "EU-DEFAULT-001"
},
"manufacturingFacility": null,
"metadata": {
"category": "textiles",
"originCountry": "PT",
"size": "M",
"facilityDetails": "[REDACTED - Privileged Access Required]"
},
"category": "textiles",
"originCountry": "PT",
"size": "M",
"facilityDetails": "[REDACTED - Privileged Access Required]"
}
]
}Route validation failure (framework default body — note statusCode/code keys, no success field): page or limit did not match ^[0-9]+$.
statusCode
integer
required
code
string
error
string
required
message
string
required
Example 400 response
{
"statusCode": 400,
"code": "FST_ERR_VALIDATION",
"error": "Bad Request",
"message": "querystring/page must match pattern \"^[0-9]+$\""
}Missing, invalid, revoked or expired credentials. Send a valid Authorization: Bearer op_dpp_token_… header.
Example 401 response
{
"success": false,
"error": "Unauthorized",
"message": "Missing or invalid authentication. Expected Bearer API key, JWT, or active session cookie."
}Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA session.
Example 403 response
{
"success": false,
"error": "Forbidden",
"message": "Insufficient permissions. Required: \"passport:create\"."
}Global rate limit exceeded (100 requests/min per IP). Inspect the x-ratelimit-* headers and retry after the indicated window.
x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset, retry-afterFastify rate-limit plugin default body.
statusCode
integer
required
code
string
error
string
required
message
string
required
Example 429 response
{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded, retry in 1 minute"
}Unexpected server error. Unhandled errors are normalized by the global error handler to this envelope with a generic message; some routes catch their own failures and return the same envelope with a route-specific message. Details are logged server-side and never returned.
Example 500 response
{
"success": false,
"error": "Internal Server Error",
"message": "An unexpected error occurred"
}