# OpenDPP Integration API — full knowledge bundle (OKF v0.1) > This file concatenates the entire OpenDPP knowledge bundle for one-shot ingestion by AI agents. The > canonical, individually-addressable concepts live under https://opendpp-node.eu/okf/ and at > https://opendpp-node.eu/openapi.json. API version: 1.4.0. ================================================================================ # FILE: /okf/overview.md ================================================================================ --- type: Reference title: OpenDPP Integration API — overview description: What the OpenDPP Integration API does, how it is organised, and where to start. resource: https://opendpp-node.eu/api-reference tags: - overview - getting started - ESPR - EU Battery Regulation timestamp: 2026-06-17T00:00:00Z --- OpenDPP is a B2B platform for **EU Digital Product Passports (DPPs)**, aligned with the data requirements of the ESPR (Regulation (EU) 2024/1781) and the EU Battery Regulation (Regulation (EU) 2023/1542). This knowledge bundle is a machine-readable projection of the **public integration surface**: everything an external system needs to create, validate, seal, publish, resolve and verify passports. It is generated from the same OpenAPI 3.1 document served at [`/openapi.json`](https://opendpp-node.eu/openapi.json); the human-readable reference is at [`/api-reference`](https://opendpp-node.eu/api-reference). # What you can do - **Create & validate** SKU/type-level passports against ESPR category rules — see [createPassport](/operations/createPassport.md) and the dry-run [validatePassport](/operations/validatePassport.md). - **Seal** a passport with an eIDAS **advanced** electronic seal and **verify** it offline — see [sealPassport](/operations/sealPassport.md) and [verifyPassportSeal](/operations/verifyPassportSeal.md), and the [sealing & verification](/guides/sealing-and-verification.md) playbook. - **Serialise** individual battery units (GS1 AI 21) and append telemetry — the [Battery Units](/tags/battery-units.md) tag. - **Resolve** a passport publicly with content negotiation (JSON-LD / AAS / UNTP credential / HTML) — the [Public Resolution](/tags/public-resolution.md) tag. - **Subscribe** to lifecycle events over signed webhooks — the [Webhooks](/tags/webhooks.md) tag. # How this bundle is organised | Directory | Contents | |-----------|----------| | [`guides/`](/guides/index.md) | Curated integration playbooks and references (auth, errors, rate limits, access tiers, sealing, interop, conformance). | | [`tags/`](/tags/index.md) | One overview per API domain, each linking to its operations. | | [`operations/`](/operations/index.md) | One concept per endpoint (method, path, parameters, request/response shapes, a curl example). | | [`schemas/`](/schemas/index.md) | One concept per request/response object, with its JSON Schema and the operations that use it. | | [`webhooks/`](/webhooks/index.md) | The lifecycle events you can subscribe to. | # Four URL zones The API spans four kinds of path: the versioned `/api/v1/*` management surface (the bulk), unversioned **public resolution** roots (`/passport`, `/01`, `/8003`, `/unit`, `/context/v1`), a `/.well-known/` certificate path, and the ops endpoints (`/health`, `/api/v1/version`). Start with [authentication](/guides/authentication.md). ================================================================================ # FILE: /okf/guides/index.md ================================================================================ # Guides * [Authentication & tenancy](authentication.md) - Authenticate with a tenant API key as a Bearer token; tenant identity is token-bound. * [Error model](errors.md) - Error envelopes returned by the API, including the richer ESPR validation-failure shape. * [Rate limits](rate-limits.md) - Per-IP request limits and the response headers that advertise them. * [Public access tiers](public-access-tiers.md) - How one resolution URL serves tiered views via capability tokens (Battery Reg. Art. 77(9)). * [Sealing & verification](sealing-and-verification.md) - Apply an eIDAS advanced electronic seal and verify it offline from the redacted document. * [Interop: AAS & UNTP credentials](interop-aas-untp.md) - The standards-conformant projections OpenDPP serves alongside JSON-LD, and where to validate them. * [Conformance honesty](conformance.md) - What OpenDPP's output conforms to today, what is partial, and what is on the roadmap. ================================================================================ # FILE: /okf/guides/authentication.md ================================================================================ --- type: Playbook title: Authentication & tenancy description: Authenticate with a tenant API key as a Bearer token; tenant identity is token-bound. resource: https://opendpp-node.eu/api-reference tags: - authentication - api keys - tenancy - security timestamp: 2026-06-17T00:00:00Z --- Authenticate with a tenant **API key** sent as a Bearer token: ``` Authorization: Bearer op_dpp_token_… ``` Keys are created in the Client Console (Developers → API keys), are shown **once** at creation, carry a role plus optional narrowed permissions and optional expiry, and can be revoked at any time. API-key clients are exempt from CSRF requirements. Public endpoints (the [Public Resolution](/tags/public-resolution.md) tag, the public validators, and the audit verifier) need no credentials — see [public access tiers](/guides/public-access-tiers.md). # Tenancy Tenant identity is **token-bound** — it is derived from your API key, never from the request host. The same paths work on the apex host (`https://opendpp-node.eu`) and on tenant workspace hosts (`https://.opendpp-node.eu`); when a workspace host is used it must match the key's tenant, or the request is rejected with `403`. # Check your identity Call [whoami](/operations/whoami.md) to see the workspace, role, permissions, operator scope, and passport usage against your tier quota for the presented key. # Write-operation gates Write operations are subject to subscription gating (**402** when a workspace subscription has lapsed) and, where the workspace enforces it, MFA (**403**). Cookie-authenticated browser sessions must additionally send the `X-CSRF-Token` double-submit header; Bearer API-key clients do not. ================================================================================ # FILE: /okf/guides/errors.md ================================================================================ --- type: Reference title: Error model description: Error envelopes returned by the API, including the richer ESPR validation-failure shape. resource: https://opendpp-node.eu/api-reference tags: - errors - validation - troubleshooting timestamp: 2026-06-17T00:00:00Z --- Authenticated endpoints return `{ success: false, error, message }` (some endpoints, and all public-resolution errors, omit `success` and return only `error` + `message`). # Validation failures ESPR metadata validation failures return a richer shape with per-field `errors[]` (plus `warnings[]` when any exist — the key is omitted entirely when there are none). `friendlyMessage` texts are localizable via `?lang=` or `Accept-Language` (28 languages, default `en`). See [ValidationErrorItem](/schemas/ValidationErrorItem.md) and the dry-run validators [validatePassport](/operations/validatePassport.md) / [validatePassportPublic](/operations/validatePassportPublic.md). Bulk endpoints report row-level problems as `errors: string[]`. Malformed JSON and query-string violations return Fastify's default `{ statusCode, code, error, message }` body. # Status codes you will encounter | Status | Meaning | |--------|---------| | `400` | Bad request or ESPR validation failed. | | `401` | Missing/invalid/revoked/expired credentials. | | `402` | Workspace subscription lapsed — writes blocked, reads unaffected. | | `403` | Authenticated but not allowed (permission, cross-workspace, or MFA-gated write). | | `404` | Resource not found or not visible to your workspace. | | `409` | Duplicate (e.g. a `(productId, operatorId)` pair that already exists). | | `413` | Request body over the per-endpoint limit. | | `429` | Rate limit exceeded — see [rate limits](/guides/rate-limits.md). | | `5xx` | Unexpected server error; details are logged server-side and never returned. | ================================================================================ # FILE: /okf/guides/rate-limits.md ================================================================================ --- type: Reference title: Rate limits description: Per-IP request limits and the response headers that advertise them. resource: https://opendpp-node.eu/api-reference tags: - rate limits - throttling - headers timestamp: 2026-06-17T00:00:00Z --- | Surface | Limit | Headers | |---------|-------|---------| | Global (all endpoints) | **100 requests/min per IP** (higher for verified crawlers) | `x-ratelimit-limit`, `x-ratelimit-remaining`, `x-ratelimit-reset` | | Public passport resolution | **30 requests/min per IP** | none | | Public validator | **10 requests/min per IP** | none | Stay under these limits with client-side queueing. On `429`, inspect the `x-ratelimit-*` headers (where present) and back off until the indicated window resets. With `Accept: text/html`, the public resolver returns an HTML page on `429` instead of JSON. ================================================================================ # FILE: /okf/guides/public-access-tiers.md ================================================================================ --- type: Reference title: Public access tiers description: How one resolution URL serves tiered views via capability tokens (Battery Reg. Art. 77(9)). resource: https://opendpp-node.eu/api-reference tags: - access - tiers - capability tokens - legitimate interest timestamp: 2026-06-17T00:00:00Z --- Public resolution endpoints serve **tiered** views of the same URL: - **Public tier** — anonymous callers see the public, redacted view. - **Legitimate-interest tier** — holders of a `dpp_li_…` capability token see the restricted fields unlocked under Battery Regulation Art. 77(9) (e.g. for batteries, Annex XIII parts 2–4: `detailedPerformance`, `lifecycleAndInUse`, `circularityAndDisassembly`). - **Authority tier** — holders of a `dpp_auth_…` token see the authority view. - **Owner tier** — the issuing tenant's own credentials see everything. Capability tokens are presented as a Bearer token or a `?grant=` query parameter. Third parties request access via the hosted request page; issuers manage grants through the [Access Grants](/tags/access-grants.md) tag ([createGrant](/operations/createGrant.md), [approveGrantRequest](/operations/approveGrantRequest.md), [revokeGrant](/operations/revokeGrant.md)). Privileged fields such as facility street addresses are never published at the public tier. ================================================================================ # FILE: /okf/guides/sealing-and-verification.md ================================================================================ --- type: Playbook title: Sealing & verification description: Apply an eIDAS advanced electronic seal and verify it offline from the redacted document. resource: https://opendpp-node.eu/security tags: - sealing - verification - eIDAS - merkle - RFC 3161 timestamp: 2026-06-17T00:00:00Z --- OpenDPP passport seals are **eIDAS advanced electronic seals**: an ECDSA P-256 signature over a Merkle root of the passport content, with an optional RFC 3161 timestamp. (This is an *advanced* seal — a *qualified* seal would require a QTSP; OpenDPP does not issue qualified seals.) # Seal a passport Call [sealPassport](/operations/sealPassport.md) with `passport:seal`. The tenant signing key is generated and held server-side in an encrypted vault and is never returned; rotate it with [rotateTenantKeys](/operations/rotateTenantKeys.md). # Verify a seal Anyone can verify a seal — no account required. [verifyPassportSeal](/operations/verifyPassportSeal.md) recomputes every Merkle leaf from the submitted values, so it requires the **unredacted** document (caller-supplied redacted-leaf hashes are deliberately not trusted) and checks that the signing workspace is bound to the economic operator declared in the payload. # Offline verification of redacted documents Redacted documents stay verifiable **offline**: masked fields keep their true leaf hashes in `proof.redactedLeaves` (see [MerkleTreeAttestationProof](/schemas/MerkleTreeAttestationProof.md)), letting any verifier rebuild the sealed root without the privileged values. Download the platform seal-CA certificate from [getSealCaCertificate](/operations/getSealCaCertificate.md). > The legacy passport seal is a vendor **MerkleTreeAttestationProof**. The standards-conformant > credential is a *separate* artifact — see [interop: AAS & UNTP](/guides/interop-aas-untp.md). ================================================================================ # FILE: /okf/guides/interop-aas-untp.md ================================================================================ --- type: Reference title: "Interop: AAS & UNTP credentials" description: The standards-conformant projections OpenDPP serves alongside JSON-LD, and where to validate them. resource: https://github.com/OpenDPP/opendpp-interop tags: - interop - AAS - IDTA - UNTP - data integrity timestamp: 2026-06-17T00:00:00Z --- Public resolution is **content-negotiated**: one URL serves several representations, so a passport never drifts between formats. Set the `Accept` header on [resolvePublicPassport](/operations/resolvePublicPassport.md), the GS1 resolvers, or [getPassport](/operations/getPassport.md): | `Accept` | Representation | |----------|----------------| | (default) | W3C JSON-LD passport document | | `application/aas+json` | Asset Administration Shell (AAS) v3.0 Environment, validated against the official IDTA-01001-3-1 schema | | `application/vc+jwt` | A conformant UNTP DigitalProductPassport credential, signed as an enveloping JOSE `vc+jwt` (ES256) | | `application/vc+ld+json` | The same credential with an embedded W3C **Data Integrity** proof (cryptosuite `ecdsa-jcs-2019`, RFC 8785 JCS, multibase base58btc) | | `text/html` | The human passport page | The per-unit resolver [resolvePublicBatteryUnit](/operations/resolvePublicBatteryUnit.md) also serves the item-granularity `vc+jwt` and `vc+ld+json` counterparts. # Validate it yourself The interoperability boundary — the official AAS + UNTP schemas, live-reproducible samples, an offline conformance validator, and the AAS + UNTP field mappings — is **open source** at [github.com/OpenDPP/opendpp-interop](https://github.com/OpenDPP/opendpp-interop) (Apache-2.0). It lets any integrator validate and verify OpenDPP's standards-conformant output without access to the product source. See also the [conformance](/guides/conformance.md) honesty matrix. ================================================================================ # FILE: /okf/guides/conformance.md ================================================================================ --- type: Reference title: Conformance honesty description: What OpenDPP's output conforms to today, what is partial, and what is on the roadmap. resource: https://github.com/OpenDPP/opendpp-interop tags: - conformance - standards - honesty timestamp: 2026-06-17T00:00:00Z --- OpenDPP is deliberately precise about what it claims. In short: - **ESPR / EU Battery Regulation** — the platform is built for **readiness** against the ESPR data requirements and the EU Battery Regulation. Whether a given product is *compliant* is the operator's determination, not OpenDPP's. - **AAS (IDTA)** — the AAS v3.0 Environment is CI-validated against the official IDTA-01001-3-1 schema. Concepts OpenDPP coins itself are honestly namespaced `urn:opendpp:*`, never presented as eCl@ss. - **UNTP / W3C Data Integrity** — the per-passport credential is a conformant UNTP DigitalProductPassport credential, emitted both as an enveloping `vc+jwt` and as an embedded `ecdsa-jcs-2019` Data Integrity proof, and CI-validated against the official UNTP DPP schema and the off-the-shelf `@digitalbazaar` cryptosuite. - **The legacy passport seal** is a vendor `MerkleTreeAttestationProof` — an eIDAS *advanced* (not qualified) seal. It is never presented as the conformant credential; that is a separate artifact. The authoritative per-path conform / partial / roadmap matrix lives in the open-source interop kit at [github.com/OpenDPP/opendpp-interop](https://github.com/OpenDPP/opendpp-interop). No competent authority certifies, verifies, or approves a product or passport through OpenDPP. ================================================================================ # FILE: /okf/tags/index.md ================================================================================ # API domains * [Passports](passports.md) - Create, validate, read, update, seal and manage the lifecycle of Digital Product Passports. * [Economic Operators](economic-operators.md) - Register and manage the economic operators (manufacturers/brands, identified by EORI or national registry id) that passports are issued on behalf of. * [Battery Units](battery-units.md) - Per-unit battery serialization (real serials, GS1 AI 21) under a SKU-level passport, plus append-only telemetry events (state of health, charge cycles, status… * [Facilities](facilities.md) - Manufacturing facility master data, identified by GS1 GLN-13 (the Unique Facility Identifier). * [Access Grants](access-grants.md) - Capability tokens implementing tiered access (Battery Regulation Art. * [Webhooks](webhooks.md) - Subscribe HTTPS endpoints to passport lifecycle events. * [Traceability & Audit](traceability-audit.md) - UNTP/EPCIS supply-chain traceability events, lineage queries, and the public seal verifier. * [Public Resolution](public-resolution.md) - Unauthenticated, content-negotiated passport resolution: GS1 Digital Link paths, passport and unit pages. * [Schemas & Vocabulary](schemas-vocabulary.md) - Machine-readable contracts: per-category ESPR JSON Schemas, the W3C JSON-LD context, and the curated materials vocabulary. * [QR Codes](qr-codes.md) - Export GS1-Digital-Link QR codes (PNG/SVG, 128–2048 px, GS1 quiet zone) for passports and battery units. * [eIDAS Keys](eidas-keys.md) - Tenant signing-key management. * [Account](account.md) - Identity of the authenticated API key / session: workspace, role, permissions, operator scope, and passport usage against the tier quota — the integration-faci… * [Service](service.md) - Service metadata and liveness. ================================================================================ # FILE: /okf/tags/passports.md ================================================================================ --- type: Reference title: Passports description: Create, validate, read, update, seal and manage the lifecycle of Digital Product Passports. resource: https://opendpp-node.eu/api-reference tags: - api domain - passports timestamp: 2026-06-17T00:00:00Z --- Create, validate, read, update, seal and manage the lifecycle of Digital Product Passports. Passport metadata is category-specific: machine-readable JSON Schemas are served live at `GET /api/v1/schemas/{category}` for textiles, batteries, electronics, chemicals and construction; the remaining categories (cosmetics, toys, iron-steel, aluminium) are validated by built-in rules — use the dry-run validators to check payloads for any category. ## Operations - [listPassports](/operations/listPassports.md) — `GET /api/v1/passports` — List passports in your workspace (paginated JSON-LD) - [createPassport](/operations/createPassport.md) — `POST /api/v1/passports` — Create (ingest) a Digital Product Passport - [validatePassport](/operations/validatePassport.md) — `POST /api/v1/passports/validate-only` — Dry-run ESPR validation of passport metadata (nothing is stored) - [validatePassportPublic](/operations/validatePassportPublic.md) — `POST /api/v1/passports/validate-only-public` — Public dry-run ESPR metadata validation (strictly rate-limited) - [bulkIngestPassports](/operations/bulkIngestPassports.md) — `POST /api/v1/passports/bulk` — Bulk-ingest up to 200 passports with per-row error reporting - [ingestPassportFromAas](/operations/ingestPassportFromAas.md) — `POST /api/v1/passports/aas/ingest` — Ingest a passport from an AAS JSON Environment (seal-verified) - [getPassport](/operations/getPassport.md) — `GET /api/v1/passports/{id}` — Fetch a single passport (content-negotiated JSON-LD / AAS / HTML) - [updatePassport](/operations/updatePassport.md) — `PUT /api/v1/passports/{id}` — Update passport metadata (versioned to history) - [deleteDraftPassport](/operations/deleteDraftPassport.md) — `DELETE /api/v1/passports/{id}` — Permanently delete a DRAFT passport - [sealPassport](/operations/sealPassport.md) — `POST /api/v1/passports/{id}/seal` — Apply the tenant's eIDAS advanced electronic seal - [updatePassportStatus](/operations/updatePassportStatus.md) — `PUT /api/v1/passports/{id}/status` — Transition passport lifecycle status (recall / decommission / reactivate) ================================================================================ # FILE: /okf/tags/economic-operators.md ================================================================================ --- type: Reference title: Economic Operators description: Register and manage the economic operators (manufacturers/brands, identified by EORI or national registry id) that passports are issued on behalf of. resource: https://opendpp-node.eu/api-reference tags: - api domain - economic-operators timestamp: 2026-06-17T00:00:00Z --- Register and manage the economic operators (manufacturers/brands, identified by EORI or national registry id) that passports are issued on behalf of. Operators with passports are archived — never hard-deleted — to preserve passport resolvability (ESPR Art. 9(2)). ## Operations - [listOperators](/operations/listOperators.md) — `GET /api/v1/operators` — List economic operators bound to your workspace - [registerOperator](/operations/registerOperator.md) — `POST /api/v1/operators` — Register an economic operator and bind it to your workspace - [getOperator](/operations/getOperator.md) — `GET /api/v1/operators/{id}` — Fetch a single bound economic operator - [updateOperator](/operations/updateOperator.md) — `PATCH /api/v1/operators/{id}` — Update an operator's name or role (regId is immutable) - [deleteOperator](/operations/deleteOperator.md) — `DELETE /api/v1/operators/{id}` — Remove an operator (archives if it has passports, else hard-deletes) - [restoreOperator](/operations/restoreOperator.md) — `POST /api/v1/operators/{id}/restore` — Restore an archived operator and its archived passports ================================================================================ # FILE: /okf/tags/battery-units.md ================================================================================ --- type: Reference title: Battery Units description: Per-unit battery serialization (real serials, GS1 AI 21) under a SKU-level passport, plus append-only telemetry events (state of health, charge cycles, status changes) per the EU Battery Regulation. resource: https://opendpp-node.eu/api-reference tags: - api domain - battery-units timestamp: 2026-06-17T00:00:00Z --- Per-unit battery serialization (real serials, GS1 AI 21) under a SKU-level passport, plus append-only telemetry events (state of health, charge cycles, status changes) per the EU Battery Regulation. ## Operations - [listBatteryUnits](/operations/listBatteryUnits.md) — `GET /api/v1/passports/{passportId}/units` — List serialised battery units under a passport - [serializeBatteryUnits](/operations/serializeBatteryUnits.md) — `POST /api/v1/passports/{passportId}/units` — Serialise individual battery units under a passport (bulk, up to 200) - [getBatteryUnit](/operations/getBatteryUnit.md) — `GET /api/v1/units/{id}` — Get one battery unit as JSON-LD with its dynamic-data history - [deleteBatteryUnit](/operations/deleteBatteryUnit.md) — `DELETE /api/v1/units/{id}` — Permanently delete a battery unit and its telemetry - [listBatteryUnitEvents](/operations/listBatteryUnitEvents.md) — `GET /api/v1/units/{id}/events` — List a battery unit's telemetry history (newest first, max 500) - [recordBatteryUnitEvent](/operations/recordBatteryUnitEvent.md) — `POST /api/v1/units/{id}/events` — Append an immutable telemetry event to a battery unit ================================================================================ # FILE: /okf/tags/facilities.md ================================================================================ --- type: Reference title: Facilities description: Manufacturing facility master data, identified by GS1 GLN-13 (the Unique Facility Identifier). resource: https://opendpp-node.eu/api-reference tags: - api domain - facilities timestamp: 2026-06-17T00:00:00Z --- Manufacturing facility master data, identified by GS1 GLN-13 (the Unique Facility Identifier). GLN, name, activity and country are public in passport documents; street addresses are never published. ## Operations - [listFacilities](/operations/listFacilities.md) — `GET /api/v1/facilities` — List facilities in the tenant workspace - [createFacility](/operations/createFacility.md) — `POST /api/v1/facilities` — Register a facility (GS1 GLN) - [getFacility](/operations/getFacility.md) — `GET /api/v1/facilities/{id}` — Get a single facility - [updateFacility](/operations/updateFacility.md) — `PUT /api/v1/facilities/{id}` — Update facility master data (GLN is immutable) - [deleteFacility](/operations/deleteFacility.md) — `DELETE /api/v1/facilities/{id}` — Delete a facility (passports are unlinked, never deleted) ================================================================================ # FILE: /okf/tags/access-grants.md ================================================================================ --- type: Reference title: Access Grants description: Capability tokens implementing tiered access (Battery Regulation Art. resource: https://opendpp-node.eu/api-reference tags: - api domain - access-grants timestamp: 2026-06-17T00:00:00Z --- Capability tokens implementing tiered access (Battery Regulation Art. 77(9) legitimate-interest access): issue, approve, deny and revoke `dpp_li_…` / `dpp_auth_…` tokens. Third parties request access via the hosted request page; granted tokens unlock restricted fields on the public resolution endpoints (Bearer or `?grant=`). ## Operations - [listGrants](/operations/listGrants.md) — `GET /api/v1/grants` — List access grants and pending access requests - [createGrant](/operations/createGrant.md) — `POST /api/v1/grants` — Issue a legitimate-interest access grant directly - [approveGrantRequest](/operations/approveGrantRequest.md) — `POST /api/v1/grants/{id}/approve` — Approve a pending access request and mint its token - [denyGrantRequest](/operations/denyGrantRequest.md) — `POST /api/v1/grants/{id}/deny` — Deny a pending access request - [revokeGrant](/operations/revokeGrant.md) — `DELETE /api/v1/grants/{id}` — Revoke an access grant (soft revocation) ================================================================================ # FILE: /okf/tags/webhooks.md ================================================================================ --- type: Reference title: Webhooks description: Subscribe HTTPS endpoints to passport lifecycle events. resource: https://opendpp-node.eu/api-reference tags: - api domain - webhooks timestamp: 2026-06-17T00:00:00Z --- Subscribe HTTPS endpoints to passport lifecycle events. Deliveries are HMAC-SHA256-signed POSTs with retry/backoff — see the webhooks section of this document for the signature scheme and payloads. ## Operations - [listWebhookSubscriptions](/operations/listWebhookSubscriptions.md) — `GET /api/v1/webhooks/subscriptions` — List webhook subscriptions (signing secrets stripped) - [createWebhookSubscription](/operations/createWebhookSubscription.md) — `POST /api/v1/webhooks/subscriptions` — Register a webhook subscription (signing secret returned once) - [updateWebhookSubscription](/operations/updateWebhookSubscription.md) — `PATCH /api/v1/webhooks/subscriptions/{id}` — Update a webhook subscription (url / events / active) - [deleteWebhookSubscription](/operations/deleteWebhookSubscription.md) — `DELETE /api/v1/webhooks/subscriptions/{id}` — Delete a webhook subscription - [rotateWebhookSecret](/operations/rotateWebhookSecret.md) — `POST /api/v1/webhooks/subscriptions/{id}/rotate-secret` — Rotate a webhook subscription's signing secret - [listWebhookDeliveries](/operations/listWebhookDeliveries.md) — `GET /api/v1/webhooks/deliveries` — List recent webhook delivery attempts (the outbox) - [testWebhookSubscription](/operations/testWebhookSubscription.md) — `POST /api/v1/webhooks/subscriptions/{id}/test` — Send a signed test event to a subscription ================================================================================ # FILE: /okf/tags/traceability-audit.md ================================================================================ --- type: Reference title: Traceability & Audit description: UNTP/EPCIS supply-chain traceability events, lineage queries, and the public seal verifier. resource: https://opendpp-node.eu/api-reference tags: - api domain - traceability-audit timestamp: 2026-06-17T00:00:00Z --- UNTP/EPCIS supply-chain traceability events, lineage queries, and the public seal verifier. The verifier checks the cryptographic seal AND that the signing workspace is bound to the economic operator declared in the payload. ## Operations - [registerTraceabilityEvent](/operations/registerTraceabilityEvent.md) — `POST /api/v1/events` — Register a UNTP/EPCIS 2.0 traceability event (VC-shaped) - [getEventLineage](/operations/getEventLineage.md) — `GET /api/v1/events/{id}/lineage` — Retrieve the upstream pedigree of an event as a recursive lineage DAG - [auditEventLineage](/operations/auditEventLineage.md) — `POST /api/v1/events/{id}/audit` — Run heuristic UFLPA/EUDR compliance screening over an event's lineage - [verifyPassportSeal](/operations/verifyPassportSeal.md) — `POST /api/v1/audit/verify` — Publicly verify a passport's eIDAS seal, certificate chain and timestamp ================================================================================ # FILE: /okf/tags/public-resolution.md ================================================================================ --- type: Reference title: Public Resolution description: "Unauthenticated, content-negotiated passport resolution: GS1 Digital Link paths, passport and unit pages." resource: https://opendpp-node.eu/api-reference tags: - api domain - public-resolution timestamp: 2026-06-17T00:00:00Z --- Unauthenticated, content-negotiated passport resolution: GS1 Digital Link paths, passport and unit pages. One URL serves JSON-LD (default), an AAS environment (`Accept: application/aas+json`), a signed UNTP Verifiable Credential (`Accept: application/vc+jwt` enveloping, or `application/vc+ld+json` embedded `ecdsa-jcs-2019` Data Integrity; item-level on `/unit/:id`), or HTML (`Accept: text/html`). Tiered by optional credentials or grant tokens. Rate limit: 30 requests/min per IP. ## Operations - [resolvePublicPassport](/operations/resolvePublicPassport.md) — `GET /passport/{id}` — Resolve a passport by UUID (JSON-LD / AAS / HTML) - [resolveGs1Gtin](/operations/resolveGs1Gtin.md) — `GET /01/{gtin14}` — GS1 Digital Link resolution by GTIN-14 (AI 01) - [resolveGs1GtinSerial](/operations/resolveGs1GtinSerial.md) — `GET /01/{gtin14}/21/{serial}` — GS1 Digital Link serialised-item redirect (AI 01 + AI 21) - [resolveGs1Grai](/operations/resolveGs1Grai.md) — `GET /8003/{grai}` — GS1 Digital Link resolution by GRAI (AI 8003) - [resolvePublicBatteryUnit](/operations/resolvePublicBatteryUnit.md) — `GET /unit/{id}` — Resolve an individual serialised battery unit ================================================================================ # FILE: /okf/tags/schemas-vocabulary.md ================================================================================ --- type: Reference title: Schemas & Vocabulary description: "Machine-readable contracts: per-category ESPR JSON Schemas, the W3C JSON-LD context, and the curated materials vocabulary." resource: https://opendpp-node.eu/api-reference tags: - api domain - schemas-vocabulary timestamp: 2026-06-17T00:00:00Z --- Machine-readable contracts: per-category ESPR JSON Schemas, the W3C JSON-LD context, and the curated materials vocabulary. ## Operations - [getSectorSchema](/operations/getSectorSchema.md) — `GET /api/v1/schemas/{category}` — Get the ESPR metadata schema for a product category - [getJsonLdContext](/operations/getJsonLdContext.md) — `GET /context/v1` — W3C JSON-LD context document for passport terms - [listMaterials](/operations/listMaterials.md) — `GET /api/v1/materials` — List the platform-curated material vocabulary ================================================================================ # FILE: /okf/tags/qr-codes.md ================================================================================ --- type: Reference title: QR Codes description: Export GS1-Digital-Link QR codes (PNG/SVG, 128–2048 px, GS1 quiet zone) for passports and battery units. resource: https://opendpp-node.eu/api-reference tags: - api domain - qr-codes timestamp: 2026-06-17T00:00:00Z --- Export GS1-Digital-Link QR codes (PNG/SVG, 128–2048 px, GS1 quiet zone) for passports and battery units. ## Operations - [getPassportQrCode](/operations/getPassportQrCode.md) — `GET /api/v1/passports/{id}/qr` — Export a print-grade GS1 Digital Link QR code for a passport - [getBatteryUnitQrCode](/operations/getBatteryUnitQrCode.md) — `GET /api/v1/units/{id}/qr` — Export a print-grade QR code for an individual battery unit ================================================================================ # FILE: /okf/tags/eidas-keys.md ================================================================================ --- type: Reference title: eIDAS Keys description: Tenant signing-key management. resource: https://opendpp-node.eu/api-reference tags: - api domain - eidas-keys timestamp: 2026-06-17T00:00:00Z --- Tenant signing-key management. Keys are generated and held server-side in an encrypted vault; private key material is never returned by any endpoint. ## Operations - [rotateTenantKeys](/operations/rotateTenantKeys.md) — `POST /api/v1/tenants/rotate-keys` — Rotate the tenant's eIDAS ECDSA signing key pair - [getSealCaCertificate](/operations/getSealCaCertificate.md) — `GET /.well-known/opendpp-seal-ca.pem` — Download the platform seal-CA certificate (PEM) ================================================================================ # FILE: /okf/tags/account.md ================================================================================ --- type: Reference title: Account description: "Identity of the authenticated API key / session: workspace, role, permissions, operator scope, and passport usage against the tier quota — the integration-facing counterpart to the console's profile endpoints." resource: https://opendpp-node.eu/api-reference tags: - api domain - account timestamp: 2026-06-17T00:00:00Z --- Identity of the authenticated API key / session: workspace, role, permissions, operator scope, and passport usage against the tier quota — the integration-facing counterpart to the console's profile endpoints. ## Operations - [whoami](/operations/whoami.md) — `GET /api/v1/whoami` — Identity of the authenticated key / session ================================================================================ # FILE: /okf/tags/service.md ================================================================================ --- type: Reference title: Service description: Service metadata and liveness. resource: https://opendpp-node.eu/api-reference tags: - api domain - service timestamp: 2026-06-17T00:00:00Z --- Service metadata and liveness. ## Operations - [getHealth](/operations/getHealth.md) — `GET /health` — Service health check - [getApiVersion](/operations/getApiVersion.md) — `GET /api/v1/version` — Running API contract version & build identity ================================================================================ # FILE: /okf/operations/index.md ================================================================================ # Passports * [GET /api/v1/passports](listPassports.md) - List passports in your workspace (paginated JSON-LD) * [POST /api/v1/passports](createPassport.md) - Create (ingest) a Digital Product Passport * [POST /api/v1/passports/validate-only](validatePassport.md) - Dry-run ESPR validation of passport metadata (nothing is stored) * [POST /api/v1/passports/validate-only-public](validatePassportPublic.md) - Public dry-run ESPR metadata validation (strictly rate-limited) * [POST /api/v1/passports/bulk](bulkIngestPassports.md) - Bulk-ingest up to 200 passports with per-row error reporting * [POST /api/v1/passports/aas/ingest](ingestPassportFromAas.md) - Ingest a passport from an AAS JSON Environment (seal-verified) * [GET /api/v1/passports/{id}](getPassport.md) - Fetch a single passport (content-negotiated JSON-LD / AAS / HTML) * [PUT /api/v1/passports/{id}](updatePassport.md) - Update passport metadata (versioned to history) * [DELETE /api/v1/passports/{id}](deleteDraftPassport.md) - Permanently delete a DRAFT passport * [POST /api/v1/passports/{id}/seal](sealPassport.md) - Apply the tenant's eIDAS advanced electronic seal * [PUT /api/v1/passports/{id}/status](updatePassportStatus.md) - Transition passport lifecycle status (recall / decommission / reactivate) # Economic Operators * [GET /api/v1/operators](listOperators.md) - List economic operators bound to your workspace * [POST /api/v1/operators](registerOperator.md) - Register an economic operator and bind it to your workspace * [GET /api/v1/operators/{id}](getOperator.md) - Fetch a single bound economic operator * [PATCH /api/v1/operators/{id}](updateOperator.md) - Update an operator's name or role (regId is immutable) * [DELETE /api/v1/operators/{id}](deleteOperator.md) - Remove an operator (archives if it has passports, else hard-deletes) * [POST /api/v1/operators/{id}/restore](restoreOperator.md) - Restore an archived operator and its archived passports # Battery Units * [GET /api/v1/passports/{passportId}/units](listBatteryUnits.md) - List serialised battery units under a passport * [POST /api/v1/passports/{passportId}/units](serializeBatteryUnits.md) - Serialise individual battery units under a passport (bulk, up to 200) * [GET /api/v1/units/{id}](getBatteryUnit.md) - Get one battery unit as JSON-LD with its dynamic-data history * [DELETE /api/v1/units/{id}](deleteBatteryUnit.md) - Permanently delete a battery unit and its telemetry * [GET /api/v1/units/{id}/events](listBatteryUnitEvents.md) - List a battery unit's telemetry history (newest first, max 500) * [POST /api/v1/units/{id}/events](recordBatteryUnitEvent.md) - Append an immutable telemetry event to a battery unit # Facilities * [GET /api/v1/facilities](listFacilities.md) - List facilities in the tenant workspace * [POST /api/v1/facilities](createFacility.md) - Register a facility (GS1 GLN) * [GET /api/v1/facilities/{id}](getFacility.md) - Get a single facility * [PUT /api/v1/facilities/{id}](updateFacility.md) - Update facility master data (GLN is immutable) * [DELETE /api/v1/facilities/{id}](deleteFacility.md) - Delete a facility (passports are unlinked, never deleted) # Access Grants * [GET /api/v1/grants](listGrants.md) - List access grants and pending access requests * [POST /api/v1/grants](createGrant.md) - Issue a legitimate-interest access grant directly * [POST /api/v1/grants/{id}/approve](approveGrantRequest.md) - Approve a pending access request and mint its token * [POST /api/v1/grants/{id}/deny](denyGrantRequest.md) - Deny a pending access request * [DELETE /api/v1/grants/{id}](revokeGrant.md) - Revoke an access grant (soft revocation) # Webhooks * [GET /api/v1/webhooks/subscriptions](listWebhookSubscriptions.md) - List webhook subscriptions (signing secrets stripped) * [POST /api/v1/webhooks/subscriptions](createWebhookSubscription.md) - Register a webhook subscription (signing secret returned once) * [PATCH /api/v1/webhooks/subscriptions/{id}](updateWebhookSubscription.md) - Update a webhook subscription (url / events / active) * [DELETE /api/v1/webhooks/subscriptions/{id}](deleteWebhookSubscription.md) - Delete a webhook subscription * [POST /api/v1/webhooks/subscriptions/{id}/rotate-secret](rotateWebhookSecret.md) - Rotate a webhook subscription's signing secret * [GET /api/v1/webhooks/deliveries](listWebhookDeliveries.md) - List recent webhook delivery attempts (the outbox) * [POST /api/v1/webhooks/subscriptions/{id}/test](testWebhookSubscription.md) - Send a signed test event to a subscription # Traceability & Audit * [POST /api/v1/events](registerTraceabilityEvent.md) - Register a UNTP/EPCIS 2.0 traceability event (VC-shaped) * [GET /api/v1/events/{id}/lineage](getEventLineage.md) - Retrieve the upstream pedigree of an event as a recursive lineage DAG * [POST /api/v1/events/{id}/audit](auditEventLineage.md) - Run heuristic UFLPA/EUDR compliance screening over an event's lineage * [POST /api/v1/audit/verify](verifyPassportSeal.md) - Publicly verify a passport's eIDAS seal, certificate chain and timestamp # Public Resolution * [GET /passport/{id}](resolvePublicPassport.md) - Resolve a passport by UUID (JSON-LD / AAS / HTML) * [GET /01/{gtin14}](resolveGs1Gtin.md) - GS1 Digital Link resolution by GTIN-14 (AI 01) * [GET /01/{gtin14}/21/{serial}](resolveGs1GtinSerial.md) - GS1 Digital Link serialised-item redirect (AI 01 + AI 21) * [GET /8003/{grai}](resolveGs1Grai.md) - GS1 Digital Link resolution by GRAI (AI 8003) * [GET /unit/{id}](resolvePublicBatteryUnit.md) - Resolve an individual serialised battery unit # Schemas & Vocabulary * [GET /api/v1/schemas/{category}](getSectorSchema.md) - Get the ESPR metadata schema for a product category * [GET /context/v1](getJsonLdContext.md) - W3C JSON-LD context document for passport terms * [GET /api/v1/materials](listMaterials.md) - List the platform-curated material vocabulary # QR Codes * [GET /api/v1/passports/{id}/qr](getPassportQrCode.md) - Export a print-grade GS1 Digital Link QR code for a passport * [GET /api/v1/units/{id}/qr](getBatteryUnitQrCode.md) - Export a print-grade QR code for an individual battery unit # eIDAS Keys * [POST /api/v1/tenants/rotate-keys](rotateTenantKeys.md) - Rotate the tenant's eIDAS ECDSA signing key pair * [GET /.well-known/opendpp-seal-ca.pem](getSealCaCertificate.md) - Download the platform seal-CA certificate (PEM) # Account * [GET /api/v1/whoami](whoami.md) - Identity of the authenticated key / session # Service * [GET /health](getHealth.md) - Service health check * [GET /api/v1/version](getApiVersion.md) - Running API contract version & build identity ================================================================================ # FILE: /okf/operations/whoami.md ================================================================================ --- type: API Endpoint title: Identity of the authenticated key / session description: Identity of the authenticated key / session resource: https://opendpp-node.eu/api/v1/whoami tags: - GET - account timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/whoami` **Domain:** [Account](/tags/account.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Returns a compact, integration-focused view of the calling credential: the workspace, the principal's role and resolved permissions, whether the session is an API key, the operator the key is scoped to (`null` = workspace-wide), and active-passport usage against the tier quota. Use it to verify a key works, discover the effective permission set, and surface remaining quota. This is the public counterpart to the console's `GET /api/v1/me`; profile, localization and billing details are intentionally not exposed here. **Permission:** none beyond a valid tenant-scoped session — any API key can call it. Platform-admin sessions are rejected with `403` (they are not tenant-scoped). **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ## Responses - **200** — The authenticated identity. → [WhoamiResponse](/schemas/WhoamiResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — The session is not scoped to a tenant workspace (e.g. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/whoami' ``` ## See also Schemas: [Error](/schemas/Error.md), [WhoamiResponse](/schemas/WhoamiResponse.md). ================================================================================ # FILE: /okf/operations/listBatteryUnits.md ================================================================================ --- type: API Endpoint title: List serialised battery units under a passport description: List serialised battery units under a passport resource: https://opendpp-node.eu/api/v1/passports/{passportId}/units tags: - GET - battery-units timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/passports/{passportId}/units` **Domain:** [Battery Units](/tags/battery-units.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Lists **all** serialised units of the passport, newest first (`createdAt` DESC). **Paginated** with `?page` (default 1) and `?limit` (default 100, max 200) — a SKU may carry many physical units; `count` is this page's size, `total`/`totalPages` describe the full set. **Permission:** `battery:read`. Operator-scoped credentials may only read passports of their own Economic Operator (403). Units are raw persisted rows (no Fastify response schema, nothing stripped). **Rate limits:** global limiter only — 100 req/min per IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `page` | query | no | integer | 1-based page number (digits only; non-numeric falls back to 1). | | `limit` | query | no | integer | Page size. | ## Responses - **200** — The passport's units. → [BatteryUnitListResponse](/schemas/BatteryUnitListResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/passports/{passportId}/units' ``` ## See also Schemas: [BatteryUnitListResponse](/schemas/BatteryUnitListResponse.md). ================================================================================ # FILE: /okf/operations/serializeBatteryUnits.md ================================================================================ --- type: API Endpoint title: Serialise individual battery units under a passport (bulk, up to 200) description: Serialise individual battery units under a passport (bulk, up to 200) resource: https://opendpp-node.eu/api/v1/passports/{passportId}/units tags: - POST - battery-units timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/passports/{passportId}/units` **Domain:** [Battery Units](/tags/battery-units.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Creates one or many **individual physical battery units** (Battery Reg. (EU) 2023/1542 Art. 77(2)) under a SKU/type-level passport. Send either a single unit object or `{"units": [...]}` with **at most 200 items** (if `units` is present and an array it is used; otherwise the whole body is treated as one unit). **Permission:** `battery:write`. Bearer API key (`op_dpp_token_…`) or session JWT; cookie-session clients must send `X-CSRF-Token`. Operator-scoped credentials may only serialise under passports of their own Economic Operator (403). Write operations pass subscription gating (402) and optional tenant MFA enforcement (403). **Per-item validation (collected as plain-string errors, not a rejection of the whole batch):** `serialNumber` is trimmed then must match `^[A-Za-z0-9._-]{1,64}$` (GS1 AI-21 recommends ≤ 20 chars); `status` must be a valid unit status; `manufacturedAt` must be Date-parseable; duplicate `(passport, serialNumber)` pairs are skipped with *"A unit with this serial already exists for this passport"*. Each created unit gets a per-unit GS1 Digital Link URI `/{01|8003}/{productId}/21/{serialNumber}` carrying the **real physical serial** in AI-21. **Predecessor linkage (Art. 77(7) repurpose/remanufacture):** `predecessorUnitId` must reference an existing unit **in your tenant** (any passport). A recycled predecessor (`ceasedAt` set) is refused — its passport has ceased to exist (Art. 77(8)). (The check keys on `ceasedAt`, which only the events-route `RECYCLED` transition stamps — a unit merely *created* with status `RECYCLED` has no `ceasedAt` and is still accepted as a predecessor.) In one transaction the new unit is created, an append-only `STATUS_CHANGE` event (`{status, successorUnitId, successorSerial}` payload) is written to the predecessor, and the predecessor's status is set to `predecessorStatus` (default `REPURPOSED`; only `REPURPOSED|REMANUFACTURED|REUSED` allowed). **Partial success:** the response is **201 when at least one unit was created**; skipped items are listed in `errors`. If *every* item failed you get **400 `Serialisation Failed`** with the same string array. A `batteryunit.created` audit event and a tenant notification are emitted on success. **Rate limits:** global limiter only — 100 req/min per IP (`x-ratelimit-*` headers). ## Request body Schema (required): [SerializeBatteryUnitsRequest](/schemas/SerializeBatteryUnitsRequest.md). ```json { "units": [ { "serialNumber": "BATT-2026-000451", "manufacturedAt": "2026-05-02T08:00:00.000Z" }, { "serialNumber": "BATT-2026-000452", "status": "IN_SERVICE", "predecessorUnitId": "5a1c9e7d-3b2f-4c8a-9e6d-7f0b1a2c3d4e", "predecessorStatus": "REMANUFACTURED" } ] } ``` ## Responses - **201** — At least one unit was serialised. → [SerializeBatteryUnitsResponse](/schemas/SerializeBatteryUnitsResponse.md) - **400** — Three shapes: (1) standard Bad Request triple when the body is not a JSON object, the units array is empty, or more than 200 units are sent; (2) Serialisation… → [BatteryUnitSerialisationFailedError](/schemas/BatteryUnitSerialisationFailedError.md), [Error](/schemas/Error.md), [FastifyDefaultBadRequest](/schemas/FastifyDefaultBadRequest.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/passports/{passportId}/units' \ --data '{"units":[{"serialNumber":"BATT-2026-000451","manufacturedAt":"2026-05-02T08:00:00.000Z"},{"serialNumber":"BATT-2026-000452","status":"IN_SERVICE","predecessorUnitId":"5a1c9e7d-3b2f-4c8a-9e6d-7f0b1a2c3d4e","predecessorStatus":"REMANUFACTURED"}]}' ``` ## See also Schemas: [BatteryUnitSerialisationFailedError](/schemas/BatteryUnitSerialisationFailedError.md), [Error](/schemas/Error.md), [FastifyDefaultBadRequest](/schemas/FastifyDefaultBadRequest.md), [SerializeBatteryUnitsRequest](/schemas/SerializeBatteryUnitsRequest.md), [SerializeBatteryUnitsResponse](/schemas/SerializeBatteryUnitsResponse.md). ================================================================================ # FILE: /okf/operations/getBatteryUnit.md ================================================================================ --- type: API Endpoint title: Get one battery unit as JSON-LD with its dynamic-data history description: Get one battery unit as JSON-LD with its dynamic-data history resource: https://opendpp-node.eu/api/v1/units/{id} tags: - GET - battery-units timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/units/{id}` **Domain:** [Battery Units](/tags/battery-units.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Returns the unit as a **JSON-LD document** (`Content-Type: application/ld+json`) in the **privileged tenant view**: `currentState` (the latest telemetry snapshot) and `dynamicData` (the **500 most recent** events, newest first by `recordedAt`) are included; the public `restrictedData` marker is absent. The embedded `ofModel` is the SKU/type passport document rendered in the **owner (unredacted) variant** — legitimate-interest-tier metadata and owner-only keys are NOT masked, unlike the anonymous public document. **Caveat:** this authenticated endpoint does **not** load lineage relations, so `repurposedFrom` is always `null` and `successorUnits` is always `[]` here even when Art. 77(7) lineage exists; the public resolver view (`GET /unit/{id}`) does resolve them. **Permission:** `battery:read`. Operator-scoped credentials may only read units whose passport belongs to their Economic Operator (403). **Rate limits:** global limiter only — 100 req/min per IP. ## Responses - **200** — The unit's JSON-LD document (privileged view, telemetry included). → [BatteryUnitJsonLd](/schemas/BatteryUnitJsonLd.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/units/{id}' ``` ## See also Schemas: [BatteryUnitJsonLd](/schemas/BatteryUnitJsonLd.md). ================================================================================ # FILE: /okf/operations/deleteBatteryUnit.md ================================================================================ --- type: API Endpoint title: Permanently delete a battery unit and its telemetry description: Permanently delete a battery unit and its telemetry resource: https://opendpp-node.eu/api/v1/units/{id} tags: - DELETE - battery-units timestamp: 2026-06-17T00:00:00Z --- `DELETE /api/v1/units/{id}` **Domain:** [Battery Units](/tags/battery-units.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. **HARD delete** — permanently removes the unit row and **cascades all of its `BatteryUnitEvent` telemetry**. This is *not* a lifecycle/status transition: to record end-of-life semantics (decommissioned, waste, recycled — incl. the Art. 77(8) public 410 tombstone) append a telemetry event with a `status` instead (`POST /api/v1/units/{id}/events`). Deletion is intended for erroneous serialisations. A `batteryunit.deleted` audit event is written. **Permission:** `battery:write`. Cookie-session clients must send `X-CSRF-Token`. Operator-scoped credentials may only delete units whose passport belongs to their Economic Operator (403). Write operations pass subscription gating (402) and optional tenant MFA enforcement (403). **Rate limits:** global limiter only — 100 req/min per IP. ## Responses - **200** — The unit and its events were permanently deleted. → [BatteryUnitDeleteResponse](/schemas/BatteryUnitDeleteResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X DELETE 'https://opendpp-node.eu/api/v1/units/{id}' ``` ## See also Schemas: [BatteryUnitDeleteResponse](/schemas/BatteryUnitDeleteResponse.md). ================================================================================ # FILE: /okf/operations/listBatteryUnitEvents.md ================================================================================ --- type: API Endpoint title: List a battery unit's telemetry history (newest first, max 500) description: List a battery unit's telemetry history (newest first, max 500) resource: https://opendpp-node.eu/api/v1/units/{id}/events tags: - GET - battery-units timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/units/{id}/events` **Domain:** [Battery Units](/tags/battery-units.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Returns the unit's append-only dynamic-data history ordered by `recordedAt` DESC, **capped at the 500 most recent events**. There is no pagination — older events beyond the cap are not retrievable via this endpoint. **Permission:** `battery:read`. Operator-scoped credentials may only read units whose passport belongs to their Economic Operator (403). Events are raw persisted rows (no Fastify response schema, nothing stripped). **Rate limits:** global limiter only — 100 req/min per IP. ## Responses - **200** — The unit's telemetry history. → [BatteryUnitEventListResponse](/schemas/BatteryUnitEventListResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/units/{id}/events' ``` ## See also Schemas: [BatteryUnitEventListResponse](/schemas/BatteryUnitEventListResponse.md). ================================================================================ # FILE: /okf/operations/recordBatteryUnitEvent.md ================================================================================ --- type: API Endpoint title: Append an immutable telemetry event to a battery unit description: Append an immutable telemetry event to a battery unit resource: https://opendpp-node.eu/api/v1/units/{id}/events tags: - POST - battery-units timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/units/{id}/events` **Domain:** [Battery Units](/tags/battery-units.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Appends one **append-only** per-unit dynamic-data record (Annex XIII / Art. 77: SoH, cycle count, remaining capacity, temperature, negative events). History is immutable — there is **no update or delete path** for events. **Permission:** `battery:write`. Cookie-session clients must send `X-CSRF-Token`. Operator-scoped credentials may only write to units whose passport belongs to their Economic Operator (403). Write operations pass subscription gating (402) and optional tenant MFA enforcement (403). **Validation (400 with the standard error triple):** `eventType` is required and must be one of `SOH_MEASUREMENT|CHARGE_CYCLE|STATUS_CHANGE|NEGATIVE_EVENT|OTHER`; `stateOfHealth` 0–100; `cycleCount` and `remainingCapacityAh` 0–9007199254740991; `temperatureC` −273.15–10000 (each may also be `null`/omitted); `status`, if present, must be a valid unit status; `recordedAt` must be Date-parseable (defaults to server time when omitted). `cycleCount` is truncated to an integer before persisting; a `payload` that is not an object or array is silently dropped (stored as `null`) — JSON **arrays** pass the server's `typeof` check and are persisted verbatim. **Status transition:** when `status` is present and differs from the unit's current status, the unit is updated **in the same transaction** as the event — this works with *any* `eventType`, though `STATUS_CHANGE` is the conventional carrier. Transitioning to **`RECYCLED`** (Art. 77(8)) additionally stamps `ceasedAt` (if not already set; never cleared), after which the public unit view becomes a 410 tombstone and the unit can no longer gain successor units. `status` itself is not locked afterwards — a later event may still set a different value — but `ceasedAt` persists, so the public 410 and the predecessor refusal are permanent. **Rate limits:** global limiter only — 100 req/min per IP. ## Request body Schema (required): [RecordBatteryUnitEventRequest](/schemas/RecordBatteryUnitEventRequest.md). ```json { "eventType": "SOH_MEASUREMENT", "stateOfHealth": 96.8, "cycleCount": 140, "remainingCapacityAh": 48.2, "temperatureC": 24.5, "payload": { "measuredBy": "BMS firmware 4.2.1" }, "recordedAt": "2026-06-12T09:41:00.000Z" } ``` ## Responses - **201** — Event appended (and, when status was supplied and differed, the unit's status transitioned in the same transaction). → [RecordBatteryUnitEventResponse](/schemas/RecordBatteryUnitEventResponse.md) - **400** — Two shapes: (1) the standard error triple from handler validation — messages: Request body must be a valid JSON object; eventType must be one of: SOH_MEASUREME… → [Error](/schemas/Error.md), [FastifyDefaultBadRequest](/schemas/FastifyDefaultBadRequest.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — The transaction failed. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/units/{id}/events' \ --data '{"eventType":"SOH_MEASUREMENT","stateOfHealth":96.8,"cycleCount":140,"remainingCapacityAh":48.2,"temperatureC":24.5,"payload":{"measuredBy":"BMS firmware 4.2.1"},"recordedAt":"2026-06-12T09:41:00.000Z"}' ``` ## See also Schemas: [Error](/schemas/Error.md), [FastifyDefaultBadRequest](/schemas/FastifyDefaultBadRequest.md), [RecordBatteryUnitEventRequest](/schemas/RecordBatteryUnitEventRequest.md), [RecordBatteryUnitEventResponse](/schemas/RecordBatteryUnitEventResponse.md). ================================================================================ # FILE: /okf/operations/listFacilities.md ================================================================================ --- type: API Endpoint title: List facilities in the tenant workspace description: List facilities in the tenant workspace resource: https://opendpp-node.eu/api/v1/facilities tags: - GET - facilities timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/facilities` **Domain:** [Facilities](/tags/facilities.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Lists all facilities registered under your tenant workspace, sorted by `createdAt` descending. **Paginated** with `?page` (default 1) and `?limit` (default 100, max 200); `count` is this page's size, `total`/`totalPages` describe the full set. A non-numeric `page`/`limit` falls back to its default. **Permission:** `facility:read` (Bearer API key or session JWT/cookie). **Operator-scoped keys:** when authenticated with an API key scoped to an Economic Operator, the list contains only facilities whose `operatorId` equals the key's operator — facilities with no operator (`operatorId: null`) are **excluded** from the list (they remain readable individually via `GET /api/v1/facilities/{id}`). The full row is returned to the owner, including the privileged address fields (`streetAddress`, `city`, `postalCode`) that public passport documents never expose (owner-only in JSON-LD; never emitted in AAS). **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `page` | query | no | integer | 1-based page number (digits only; non-numeric falls back to 1). | | `limit` | query | no | integer | Page size. | ## Responses - **200** — Facility list. → [FacilityListEnvelope](/schemas/FacilityListEnvelope.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/facilities' ``` ## See also Schemas: [FacilityListEnvelope](/schemas/FacilityListEnvelope.md). ================================================================================ # FILE: /okf/operations/createFacility.md ================================================================================ --- type: API Endpoint title: Register a facility (GS1 GLN) description: Register a facility (GS1 GLN) resource: https://opendpp-node.eu/api/v1/facilities tags: - POST - facilities timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/facilities` **Domain:** [Facilities](/tags/facilities.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Registers a manufacturing/processing facility as tenant-scoped master data, backing the Unique Facility Identifier (UFI, EN 18219). Passports reference facilities via `facilityId`. **Permission:** `facility:write`. Authenticate with a Bearer API key (`op_dpp_token_…`) or a session JWT; cookie-authenticated sessions must also send the `X-CSRF-Token` header (double-submit against the `opendpp_csrf` cookie) — Bearer clients are exempt. Write permissions are subscription-gated: a lapsed workspace subscription returns **402**. **GLN validation:** `gln` is trimmed, then must be exactly 13 digits with a valid GS1 modulo-10 check digit (same weighting algorithm as GTIN). The GLN is unique **platform-wide** (database unique constraint), so a duplicate returns **409** even if the existing facility belongs to another tenant. **Country:** `country` must match `^[A-Za-z]{2}$` after trimming and is stored uppercased. **Operator binding:** if `operatorId` is supplied (non-empty), that Economic Operator must be bound to your tenant workspace, otherwise **403**. An empty/whitespace `operatorId` is stored as `null`. Requests authenticated with an **operator-scoped API key** may only attach facilities to their own operator: a mismatched `operatorId` returns **403**, and when omitted the key's operator id is applied automatically. `activity`, `streetAddress`, `city` and `postalCode` are trimmed; empty/whitespace values are stored as `null`. **Public/privileged field split:** the public **JSON-LD** passport document exposes `id`, `gln`, `name`, `activity` and `country` of a linked facility; the public **AAS** export emits only the GLN, name and country (`manufacturingFacilityGln` / `manufacturingFacilityName` / `manufacturingFacilityCountry`, plus the GLN as a `urn:gs1:gln:` global asset reference) — the facility `id` and `activity` are never emitted in AAS. `streetAddress`, `city` and `postalCode` are owner-only in both formats. This endpoint returns the full row to you as the owner. Emits a `facility.created` audit event and an in-app notification. **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ## Request body Schema (required): [FacilityCreateRequest](/schemas/FacilityCreateRequest.md). ```json { "gln": "0950110153014", "name": "Munich Cell Assembly Plant", "activity": "Cell assembly", "streetAddress": "Werkstrasse 12", "city": "Munich", "postalCode": "80331", "country": "DE", "operatorId": "4f6d2c1e-8a9b-4d3e-b7c5-0a1f2e3d4c5b" } ``` ## Responses - **201** — Facility registered. → [FacilityCreatedEnvelope](/schemas/FacilityCreatedEnvelope.md) - **400** — Invalid request body. → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Forbidden. → [Error](/schemas/Error.md) - **409** — A facility with this GLN is already registered. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Internal error (standard envelope). → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/facilities' \ --data '{"gln":"0950110153014","name":"Munich Cell Assembly Plant","activity":"Cell assembly","streetAddress":"Werkstrasse 12","city":"Munich","postalCode":"80331","country":"DE","operatorId":"4f6d2c1e-8a9b-4d3e-b7c5-0a1f2e3d4c5b"}' ``` ## See also Schemas: [Error](/schemas/Error.md), [FacilityCreateRequest](/schemas/FacilityCreateRequest.md), [FacilityCreatedEnvelope](/schemas/FacilityCreatedEnvelope.md). ================================================================================ # FILE: /okf/operations/getFacility.md ================================================================================ --- type: API Endpoint title: Get a single facility description: Get a single facility resource: https://opendpp-node.eu/api/v1/facilities/{id} tags: - GET - facilities timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/facilities/{id}` **Domain:** [Facilities](/tags/facilities.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Fetches one facility by id, scoped to your tenant workspace. **Permission:** `facility:read`. **Operator-scoped keys:** if the facility belongs to a *different* Economic Operator than the key's scope, the response is **403**. Facilities with no operator (`operatorId: null`) **are** readable by operator-scoped keys here, even though they are excluded from the list endpoint. Returns the full row including the privileged address fields (`streetAddress`, `city`, `postalCode`) that public passport documents never expose. (Public exposure of a linked facility: the JSON-LD document shows `id`/`gln`/`name`/`activity`/`country`; the AAS export only the GLN, name and country.) **404 body:** standard envelope with message `Facility not found under your Tenant workspace`. **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ## Responses - **200** — Facility found. → [FacilityEnvelope](/schemas/FacilityEnvelope.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Forbidden. → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/facilities/{id}' ``` ## See also Schemas: [Error](/schemas/Error.md), [FacilityEnvelope](/schemas/FacilityEnvelope.md). ================================================================================ # FILE: /okf/operations/updateFacility.md ================================================================================ --- type: API Endpoint title: Update facility master data (GLN is immutable) description: Update facility master data (GLN is immutable) resource: https://opendpp-node.eu/api/v1/facilities/{id} tags: - PUT - facilities timestamp: 2026-06-17T00:00:00Z --- `PUT /api/v1/facilities/{id}` **Domain:** [Facilities](/tags/facilities.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Partially updates a facility's master data. **The GLN itself is immutable** — it is the resolvable UFI identifier; a `gln` key in the body is silently ignored (as is `operatorId` — the operator binding cannot be changed here). **Permission:** `facility:write`. Cookie sessions must send `X-CSRF-Token`; write permissions are subscription-gated (**402** when lapsed). **Field semantics (all optional):** - `name` — applied only when a non-empty string; an empty/whitespace or non-string value is silently ignored (`name` can never be cleared). - `activity`, `streetAddress`, `city`, `postalCode` — applied whenever the key is *present* in the body: the value is stringified and trimmed; anything that trims to empty (`null`, `""`, or a whitespace-only string) **clears the field to null** — the same normalization as POST. - `country` — when present as a string it must match `^[A-Za-z]{2}$` (else **400**) and is stored uppercased; a non-string value is silently ignored. An empty body (or one with no recognized fields) is accepted: the response is **200** with the otherwise-unchanged row, though `updatedAt` is still bumped. **Operator-scoped keys:** updating a facility that belongs to a different Economic Operator returns **403**; facilities with `operatorId: null` are updatable. Emits a `facility.updated` audit event recording the changed fields. **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ## Request body Schema: [FacilityUpdateRequest](/schemas/FacilityUpdateRequest.md). ```json { "name": "Munich Cell Assembly Plant — Hall B", "activity": "Final manufacturing", "streetAddress": "Werkstrasse 14", "country": "DE" } ``` ## Responses - **200** — Updated facility (full row). → [FacilityEnvelope](/schemas/FacilityEnvelope.md) - **400** — country was present as a string but is not a 2-letter ISO code. → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Forbidden. → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X PUT 'https://opendpp-node.eu/api/v1/facilities/{id}' \ --data '{"name":"Munich Cell Assembly Plant — Hall B","activity":"Final manufacturing","streetAddress":"Werkstrasse 14","country":"DE"}' ``` ## See also Schemas: [Error](/schemas/Error.md), [FacilityEnvelope](/schemas/FacilityEnvelope.md), [FacilityUpdateRequest](/schemas/FacilityUpdateRequest.md). ================================================================================ # FILE: /okf/operations/deleteFacility.md ================================================================================ --- type: API Endpoint title: Delete a facility (passports are unlinked, never deleted) description: Delete a facility (passports are unlinked, never deleted) resource: https://opendpp-node.eu/api/v1/facilities/{id} tags: - DELETE - facilities timestamp: 2026-06-17T00:00:00Z --- `DELETE /api/v1/facilities/{id}` **Domain:** [Facilities](/tags/facilities.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Removes the facility master-data row. **Passports are never deleted by this operation**: `Passport.facilityId` is a `SET NULL` foreign key, so any passports referencing the facility simply lose their UFI link (`facilityId` becomes `null`) and remain fully intact and publicly resolvable. **Permission:** `facility:write`. Cookie sessions must send `X-CSRF-Token`; write permissions are subscription-gated (**402** when lapsed). **Operator-scoped keys:** deleting a facility that belongs to a different Economic Operator returns **403**; facilities with `operatorId: null` are deletable. Emits a `facility.deleted` audit event and an in-app notification. **404 body:** standard envelope with message `Facility not found under your Tenant workspace`. **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ## Responses - **200** — Facility deleted. → [FacilityDeletedEnvelope](/schemas/FacilityDeletedEnvelope.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Forbidden. → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X DELETE 'https://opendpp-node.eu/api/v1/facilities/{id}' ``` ## See also Schemas: [Error](/schemas/Error.md), [FacilityDeletedEnvelope](/schemas/FacilityDeletedEnvelope.md). ================================================================================ # FILE: /okf/operations/listGrants.md ================================================================================ --- type: API Endpoint title: List access grants and pending access requests description: List access grants and pending access requests resource: https://opendpp-node.eu/api/v1/grants tags: - GET - access-grants timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/grants` **Domain:** [Access Grants](/tags/access-grants.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Lists the workspace's access grants — capability-token grants for the Battery Regulation's restricted data tiers (Reg. (EU) 2023/1542 Art. 77(9), Annex XIII(2)–(4)) — including undecided third-party access **requests** (`status: PENDING`, `issuerType: REQUEST`) submitted via the hosted request-access page. **Permission:** `grant:read`. **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). Paginated with `?page` (default 1) and `?limit` (default 100, max 200), grouped by `status` ascending (alphabetical: `ACTIVE`, `DENIED`, `PENDING`, `REVOKED`) and newest-first within each group. `AUTHORITY` grants (platform-issued market-surveillance access) are listed for transparency but are not tenant-revocable (`revocable: false`). Raw capability tokens are never included — only issuance/approval responses contain them, once. **Pagination:** results are paged with `?page` (default 1) and `?limit` (default 100, max 200). The response now also carries `success`, `count`, `total` and `totalPages` alongside `grants`. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `page` | query | no | integer | 1-based page number (digits only; non-numeric falls back to 1). | | `limit` | query | no | integer | Page size. | ## Responses - **200** — The workspace's grants and requests (most recent 500). → [GrantListResponse](/schemas/GrantListResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/grants' ``` ## See also Schemas: [GrantListResponse](/schemas/GrantListResponse.md). ================================================================================ # FILE: /okf/operations/createGrant.md ================================================================================ --- type: API Endpoint title: Issue a legitimate-interest access grant directly description: Issue a legitimate-interest access grant directly resource: https://opendpp-node.eu/api/v1/grants tags: - POST - access-grants timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/grants` **Domain:** [Access Grants](/tags/access-grants.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Directly issues an `ACTIVE` legitimate-interest access grant (no pending request involved) and mints its capability token. The raw token (`dpp_li_` + 32 hex characters) is returned **once** in this response; only its SHA-256 hash is stored. The grantee presents it to the public resolution endpoints as `Authorization: Bearer dpp_li_…` or `?grant=dpp_li_…` to unlock the restricted (tier-2 / per-unit) data of the granted scope. **Permission:** `grant:write` (write operations are subject to subscription gating, so 402 is possible). Cookie-session clients must send the `X-CSRF-Token` header; Bearer clients are exempt. On workspaces that enforce multi-factor authentication, user sessions that did not authenticate with a second factor receive 403 on writes (API-key clients are exempt). **Rate limit:** global limiter, 100 requests/min per IP. Scope semantics: - `UNIT` — `batteryUnitId` is required; the unit must belong to this workspace. The unit's parent `passportId` is recorded on the grant. - `PASSPORT` — `passportId` is required; the passport must belong to this workspace and must not be a `DRAFT` (drafts return 404). - `TENANT` — workspace-wide; no target id needed. `expiresAt` is required, must be in the future, and at most **366 days** out. This endpoint always mints `kind: LEGITIMATE_INTEREST` — `AUTHORITY` (`dpp_auth_…`) grants are platform-issued only and cannot be created here. The issuance is audited as `grant.issued`. String fields longer than their documented maximum are **silently truncated**, not rejected; unknown fields are ignored. ## Request body Schema (required): [CreateGrantRequest](/schemas/CreateGrantRequest.md). ```json { "granteeName": "Dr. Elena Varga", "granteeEmail": "e.varga@inspection-example.eu", "organization": "EU Battery Inspection Services", "purpose": "State-of-health verification for second-life suitability assessment under Art. 77(9).", "scopeType": "UNIT", "batteryUnitId": "7c3a91d5-2e4f-4b6a-8c0d-1e2f3a4b5c6d", "expiresAt": "2026-09-12T09:41:00.000Z" } ``` ## Responses - **201** — Grant issued. → [GrantIssuedResponse](/schemas/GrantIssuedResponse.md) - **400** — Validation failure. → [GrantRouteError](/schemas/GrantRouteError.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — For scopeType UNIT/PASSPORT: the target id does not exist in this workspace (cross-tenant targets and DRAFT passports are indistinguishable from missing ones). → [GrantRouteError](/schemas/GrantRouteError.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/grants' \ --data '{"granteeName":"Dr. Elena Varga","granteeEmail":"e.varga@inspection-example.eu","organization":"EU Battery Inspection Services","purpose":"State-of-health verification for second-life suitability assessment under Art. 77(9).","scopeType":"UNIT","batteryUnitId":"7c3a91d5-2e4f-4b6a-8c0d-1e2f3a4b5c6d","expiresAt":"2026-09-12T09:41:00.000Z"}' ``` ## See also Schemas: [CreateGrantRequest](/schemas/CreateGrantRequest.md), [GrantIssuedResponse](/schemas/GrantIssuedResponse.md), [GrantRouteError](/schemas/GrantRouteError.md). ================================================================================ # FILE: /okf/operations/approveGrantRequest.md ================================================================================ --- type: API Endpoint title: Approve a pending access request and mint its token description: Approve a pending access request and mint its token resource: https://opendpp-node.eu/api/v1/grants/{id}/approve tags: - POST - access-grants timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/grants/{id}/approve` **Domain:** [Access Grants](/tags/access-grants.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Approves a `PENDING` third-party access request (submitted via the hosted request-access page). Approval mints the legitimate-interest capability token **at this moment** — pending requests carry no token — sets `status: ACTIVE`, records `decidedAt`/`decidedBy`, and replaces the request's provisional 90-day expiry with the `expiresAt` you supply (required; future; max **366 days** out). The raw token is returned **once** in this response. If the request has a `granteeEmail`, the grantee is additionally e-mailed an inspection link containing the token (`…/unit/{batteryUnitId}?grant=dpp_li_…` or `…/passport/{passportId}?grant=dpp_li_…`) — the only other place the raw token ever exists. The decision is audited as `grant.approved`. **Permission:** `grant:write` (subscription gating ⇒ 402 possible; cookie sessions need `X-CSRF-Token`; on workspaces enforcing multi-factor authentication, user sessions without a second factor get 403 — API-key clients exempt). **Rate limit:** global limiter, 100 requests/min per IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | The access-request (AccessGrant) id. | ## Request body Schema (required): [ApproveGrantRequest](/schemas/ApproveGrantRequest.md). ```json { "expiresAt": "2026-09-12T09:41:00.000Z" } ``` ## Responses - **200** — Request approved; the capability token is shown only here (and in the grantee e-mail). → [GrantIssuedResponse](/schemas/GrantIssuedResponse.md) - **400** — Invalid expiresAt. → [GrantRouteError](/schemas/GrantRouteError.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — No grant with this id exists in this workspace. → [GrantRouteError](/schemas/GrantRouteError.md) - **409** — The grant is not PENDING (already decided, active, or revoked). → [GrantRouteError](/schemas/GrantRouteError.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/grants/{id}/approve' \ --data '{"expiresAt":"2026-09-12T09:41:00.000Z"}' ``` ## See also Schemas: [ApproveGrantRequest](/schemas/ApproveGrantRequest.md), [GrantIssuedResponse](/schemas/GrantIssuedResponse.md), [GrantRouteError](/schemas/GrantRouteError.md). ================================================================================ # FILE: /okf/operations/denyGrantRequest.md ================================================================================ --- type: API Endpoint title: Deny a pending access request description: Deny a pending access request resource: https://opendpp-node.eu/api/v1/grants/{id}/deny tags: - POST - access-grants timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/grants/{id}/deny` **Domain:** [Access Grants](/tags/access-grants.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Denies a `PENDING` third-party access request: sets `status: DENIED` and records `decidedAt`/`decidedBy`. No token is ever minted for a denied request, and no e-mail is sent to the requester. The decision is audited as `grant.denied`. The request body, if any, is ignored. **Permission:** `grant:write` (subscription gating ⇒ 402 possible; cookie sessions need `X-CSRF-Token`; on workspaces enforcing multi-factor authentication, user sessions without a second factor get 403 — API-key clients exempt). **Rate limit:** global limiter, 100 requests/min per IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | The access-request (AccessGrant) id. | ## Responses - **200** — Request denied. → [GrantDecisionResponse](/schemas/GrantDecisionResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — No grant with this id exists in this workspace. → [GrantRouteError](/schemas/GrantRouteError.md) - **409** — The grant is not PENDING (already decided, active, or revoked). → [GrantRouteError](/schemas/GrantRouteError.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X POST 'https://opendpp-node.eu/api/v1/grants/{id}/deny' ``` ## See also Schemas: [GrantDecisionResponse](/schemas/GrantDecisionResponse.md), [GrantRouteError](/schemas/GrantRouteError.md). ================================================================================ # FILE: /okf/operations/revokeGrant.md ================================================================================ --- type: API Endpoint title: Revoke an access grant (soft revocation) description: Revoke an access grant (soft revocation) resource: https://opendpp-node.eu/api/v1/grants/{id} tags: - DELETE - access-grants timestamp: 2026-06-17T00:00:00Z --- `DELETE /api/v1/grants/{id}` **Domain:** [Access Grants](/tags/access-grants.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Soft-revokes a grant: sets `status: REVOKED` and `revokedAt` (the row is retained for audit; the public resolvers reject the token from then on). Audited as `grant.revoked`. Behavioral caveats (no status precondition — only the kind is checked): - Works on a grant in **any** status: revoking a `PENDING` request withdraws it; revoking a `DENIED` grant flips it to `REVOKED`. - Re-revoking an already-`REVOKED` grant returns 200 again and preserves the original `revokedAt`. - `AUTHORITY` grants (`kind: AUTHORITY`, platform-issued market-surveillance access) are **not tenant-revocable** — 403. Battery Reg. Art. 77 market-surveillance access must not depend on manufacturer consent; platform admins manage those. **Permission:** `grant:write` (subscription gating ⇒ 402 possible; cookie sessions need `X-CSRF-Token`; on workspaces enforcing multi-factor authentication, user sessions without a second factor get 403 — API-key clients exempt). **Rate limit:** global limiter, 100 requests/min per IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | The grant (AccessGrant) id. | ## Responses - **200** — Grant revoked (idempotent: re-revoking keeps the original revokedAt). → [GrantDecisionResponse](/schemas/GrantDecisionResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Two distinct bodies share this status: (1) route-level — the grant is an AUTHORITY grant and cannot be revoked by the workspace; body is {error, message} witho… → [Error](/schemas/Error.md), [GrantRouteError](/schemas/GrantRouteError.md) - **404** — No grant with this id exists in this workspace. → [GrantRouteError](/schemas/GrantRouteError.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X DELETE 'https://opendpp-node.eu/api/v1/grants/{id}' ``` ## See also Schemas: [Error](/schemas/Error.md), [GrantDecisionResponse](/schemas/GrantDecisionResponse.md), [GrantRouteError](/schemas/GrantRouteError.md). ================================================================================ # FILE: /okf/operations/listOperators.md ================================================================================ --- type: API Endpoint title: List economic operators bound to your workspace description: List economic operators bound to your workspace resource: https://opendpp-node.eu/api/v1/operators tags: - GET - economic-operators timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/operators` **Domain:** [Economic Operators](/tags/economic-operators.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Returns the economic operators bound to your workspace, ordered by name. Active operators only unless `?archived=true` is passed (archived operators are off-boarded but their passports are retained and still publicly resolvable). Each entry is the same `OperatorRow` shape returned by `POST`/`PATCH /api/v1/operators` — use the `id` to attribute a passport (`operatorId` on `POST /api/v1/passports`) or to address `PATCH`/`DELETE`. **Permission:** `operator:read`. Requests authenticated with an **operator-scoped API key** see only their own operator. **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `archived` | query | no | string | Set true to include archived (off-boarded) operators. | ## Responses - **200** — The bound operators. → [OperatorListResponse](/schemas/OperatorListResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/operators' ``` ## See also Schemas: [OperatorListResponse](/schemas/OperatorListResponse.md). ================================================================================ # FILE: /okf/operations/registerOperator.md ================================================================================ --- type: API Endpoint title: Register an economic operator and bind it to your workspace description: Register an economic operator and bind it to your workspace resource: https://opendpp-node.eu/api/v1/operators tags: - POST - economic-operators timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/operators` **Domain:** [Economic Operators](/tags/economic-operators.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Registers an economic operator (manufacturer, importer, supplier, …) and binds it to your workspace. **Permission:** `operator:create`. Cookie-session clients must send the `X-CSRF-Token` header (double-submit); Bearer clients (API key / JWT) are exempt. **Deduplication (per workspace):** operators are scoped to your workspace — `regId` is unique *within* your workspace, not across the platform. If **your workspace** already has an operator with the submitted `regId`, that existing record is returned and the submitted `name`, `role` and `regIdScheme` are **ignored**. A `regId` already used by *another* workspace is irrelevant — you always get **your own** operator row (so one workspace can never bind to, rename, or archive another's operator). The call is idempotent: re-registering an already-bound operator succeeds with `201` again. The per-workspace match includes **archived** operators: if your workspace's operator for that `regId` is archived, the archived record is returned as-is (`archivedAt` non-null) with `201` — registration does not un-archive it; use `POST /api/v1/operators/{id}/restore` to reactivate it. **Registration-id integrity:** fabricated `EORI-MOCK…` ids are rejected on every path. When `regIdScheme` is `EORI`, `regId` must match `^[A-Z]{2}[A-Za-z0-9]{1,15}$` (2-letter ISO 3166 country prefix followed by up to 15 alphanumerics, e.g. `DE1234567890`). Validation is syntax-only — existence is not checked against the EU EORI online validation service. Side effects: an `operator.created` audit event and an in-app notification are recorded. **Rate limit:** global limiter, 100 requests/min/IP (429 carries `x-ratelimit-*` headers). ## Request body Schema (required): [RegisterOperatorRequest](/schemas/RegisterOperatorRequest.md). ```json { "name": "Default EU Manufacturing Operator", "regId": "EU-DEFAULT-001", "role": "MANUFACTURER" } ``` ## Responses - **201** — Operator registered (or an existing operator with the same regId was bound to your workspace). → [RegisterOperatorResponse](/schemas/RegisterOperatorResponse.md) - **400** — Two distinct bodies. → [Error](/schemas/Error.md), [OperatorMinimalError](/schemas/OperatorMinimalError.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Database/handler failure. → [Error](/schemas/Error.md), [OperatorMinimalError](/schemas/OperatorMinimalError.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/operators' \ --data '{"name":"Default EU Manufacturing Operator","regId":"EU-DEFAULT-001","role":"MANUFACTURER"}' ``` ## See also Schemas: [Error](/schemas/Error.md), [OperatorMinimalError](/schemas/OperatorMinimalError.md), [RegisterOperatorRequest](/schemas/RegisterOperatorRequest.md), [RegisterOperatorResponse](/schemas/RegisterOperatorResponse.md). ================================================================================ # FILE: /okf/operations/getOperator.md ================================================================================ --- type: API Endpoint title: Fetch a single bound economic operator description: Fetch a single bound economic operator resource: https://opendpp-node.eu/api/v1/operators/{id} tags: - GET - economic-operators timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/operators/{id}` **Domain:** [Economic Operators](/tags/economic-operators.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Fetches one economic operator by UUID, scoped to your workspace (`404` if no operator with that id exists in your workspace). **Permission:** `operator:read`. An **operator-scoped API key** may only fetch its own operator (`403` otherwise). **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Operator UUID (EconomicOperator.id). | ## Responses - **200** — The operator. → [OperatorGetResponse](/schemas/OperatorGetResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/operators/{id}' ``` ## See also Schemas: [OperatorGetResponse](/schemas/OperatorGetResponse.md). ================================================================================ # FILE: /okf/operations/updateOperator.md ================================================================================ --- type: API Endpoint title: Update an operator's name or role (regId is immutable) description: Update an operator's name or role (regId is immutable) resource: https://opendpp-node.eu/api/v1/operators/{id} tags: - PATCH - economic-operators timestamp: 2026-06-17T00:00:00Z --- `PATCH /api/v1/operators/{id}` **Domain:** [Economic Operators](/tags/economic-operators.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Edits an operator bound to your workspace. Only `name` and `role` are editable; `regId` is the legal registry identifier and is **intentionally immutable** here (register the operator again under the correct id instead). Non-string or whitespace-only values are silently ignored; submitted values are trimmed. If no usable field is supplied (every field missing, non-string, or whitespace-only — including an empty object `{}` or an omitted body), the current operator row is returned unchanged with `200` and no audit event is written. The handler does not diff against current values: supplying a value identical to the current one still performs an update and writes an audit event. **Permission:** `operator:write`. Cookie-session clients must send `X-CSRF-Token`; Bearer clients are exempt. **Tenant-scoped:** operators are scoped to your workspace — `regId` is not globally unique. Registering a `regId` that another workspace also uses creates **your own** operator row; `name`/`role` edits never affect another workspace. When a change is applied, an `operator.updated` audit event is recorded. Unhandled database errors are normalized by the global error handler to the standard `{success: false, error, message}` envelope with a generic message (details are logged server-side). **Rate limit:** global limiter, 100 requests/min/IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Operator UUID (EconomicOperator.id). | ## Request body Schema: [UpdateOperatorRequest](/schemas/UpdateOperatorRequest.md). ```json { "name": "Default EU Manufacturing Operator B.V.", "role": "IMPORTER" } ``` ## Responses - **200** — The (possibly unchanged) operator row. → [UpdateOperatorResponse](/schemas/UpdateOperatorResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The operator does not exist or is not bound to your workspace. → [OperatorMinimalError](/schemas/OperatorMinimalError.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Server-side failure. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X PATCH 'https://opendpp-node.eu/api/v1/operators/{id}' \ --data '{"name":"Default EU Manufacturing Operator B.V.","role":"IMPORTER"}' ``` ## See also Schemas: [Error](/schemas/Error.md), [OperatorMinimalError](/schemas/OperatorMinimalError.md), [UpdateOperatorRequest](/schemas/UpdateOperatorRequest.md), [UpdateOperatorResponse](/schemas/UpdateOperatorResponse.md). ================================================================================ # FILE: /okf/operations/deleteOperator.md ================================================================================ --- type: API Endpoint title: Remove an operator (archives if it has passports, else hard-deletes) description: Remove an operator (archives if it has passports, else hard-deletes) resource: https://opendpp-node.eu/api/v1/operators/{id} tags: - DELETE - economic-operators timestamp: 2026-06-17T00:00:00Z --- `DELETE /api/v1/operators/{id}` **Domain:** [Economic Operators](/tags/economic-operators.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Removes an operator, choosing automatically between two outcomes (ESPR Art. 9(2)/77 passport-persistence compliance — an operator that still has passports must never be hard-deleted): - **Archive (soft delete)** — if the operator has one or more passports, it is archived instead of deleted: `archivedAt` is set on the operator and every active passport of the operator is archived with a `retentionUntil` deadline set to a platform-configured retention period from now (default 15 years). Archived passports remain **publicly resolvable** (the persistence duty) but are excluded from active management lists. Response: `{success: true, archived: true, archivedPassports: }`. Fully reversible via `POST /api/v1/operators/{id}/restore`. - **Hard delete** — if the operator has no passports it is permanently deleted (tenant bindings cascade-delete; user/facility/API-key references are set to null). Response: `{success: true, archived: false}` — no `archivedPassports` field. - **Fallback** — if the hard delete fails on a residual foreign-key reference, the operator is archived instead and the response is `{success: true, archived: true}` **without** `archivedPassports`. If even the fallback archive fails, `409` is returned. **Permission:** `operator:write`. Cookie-session clients must send `X-CSRF-Token`; Bearer clients are exempt. The operator must be bound to your workspace (`404` otherwise). **Tenant-scoped:** this affects only **your** workspace's operator and passports — operators are not shared across workspaces. Side effects: an `operator.archived` or `operator.deleted` audit event plus an in-app notification — on the primary archive and hard-delete paths only; the foreign-key fallback archive writes **no** audit event or notification. Unhandled database errors are normalized by the global error handler to the standard `{success: false, error, message}` envelope with a generic message (details are logged server-side). **Rate limit:** global limiter, 100 requests/min/IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Operator UUID (EconomicOperator.id). | ## Responses - **200** — Operator removed. → [DeleteOperatorResponse](/schemas/DeleteOperatorResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The operator does not exist or is not bound to your workspace. → [OperatorMinimalError](/schemas/OperatorMinimalError.md) - **409** — The operator could neither be hard-deleted nor archived (both attempts failed). → [OperatorMinimalError](/schemas/OperatorMinimalError.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Server-side failure. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X DELETE 'https://opendpp-node.eu/api/v1/operators/{id}' ``` ## See also Schemas: [DeleteOperatorResponse](/schemas/DeleteOperatorResponse.md), [Error](/schemas/Error.md), [OperatorMinimalError](/schemas/OperatorMinimalError.md). ================================================================================ # FILE: /okf/operations/restoreOperator.md ================================================================================ --- type: API Endpoint title: Restore an archived operator and its archived passports description: Restore an archived operator and its archived passports resource: https://opendpp-node.eu/api/v1/operators/{id}/restore tags: - POST - economic-operators timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/operators/{id}/restore` **Domain:** [Economic Operators](/tags/economic-operators.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Un-archives an operator that was soft-deleted by `DELETE /api/v1/operators/{id}` and brings its archived passports back into the active catalogue: clears the operator's `archivedAt`, then clears `archivedAt` and `retentionUntil` on every archived passport of the operator **except** passports that were independently `DECOMMISSIONED` (those keep their own retention clock and stay archived). Safe to call on a non-archived operator — it simply restores any archived passports the operator may have (`restoredPassports` may be `0`). No request body. **Permission:** `operator:write`. Cookie-session clients must send `X-CSRF-Token`; Bearer clients are exempt. `404` if the operator is not bound to your workspace. Side effects: an `operator.restored` audit event and an in-app notification. Unhandled database errors are normalized by the global error handler to the standard `{success: false, error, message}` envelope with a generic message (details are logged server-side). **Rate limit:** global limiter, 100 requests/min/IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Operator UUID (EconomicOperator.id). | ## Responses - **200** — Operator un-archived. → [RestoreOperatorResponse](/schemas/RestoreOperatorResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The operator does not exist or is not bound to your workspace. → [OperatorMinimalError](/schemas/OperatorMinimalError.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Server-side failure. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X POST 'https://opendpp-node.eu/api/v1/operators/{id}/restore' ``` ## See also Schemas: [Error](/schemas/Error.md), [OperatorMinimalError](/schemas/OperatorMinimalError.md), [RestoreOperatorResponse](/schemas/RestoreOperatorResponse.md). ================================================================================ # FILE: /okf/operations/rotateTenantKeys.md ================================================================================ --- type: API Endpoint title: Rotate the tenant's eIDAS ECDSA signing key pair description: Rotate the tenant's eIDAS ECDSA signing key pair resource: https://opendpp-node.eu/api/v1/tenants/rotate-keys tags: - POST - eidas-keys timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/tenants/rotate-keys` **Domain:** [eIDAS Keys](/tags/eidas-keys.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Generates a brand-new ECDSA **prime256v1 (P-256)** key pair for your workspace's eIDAS advanced-seal signing and rotates it into the encrypted database vault, replacing the previous key. No request body is required; a valid JSON body, if sent, is ignored. What happens: - The new private key (PKCS#8) is encrypted with AES-256-GCM (per-entry HKDF-derived key; the tenant id is bound as GCM additional authenticated data) and upserted into the vault — the **previous private key is overwritten and unrecoverable**. - The tenant's published `eidasPublicKey` is updated to the new public key (SPKI PEM), which is also returned in the response. - A best-effort X.509 identity certificate is minted from the platform seal CA, binding the new key to the tenant's legal name (eIDAS Art. 36(1)(b) creator identification); a certificate-minting failure does **not** fail the rotation (the certificate fields simply stay null until backfilled). **Operational impact:** rotation does not invalidate existing seals. Each sealed passport embeds the signing public key and certificate chain at sealing time, so previously sealed passports keep verifying with their embedded key material. Passports sealed after rotation use the new key. **Permission:** `key:write`. Cookie-session clients must send `X-CSRF-Token`; Bearer clients are exempt. **Rate limit:** global limiter, 100 requests/min/IP. ## Responses - **200** — Key pair rotated. → [RotateTenantKeysResponse](/schemas/RotateTenantKeysResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Key generation, vault encryption, or database failure. → [Error](/schemas/Error.md), [OperatorMinimalError](/schemas/OperatorMinimalError.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X POST 'https://opendpp-node.eu/api/v1/tenants/rotate-keys' ``` ## See also Schemas: [Error](/schemas/Error.md), [OperatorMinimalError](/schemas/OperatorMinimalError.md), [RotateTenantKeysResponse](/schemas/RotateTenantKeysResponse.md). ================================================================================ # FILE: /okf/operations/listPassports.md ================================================================================ --- type: API Endpoint title: List passports in your workspace (paginated JSON-LD) description: List passports in your workspace (paginated JSON-LD) resource: https://opendpp-node.eu/api/v1/passports tags: - GET - passports timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/passports` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. 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 | Required | Type | Description | |------|----|----------|------|-------------| | `` | | no | — | — | | `` | | no | — | — | | `category` | query | no | string | Exact-match filter on metadata.category. | | `originCountry` | query | no | string | Exact-match filter on metadata.originCountry (ISO 3166-1 alpha-2, e.g. | ## Responses - **200** — Paginated list of JSON-LD passport documents. → [PassportListResponse](/schemas/PassportListResponse.md) - **400** — Route validation failure (framework default body — note statusCode/code keys, no success field): page or limit did not match ^[0-9]+$. - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/passports' ``` ## See also Schemas: [PassportListResponse](/schemas/PassportListResponse.md). ================================================================================ # FILE: /okf/operations/createPassport.md ================================================================================ --- type: API Endpoint title: Create (ingest) a Digital Product Passport description: Create (ingest) a Digital Product Passport resource: https://opendpp-node.eu/api/v1/passports tags: - POST - passports timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/passports` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Creates a SKU/type-level Digital Product Passport. **Permission:** `passport:create` (Bearer `op_dpp_token_…` API key or session JWT; cookie sessions must also send the `X-CSRF-Token` double-submit header). Write operations are subject to subscription gating (**402**) and, where the workspace enforces it, MFA (**403**). **Rate limit:** global 100 requests/min per IP (`x-ratelimit-*` headers). **Body limit: 1 MiB (1,048,576 bytes)** → **413** beyond that. **Validation.** Unless `draft: true`, `metadata` is validated against the ESPR category rules for `metadata.category` plus cross-field rules (e.g. `materialComposition` percentages must sum to 100 ±0.1, `originCountry` must be a real ISO 3166-1 alpha-2 code), and the product's EPCIS traceability lineage is audited. For five categories (textiles, batteries, electronics, chemicals, construction) the authoritative per-category JSON Schema is served live at `GET /api/v1/schemas/{category}`; the other four (cosmetics, toys, iron-steel, aluminium) are validated by built-in server-side rules and `GET /api/v1/schemas/{category}` returns **404** for them. Failure returns the **400 Validation Failed** body with per-field `errors[]` (plus `warnings[]` when any exist — the key is omitted entirely when there are none). A passing payload may still produce non-blocking `warnings[]`, echoed in the 201. `friendlyMessage` texts are localized via `?lang=` or `Accept-Language` (default `en`); category-validity errors (`metadata.category` missing or unknown) carry no `friendlyMessage`. **Drafts.** `draft: true` skips ALL validation, stores the passport with `status: "DRAFT"` (not publicly resolvable), returns `message: "Draft passport saved"` with `warnings: []`, and does **not** emit a webhook. **Identifier handling.** `productId` may be a GTIN-14 (14 digits, GS1 mod-10 check digit), a GRAI (14-digit numeric asset id + up to 16 alphanumeric serial chars), or a free-form SKU. A valid GTIN-14 is auto-copied into `metadata.gtin` (a GRAI into `metadata.grai`) before storage. The server mints a UUID passport id and a GS1 Digital Link URI `https://opendpp-node.eu/{01|8003}/{productId}/21/{passportId}`. **Operator binding.** With `operatorId` omitted, the passport is attributed to the first economic operator bound to your workspace; if no operator is bound at all the request fails **400** (the API never fabricates an operator identity — register one via `POST /api/v1/operators`). An `operatorId` not bound to your workspace → **403**. Operator-scoped API keys force their own operator and **403** on mismatch. The `(productId, operatorId)` pair is unique → **409** on duplicates. An optional `facilityId` must reference a Facility in your workspace (**400** otherwise). **Webhook:** non-draft creation transactionally enqueues a `passport.ingested` event whose payload is the public redacted JSON-LD passport document (same masking as the 201 `passport` field). Drafts emit nothing. **Response caveats:** the 201 `passport` field is the **public, redacted** JSON-LD representation — even for the creator. The owner-only metadata key `facilityDetails` is replaced with the literal placeholder `"[REDACTED - Privileged Access Required]"` (it appears as the placeholder even when you did not supply it), and for `category: "batteries"` the restricted legitimate-interest keys `detailedPerformance`, `lifecycleAndInUse` and `circularityAndDisassembly` (Battery Reg. Annex XIII parts 2-4) are masked the same way when present. `enrichment` is stored outside the validated metadata and Merkle seal and never appears in the JSON-LD document. Only `success`, `message`, `passport`, and `warnings` are emitted at the top level of the 201 body. **Other 400 bodies:** non-validation failures (whitespace-only `productId`, no bound operator, unknown `facilityId`) reuse status 400 with the plain `{"success": false, "error": "Bad Request", "message": …}` triple and **no** `errors`/`warnings` arrays. Requests rejected before the handler runs — request-body schema violations (e.g. missing `productId`) and malformed JSON — come back as just `{"error": "Bad Request", "message": …}`. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `lang` | query | no | string | Locale for localized friendlyMessage validation texts. | ## Request body Schema (required): [PassportCreateRequest](/schemas/PassportCreateRequest.md). ```json { "productId": "09501101530003", "operatorId": "5c1e0f3a-7b2d-4c8e-a91f-2d3e4f5a6b7c", "metadata": { "category": "iron-steel", "originCountry": "DE", "materialComposition": [ { "material": "Recycled steel", "percentage": 62.5 }, { "material": "Virgin steel", "percentage": 37.5 } ], "facilityDetails": [ { "facilityName": "Musterstahl Works Duisburg", "location": "Duisburg, DE", "activity": "Hot rolling" } ], "regulatoryCompliance": { "ceMarking": true, "certificates": [ { "name": "EN 10025-2 Mill Certificate", "referenceNumber": "MC-2026-00417", "issuer": "TUV Rheinland" } ] }, "scrapMetalContentRatio": 62.5, "tensileStrengthClass": "S355", "carbonEmissionIntensityPerTon": 1.42 }, "enrichment": { "tagline": "Low-carbon structural steel", "images": [ { "url": "https://cdn.example.com/steel-beam.jpg", "caption": "S355 beam" } ] } } ``` ## Responses - **201** — Passport created (or draft saved). → [PassportIngestCreated](/schemas/PassportIngestCreated.md) - **400** — Three variants share this status: (1) Validation Failed — the metadata failed ESPR category / cross-field / traceability validation; carries per-field errors[]… → [Error](/schemas/Error.md), [ValidationErrorItem](/schemas/ValidationErrorItem.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **409** — A passport already exists for this (productId, operatorId) pair. → [Error](/schemas/Error.md) - **413** — Body exceeds the 1 MiB (1,048,576-byte) body limit. - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/passports' \ --data '{"productId":"09501101530003","operatorId":"5c1e0f3a-7b2d-4c8e-a91f-2d3e4f5a6b7c","metadata":{"category":"iron-steel","originCountry":"DE","materialComposition":[{"material":"Recycled steel","percentage":62.5},{"material":"Virgin steel","percentage":37.5}],"facilityDetails":[{"facilityName":"Musterstahl Works Duisburg","location":"Duisburg, DE","activity":"Hot rolling"}],"regulatoryCompliance":{"ceMarking":true,"certificates":[{"name":"EN 10025-2 Mill Certificate","referenceNumber":"MC-2026-00417","issuer":"TUV Rheinland"}]},"scrapMetalContentRatio":62.5,"tensileStrengthClass":"S355","carbonEm…' ``` ## See also Schemas: [Error](/schemas/Error.md), [PassportCreateRequest](/schemas/PassportCreateRequest.md), [PassportIngestCreated](/schemas/PassportIngestCreated.md), [ValidationErrorItem](/schemas/ValidationErrorItem.md). ================================================================================ # FILE: /okf/operations/validatePassport.md ================================================================================ --- type: API Endpoint title: Dry-run ESPR validation of passport metadata (nothing is stored) description: Dry-run ESPR validation of passport metadata (nothing is stored) resource: https://opendpp-node.eu/api/v1/passports/validate-only tags: - POST - passports timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/passports/validate-only` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Runs the full ESPR category compliance validation on a metadata payload **without persisting anything** — intended for pre-flight checks in integration pipelines. **Permission:** `passport:create` (Bearer API key or session JWT + CSRF for cookie sessions). Despite being read-only in effect, it is gated as a write permission, so subscription gating (**402**) applies. **Rate limit:** global 100 requests/min per IP. **Body limit: 262,144 bytes (256 KiB)** → **413** beyond that. **Behavioral caveats:** - The EPCIS **traceability lineage audit is NOT run** here (it only runs at real ingestion), so a payload can pass this dry-run and still fail `POST /api/v1/passports` on traceability errors. - `operatorId` is accepted by the body schema but **ignored** by the handler. - The 200 body always carries `errors: []`; `warnings` is **omitted entirely** when there are none (it is not an empty array). The same omission applies to `warnings` on the 400 Validation Failed body. - `friendlyMessage` localization via `?lang=` / `Accept-Language` (28 languages, default `en`); category-validity errors (`metadata.category` missing or unknown) carry no `friendlyMessage`. - Structural rejections of the request body (e.g. missing `productId`, non-object `metadata`) and malformed JSON return just `{"error": "Bad Request", "message": …}`; the only structurally bad input that reaches the handler is a whitespace-only `productId`, answered with the fuller `Bad Request` body shown below. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `lang` | query | no | string | Locale for localized friendlyMessage validation texts (en, bg, hr, cs, da, nl, et, fi, fr, de, el, hu, ga, it, lv, lt, mt, pl, pt, ro, sk, sl, es, sv, no, is,… | ## Request body Schema (required): [PassportValidateOnlyRequest](/schemas/PassportValidateOnlyRequest.md). ```json { "productId": "09501101530003", "metadata": { "category": "iron-steel", "originCountry": "DE", "materialComposition": [ { "material": "Recycled steel", "percentage": 62.5 }, { "material": "Virgin steel", "percentage": 37.5 } ], "facilityDetails": [ { "facilityName": "Musterstahl Works Duisburg", "location": "Duisburg, DE", "activity": "Hot rolling" } ], "regulatoryCompliance": { "ceMarking": true, "certificates": [ { "name": "EN 10025-2 Mill Certificate", "referenceNumber": "MC-2026-00417", "issuer": "TUV Rheinland" } ] }, "scrapMetalContentRatio": 62.5, "tensileStrengthClass": "S355", "carbonEmissionIntensityPerTon": 1.42 } } ``` ## Responses - **200** — Metadata is valid for its ESPR category. → [PassportValidateOnlyResult](/schemas/PassportValidateOnlyResult.md) - **400** — Validation failed, or the request body was structurally invalid. → [PassportValidateOnlyError](/schemas/PassportValidateOnlyError.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **413** — Body exceeds the 262,144-byte (256 KiB) route body limit. - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/passports/validate-only' \ --data '{"productId":"09501101530003","metadata":{"category":"iron-steel","originCountry":"DE","materialComposition":[{"material":"Recycled steel","percentage":62.5},{"material":"Virgin steel","percentage":37.5}],"facilityDetails":[{"facilityName":"Musterstahl Works Duisburg","location":"Duisburg, DE","activity":"Hot rolling"}],"regulatoryCompliance":{"ceMarking":true,"certificates":[{"name":"EN 10025-2 Mill Certificate","referenceNumber":"MC-2026-00417","issuer":"TUV Rheinland"}]},"scrapMetalContentRatio":62.5,"tensileStrengthClass":"S355","carbonEmissionIntensityPerTon":1.42}}' ``` ## See also Schemas: [PassportValidateOnlyError](/schemas/PassportValidateOnlyError.md), [PassportValidateOnlyRequest](/schemas/PassportValidateOnlyRequest.md), [PassportValidateOnlyResult](/schemas/PassportValidateOnlyResult.md). ================================================================================ # FILE: /okf/operations/validatePassportPublic.md ================================================================================ --- type: API Endpoint title: Public dry-run ESPR metadata validation (strictly rate-limited) description: Public dry-run ESPR metadata validation (strictly rate-limited) resource: https://opendpp-node.eu/api/v1/passports/validate-only-public tags: - POST - passports timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/passports/validate-only-public` **Domain:** [Passports](/tags/passports.md) **Authentication:** **Public** — no authentication required. Identical validation semantics to `POST /api/v1/passports/validate-only`, but **fully public — no authentication of any kind**, intended for try-before-you-buy schema checks. Nothing is persisted. **Rate limit: 10 requests/min per IP** — a strict per-route limit that **replaces** the global 100/min for this endpoint (emits `x-ratelimit-limit` / `x-ratelimit-remaining` / `x-ratelimit-reset` headers and `retry-after` on 429). **Body limit: 65,536 bytes (64 KiB)** → **413** beyond that. These caps exist because the endpoint runs the full validation engine unauthenticated (DoS mitigation). **Behavioral caveats:** - No tenant context: the EPCIS traceability lineage audit is **not** run, and `operatorId` is accepted but ignored. - The 200 body always carries `errors: []`; `warnings` is omitted entirely when there are none (same omission on the 400 Validation Failed body). - Error/warning `friendlyMessage` localization via `?lang=` / `Accept-Language` (28 languages, default `en`); category-validity errors carry no `friendlyMessage`. - Structural rejections of the request body (e.g. missing `productId`) and malformed JSON return just `{"error": "Bad Request", "message": …}`; a whitespace-only `productId` gets the fuller `Bad Request` body shown below. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `lang` | query | no | string | Locale for localized friendlyMessage validation texts (en, bg, hr, cs, da, nl, et, fi, fr, de, el, hu, ga, it, lv, lt, mt, pl, pt, ro, sk, sl, es, sv, no, is,… | ## Request body Schema (required): [PassportValidateOnlyRequest](/schemas/PassportValidateOnlyRequest.md). ```json { "productId": "09501101530003", "metadata": { "category": "iron-steel", "originCountry": "DE", "materialComposition": [ { "material": "Recycled steel", "percentage": 62.5 }, { "material": "Virgin steel", "percentage": 37.5 } ], "facilityDetails": [ { "facilityName": "Musterstahl Works Duisburg", "location": "Duisburg, DE", "activity": "Hot rolling" } ], "regulatoryCompliance": { "ceMarking": true, "certificates": [ { "name": "EN 10025-2 Mill Certificate", "referenceNumber": "MC-2026-00417", "issuer": "TUV Rheinland" } ] }, "scrapMetalContentRatio": 62.5, "tensileStrengthClass": "S355", "carbonEmissionIntensityPerTon": 1.42 } } ``` ## Responses - **200** — Metadata is valid for its ESPR category. → [PassportValidateOnlyResult](/schemas/PassportValidateOnlyResult.md) - **400** — Validation failed or the body was structurally invalid — same three variants as the authenticated validate-only endpoint. → [PassportValidateOnlyError](/schemas/PassportValidateOnlyError.md) - **413** — Body exceeds the 65,536-byte (64 KiB) route body limit. - **429** — Rate limit exceeded. - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/passports/validate-only-public' \ --data '{"productId":"09501101530003","metadata":{"category":"iron-steel","originCountry":"DE","materialComposition":[{"material":"Recycled steel","percentage":62.5},{"material":"Virgin steel","percentage":37.5}],"facilityDetails":[{"facilityName":"Musterstahl Works Duisburg","location":"Duisburg, DE","activity":"Hot rolling"}],"regulatoryCompliance":{"ceMarking":true,"certificates":[{"name":"EN 10025-2 Mill Certificate","referenceNumber":"MC-2026-00417","issuer":"TUV Rheinland"}]},"scrapMetalContentRatio":62.5,"tensileStrengthClass":"S355","carbonEmissionIntensityPerTon":1.42}}' ``` ## See also Schemas: [PassportValidateOnlyError](/schemas/PassportValidateOnlyError.md), [PassportValidateOnlyRequest](/schemas/PassportValidateOnlyRequest.md), [PassportValidateOnlyResult](/schemas/PassportValidateOnlyResult.md). ================================================================================ # FILE: /okf/operations/bulkIngestPassports.md ================================================================================ --- type: API Endpoint title: Bulk-ingest up to 200 passports with per-row error reporting description: Bulk-ingest up to 200 passports with per-row error reporting resource: https://opendpp-node.eu/api/v1/passports/bulk tags: - POST - passports timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/passports/bulk` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Ingests up to **200** passports in one request with **partial-success semantics**: each row is validated and inserted independently; failed rows are skipped and reported as human-readable strings in `errors[]`. Returns **201** as long as at least one row was inserted (even with row errors); returns **400 Bulk Ingestion Failed** only when **every** row failed. **Permission:** `passport:create` (Bearer API key or session JWT + CSRF for cookie sessions; subscription gating → **402**). **Rate limit:** global 100 requests/min per IP. **Body limit: 1 MiB (1,048,576 bytes)** → **413** beyond that; in practice the `maxItems: 200` envelope cap is the effective bound for typical rows. Envelope violations — empty array, more than 200 items, missing `passports` — are rejected before any row is processed, with the full default validation error body (`{statusCode, code, error, message}`). **Per-row behavior (differences from `POST /api/v1/passports`):** - Rows are validated with the ESPR category engine only — the **EPCIS traceability audit is NOT run** for bulk rows. - No `draft` support: every inserted row is created with `status: "ACTIVE"`. No `enrichment` support. - A valid GTIN-14/GRAI `productId` is **not** auto-copied into `metadata.gtin`/`metadata.grai` (unlike single ingestion). - Operator resolution per row: explicit `operatorId` must be bound to your workspace; otherwise the workspace's first bound operator is used; operator-scoped API keys force their operator. Lookups are cached within the request. - Duplicate `(productId, operatorId)` rows, unknown facilities, and per-row DB failures become `errors[]` strings (prefixed `[SKU: ]`), never a request-level failure. - Each successfully inserted row **transactionally enqueues a `passport.ingested` webhook event** (public redacted JSON-LD payload). - Row validation messages use the localized `friendlyMessage` where the engine provides one (`?lang=` / `Accept-Language`); category-validity errors fall back to the technical `path: message` form. Note the 400 `Bulk Ingestion Failed` body has **no `message` field**, and `errors` is an array of **strings** (not objects). ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `lang` | query | no | string | Locale for the localized validation text inside per-row errors[] strings (en, bg, hr, cs, da, nl, et, fi, fr, de, el, hu, ga, it, lv, lt, mt, pl, pt, ro, sk, s… | ## Request body Schema (required): [PassportBulkRequest](/schemas/PassportBulkRequest.md). ```json { "passports": [ { "productId": "09501101530003", "metadata": { "category": "iron-steel", "originCountry": "DE", "materialComposition": [ { "material": "Recycled steel", "percentage": 62.5 }, { "material": "Virgin steel", "percentage": 37.5 } ], "facilityDetails": [ { "facilityName": "Musterstahl Works Duisburg", "location": "Duisburg, DE", "activity": "Hot rolling" } ], "regulatoryCompliance": { "ceMarking": true, "certificates": [ { "name": "EN 10025-2 Mill Certificate", "referenceNumber": "MC-2026-00417", "issuer": "TUV Rheinland" } ] }, "scrapMetalContentRatio": 62.5, "tensileStrengthClass": "S355", "carbonEmissionIntensityPerTon": 1.42 } }, { "productId": "09501101530010", "metadata": { "category": "iron-steel", "originCountry": "DE", "materialComposition": [ { "material": "Recycled steel", "percentage": 100 } ], "facilityDetails": [ { "facilityName": "Musterstahl Works Duisburg", "location": "Duisburg, DE", "activity": "Cold rolling" } ], "regulatoryCompliance": { "ceMarking": true, "certificates": [ { "name": "EN 10025-2 Mill Certificate", "referenceNumber": "MC-2026-00418", "issuer": "TUV Rheinland" } ] }, "scrapMetalContentRatio": 100, "tensileStrengthClass": "S275", "carbonEmissionIntensityPerTon": 0.61 } } ] } ``` ## Responses - **201** — Bulk run finished with at least one inserted row. → [PassportBulkResult](/schemas/PassportBulkResult.md) - **400** — Either every row failed (Bulk Ingestion Failed, with string errors[] and no message field), or the request never reached row processing: envelope violations of… → [PassportBulkFailure](/schemas/PassportBulkFailure.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **413** — Body exceeds the 1 MiB (1,048,576-byte) body limit. - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/passports/bulk' \ --data '{"passports":[{"productId":"09501101530003","metadata":{"category":"iron-steel","originCountry":"DE","materialComposition":[{"material":"Recycled steel","percentage":62.5},{"material":"Virgin steel","percentage":37.5}],"facilityDetails":[{"facilityName":"Musterstahl Works Duisburg","location":"Duisburg, DE","activity":"Hot rolling"}],"regulatoryCompliance":{"ceMarking":true,"certificates":[{"name":"EN 10025-2 Mill Certificate","referenceNumber":"MC-2026-00417","issuer":"TUV Rheinland"}]},"scrapMetalContentRatio":62.5,"tensileStrengthClass":"S355","carbonEmissionIntensityPerTon":1.42}},{"produc…' ``` ## See also Schemas: [PassportBulkFailure](/schemas/PassportBulkFailure.md), [PassportBulkRequest](/schemas/PassportBulkRequest.md), [PassportBulkResult](/schemas/PassportBulkResult.md). ================================================================================ # FILE: /okf/operations/ingestPassportFromAas.md ================================================================================ --- type: API Endpoint title: Ingest a passport from an AAS JSON Environment (seal-verified) description: Ingest a passport from an AAS JSON Environment (seal-verified) resource: https://opendpp-node.eu/api/v1/passports/aas/ingest tags: - POST - passports timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/passports/aas/ingest` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Ingests (creates **or updates**) a Digital Product Passport from an Industry-4.0 **Asset Administration Shell (AAS) JSON Environment** — the same format produced by OpenDPP's own AAS export. **Permission:** `passport:create` (Bearer API key or session JWT + CSRF for cookie sessions; subscription gating → **402**). **Rate limit:** global 100 requests/min per IP. **Body limit: 262,144 bytes (256 KiB)** → **413**. **Parsing.** The environment must contain a `submodels` array including a submodel with `idShort: "ComplianceMetadata"`, whose `submodelElements` are parsed back into the metadata object; missing it fails 400 (`Ingestion Failed`). `productId` is resolved from `metadata.gtin` || `metadata.grai` || `metadata.productId` || the first shell's `assetInformation.specificAssetIds` entry named `productId` — unresolvable → 400 `Bad Request`. The parsed metadata then passes the full ESPR category validation **plus the EPCIS traceability audit** (400 `Validation Failed` with `errors[]`). **eIDAS seal verification.** If the environment embeds an `eidasVerificationSeal` submodel (`digitalSealHash` / `cryptographicSignature` / `pemPublicKey` elements), the seal is verified against **your tenant's server-held eIDAS public key** — never the key embedded in the request (self-signing is rejected by design). An embedded seal that fails verification → **400 `Signature Verification Failed`**; this includes the case where your workspace holds no matching key. `isSealed`/`signatureVerified` in the 201 echo the outcome (both `false` for unsealed documents). **Upsert semantics.** If a passport already exists for the resolved `(productId, operator)` pair: a **sealed** existing passport refuses re-ingestion (**403** — re-seal explicitly after changes); an unsealed one has its metadata, Merkle tree and seal fields **replaced**, still answering **201**. Operator resolution: operator-scoped API keys use their own operator and **403** when that operator is not bound to your workspace; otherwise the workspace's first bound operator is used; none bound → 400. **Caveats:** - **NO webhook is emitted** — unlike `POST /api/v1/passports` and `/bulk`, AAS ingestion never enqueues `passport.ingested` (or any other event). - The catch-all error path returns **400 `Ingestion Failed`** with the underlying parse/processing message — even for internal failures (this handler does not emit its own 500). - Validation `friendlyMessage` localization via `?lang=` / `Accept-Language`; category-validity errors carry no `friendlyMessage`. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `lang` | query | no | string | Locale for localized friendlyMessage validation texts (en, bg, hr, cs, da, nl, et, fi, fr, de, el, hu, ga, it, lv, lt, mt, pl, pt, ro, sk, sl, es, sv, no, is,… | ## Request body Schema (required): [AasEnvironmentInput](/schemas/AasEnvironmentInput.md). ## Responses - **201** — Passport created or (if it existed unsealed) updated from the AAS environment. → [AasIngestCreated](/schemas/AasIngestCreated.md) - **400** — Four variants share this status: Bad Request (non-object body, unresolvable productId, no bound operator), Signature Verification Failed (embedded seal invalid… → [Error](/schemas/Error.md), [ValidationErrorItem](/schemas/ValidationErrorItem.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **413** — Body exceeds the 262,144-byte (256 KiB) route body limit. - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X POST 'https://opendpp-node.eu/api/v1/passports/aas/ingest' ``` ## See also Schemas: [AasEnvironmentInput](/schemas/AasEnvironmentInput.md), [AasIngestCreated](/schemas/AasIngestCreated.md), [Error](/schemas/Error.md), [ValidationErrorItem](/schemas/ValidationErrorItem.md). ================================================================================ # FILE: /okf/operations/getPassport.md ================================================================================ --- type: API Endpoint title: Fetch a single passport (content-negotiated JSON-LD / AAS / HTML) description: Fetch a single passport (content-negotiated JSON-LD / AAS / HTML) resource: https://opendpp-node.eu/api/v1/passports/{id} tags: - GET - passports timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/passports/{id}` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Owner-side alias of the public resolver. Accepts either the passport **UUID** or its caller-supplied **`productId`** (GTIN-14 / GRAI / SKU), scoped to operators bound to your workspace. After the scoped lookup the request is **re-dispatched internally to `GET /passport/{uuid}`**, forwarding all request headers, and the inner response (status, content type, body) is returned as-is. **Permission:** `passport:read` (read-only — no subscription/402 gate). **Content negotiation** (substring match on `Accept`): `application/aas+json` → role-filtered AAS environment; `text/html` → SSR passport page; anything else (including `application/json`, `*/*`, or no header) → JSON-LD with `Content-Type: application/ld+json` (the default). **Access-tier caveat (privilege is resolved from the *forwarded* headers, not the already-authenticated context):** only **database API keys** (`Authorization: Bearer op_dpp_token_…`) of the owning or operator-bound tenant are recognized as owner by the inner resolver. Those callers get the **owner-tier** document: `facilityDetails` and battery restricted keys unmasked, `manufacturingFacility` includes `streetAddress`/`city`/`postalCode`, and DRAFT passports are visible. Callers authenticated with a **JWT session** (login cookie or bearer JWT) receive the **public-redacted** tier instead, and DRAFT passports answer 404 with the forwarded public body (no `success` field). Every successful resolution records an anonymized-IP access audit entry. **Rate limits:** global limiter 100 req/min/IP with `x-ratelimit-*` headers, **plus** the forwarded public resolver's own limiter (30 req/min/IP, no headers) — both 429 shapes are possible (see 429). ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Passport UUID or caller-supplied productId (GTIN-14 / GRAI / SKU). | ## Responses - **200** — The resolved passport. → [PassportAasEnvironment](/schemas/PassportAasEnvironment.md), [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — Two distinct bodies. → [Error](/schemas/Error.md) - **429** — Two possible sources. - **500** — Unexpected failure. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/passports/{id}' ``` ## See also Schemas: [Error](/schemas/Error.md), [PassportAasEnvironment](/schemas/PassportAasEnvironment.md), [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md). ================================================================================ # FILE: /okf/operations/updatePassport.md ================================================================================ --- type: API Endpoint title: Update passport metadata (versioned to history) description: Update passport metadata (versioned to history) resource: https://opendpp-node.eu/api/v1/passports/{id} tags: - PUT - passports timestamp: 2026-06-17T00:00:00Z --- `PUT /api/v1/passports/{id}` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Replaces the passport's `metadata` (the Merkle root and leaf hashes are recomputed) and snapshots the **previous** metadata into the passport's version history (version = count + 1, `changedBy` = user email or `api-key:`, `changeReason` defaults to `"API Update"`). **Permission:** `passport:update` (write — subscription gating applies, see 402). Cookie sessions must send `X-CSRF-Token` (double-submit); Bearer/API-key clients are exempt. **Lookup:** by passport **UUID only** — `productId` aliasing is NOT supported on this endpoint. The passport must belong to an operator bound to your workspace. **Draft semantics (`draft` flag):** - `draft: true` **skips ESPR validation entirely** and forces `status: "DRAFT"` — note this also demotes an already-published (ACTIVE/RECALLED/DECOMMISSIONED) passport back to DRAFT. - `draft` absent/false: `metadata` is validated against the ESPR category rules (400 on failure — see below). If the passport was a DRAFT it is **published**: status becomes `ACTIVE`, a `passport.ingested` webhook is enqueued transactionally (public-redacted JSON-LD payload) and an in-app notification is created best-effort afterwards. Live statuses are left untouched. **Validation divergence:** the 400 validation body here contains `errors` but — unlike `POST /api/v1/passports` — **never a `warnings` array**. `friendlyMessage` is localized via the `lang` query parameter or `Accept-Language` (28 languages, default `en`; unsupported values silently fall back). **Sealed passports are immutable in place:** if `digitalSeal` is set the update is refused with **403** (message: "This passport is sealed and cannot be edited in place — editing would invalidate the eIDAS advanced electronic seal. Re-seal explicitly after any change."). **Facility:** omit `facilityId` to leave it unchanged; pass `null` or `""` to detach; pass a facility UUID owned by your tenant to attach (400 if not found in your workspace). **Enrichment:** include the `enrichment` key (even as `null`/`{}`) to overwrite the presentational marketing block; omit it to leave it unchanged. Values are sanitized server-side (truncated/sliced, http(s) URLs only), never rejected. **Response caveat:** the returned `passport` document is serialized at the **public** redaction tier — `facilityDetails` (and battery restricted keys) appear as `"[REDACTED - Privileged Access Required]"` even though you are the owner. **Rate limits:** global limiter, 100 req/min/IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Passport UUID. | | `lang` | query | no | string | Locale for friendlyMessage localization in validation errors. | ## Request body Schema (required): [PassportUpdateRequest](/schemas/PassportUpdateRequest.md). ```json { "metadata": { "category": "textiles", "originCountry": "PT", "materialComposition": [ { "material": "Organic cotton", "percentage": 80 }, { "material": "Recycled polyester", "percentage": 20 } ], "fiberComposition": [ { "fiber": "cotton", "percentage": 80 }, { "fiber": "polyester", "percentage": 20 } ], "careInstructions": "Machine wash cold, line dry", "size": "M", "facilityDetails": [ { "facilityName": "Aurora Spinning Mill", "location": "Porto, PT", "activity": "Spinning", "eudrPlots": [ { "plotId": "PLOT-001", "polygonType": "point", "coordinates": [ { "lat": 41.1579, "lng": -8.6291 } ] } ], "traceabilityDocs": [ { "documentName": "GOTS scope certificate", "documentHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "documentUrl": "https://docs.aurora-textiles.example/gots.pdf" } ] } ], "regulatoryCompliance": { "ceMarking": true, "certificates": [ { "name": "GOTS", "referenceNumber": "GOTS-2026-0042", "issuer": "Control Union", "validUntil": "2027-05-31" } ] } }, "changeReason": "Updated fiber composition after supplier audit", "facilityId": "5b21cf12-7d24-4a8e-9a3c-2f1f4f4f9d10" } ``` ## Responses - **200** — Updated (or published) passport. → [PassportUpdateResponse](/schemas/PassportUpdateResponse.md) - **400** — Either a plain Bad Request — body is not a JSON object; metadata missing/not an object; facilityId not found in your workspace (Facility not found… → [Error](/schemas/Error.md), [PassportUpdateValidationError](/schemas/PassportUpdateValidationError.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — History snapshot or transactional update failure returns the standard envelope (message may echo the internal error text or fall back to "Failed to update pass… → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X PUT 'https://opendpp-node.eu/api/v1/passports/{id}' \ --data '{"metadata":{"category":"textiles","originCountry":"PT","materialComposition":[{"material":"Organic cotton","percentage":80},{"material":"Recycled polyester","percentage":20}],"fiberComposition":[{"fiber":"cotton","percentage":80},{"fiber":"polyester","percentage":20}],"careInstructions":"Machine wash cold, line dry","size":"M","facilityDetails":[{"facilityName":"Aurora Spinning Mill","location":"Porto, PT","activity":"Spinning","eudrPlots":[{"plotId":"PLOT-001","polygonType":"point","coordinates":[{"lat":41.1579,"lng":-8.6291}]}],"traceabilityDocs":[{"documentName":"GOTS scope certificate","d…' ``` ## See also Schemas: [Error](/schemas/Error.md), [PassportUpdateRequest](/schemas/PassportUpdateRequest.md), [PassportUpdateResponse](/schemas/PassportUpdateResponse.md), [PassportUpdateValidationError](/schemas/PassportUpdateValidationError.md). ================================================================================ # FILE: /okf/operations/deleteDraftPassport.md ================================================================================ --- type: API Endpoint title: Permanently delete a DRAFT passport description: Permanently delete a DRAFT passport resource: https://opendpp-node.eu/api/v1/passports/{id} tags: - DELETE - passports timestamp: 2026-06-17T00:00:00Z --- `DELETE /api/v1/passports/{id}` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Hard-deletes a passport **only while it is a DRAFT** (never published, not publicly resolvable, no retention duty). Children (history, access logs, battery units) cascade on delete. Published passports (ACTIVE/RECALLED/DECOMMISSIONED) are refused with **409** — they must be decommissioned/archived through the status lifecycle (`PUT /api/v1/passports/{id}/status`) to satisfy the ESPR Art. 9(2) persistence duty. **Permission:** `passport:update` (write — subscription gating applies, see 402). Cookie sessions must send `X-CSRF-Token`; Bearer/API-key clients are exempt. **Lookup:** by passport **UUID only** (no `productId` aliasing) and only within the passport's **owning tenant** — an operator-binding alone is not sufficient, unlike PUT. **Rate limits:** global limiter, 100 req/min/IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Passport UUID. | ## Responses - **200** — Draft deleted. - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **409** — The passport is not a DRAFT — published passports cannot be hard-deleted. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected failure. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X DELETE 'https://opendpp-node.eu/api/v1/passports/{id}' ``` ## See also Schemas: [Error](/schemas/Error.md). ================================================================================ # FILE: /okf/operations/sealPassport.md ================================================================================ --- type: API Endpoint title: Apply the tenant's eIDAS advanced electronic seal description: Apply the tenant's eIDAS advanced electronic seal resource: https://opendpp-node.eu/api/v1/passports/{id}/seal tags: - POST - passports timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/passports/{id}/seal` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Signs the passport's Merkle root (SHA-256 tree over the key-sorted top-level `metadata` entries) with the tenant's vault-held **ECDSA P-256 (prime256v1)** private key, producing an eIDAS **advanced** electronic seal (this is a local cryptographic seal — NOT a Commission/EU-registry registration, and NOT a qualified seal). The base64 signature is stored as `digitalSeal` together with the signing public key (PEM), the X.509 chain binding the key to the tenant's legal identity (surfaced as `proof.x5c`, leaf first, base64 DER), and — **best-effort, opt-in** — an RFC 3161 trusted timestamp over SHA-256(merkleRoot) (`proof.rfc3161`; a TSA outage or missing configuration never blocks sealing, the field is simply absent). A `passport.sealed` webhook is enqueued transactionally with the update (payload: the public-redacted JSON-LD document including the full `proof` block). **Permission:** `passport:seal` (write — subscription gating applies, see 402). Cookie sessions must send `X-CSRF-Token`; Bearer/API-key clients are exempt. **Lookup:** passport **UUID or `productId`** (UUID tried first), restricted to the passport's **owning tenant**. **Behavioral caveats:** - The route does **not** modify the passport's `status` — despite the success message's "and published" wording, a DRAFT stays a DRAFT after sealing. Publish via `PUT /api/v1/passports/{id}` (validated save) instead. - Re-sealing an already-sealed passport is allowed and **overwrites** the previous seal/timestamp. - Once sealed, in-place metadata edits are refused (403 on `PUT /api/v1/passports/{id}`). - Requires the tenant's eIDAS key pair to exist — otherwise 400. - The returned `passport` document is serialized at the **public** redaction tier (masked keys keep their true leaf hashes in `proof.redactedLeaves`, so the seal stays offline-verifiable after redaction). **Rate limits:** global limiter, 100 req/min/IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Passport UUID or caller-supplied productId (GTIN-14 / GRAI / SKU). | ## Responses - **200** — Passport sealed. → [PassportSealResponse](/schemas/PassportSealResponse.md) - **400** — Missing identifier, or the tenant has no eIDAS key pair configured. → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X POST 'https://opendpp-node.eu/api/v1/passports/{id}/seal' ``` ## See also Schemas: [Error](/schemas/Error.md), [PassportSealResponse](/schemas/PassportSealResponse.md). ================================================================================ # FILE: /okf/operations/updatePassportStatus.md ================================================================================ --- type: API Endpoint title: Transition passport lifecycle status (recall / decommission / reactivate) description: Transition passport lifecycle status (recall / decommission / reactivate) resource: https://opendpp-node.eu/api/v1/passports/{id}/status tags: - PUT - passports timestamp: 2026-06-17T00:00:00Z --- `PUT /api/v1/passports/{id}/status` **Domain:** [Passports](/tags/passports.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Transitions a **published** passport between live lifecycle states. The request body carries only `status` (any other keys are ignored — there is no `reason` field; the history entry's change reason is auto-generated as `Status changed: `). **Permission:** `passport:update` (write — subscription gating applies, see 402). Cookie sessions must send `X-CSRF-Token`; Bearer/API-key clients are exempt. **Lookup:** passport **UUID or `productId`** (UUID tried first), scoped to operators bound to your workspace. **Effects:** - `DECOMMISSIONED` — sets `retentionUntil = now + the configured retention period` (default 15 years), starting the minimum-availability retention clock. The passport stays publicly resolvable. - `ACTIVE` (reactivation) — clears `retentionUntil` **and** `archivedAt`. - `RECALLED` — marks the product recalled. - The status change, the version-history entry (who/when/what) and the webhook enqueue are **transactional**; an in-app notification is created **best-effort after the transaction commits** (a notification failure never affects the response). **Webhooks:** `RECALLED` enqueues `passport.recalled`; any other transition (`DECOMMISSIONED`, reactivate-to-`ACTIVE`) enqueues `passport.status_updated` — note that `passport.status_updated` is **not** an explicitly subscribable event filter, so only wildcard (`"*"`) webhook subscriptions receive it. Payloads are the public-redacted JSON-LD document. **Caveats:** DRAFT passports are refused with 409 (publish first via a validated `PUT /api/v1/passports/{id}`). Sealed passports CAN change status — `status` is stored alongside the document, not inside the sealed metadata Merkle tree. The returned `passport` document is serialized at the **public** redaction tier. **Rate limits:** global limiter, 100 req/min/IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Passport UUID or caller-supplied productId (GTIN-14 / GRAI / SKU). | ## Request body Schema (required): [PassportStatusUpdateRequest](/schemas/PassportStatusUpdateRequest.md). ```json { "status": "DECOMMISSIONED" } ``` ## Responses - **200** — Status updated. → [PassportStatusUpdateResponse](/schemas/PassportStatusUpdateResponse.md) - **400** — status missing or not one of the allowed values. → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **409** — The passport is a DRAFT — drafts cannot transition to a live status here. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X PUT 'https://opendpp-node.eu/api/v1/passports/{id}/status' \ --data '{"status":"DECOMMISSIONED"}' ``` ## See also Schemas: [Error](/schemas/Error.md), [PassportStatusUpdateRequest](/schemas/PassportStatusUpdateRequest.md), [PassportStatusUpdateResponse](/schemas/PassportStatusUpdateResponse.md). ================================================================================ # FILE: /okf/operations/resolvePublicPassport.md ================================================================================ --- type: API Endpoint title: Resolve a passport by UUID (JSON-LD / AAS / HTML) description: Resolve a passport by UUID (JSON-LD / AAS / HTML) resource: https://opendpp-node.eu/passport/{id} tags: - GET - public-resolution timestamp: 2026-06-17T00:00:00Z --- `GET /passport/{id}` **Domain:** [Public Resolution](/tags/public-resolution.md) **Authentication:** **Tiered** — resolves anonymously (public view) or with a Bearer API key / capability token for a fuller view. Public, content-negotiated resolution of a Digital Product Passport by its server-assigned UUID. Lookup is by primary key only — GTIN/GRAI/serial lookups go through the GS1 Digital Link gateway (`GET /01/{gtin14}`, `GET /8003/{grai}`). **Content negotiation** (substring match on `Accept`, checked in order): `application/aas+json` (or bare `aas+json`) → role-filtered Asset Administration Shell environment; `application/vc+jwt` (or bare `vc+jwt`) → a signed UNTP DigitalProductPassport credential (public tier; `406 Not Acceptable` when the passport has no manufacturing facility with a country of production); `application/vc+ld+json` (or bare `vc+ld+json`) → the same credential with an embedded W3C Data Integrity proof (`ecdsa-jcs-2019`), same `406` condition; `text/html` → server-rendered passport page; anything else (including `application/json`, `*/*`, or no header) → JSON-LD (`application/ld+json`, the default). `Vary: Accept` is always set on the 200. **Access tiers** — no permission string (public endpoint). Credentials are *optional* and never produce 401/402/403 here; an invalid or foreign credential silently degrades to the public tier: - **Public** (anonymous): restricted metadata keys (for category `batteries`: `detailedPerformance`, `lifecycleAndInUse`, `circularityAndDisassembly` — masked only when present) and the owner-only key `facilityDetails` (present-as-placeholder in every non-owner response, even when the underlying metadata never contained it) carry the literal placeholder `[REDACTED - Privileged Access Required]`. Each masked key that exists in the sealed metadata keeps its true Merkle leaf hash in `proof.redactedLeaves`, so the eIDAS seal stays offline-verifiable after redaction; a placeholder-valued key with no `redactedLeaves` entry was never in the sealed metadata and must be excluded when rebuilding the root. - **Legitimate interest / authority**: a capability grant token — `dpp_li_…` (tenant-issued) or `dpp_auth_…` (platform-issued, not tenant-revocable) — sent as `Authorization: Bearer ` or `?grant=`, with TENANT or PASSPORT scope covering this passport, unlocks the restricted tier-2 keys. `facilityDetails`, the facility street address and DRAFT passports stay hidden. Grant-unlocked responses add `Cache-Control: private, no-store` and `Referrer-Policy: no-referrer`. - **Owner**: a tenant **API key** (`op_dpp_token_…`, shown once at creation) belonging to the owning tenant or to a tenant bound to the passport's economic operator — sent as Bearer or, legacy, as the literal value of the `opendpp_session` cookie. Only API keys are matched on the public resolvers: a Console JWT login session in that cookie does **not** unlock owner tier (it silently resolves as public). Owners see everything, including DRAFT passports, owner-only metadata keys and the facility street address (`manufacturingFacility.streetAddress`/`city`/`postalCode`). In the AAS representation the owner credential's API-key role drives element filtering; a grant maps to the `legitimate_interest` filter tier, anonymous to `public`. DRAFT passports are hidden from everyone but the owner (404 with a body identical to a true miss). Every resolution is recorded in the passport's access audit log with an anonymized IP. **Rate limit:** 30 requests/min/IP via a per-process in-memory limiter; its 429 body is the two-field public error shape (no `success` field). This limiter adds no headers of its own — the `x-ratelimit-*` headers still present on responses (including these 429s) belong to the global platform limit (100 req/min/IP, 600/min for known crawler user agents), which applies on top. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | The passport's server-assigned UUID (returned as id on creation and embedded as AI-21 in the SKU-level Digital Link URI). | | `grant` | query | no | string | Capability grant token (dpp_li_… legitimate-interest, dpp_auth_… authority) — the inspection-link path for QR-scanning inspectors who cannot set headers. | ## Responses - **200** — The passport in the negotiated representation. → [AasEnvironment](/schemas/AasEnvironment.md), [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) - **400** — Passport identifier missing. → [Error](/schemas/Error.md) - **404** — No passport with that UUID — or the passport is a DRAFT and the caller is not owner-tier (identical body, deliberate). → [Error](/schemas/Error.md) - **429** — Public-resolution rate limit exceeded (30 requests/min per IP; no rate-limit headers). → [Error](/schemas/Error.md) - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ # optional for the public tier -X GET 'https://opendpp-node.eu/passport/{id}' ``` ## See also Schemas: [AasEnvironment](/schemas/AasEnvironment.md), [Error](/schemas/Error.md), [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md). ================================================================================ # FILE: /okf/operations/resolveGs1Gtin.md ================================================================================ --- type: API Endpoint title: GS1 Digital Link resolution by GTIN-14 (AI 01) description: GS1 Digital Link resolution by GTIN-14 (AI 01) resource: https://opendpp-node.eu/01/{gtin14} tags: - GET - public-resolution timestamp: 2026-06-17T00:00:00Z --- `GET /01/{gtin14}` **Domain:** [Public Resolution](/tags/public-resolution.md) **Authentication:** **Tiered** — resolves anonymously (public view) or with a Bearer API key / capability token for a fuller view. Unified GS1 Digital Link gateway, GTIN branch. The GTIN-14 is matched against `metadata.gtin`, `metadata.grai`, or the passport's `productId`. On tenant workspaces (`https://{tenant}.opendpp-node.eu`) the lookup is scoped to that tenant — an unknown subdomain returns 404. Without a tenant scope, a GTIN matching more than one passport is rejected with 400 (ambiguous); disambiguate via a brand subdomain (the `?subdomain=` query override is honoured in non-production environments only). Content negotiation (JSON-LD default / `application/aas+json` / `application/vc+jwt` / `application/vc+ld+json` / `text/html`, `Vary: Accept` always set), access tiers (public / `dpp_li_…`·`dpp_auth_…` grant via Bearer or `?grant=` / owner = a tenant **API key** sent as Bearer or, legacy, as the literal `opendpp_session` cookie value — Console JWT login sessions do **not** unlock owner tier), DRAFT hiding, access-audit logging (anonymized IP), and grant response headers (`Cache-Control: private, no-store`, `Referrer-Policy: no-referrer`) are identical to `GET /passport/{id}` — see that operation for the full tier semantics. No permission string (public endpoint); invalid credentials silently degrade to the public tier, never 401/403. The gateway also accepts additional GS1 AI key/value path pairs after the GTIN; the only one acted on is AI 21 (serial) — documented separately as `GET /01/{gtin14}/21/{serial}`. (The underlying route is `GET /{ai}/*`; AI prefixes other than `01` and `8003` get a 400.) **Rate limit:** 30 requests/min/IP, per-process in-memory limiter; two-field 429 body without `success`. The limiter adds no headers of its own — `x-ratelimit-*` headers on responses come from the global platform limit (100 req/min/IP), which applies on top. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `gtin14` | path | yes | string | GTIN-14: exactly 14 digits with a valid GS1 modulo-10 check digit (the check digit is validated server-side — the pattern alone is not sufficient). | | `grant` | query | no | string | Capability grant token (dpp_li_… / dpp_auth_…); equivalent to Authorization: Bearer. | ## Responses - **200** — The matched passport in the negotiated representation (same envelope as GET /passport/{id}). → [AasEnvironment](/schemas/AasEnvironment.md), [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) - **400** — Invalid GTIN-14 (must be 14 digits with a valid modulo-10 check digit) or — when no tenant scope is in play and no AI-21 serial was given — an ambiguous lookup… → [Error](/schemas/Error.md) - **404** — No passport matches the identifier (content-negotiated: HTML page for Accept: text/html, JSON otherwise; Vary: Accept set), a DRAFT match was hidden from a non… → [Error](/schemas/Error.md) - **429** — Public-resolution rate limit exceeded (30 requests/min per IP; no rate-limit headers). → [Error](/schemas/Error.md) - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ # optional for the public tier -X GET 'https://opendpp-node.eu/01/{gtin14}' ``` ## See also Schemas: [AasEnvironment](/schemas/AasEnvironment.md), [Error](/schemas/Error.md), [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md). ================================================================================ # FILE: /okf/operations/resolveGs1GtinSerial.md ================================================================================ --- type: API Endpoint title: GS1 Digital Link serialised-item redirect (AI 01 + AI 21) description: GS1 Digital Link serialised-item redirect (AI 01 + AI 21) resource: https://opendpp-node.eu/01/{gtin14}/21/{serial} tags: - GET - public-resolution timestamp: 2026-06-17T00:00:00Z --- `GET /01/{gtin14}/21/{serial}` **Domain:** [Public Resolution](/tags/public-resolution.md) **Authentication:** **Public** — no authentication required. GS1 Digital Link resolution of an *individual serialised item*. This path never returns a document directly — on success it issues a `302` redirect (the query string, including `?grant=`, is preserved on the `Location` URL): 1. If the GTIN resolves to a SKU/type passport that has a serialised battery unit whose `serialNumber` equals the AI-21 value → `302` to `/unit/{unitId}` (Battery Reg. Art. 77(2) per-unit view). 2. Otherwise (legacy fallback) the AI-21 value is matched against the passport UUID, `metadata.serialNumber`, or `metadata["21"]`; if a passport matches → `302` to `/passport/{passportId}`. 3. Otherwise → `404` (content-negotiated). The ambiguity check of the bare-GTIN branch is skipped when an AI-21 serial is present. The redirect handler itself never evaluates credentials — access tiers (owner / grant / public) apply at the redirect target; carry the grant in `?grant=` (preserved across the redirect) or re-send the `Authorization` header to the target. On tenant subdomains the lookup is scoped to that tenant (unknown subdomain → 404, JSON only). No permission string (public endpoint). **Rate limit:** 30 requests/min/IP (in-memory public limiter; two-field 429 body without `success`). The limiter adds no headers of its own — `x-ratelimit-*` headers come from the global platform limit, which applies on top — and the redirect target counts as a second request against both. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `gtin14` | path | yes | string | GTIN-14: exactly 14 digits with a valid GS1 modulo-10 check digit (validated server-side). | | `serial` | path | yes | string | GS1 AI-21 serial. | | `grant` | query | no | string | Capability grant token. | ## Responses - **302** — Redirect to the resolved resource. - **400** — Invalid GTIN-14 (format / modulo-10 check digit). → [Error](/schemas/Error.md) - **404** — Neither a serialised unit nor a fallback passport matches (content-negotiated HTML/JSON, Vary: Accept), or unknown tenant subdomain (JSON only). → [Error](/schemas/Error.md) - **429** — Public-resolution rate limit exceeded (30 requests/min per IP; no rate-limit headers). → [Error](/schemas/Error.md) - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -X GET 'https://opendpp-node.eu/01/{gtin14}/21/{serial}' ``` ## See also Schemas: [Error](/schemas/Error.md). ================================================================================ # FILE: /okf/operations/resolveGs1Grai.md ================================================================================ --- type: API Endpoint title: GS1 Digital Link resolution by GRAI (AI 8003) description: GS1 Digital Link resolution by GRAI (AI 8003) resource: https://opendpp-node.eu/8003/{grai} tags: - GET - public-resolution timestamp: 2026-06-17T00:00:00Z --- `GET /8003/{grai}` **Domain:** [Public Resolution](/tags/public-resolution.md) **Authentication:** **Tiered** — resolves anonymously (public view) or with a Bearer API key / capability token for a fuller view. Unified GS1 Digital Link gateway, GRAI branch (Global Returnable Asset Identifier). The GRAI is matched against `metadata.gtin`, `metadata.grai`, or the passport's `productId`. Everything else — content negotiation (JSON-LD default / `application/aas+json` / `application/vc+jwt` / `application/vc+ld+json` / `text/html`, `Vary: Accept`), access tiers (public / grant `dpp_li_…`·`dpp_auth_…` via Bearer or `?grant=` / owner = tenant API key as Bearer or legacy `opendpp_session` cookie value, never a Console JWT session), DRAFT hiding, tenant-subdomain scoping, the no-tenant-scope ambiguity 400, access-audit logging, grant response headers, and the 30 req/min/IP in-memory rate limit (two-field 429 body without `success`; the limiter adds no headers of its own — `x-ratelimit-*` headers come from the global 100 req/min/IP limit, which applies on top) — is identical to `GET /01/{gtin14}`; see that operation and `GET /passport/{id}` for full semantics. An additional `/21/{serial}` AI pair after the GRAI behaves exactly like `GET /01/{gtin14}/21/{serial}` (302 redirect to `/unit/{id}` or `/passport/{id}`). No permission string (public endpoint). ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `grai` | path | yes | string | GRAI: a 14-digit numeric asset identifier with a valid GS1 modulo-10 check digit (validated server-side), followed by an optional alphanumeric serial component… | | `grant` | query | no | string | Capability grant token (dpp_li_… / dpp_auth_…); equivalent to Authorization: Bearer. | ## Responses - **200** — The matched passport in the negotiated representation (same envelope as GET /passport/{id}). → [AasEnvironment](/schemas/AasEnvironment.md), [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) - **400** — Invalid GRAI (format / check digit) or — without tenant scope and AI-21 serial — an ambiguous lookup. → [Error](/schemas/Error.md) - **404** — No passport matches (content-negotiated HTML/JSON, Vary: Accept), DRAFT hidden from non-owner, or unknown tenant subdomain (JSON only). → [Error](/schemas/Error.md) - **429** — Public-resolution rate limit exceeded (30 requests/min per IP; no rate-limit headers). → [Error](/schemas/Error.md) - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ # optional for the public tier -X GET 'https://opendpp-node.eu/8003/{grai}' ``` ## See also Schemas: [AasEnvironment](/schemas/AasEnvironment.md), [Error](/schemas/Error.md), [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md). ================================================================================ # FILE: /okf/operations/resolvePublicBatteryUnit.md ================================================================================ --- type: API Endpoint title: Resolve an individual serialised battery unit description: Resolve an individual serialised battery unit resource: https://opendpp-node.eu/unit/{id} tags: - GET - public-resolution timestamp: 2026-06-17T00:00:00Z --- `GET /unit/{id}` **Domain:** [Public Resolution](/tags/public-resolution.md) **Authentication:** **Tiered** — resolves anonymously (public view) or with a Bearer API key / capability token for a fuller view. Public, content-negotiated view of one individual serialised unit (battery; Reg. (EU) 2023/1542 Art. 77(2)) by its unit UUID, including the embedded SKU/type passport (`ofModel`, masked by the same tier rules as `GET /passport/{id}`). **Content negotiation:** `Accept` containing `application/vc+jwt` (or bare `vc+jwt`) → a signed PER-UNIT (item-granularity) UNTP DigitalProductPassport credential (public tier; `406 Not Acceptable` when the unit's type passport has no manufacturing facility with a country of production); `application/vc+ld+json` (or bare `vc+ld+json`) → the same per-unit credential with an embedded `ecdsa-jcs-2019` Data Integrity proof, same `406`; `text/html` → server-rendered unit page; everything else → JSON-LD (`application/ld+json`). No AAS representation on this route. `Vary: Accept` always set on the 200. The `410` tombstone check (below) precedes content negotiation, so a recycled/ceased unit never yields a `vc+jwt` or `vc+ld+json`. **Per-unit telemetry is never public** (Annex XIII(2)-(4)): anonymous responses omit `currentState`/`dynamicData` and instead carry a `restrictedData` notice with a `/request-access` pointer. An owner credential — a tenant **API key** (`op_dpp_token_…`) of the owning or operator-bound tenant, sent as Bearer or, legacy, as the literal `opendpp_session` cookie value (a Console JWT login session does **not** unlock owner tier) — or a valid grant token (`dpp_li_…`/`dpp_auth_…` as Bearer or `?grant=`; TENANT, PASSPORT or UNIT scope) unlocks `currentState` and `dynamicData` — up to the 500 most recent events, newest first. Invalid credentials silently degrade to the public tier (never 401/402/403). Grant-unlocked responses add `Cache-Control: private, no-store` + `Referrer-Policy: no-referrer`. No permission string (public endpoint). **Art. 77(8) tombstone:** once the unit's status is `RECYCLED` (or `ceasedAt` is set) this URL answers `410 Gone` with a minimal tombstone for everyone — grants and owner credentials do NOT override it (the owning tenant retains internal access via `GET /api/v1/units/{id}`). Every resolution is access-audit-logged with an anonymized IP. **Rate limit:** 30 requests/min/IP (in-memory public limiter; two-field 429 body without `success`). The limiter adds no headers of its own — `x-ratelimit-*` headers come from the global platform limit, which applies on top. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | The battery unit's server-assigned UUID (AI-21 serial resolution via GET /01/{gtin14}/21/{serial} redirects here). | | `grant` | query | no | string | Capability grant token (dpp_li_… / dpp_auth_…); equivalent to Authorization: Bearer. | ## Responses - **200** — The unit document in the negotiated representation. → [PublicBatteryUnitJsonLd](/schemas/PublicBatteryUnitJsonLd.md) - **404** — No unit with that id (a malformed UUID also resolves to this 404). → [Error](/schemas/Error.md) - **410** — Gone — the unit was RECYCLED (or ceasedAt is set): the battery passport has ceased to exist (Art. → [BatteryUnitTombstoneJsonLd](/schemas/BatteryUnitTombstoneJsonLd.md) - **429** — Public-resolution rate limit exceeded (30 requests/min per IP; no rate-limit headers). → [Error](/schemas/Error.md) - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ # optional for the public tier -X GET 'https://opendpp-node.eu/unit/{id}' ``` ## See also Schemas: [BatteryUnitTombstoneJsonLd](/schemas/BatteryUnitTombstoneJsonLd.md), [Error](/schemas/Error.md), [PublicBatteryUnitJsonLd](/schemas/PublicBatteryUnitJsonLd.md). ================================================================================ # FILE: /okf/operations/getSealCaCertificate.md ================================================================================ --- type: API Endpoint title: Download the platform seal-CA certificate (PEM) description: Download the platform seal-CA certificate (PEM) resource: https://opendpp-node.eu/.well-known/opendpp-seal-ca.pem tags: - GET - eidas-keys timestamp: 2026-06-17T00:00:00Z --- `GET /.well-known/opendpp-seal-ca.pem` **Domain:** [eIDAS Keys](/tags/eidas-keys.md) **Authentication:** **Public** — no authentication required. Downloads the platform seal-CA certificate as PEM. Third parties pin this CA to validate the `x5c` certificate chains embedded in sealed-passport `proof` blocks — the chain's leaf certificate binds a tenant's signing key to its legal identity (eIDAS Art. 36(1)(b) creator identification; the seal is an eIDAS *advanced*, not qualified, electronic seal). The certificate is provisioned server-side on first use. No authentication, no permission (public endpoint). Successful responses carry `Cache-Control: public, max-age=3600`. Like every documented path except `/health`, a request on an unknown tenant workspace host receives a platform-level JSON 404 before this handler runs. **Rate limit:** 30 requests/min/IP via the in-memory public limiter — note that this route's 429 body carries ONLY `{"error": "Too Many Requests"}` (no `message`, no `success`), unlike the other public resolvers. The global platform limit (100 req/min/IP) applies on top: a global-limit 429 carries the platform's default `{statusCode, error, message}` body instead, and the global limiter's `x-ratelimit-*` headers appear on every response from this route. Returns `503` if the seal CA cannot be provisioned or loaded. ## Responses - **200** — The CA certificate, PEM-encoded. - **429** — Rate limited (30/min/IP in-memory public limiter). - **503** — Seal CA not available (provisioning/load failure). → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -X GET 'https://opendpp-node.eu/.well-known/opendpp-seal-ca.pem' ``` ## See also Schemas: [Error](/schemas/Error.md). ================================================================================ # FILE: /okf/operations/getSectorSchema.md ================================================================================ --- type: API Endpoint title: Get the ESPR metadata schema for a product category description: Get the ESPR metadata schema for a product category resource: https://opendpp-node.eu/api/v1/schemas/{category} tags: - GET - schemas-vocabulary timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/schemas/{category}` **Domain:** [Schemas & Vocabulary](/tags/schemas-vocabulary.md) **Authentication:** **Public** — no authentication required. Returns the machine-readable ESPR `metadata` schema for a product category. **Default representation** (any `Accept` NOT containing `application/ld+json`): the category's JSON Schema **draft-07** document, served as `application/schema+json`, with each known field annotated server-side with a plain-English `description` (the annotations are AJV-ignored — validation behavior is unchanged). **With `Accept: application/ld+json`:** a small JSON-LD `@context` for the category vocabulary instead. Note: the route does not set `Vary: Accept`. The category path segment is lower-cased before lookup. Machine-readable schemas exist for **5** of the 9 ESPR categories: `textiles`, `batteries`, `electronics`, `chemicals`, `construction`. The remaining 4 categories accepted by passport metadata validation (`cosmetics`, `toys`, `iron-steel`, `aluminium`) are validated by built-in server rules and currently return `404` from this endpoint. No authentication, no permission (public endpoint). No custom rate limiter — only the global platform limit applies (100 req/min/IP, standard `x-ratelimit-*` headers). Like every documented path except `/health`, a request on an unknown tenant workspace host receives a platform-level JSON 404 (`No tenant company found for subdomain: …`, no `success` field) before this handler runs. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `category` | path | yes | string | ESPR product category (case-insensitive). | ## Responses - **200** — The category schema (default) or its JSON-LD vocabulary context (Accept: application/ld+json). → [SectorJsonSchemaDocument](/schemas/SectorJsonSchemaDocument.md), [SectorVocabularyContext](/schemas/SectorVocabularyContext.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). ## Example ```bash curl -s \ -X GET 'https://opendpp-node.eu/api/v1/schemas/{category}' ``` ## See also Schemas: [SectorJsonSchemaDocument](/schemas/SectorJsonSchemaDocument.md), [SectorVocabularyContext](/schemas/SectorVocabularyContext.md). ================================================================================ # FILE: /okf/operations/getJsonLdContext.md ================================================================================ --- type: API Endpoint title: W3C JSON-LD context document for passport terms description: W3C JSON-LD context document for passport terms resource: https://opendpp-node.eu/context/v1 tags: - GET - schemas-vocabulary timestamp: 2026-06-17T00:00:00Z --- `GET /context/v1` **Domain:** [Schemas & Vocabulary](/tags/schemas-vocabulary.md) **Authentication:** **Public** — no authentication required. Serves the resolvable W3C JSON-LD `@context` document for the core Digital Product Passport terms referenced from every passport JSON-LD representation. Static, fixed content (`application/ld+json`): maps the DPP terms to `https://opendpp-node.eu/ns/dpp#…` IRIs and `createdAt`/`updatedAt` to schema.org `dateCreated`/`dateModified`. No authentication, no permission (public endpoint). No custom rate limiter — only the global platform limit applies (100 req/min/IP, standard `x-ratelimit-*` headers). Like every documented path except `/health`, a request on an unknown tenant workspace host receives a platform-level JSON 404 before this handler runs. ## Responses - **200** — The JSON-LD context document (fixed content). → [DppJsonLdContextDocument](/schemas/DppJsonLdContextDocument.md) - **429** — Global rate limit exceeded (100 requests/min per IP). ## Example ```bash curl -s \ -X GET 'https://opendpp-node.eu/context/v1' ``` ## See also Schemas: [DppJsonLdContextDocument](/schemas/DppJsonLdContextDocument.md). ================================================================================ # FILE: /okf/operations/getHealth.md ================================================================================ --- type: API Endpoint title: Service health check description: Service health check resource: https://opendpp-node.eu/health tags: - GET - service timestamp: 2026-06-17T00:00:00Z --- `GET /health` **Domain:** [Service](/tags/service.md) **Authentication:** **Public** — no authentication required. Liveness probe. Always returns 200 with the service identity, the current server time (ISO 8601 UTC with milliseconds), and the running build identity (`apiVersion`/`commit`/`builtAt` — the same fields as `GET /api/v1/version`). No authentication, no permission. Subject only to the global platform rate limit (100 req/min/IP, 600/min for known crawler user agents; standard `x-ratelimit-*` headers on responses). This is the one path exempt from tenant-subdomain resolution — it answers 200 on any host. ## Responses - **200** — Service is up. → [HealthStatus](/schemas/HealthStatus.md) - **429** — Global rate limit exceeded (100 requests/min per IP). ## Example ```bash curl -s \ -X GET 'https://opendpp-node.eu/health' ``` ## See also Schemas: [HealthStatus](/schemas/HealthStatus.md). ================================================================================ # FILE: /okf/operations/getApiVersion.md ================================================================================ --- type: API Endpoint title: Running API contract version & build identity description: Running API contract version & build identity resource: https://opendpp-node.eu/api/v1/version tags: - GET - service timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/version` **Domain:** [Service](/tags/service.md) **Authentication:** **Public** — no authentication required. Returns the SemVer of the public API contract currently served (`apiVersion`), plus the source build identity (`commit`, `builtAt`). The contract's MAJOR equals the `/api/v1` URL major; a breaking change ships as a new `/api/v1`-style major (`/api/v2`), never as an edit to this contract — so a stable `apiVersion` major is a safe thing for an integration or a generated SDK to pin to. `commit`/`builtAt` read `"unknown"` when a build did not inject them. No authentication, no permission; subject only to the global platform rate limit (100 req/min/IP). ## Responses - **200** — The running API contract version and build identity. → [ServiceVersion](/schemas/ServiceVersion.md) - **429** — Global rate limit exceeded (100 requests/min per IP). ## Example ```bash curl -s \ -X GET 'https://opendpp-node.eu/api/v1/version' ``` ## See also Schemas: [ServiceVersion](/schemas/ServiceVersion.md). ================================================================================ # FILE: /okf/operations/getPassportQrCode.md ================================================================================ --- type: API Endpoint title: Export a print-grade GS1 Digital Link QR code for a passport description: Export a print-grade GS1 Digital Link QR code for a passport resource: https://opendpp-node.eu/api/v1/passports/{id}/qr tags: - GET - qr-codes timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/passports/{id}/qr` **Domain:** [QR Codes](/tags/qr-codes.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Renders the passport's GS1 Digital Link URI (its `digitalLinkUri`, e.g. `https://opendpp-node.eu/01/09501101530003/21/{passportUuid}`) as a print-grade QR code and returns it as a binary file download. The printed carrier resolves through the public GS1 gateway. **Permission:** `passport:read` (read-only — subscription status is **not** checked on `:read` permissions, so this endpoint never returns 402). Works with a Bearer API key, a Bearer JWT, or the `opendpp_session` cookie — plain same-origin `` downloads are supported for browser sessions. **Identifier resolution:** `{id}` is matched first against the passport UUID, then against the caller-supplied `productId` (GTIN-14/GRAI/SKU), always scoped to your tenant. Credentials scoped to an Economic Operator receive **403** (`Your access is restricted to Economic Operator: `) when the passport belongs to a different operator. **QR rendering:** 4-module quiet zone (GS1 guidance); error-correction level per `ecl` (default `Q`, the GS1 recommendation for product labels); `size` is **clamped** to 128–2048 px — out-of-range values are clamped to the nearest bound, not rejected, and fractional values are truncated. The response carries `Content-Disposition: attachment; filename="qr-.png"` (or `.svg`); the filename base is the passport's `productId` with characters outside `[A-Za-z0-9._-]` replaced by `_`, truncated to 80 characters. **Errors:** an invalid query option returns **400** with one of these exact messages: `format must be png or svg`, `size must be a number`, `ecl must be M, Q or H`. An unknown passport returns **404** with message `Passport not found under your Tenant workspace`. **Rate limit:** global limiter only — 100 requests/min/IP (standard `x-ratelimit-*` headers). ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Passport UUID, or the caller-supplied productId (GTIN-14/GRAI/SKU) as a fallback. | | `format` | query | no | string | Output image format. | | `size` | query | no | integer | Rendered width in pixels (PNG) / SVG width attribute. | | `ecl` | query | no | string | QR error-correction level: M (~15% recovery), Q (~25%, GS1 product-label guidance, default) or H (~30%). | ## Responses - **200** — QR code image encoding the passport's digitalLinkUri. - **400** — Invalid query option. → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/passports/{id}/qr' ``` ## See also Schemas: [Error](/schemas/Error.md). ================================================================================ # FILE: /okf/operations/getBatteryUnitQrCode.md ================================================================================ --- type: API Endpoint title: Export a print-grade QR code for an individual battery unit description: Export a print-grade QR code for an individual battery unit resource: https://opendpp-node.eu/api/v1/units/{id}/qr tags: - GET - qr-codes timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/units/{id}/qr` **Domain:** [QR Codes](/tags/qr-codes.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Renders the battery unit's GS1 Digital Link URI as a print-grade QR code — the AI-21 path segment carries the unit's **real physical serial number** (e.g. `https://opendpp-node.eu/01/09501101530003/21/BAT-2026-000123`). This is the carrier each individual battery must wear (per-unit passports, Battery Regulation Art. 77(2)). **Permission:** `battery:read` (read-only — subscription status is **not** checked on `:read` permissions, so this endpoint never returns 402). Works with a Bearer API key, a Bearer JWT, or the `opendpp_session` cookie. **Identifier resolution:** `{id}` is the BatteryUnit **UUID only** — unlike the passport QR route there is **no** serial-number fallback. Lookup is tenant-scoped. Credentials scoped to an Economic Operator receive **403** (`Your access is restricted to Economic Operator: `) when the unit's parent passport belongs to a different operator. **QR rendering:** identical pipeline to the passport QR export — 4-module quiet zone, `ecl` default `Q`, `size` clamped to 128–2048 px (clamped, not rejected; fractions truncated). The response carries `Content-Disposition: attachment; filename="qr-.png"` (or `.svg`); the filename base is the unit's `serialNumber` with characters outside `[A-Za-z0-9._-]` replaced by `_`, truncated to 80 characters. **Errors:** an invalid query option returns **400** with one of these exact messages: `format must be png or svg`, `size must be a number`, `ecl must be M, Q or H`. An unknown unit returns **404** with message `Battery unit not found under your Tenant workspace`. **Rate limit:** global limiter only — 100 requests/min/IP (standard `x-ratelimit-*` headers). ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | BatteryUnit UUID (primary key). | | `format` | query | no | string | Output image format. | | `size` | query | no | integer | Rendered width in pixels (PNG) / SVG width attribute. | | `ecl` | query | no | string | QR error-correction level: M (~15% recovery), Q (~25%, GS1 product-label guidance, default) or H (~30%). | ## Responses - **200** — QR code image encoding the unit's digitalLinkUri (AI-21 = real physical serial). - **400** — Invalid query option. → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/units/{id}/qr' ``` ## See also Schemas: [Error](/schemas/Error.md). ================================================================================ # FILE: /okf/operations/listMaterials.md ================================================================================ --- type: API Endpoint title: List the platform-curated material vocabulary description: List the platform-curated material vocabulary resource: https://opendpp-node.eu/api/v1/materials tags: - GET - schemas-vocabulary timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/materials` **Domain:** [Schemas & Vocabulary](/tags/schemas-vocabulary.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Lists active entries from the platform-global material vocabulary that powers the searchable material/fiber/chemistry pickers in the passport form. This is shared reference data, deliberately **not** tenant-scoped, so DPP data stays comparable across tenants. **Auth:** any authenticated session — Bearer API key, Bearer JWT, or `opendpp_session` cookie. **No specific permission string is required** and subscription status is not checked, so this endpoint never returns 402. On a tenant-subdomain host, credentials belonging to a different tenant receive **403** with message `Cross-tenant access blocked.`. **Filtering & ordering:** `kind` filters by vocabulary kind — an unrecognized value is **silently ignored** (the filter simply isn't applied; no 400). `search` is a trimmed, case-insensitive substring match on `name` (blank values ignored). Only active entries are returned, ordered by `kind` ascending then `name` ascending. `limit` is clamped to 1–1000 (default 1000); there is no pagination. **Envelope caveat:** the 200 body is `{ "materials": [...] }` — there is **no `success` field** on this endpoint. **Curation:** this vocabulary is curated by the platform operator — the API is read-only for tenant credentials. Free-text material values in passport metadata remain allowed but are never auto-added to this vocabulary. **Rate limit:** global limiter only — 100 requests/min/IP (standard `x-ratelimit-*` headers). ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `kind` | query | no | string | Filter by vocabulary kind. | | `search` | query | no | string | Case-insensitive substring match on the entry name (value is trimmed; blank values ignored). | | `limit` | query | no | integer | Maximum number of entries to return. | ## Responses - **200** — Active vocabulary entries matching the filters, ordered by kind then name ascending. → [MaterialVocabularyListResponse](/schemas/MaterialVocabularyListResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/materials' ``` ## See also Schemas: [MaterialVocabularyListResponse](/schemas/MaterialVocabularyListResponse.md). ================================================================================ # FILE: /okf/operations/registerTraceabilityEvent.md ================================================================================ --- type: API Endpoint title: Register a UNTP/EPCIS 2.0 traceability event (VC-shaped) description: Register a UNTP/EPCIS 2.0 traceability event (VC-shaped) resource: https://opendpp-node.eu/api/v1/events tags: - POST - traceability-audit timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/events` **Domain:** [Traceability & Audit](/tags/traceability-audit.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Registers a supply-chain traceability event carried as a VC-shaped UNTP credential and persists it as an EPCIS 2.0 event row scoped to your tenant. **Permission:** `passport:update` (write operation — subscription gating applies, see 402). When the node operator enforces MFA, writes from user-backed sessions (cookie or Bearer JWT) whose MFA policy requires a second factor (user policy `REQUIRED`, or `DEFAULT` with the workspace's MFA-by-default setting, which is on by default) receive 403 without one; API-key clients are exempt. Cookie-session clients must send the `X-CSRF-Token` header (double-submit with the `opendpp_csrf` cookie); Bearer JWT / API-key clients are exempt. **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). **Validation pipeline (in order):** 1. *Structural* — the body must be an object containing `credentialSubject`, otherwise 400 `Bad Request`. 2. *EPCIS rule* — `action` is strictly forbidden on `TransformationEvent` (any non-null value → 400 `Schema Validation Error`). 3. *Cryptographic* — the ECDSA (P-256 / SHA-256) signature in `proof.proofValue` (base64) is verified over the deterministically canonicalized credential (key-sorted JSON with `proof.proofValue` blanked — OpenDPP's own canonicalization scheme, NOT RFC 8785 JCS, so this is not a conformant W3C Data Integrity suite). The verification key is resolved in trust order: (a) an embedded `proof.verificationMethod.x5c` chain, accepted ONLY when the node has eIDAS trust anchors configured, the chain validates against them, every certificate is currently valid, and the leaf attests the issuer; (b) the registered eIDAS public key of the tenant whose subdomain or company name EXACTLY equals the trailing `:`-segment of the issuer DID. If no key resolves or the signature does not verify → 400 `Cryptographic Verification Failed`. 4. *Operator scoping* — if your API key is scoped to an Economic Operator, the credential's declared operator DID — the `issuer` DID, or `credentialSubject.responsibleOperatorDid` only when `issuer` is absent — must contain the bound operator's registration id (e.g. `EU-DEFAULT-001`), otherwise 403 with `message: "Your access is restricted to Economic Operator: ()"`. **Persistence:** the stored event id is ALWAYS server-generated (UUID) — the credential's own `id` is never used as the primary key (prevents cross-tenant id squatting); the issuer DID is retained as `issuerDid`. Defaults applied on write: `bizStep` → `urn:epcglobal:cbv:bizstep:receiving`; `disposition` → `urn:epcglobal:cbv:disp:in_progress`; `readPoint` → `geo:,` derived from `credentialSubject.originLocation` when present; `bizLocation` → `responsibleOperatorDid`; `eventTime` → `issuanceDate`, else the server clock; `epcList` → `[credentialSubject.id]` when not supplied as an array (or `[]`). The row is stored with `isUntpCompliant: true` and the `proof.proofValue` retained. **Caveats:** `credentialSubject.eventType` must be one of the documented event-type values and `action` (when present) one of `ADD`/`OBSERVE`/`DELETE` — both map to server-side enums, and a missing or unknown value is only rejected at the persistence layer and surfaces as the 500 `Database Persistence Failed` body, not as a 400. Note the 201 envelope is `{status: "success", ...}`, NOT the usual `{success: true, ...}` shape. This endpoint does not create lineage edges between events; the lineage DAG read by `GET /api/v1/events/{id}/lineage` is built from lineage relations maintained separately on the node. ## Request body Schema (required): [UntpEventCredential](/schemas/UntpEventCredential.md). ```json { "@context": [ "https://www.w3.org/ns/credentials/v2", "https://vocabulary.uncefact.org/untp/dpp/" ], "id": "urn:uuid:0e7a2c1c-6f4e-4a08-9d2e-3b1f5a7c9d10", "type": [ "VerifiableCredential", "DigitalTraceabilityEvent" ], "issuer": "did:web:opendpp-node.eu:EU-DEFAULT-001:demo", "issuanceDate": "2026-06-12T09:41:00.000Z", "credentialSubject": { "id": "urn:epc:id:sgtin:0950110153.0003.SN-2026-000123", "eventType": "ObjectEvent", "action": "OBSERVE", "bizStep": "urn:epcglobal:cbv:bizstep:shipping", "disposition": "urn:epcglobal:cbv:disp:in_transit", "readPoint": "geo:41.1496,-8.6109", "bizLocation": "urn:epc:id:sgln:0950110153000..0", "eventTime": "2026-06-12T08:30:00.000Z", "epcList": [ "urn:epc:id:sgtin:0950110153.0003.SN-2026-000123" ], "responsibleOperatorDid": "did:web:opendpp-node.eu:EU-DEFAULT-001:demo" }, "proof": { "type": "DataIntegrityProof", "created": "2026-06-12T09:41:00.000Z", "proofPurpose": "assertionMethod", "verificationMethod": "did:web:opendpp-node.eu:EU-DEFAULT-001:demo#key-1", "proofValue": "MEUCIQDkx0VqFholm0Oa7lzwL9C5cqcRBYRJWcExampleEcdsaDerAiBJ4dY0YxV5n7pUq2tHj8sExampleSecondIntegerValue9w==" } } ``` ## Responses - **201** — Event registered. → [TraceEventRegistered](/schemas/TraceEventRegistered.md) - **400** — Route-specific validation failure, always {success: false, error, message} with one of three error values: Bad Request (body missing or no credentialSubject),… → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Persistence failure — also returned when credentialSubject.eventType/action is missing or not a valid enum value (the server-side enum rejects the row). → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/events' \ --data '{"@context":["https://www.w3.org/ns/credentials/v2","https://vocabulary.uncefact.org/untp/dpp/"],"id":"urn:uuid:0e7a2c1c-6f4e-4a08-9d2e-3b1f5a7c9d10","type":["VerifiableCredential","DigitalTraceabilityEvent"],"issuer":"did:web:opendpp-node.eu:EU-DEFAULT-001:demo","issuanceDate":"2026-06-12T09:41:00.000Z","credentialSubject":{"id":"urn:epc:id:sgtin:0950110153.0003.SN-2026-000123","eventType":"ObjectEvent","action":"OBSERVE","bizStep":"urn:epcglobal:cbv:bizstep:shipping","disposition":"urn:epcglobal:cbv:disp:in_transit","readPoint":"geo:41.1496,-8.6109","bizLocation":"urn:epc:id:sgln:09501101530…' ``` ## See also Schemas: [Error](/schemas/Error.md), [TraceEventRegistered](/schemas/TraceEventRegistered.md), [UntpEventCredential](/schemas/UntpEventCredential.md). ================================================================================ # FILE: /okf/operations/getEventLineage.md ================================================================================ --- type: API Endpoint title: Retrieve the upstream pedigree of an event as a recursive lineage DAG description: Retrieve the upstream pedigree of an event as a recursive lineage DAG resource: https://opendpp-node.eu/api/v1/events/{id}/lineage tags: - GET - traceability-audit timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/events/{id}/lineage` **Domain:** [Traceability & Audit](/tags/traceability-audit.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Returns the full upstream pedigree of a traceability event as a recursive Directed Acyclic Graph: the root event plus, in `parents`, every event linked upstream through lineage relations registered on the node, walked transitively (parents of parents). A shared ancestor reached through multiple downstream paths is repeated under EACH path — the DAG is expanded into a tree in the response, not deduplicated; only a true cycle aborts the walk (400). **Permission:** `passport:read`. Every node in the walk — the root AND each upstream parent — is scoped to the caller's tenant; an event belonging to another tenant is invisible and the request fails with 404 (no cross-tenant pedigree reads). Sessions with the `SUPER_ADMIN` role are exempt from tenant scoping. **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). **Caveats:** if the lineage graph contains a circular reference the walk aborts with 400. Any other failure (unknown id, other-tenant id, missing parent) is reported as the same deliberately generic 404 body. `eventTime` is serialized as ISO 8601 UTC; `epcs` is parsed from the stored EPC list (a non-array value degrades to `[]`); `location` mirrors the stored `bizLocation`. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | EPCIS event id — the server-generated UUID returned as eventId by POST /api/v1/events. | ## Responses - **200** — The lineage DAG rooted at the requested event. → [TraceLineageResponse](/schemas/TraceLineageResponse.md) - **400** — Circular reference detected while walking the lineage graph. → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The event does not exist, belongs to another tenant, or an upstream node could not be retrieved. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/events/{id}/lineage' ``` ## See also Schemas: [Error](/schemas/Error.md), [TraceLineageResponse](/schemas/TraceLineageResponse.md). ================================================================================ # FILE: /okf/operations/auditEventLineage.md ================================================================================ --- type: API Endpoint title: Run heuristic UFLPA/EUDR compliance screening over an event's lineage description: Run heuristic UFLPA/EUDR compliance screening over an event's lineage resource: https://opendpp-node.eu/api/v1/events/{id}/audit tags: - POST - traceability-audit timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/events/{id}/audit` **Domain:** [Traceability & Audit](/tags/traceability-audit.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Walks the same upstream lineage DAG as `GET /api/v1/events/{id}/lineage` and screens every node's location data against two heuristic rules: - **UFLPA** — flags any node whose `bizLocation` starts with `CN-65` (ISO 3166-2 Xinjiang), contains the keyword `XINJIANG` (case-insensitive), or whose `readPoint` contains the coordinate pair `43.8256,87.6168`. - **EUDR** — flags any node whose `readPoint` parses as `geo:,` (or bare `,`) coordinates inside the sample deforestation polygon lat −5.0…−3.0, lng −65.0…−60.0. These are geographic screening heuristics evaluated against the data registered on this node — not a legal compliance determination. **Permission:** `passport:read` (a read permission despite the POST verb — no subscription gating). Cookie-session clients must send the `X-CSRF-Token` header; Bearer clients are exempt. Tenant scoping and the `SUPER_ADMIN` bypass are identical to the lineage endpoint. **No request body is read** — send an empty body (an empty or absent JSON body is accepted). **Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers). When zero violations are found, the response embeds a `TraceabilityComplianceCertificate` object (status `VERIFIED_COMPLIANT`, standards `EUDR-2026` / `UFLPA-2026`); otherwise `certificate` is `null` and `errors` lists each violation as a human-readable string. ANY failure — unknown event id, other-tenant id, or even a circular lineage graph — is reported as the same generic 404 body. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | EPCIS event id — the server-generated UUID returned as eventId by POST /api/v1/events. | ## Responses - **200** — Audit completed (compliant or not — violations are reported in-band, not as HTTP errors). → [TraceComplianceAuditResponse](/schemas/TraceComplianceAuditResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The audit could not be completed: unknown event id, an event belonging to another tenant, or a circular lineage graph. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X POST 'https://opendpp-node.eu/api/v1/events/{id}/audit' ``` ## See also Schemas: [Error](/schemas/Error.md), [TraceComplianceAuditResponse](/schemas/TraceComplianceAuditResponse.md). ================================================================================ # FILE: /okf/operations/verifyPassportSeal.md ================================================================================ --- type: API Endpoint title: Publicly verify a passport's eIDAS seal, certificate chain and timestamp description: Publicly verify a passport's eIDAS seal, certificate chain and timestamp resource: https://opendpp-node.eu/api/v1/audit/verify tags: - POST - traceability-audit timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/audit/verify` **Domain:** [Traceability & Audit](/tags/traceability-audit.md) **Authentication:** **Public** — no authentication required. **Public seal-verification API** — cryptographically verifies that a Digital Product Passport document was sealed by an economic-operator tenant registered on this node and has not been tampered with. No authentication required. **Rate limit:** custom in-memory token bucket, **30 requests/min per IP** (per app instance). This bucket emits no rate-limit headers of its own — any `x-ratelimit-*` headers on responses (including this 429) come from the global 100 req/min limiter and describe that budget, not the 30/min one. The 429 body is the two-field `{"error": "Too Many Requests", "message": "Rate limit exceeded."}`. **Input resolution.** `payload` is required. `signature` and `publicKey` may be supplied top-level, or are extracted from the document's embedded proof block: `signature` ← `payload.proof.proofValue` (else `payload.proof.signatureValue`); `publicKey` ← `payload.proof.publicKeyPem`, else the leaf certificate's SPKI when `payload.proof.x5c` is present. If, after extraction, any of the three is still missing → 400. The public key is CRLF-normalized and trimmed before matching. **Verification pipeline (in order):** 1. **Certificate-chain report (optional).** If `payload.proof.x5c` is a non-empty array of base64-DER certificates (leaf first), the chain is parsed and a `certificate` report is built: the leaf's `subject` / `issuer` / `validFrom` / `validTo` (X.509 textual dates such as `Jan 10 00:00:00 2026 GMT` — NOT ISO 8601), `chainValid` (every link signature-verifies against the next certificate, every certificate is inside its validity window, and the top of the chain is anchored to this node's seal CA — SHA-256 fingerprint match or signature under the CA key; the CA is published at `GET /.well-known/opendpp-seal-ca.pem`), and `keyMatchesProof` (the leaf SPKI equals the supplied `publicKey`, whitespace-insensitive; always `true` when no explicit key was supplied). An unparseable chain yields `{"chainValid": false, "error": "Unparseable x5c certificate chain"}` and does NOT fail the request. This reports the CERTIFIED identity of the seal creator (eIDAS Art. 36(1)(b)). The report is attached only to the final verification outcome (step 4) — the two policy-failure responses below omit it. 2. **Key-registration gate.** The `publicKey` must exactly match the registered eIDAS public key of a tenant on this node (trailing-newline tolerant) — otherwise HTTP **200** with `verified: false` and an explanatory `message`. Verification-policy failures are reported in-band, never as HTTP errors. 3. **Operator-binding gate (fail-closed).** If the payload declares an operator registration id (`payload.operator.regId`, else `payload.economicOperator.regId`), that id MUST resolve to an Economic Operator registered on this node AND that operator MUST be bound to the signing tenant (a workspace–operator binding registered on this node). A declared operator that is unregistered, or registered but not bound to the key-owning tenant, → 200 `verified: false` with an explanatory `message`. Payloads that declare no operator id skip this gate. 4. **Signature verification (two phases).** *Phase 1 — Merkle seal:* when `payload.metadata` is an object (or, when the `metadata` key is entirely absent, the whole `payload` is treated as the metadata), the SHA-256 Merkle tree over the metadata's top-level properties is rebuilt and the base64 ECDSA (P-256 / SHA-256) `signature` is verified against the recomputed root. Every leaf is recomputed from the actual values — caller-supplied redacted-leaf hashes are NOT accepted (they would let a tampered field be smuggled past verification), so a publicly redacted document will not pass the Merkle phase: verify the unredacted, privileged document. *Phase 2 — fallback:* if the Merkle phase does not verify, the signature is verified over the deterministic key-sorted canonicalization of the entire `payload`. 5. **RFC 3161 timestamp report (optional).** When `payload.proof.rfc3161.token` is a non-empty base64-DER TimeStampToken, the response includes `timestamp` with the TSA-asserted `genTime` parsed from the token's TSTInfo (or `genTime: null` plus a `note` when the token cannot be parsed). This reports presence + asserted time only — full cryptographic TSR validation is the verifier's own step (e.g. `openssl ts -verify` against the TSA certificate). Like `certificate`, it appears only on the final verification outcome. **Outcome.** A processed verification ALWAYS returns HTTP 200 with `verified: true|false`; 400 is reserved for missing parameters or an exception thrown while verifying (e.g. an undecodable public key). `certificate` and `timestamp` are attached only when verification proceeds past the key-registration and operator-binding gates — the two policy `verified: false` responses contain only `{success, verified, message}`, even when an x5c chain and/or an RFC 3161 token were supplied. The 400 bodies on this public endpoint are `{"success": false, "message": "..."}` — they include `success` but OMIT the `error` field. (A syntactically malformed JSON body is rejected earlier by the framework with its default `{statusCode, error, message}` body; a POST with no body at all — no `Content-Type` — fails before processing with a framework-default 500, so send at least `{}`. An empty `application/json` body is treated as `{}` and yields the documented 400.) ## Request body Schema (required): [SealVerifyRequest](/schemas/SealVerifyRequest.md). ```json { "payload": { "passportId": "9b2fa884-5b1d-4c0e-9a3f-2d7c8e1f6a45", "productId": "09501101530003", "operator": { "name": "OpenDPP Demo Eco Industries", "regId": "EU-DEFAULT-001" }, "metadata": { "category": "textiles", "originCountry": "PT", "materialComposition": [ { "material": "Organic Cotton", "percentage": 80 }, { "material": "Recycled Polyester", "percentage": 20 } ] }, "proof": { "type": "DataIntegrityProof", "proofValue": "MEQCIB3pZ8sVxampleMerkleRootSealSignatureFirstIntegerAiAW6kQexampleSecondDerIntegerValue0123456789abcd==", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEexampleDemoTenantSealPublicKey\n0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNO=\n-----END PUBLIC KEY-----", "x5c": [ "MIIB2zCCAYGgAwIBAgIUExampleLeafSealCertificateBase64Der", "MIIB4TCCAYagAwIBAgIUExampleNodeSealCaCertificateBase64Der" ], "rfc3161": { "token": "MIIKlAYJKoZIhvcNAQcCoIIKhTCCCoECAQMxDzANBglghkgBZQMEAgEFADCBExampleTimeStampTokenDer" } } } } ``` ## Responses - **200** — Verification processed. → [SealVerifyResponse](/schemas/SealVerifyResponse.md) - **400** — Missing cryptographic parameters (after proof-block extraction), or an exception during verification (e.g. - **429** — Public-resolution rate limit exceeded (30 requests/min per IP; no rate-limit headers). → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/audit/verify' \ --data '{"payload":{"passportId":"9b2fa884-5b1d-4c0e-9a3f-2d7c8e1f6a45","productId":"09501101530003","operator":{"name":"OpenDPP Demo Eco Industries","regId":"EU-DEFAULT-001"},"metadata":{"category":"textiles","originCountry":"PT","materialComposition":[{"material":"Organic Cotton","percentage":80},{"material":"Recycled Polyester","percentage":20}]},"proof":{"type":"DataIntegrityProof","proofValue":"MEQCIB3pZ8sVxampleMerkleRootSealSignatureFirstIntegerAiAW6kQexampleSecondDerIntegerValue0123456789abcd==","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEexampleDemoTenantSe…' ``` ## See also Schemas: [SealVerifyRequest](/schemas/SealVerifyRequest.md), [SealVerifyResponse](/schemas/SealVerifyResponse.md). ================================================================================ # FILE: /okf/operations/listWebhookSubscriptions.md ================================================================================ --- type: API Endpoint title: List webhook subscriptions (signing secrets stripped) description: List webhook subscriptions (signing secrets stripped) resource: https://opendpp-node.eu/api/v1/webhooks/subscriptions tags: - GET - webhooks timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/webhooks/subscriptions` **Domain:** [Webhooks](/tags/webhooks.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Lists all webhook subscriptions of the calling workspace. Unpaginated (the per-workspace cap is 25). **Permission:** `webhook:read` (read permissions are not subscription-gated, so no `402`). The HMAC signing `secret` is **stripped from every row** — it is returned once by the create endpoint and once by `rotate-secret`. `isActive` reflects whether the subscription receives deliveries; toggle it with `PATCH /api/v1/webhooks/subscriptions/{id}`. Global rate limit 100 requests/min/IP. ## Responses - **200** — All subscriptions of the workspace, secrets removed. → [WebhookSubscriptionListResponse](/schemas/WebhookSubscriptionListResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/webhooks/subscriptions' ``` ## See also Schemas: [Error](/schemas/Error.md), [WebhookSubscriptionListResponse](/schemas/WebhookSubscriptionListResponse.md). ================================================================================ # FILE: /okf/operations/createWebhookSubscription.md ================================================================================ --- type: API Endpoint title: Register a webhook subscription (signing secret returned once) description: Register a webhook subscription (signing secret returned once) resource: https://opendpp-node.eu/api/v1/webhooks/subscriptions tags: - POST - webhooks timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/webhooks/subscriptions` **Domain:** [Webhooks](/tags/webhooks.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Registers an endpoint to receive passport lifecycle webhooks for the calling workspace. **Permission:** `webhook:write`. Cookie-session callers must send the `X-CSRF-Token` header (double-submit); Bearer API-key/JWT clients are exempt. Write permissions are additionally gated on an active workspace subscription (`402`). **URL validation (SSRF guard):** `url` must be an absolute `http(s)` URL. At registration the hostname is DNS-resolved and the request is rejected with `400` if any resolved A/AAAA record is loopback, RFC 1918/CGNAT private, link-local / cloud-metadata (`169.254.0.0/16`), multicast, or an equivalent IPv6 range. At delivery time the socket is pinned to the validated IP and redirects are never followed. **Event filters:** `events` must be a non-empty array drawn from `passport.ingested`, `passport.updated`, `passport.sealed`, `passport.recalled`, `passport.status_updated`, `*`. The `*` wildcard matches every emitted event. **Signing secret — shown once:** the `201` response contains the full subscription row **including** the HMAC-SHA256 signing secret (`whsec_` + 32 lowercase hex chars, server-generated, never client-supplied). This is the only time the secret is ever returned: the list endpoint strips it and there is no rotation or update endpoint — delete and re-create to rotate. **Limits:** maximum **25 subscriptions per workspace** (`409 Conflict`). Global rate limit 100 requests/min/IP (`429` with `x-ratelimit-*` headers). Unknown request-body fields are ignored. ## Request body Schema (required): [WebhookSubscriptionCreateRequest](/schemas/WebhookSubscriptionCreateRequest.md). ```json { "url": "https://erp.example.com/hooks/opendpp", "events": [ "passport.ingested", "passport.sealed" ] } ``` ## Responses - **201** — Subscription registered. → [WebhookSubscriptionCreateResponse](/schemas/WebhookSubscriptionCreateResponse.md) - **400** — Validation failure. → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **409** — The workspace already has 25 webhook subscriptions (the per-tenant cap). → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Persistence failed (details logged server-side) — message "Failed to register webhook subscription.". → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X POST 'https://opendpp-node.eu/api/v1/webhooks/subscriptions' \ --data '{"url":"https://erp.example.com/hooks/opendpp","events":["passport.ingested","passport.sealed"]}' ``` ## See also Schemas: [Error](/schemas/Error.md), [WebhookSubscriptionCreateRequest](/schemas/WebhookSubscriptionCreateRequest.md), [WebhookSubscriptionCreateResponse](/schemas/WebhookSubscriptionCreateResponse.md). ================================================================================ # FILE: /okf/operations/updateWebhookSubscription.md ================================================================================ --- type: API Endpoint title: Update a webhook subscription (url / events / active) description: Update a webhook subscription (url / events / active) resource: https://opendpp-node.eu/api/v1/webhooks/subscriptions/{id} tags: - PATCH - webhooks timestamp: 2026-06-17T00:00:00Z --- `PATCH /api/v1/webhooks/subscriptions/{id}` **Domain:** [Webhooks](/tags/webhooks.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Partially updates a subscription: any of `url`, `events`, `isActive` (key present = set, omitted = unchanged). A new `url` is re-validated by the DNS-resolving SSRF guard (same rules as creation); `events` must be a non-empty subset of the allowed filters. The signing `secret` is **not** editable here — use `rotate-secret`. An empty body returns the current subscription unchanged. The response **strips the secret**. **Permission:** `webhook:write`. Cookie sessions must send `X-CSRF-Token`; write permissions are subscription-gated (**402** when lapsed). **Rate limit:** global limiter, 100 requests/min per IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Webhook subscription UUID (as returned at creation / by the list endpoint). | ## Request body Schema: [WebhookSubscriptionUpdateRequest](/schemas/WebhookSubscriptionUpdateRequest.md). ```json { "events": [ "passport.sealed", "passport.recalled" ], "isActive": false } ``` ## Responses - **200** — Updated subscription (secret stripped). → [WebhookSubscriptionUpdateResponse](/schemas/WebhookSubscriptionUpdateResponse.md) - **400** — Invalid url (not a string / SSRF-rejected), empty/invalid events, or non-boolean isActive. → [Error](/schemas/Error.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -H 'Content-Type: application/json' \ -X PATCH 'https://opendpp-node.eu/api/v1/webhooks/subscriptions/{id}' \ --data '{"events":["passport.sealed","passport.recalled"],"isActive":false}' ``` ## See also Schemas: [Error](/schemas/Error.md), [WebhookSubscriptionUpdateRequest](/schemas/WebhookSubscriptionUpdateRequest.md), [WebhookSubscriptionUpdateResponse](/schemas/WebhookSubscriptionUpdateResponse.md). ================================================================================ # FILE: /okf/operations/deleteWebhookSubscription.md ================================================================================ --- type: API Endpoint title: Delete a webhook subscription description: Delete a webhook subscription resource: https://opendpp-node.eu/api/v1/webhooks/subscriptions/{id} tags: - DELETE - webhooks timestamp: 2026-06-17T00:00:00Z --- `DELETE /api/v1/webhooks/subscriptions/{id}` **Domain:** [Webhooks](/tags/webhooks.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Deletes a webhook subscription, stopping future deliveries to its endpoint. **Permission:** `webhook:write` (cookie sessions must send `X-CSRF-Token`; write permissions are subscription-gated, `402`). The lookup is tenant-scoped: an `id` that exists but belongs to another workspace returns the same `404` with message `"Webhook subscription not found under your tenant"`. Deleting and re-creating is the only way to rotate a signing secret. Global rate limit 100 requests/min/IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Webhook subscription UUID (as returned at creation / by the list endpoint). | ## Responses - **200** — Subscription deleted. → [WebhookSubscriptionDeleteResponse](/schemas/WebhookSubscriptionDeleteResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X DELETE 'https://opendpp-node.eu/api/v1/webhooks/subscriptions/{id}' ``` ## See also Schemas: [Error](/schemas/Error.md), [WebhookSubscriptionDeleteResponse](/schemas/WebhookSubscriptionDeleteResponse.md). ================================================================================ # FILE: /okf/operations/rotateWebhookSecret.md ================================================================================ --- type: API Endpoint title: Rotate a webhook subscription's signing secret description: Rotate a webhook subscription's signing secret resource: https://opendpp-node.eu/api/v1/webhooks/subscriptions/{id}/rotate-secret tags: - POST - webhooks timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/webhooks/subscriptions/{id}/rotate-secret` **Domain:** [Webhooks](/tags/webhooks.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Mints a fresh HMAC-SHA256 signing secret for the subscription and returns it **once** (the old secret stops validating immediately). Use this after a suspected secret leak, or on a rotation schedule. There is no request body. Update your receiver to verify signatures with the new secret as soon as you rotate — deliveries in flight use whichever secret was current when signed. **Permission:** `webhook:write`. Cookie sessions must send `X-CSRF-Token`; write permissions are subscription-gated (**402** when lapsed). **Rate limit:** global limiter, 100 requests/min per IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Webhook subscription UUID (as returned at creation / by the list endpoint). | ## Responses - **200** — Rotated. → [WebhookSecretRotateResponse](/schemas/WebhookSecretRotateResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X POST 'https://opendpp-node.eu/api/v1/webhooks/subscriptions/{id}/rotate-secret' ``` ## See also Schemas: [WebhookSecretRotateResponse](/schemas/WebhookSecretRotateResponse.md). ================================================================================ # FILE: /okf/operations/listWebhookDeliveries.md ================================================================================ --- type: API Endpoint title: List recent webhook delivery attempts (the outbox) description: List recent webhook delivery attempts (the outbox) resource: https://opendpp-node.eu/api/v1/webhooks/deliveries tags: - GET - webhooks timestamp: 2026-06-17T00:00:00Z --- `GET /api/v1/webhooks/deliveries` **Domain:** [Webhooks](/tags/webhooks.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Returns recent delivery records (the outbox), newest first, for debugging endpoint failures. Records are **event-level** (one per emitted event, fanned out to all matching subscriptions), not per-subscription — `status` reflects the event's overall delivery state and `errorMessage` joins per-endpoint errors. Payloads are **not** included. Filter with `?status=PENDING|DELIVERED|FAILED` and cap with `?limit=` (1–200, default 50; a non-numeric value falls back to the default). **Permission:** `webhook:read`. **Rate limit:** global limiter, 100 requests/min per IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `status` | query | no | string | Filter by delivery state. | | `limit` | query | no | integer | Max records to return. | ## Responses - **200** — Recent delivery records, newest first. → [WebhookDeliveriesResponse](/schemas/WebhookDeliveriesResponse.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X GET 'https://opendpp-node.eu/api/v1/webhooks/deliveries' ``` ## See also Schemas: [WebhookDeliveriesResponse](/schemas/WebhookDeliveriesResponse.md). ================================================================================ # FILE: /okf/operations/testWebhookSubscription.md ================================================================================ --- type: API Endpoint title: Send a signed test event to a subscription description: Send a signed test event to a subscription resource: https://opendpp-node.eu/api/v1/webhooks/subscriptions/{id}/test tags: - POST - webhooks timestamp: 2026-06-17T00:00:00Z --- `POST /api/v1/webhooks/subscriptions/{id}/test` **Domain:** [Webhooks](/tags/webhooks.md) **Authentication:** **API key required** — `Authorization: Bearer op_dpp_token_…`. Delivers a single **signed sample** event to the subscription's URL right now and reports the outcome — use it to confirm your endpoint is reachable and that your signature verification works, without waiting for a real passport event. The payload is a representative public JSON-LD passport document marked `_test: true`; it is signed exactly like a production delivery (HMAC-SHA256 over `${timestamp}.${body}`). The event type is a concrete value from the subscription's filter (the `*` wildcard is skipped; defaults to `passport.sealed`). **Permission:** `webhook:write`. **Rate limit:** global limiter, 100 requests/min per IP. ## Parameters | Name | In | Required | Type | Description | |------|----|----------|------|-------------| | `id` | path | yes | string | Webhook subscription UUID (as returned at creation / by the list endpoint). | ## Responses - **200** — The test delivery was attempted (check delivered/statusCode for the receiver's response). → [WebhookTestResult](/schemas/WebhookTestResult.md) - **401** — Missing, invalid, revoked or expired credentials. → [Error](/schemas/Error.md) - **402** — The workspace subscription is lapsed or its grace period has expired — write operations are blocked until billing is restored. → [Error](/schemas/Error.md) - **403** — Authenticated but not allowed: the key lacks the required permission, the request crosses workspaces, or an MFA-gated write was attempted without an MFA sessio… → [Error](/schemas/Error.md) - **404** — The resource does not exist or is not visible to the calling workspace. → [Error](/schemas/Error.md) - **429** — Global rate limit exceeded (100 requests/min per IP). - **500** — Unexpected server error. → [Error](/schemas/Error.md) ## Example ```bash curl -s \ -H 'Authorization: Bearer op_dpp_token_…' \ -X POST 'https://opendpp-node.eu/api/v1/webhooks/subscriptions/{id}/test' ``` ## See also Schemas: [WebhookTestResult](/schemas/WebhookTestResult.md). ================================================================================ # FILE: /okf/schemas/index.md ================================================================================ # Schemas * [AasEnvironment](AasEnvironment.md) - An Asset Administration Shell (AAS) v3.0 environment export of the passport, served as application/aas+json. * [AasEnvironmentInput](AasEnvironmentInput.md) - An Asset Administration Shell (AAS) JSON Environment — the format produced by OpenDPP's AAS export of a passport. * [AasIngestCreated](AasIngestCreated.md) - 201 envelope of POST /api/v1/passports/aas/ingest. * [ApproveGrantRequest](ApproveGrantRequest.md) - Approval body — only the final expiry is supplied; everything else comes from the original request. * [BatteryUnitCreateItem](BatteryUnitCreateItem.md) - One unit to serialise. * [BatteryUnitCurrentState](BatteryUnitCurrentState.md) - Latest recorded measurement of the unit (owner/grant tiers only). * [BatteryUnitDeleteResponse](BatteryUnitDeleteResponse.md) * [BatteryUnitDynamicDataEvent](BatteryUnitDynamicDataEvent.md) - One telemetry event in the JSON-LD dynamicData history (privileged view only). * [BatteryUnitEventListResponse](BatteryUnitEventListResponse.md) * [BatteryUnitEventNode](BatteryUnitEventNode.md) - One append-only telemetry event (owner/grant tiers only). * [BatteryUnitEventRow](BatteryUnitEventRow.md) - One immutable per-unit telemetry record (raw persisted row). * [BatteryUnitEventType](BatteryUnitEventType.md) - Per-unit dynamic-data event category (Annex XIII / Art. * [BatteryUnitJsonLd](BatteryUnitJsonLd.md) - JSON-LD document for one serialised battery unit, privileged tenant view (isPrivileged=true): includes currentState + dynamicData telemetry… * [BatteryUnitLineageRef](BatteryUnitLineageRef.md) - Public lineage pointer between battery units (Art. * [BatteryUnitListResponse](BatteryUnitListResponse.md) * [BatteryUnitRestrictedDataNotice](BatteryUnitRestrictedDataNotice.md) - Marker replacing per-unit telemetry in anonymous (public-tier) responses, with a pointer for requesting legitimate-interest access (Reg. * [BatteryUnitRow](BatteryUnitRow.md) - One physical serialised battery (raw persisted row — these routes declare no Fastify response schema, so all model fields are returned as-i… * [BatteryUnitSerialisationFailedError](BatteryUnitSerialisationFailedError.md) - 400 body when every item in the serialisation batch failed. * [BatteryUnitStatus](BatteryUnitStatus.md) - Annex XIII battery-status vocabulary (Battery Reg. * [BatteryUnitTombstoneJsonLd](BatteryUnitTombstoneJsonLd.md) - Art. * [CreateGrantRequest](CreateGrantRequest.md) - Direct-issuance body. * [DeleteOperatorResponse](DeleteOperatorResponse.md) * [DppJsonLdContextDocument](DppJsonLdContextDocument.md) - The fixed W3C JSON-LD context document served by GET /context/v1: maps DigitalProductPassport, economicOperator, metadata, digitalSeal, sig… * [EconomicOperatorNode](EconomicOperatorNode.md) - Embedded economic-operator JSON-LD node (public in all tiers). * [Error](Error.md) - Standard error body. * [FacilityCreateRequest](FacilityCreateRequest.md) * [FacilityCreatedEnvelope](FacilityCreatedEnvelope.md) * [FacilityDeletedEnvelope](FacilityDeletedEnvelope.md) * [FacilityEnvelope](FacilityEnvelope.md) * [FacilityListEnvelope](FacilityListEnvelope.md) * [FacilityRow](FacilityRow.md) - A facility (GS1 GLN) master-data row, exactly as stored. * [FacilityUpdateRequest](FacilityUpdateRequest.md) - Partial update. * [FastifyDefaultBadRequest](FastifyDefaultBadRequest.md) - Fastify's default 400 error body, returned when a syntactically malformed JSON request body is rejected by the framework before the handler… * [GrantDecisionResponse](GrantDecisionResponse.md) - Returned by deny and revoke: the updated grant, no token. * [GrantIssuedResponse](GrantIssuedResponse.md) - Returned by direct issuance (201) and request approval (200). * [GrantListResponse](GrantListResponse.md) - List envelope for GET /api/v1/grants (paginated). * [GrantRouteError](GrantRouteError.md) - Error body used by the grants endpoints' route-level errors (400/403/404/409). * [GrantRow](GrantRow.md) - Tenant-facing projection of an access grant. * [HealthStatus](HealthStatus.md) - Health-check body of GET /health. * [MaterialVocabularyListResponse](MaterialVocabularyListResponse.md) - Envelope of GET /api/v1/materials. * [MaterialVocabularyRow](MaterialVocabularyRow.md) - One entry of the platform-curated material vocabulary. * [MerkleTreeAttestationProof](MerkleTreeAttestationProof.md) - OpenDPP's own proof type — an eIDAS ADVANCED electronic seal: an ECDSA prime256v1 signature over a SHA-256 Merkle root of the key-sorted me… * [OperatorGetResponse](OperatorGetResponse.md) * [OperatorListResponse](OperatorListResponse.md) * [OperatorMinimalError](OperatorMinimalError.md) - Minimal error envelope used by the operator/key self-service handlers — note the standard error key is ABSENT (unlike the shared Error sche… * [OperatorRow](OperatorRow.md) - An economic-operator record (EconomicOperator). * [PassportAasEnvironment](PassportAasEnvironment.md) - IDTA Asset Administration Shell environment (returned when Accept contains application/aas+json), role-filtered for the caller's access tie… * [PassportBulkFailure](PassportBulkFailure.md) - 400 body of POST /api/v1/passports/bulk when EVERY row failed. * [PassportBulkRequest](PassportBulkRequest.md) * [PassportBulkResult](PassportBulkResult.md) - 201 partial-success envelope of POST /api/v1/passports/bulk. * [PassportBulkRow](PassportBulkRow.md) - One bulk-ingestion row. * [PassportCreateRequest](PassportCreateRequest.md) * [PassportEnrichmentInput](PassportEnrichmentInput.md) - Optional presentational (non-regulatory) marketing enrichment, stored OUTSIDE the ESPR-validated metadata and the Merkle seal; it never app… * [PassportIngestCreated](PassportIngestCreated.md) - 201 envelope of POST /api/v1/passports. * [PassportListItem](PassportListItem.md) - One JSON-LD passport document as it appears in GET /api/v1/passports list responses. * [PassportListResponse](PassportListResponse.md) - Envelope of GET /api/v1/passports. * [PassportMetadataInput](PassportMetadataInput.md) - The ESPR product metadata payload. * [PassportSealResponse](PassportSealResponse.md) - 200 envelope of POST /api/v1/passports/{id}/seal. * [PassportStatusUpdateRequest](PassportStatusUpdateRequest.md) - Body of PUT /api/v1/passports/{id}/status. * [PassportStatusUpdateResponse](PassportStatusUpdateResponse.md) - 200 envelope of PUT /api/v1/passports/{id}/status. * [PassportUpdateRequest](PassportUpdateRequest.md) - Body of PUT /api/v1/passports/{id}. * [PassportUpdateResponse](PassportUpdateResponse.md) - 200 envelope of PUT /api/v1/passports/{id}. * [PassportUpdateValidationError](PassportUpdateValidationError.md) - 400 ESPR validation failure body of PUT /api/v1/passports/{id}. * [PassportValidateOnlyError](PassportValidateOnlyError.md) - 400 envelope of the validate-only endpoints. * [PassportValidateOnlyRequest](PassportValidateOnlyRequest.md) * [PassportValidateOnlyResult](PassportValidateOnlyResult.md) - 200 envelope of the validate-only endpoints (only the declared keys are emitted). * [PublicBatteryUnitJsonLd](PublicBatteryUnitJsonLd.md) - Public JSON-LD document for one individual serialised battery unit (Reg. * [PublicFacilityNode](PublicFacilityNode.md) - Embedded manufacturing-facility JSON-LD node — the GS1 GLN-backed Unique Facility Identifier (UFI, EN 18219). * [PublicPassportJsonLd](PublicPassportJsonLd.md) - The public, redacted JSON-LD Digital Product Passport document (application/ld+json). * [RecordBatteryUnitEventRequest](RecordBatteryUnitEventRequest.md) - One telemetry record. * [RecordBatteryUnitEventResponse](RecordBatteryUnitEventResponse.md) * [RegisterOperatorRequest](RegisterOperatorRequest.md) * [RegisterOperatorResponse](RegisterOperatorResponse.md) * [RestoreOperatorResponse](RestoreOperatorResponse.md) * [RotateTenantKeysResponse](RotateTenantKeysResponse.md) * [SealCertificateReport](SealCertificateReport.md) - Present only for x5c-carrying proofs AND only when verification proceeds past the key-registration and operator-binding gates (the two poli… * [SealTimestampReport](SealTimestampReport.md) - Present only when payload.proof.rfc3161.token was supplied AND verification proceeds past the key-registration and operator-binding gates (… * [SealVerifyRequest](SealVerifyRequest.md) - Verification request. * [SealVerifyResponse](SealVerifyResponse.md) - Always HTTP 200 once the request is well-formed. * [SectorJsonSchemaDocument](SectorJsonSchemaDocument.md) - A JSON Schema draft-07 document describing the ESPR metadata payload for one product category, served as application/schema+json. * [SectorVocabularyContext](SectorVocabularyContext.md) - Per-category JSON-LD vocabulary context, returned by GET /api/v1/schemas/{category} when Accept contains application/ld+json. * [SerializeBatteryUnitsRequest](SerializeBatteryUnitsRequest.md) - Either a single unit object, or a batch wrapper {units: [...]}. * [SerializeBatteryUnitsResponse](SerializeBatteryUnitsResponse.md) - Returned (201) when at least one unit was created. * [ServiceVersion](ServiceVersion.md) - Running API contract version and source build identity, returned by GET /api/v1/version. * [TraceComplianceAuditResponse](TraceComplianceAuditResponse.md) * [TraceComplianceCertificate](TraceComplianceCertificate.md) * [TraceEventRegistered](TraceEventRegistered.md) - 201 envelope of POST /api/v1/events. * [TraceLineageNode](TraceLineageNode.md) - One node of the recursive upstream lineage DAG. * [TraceLineageResponse](TraceLineageResponse.md) * [UntpEventCredential](UntpEventCredential.md) - A UNTP/EPCIS 2.0 traceability event wrapped as a VC-shaped credential (a vendor proof, not a conformant W3C VC). * [UntpEventCredentialSubject](UntpEventCredentialSubject.md) - The EPCIS event payload. * [UntpEventProof](UntpEventProof.md) - Credential proof. * [UntpVerificationMethod](UntpVerificationMethod.md) - Embedded verification-method object. * [UpdateOperatorRequest](UpdateOperatorRequest.md) - Both fields are optional. * [UpdateOperatorResponse](UpdateOperatorResponse.md) * [ValidationErrorItem](ValidationErrorItem.md) - One field-level finding from ESPR category validation. * [WebhookDeliveriesResponse](WebhookDeliveriesResponse.md) * [WebhookDeliveryRow](WebhookDeliveryRow.md) - One outbox delivery record (event-level). * [WebhookEnvelope](WebhookEnvelope.md) - The signed body of every webhook delivery. * [WebhookEventFilter](WebhookEventFilter.md) - Subscribable event filter values. * [WebhookSecretRotateResponse](WebhookSecretRotateResponse.md) * [WebhookSubscriptionCreateRequest](WebhookSubscriptionCreateRequest.md) * [WebhookSubscriptionCreateResponse](WebhookSubscriptionCreateResponse.md) * [WebhookSubscriptionDeleteResponse](WebhookSubscriptionDeleteResponse.md) * [WebhookSubscriptionListResponse](WebhookSubscriptionListResponse.md) * [WebhookSubscriptionRow](WebhookSubscriptionRow.md) - A webhook subscription row with the HMAC signing secret stripped (it is shown exactly once, in the 201 create response). * [WebhookSubscriptionUpdateRequest](WebhookSubscriptionUpdateRequest.md) - All fields optional; include only what you want to change. * [WebhookSubscriptionUpdateResponse](WebhookSubscriptionUpdateResponse.md) * [WebhookSubscriptionWithSecret](WebhookSubscriptionWithSecret.md) - The full subscription row as returned ONLY by the 201 create response — includes the HMAC-SHA256 signing secret. * [WebhookTestResult](WebhookTestResult.md) * [WhoamiResponse](WhoamiResponse.md) ================================================================================ # FILE: /okf/schemas/AasEnvironment.md ================================================================================ --- type: Schema title: AasEnvironment description: An Asset Administration Shell (AAS) v3.0 environment export of the passport, served as application/aas+json. resource: https://opendpp-node.eu/openapi.json#/components/schemas/AasEnvironment tags: - schema timestamp: 2026-06-17T00:00:00Z --- An Asset Administration Shell (AAS) v3.0 environment export of the passport, served as `application/aas+json`. Three top-level keys: `assetAdministrationShells` (asset identity — `urn:opendpp:aas:{passportId}` / `urn:opendpp:asset:{operatorId}:{productId}`, GS1 GLN-qualified specific asset ids), `submodels`, and `conceptDescriptions` (semantic concept records from the admin-curated registry, `urn:opendpp:concept:…`; empty array when the registry is empty). Submodels: a `GeneralProductInformation` submodel (`urn:opendpp:submodel:general:{passportId}`), a `ComplianceMetadata` submodel (`urn:opendpp:submodel:compliance`) mapping the passport metadata through the concept registry, and — whenever the issuing tenant's eIDAS signing key is provisioned (the normal case) — an `eidasVerificationSeal` submodel (`urn:opendpp:submodel:security-seal:{passportId}`) carrying `digitalSealHash`, `cryptographicSignature`, `pemPublicKey` and an optional `x509CertificateChain`. The seal submodel is present for EVERY access tier. Role filtering strips restricted and commercial owner-only elements from the `ComplianceMetadata` submodel before sending: owner credentials are filtered by their API-key role, grant holders by the `legitimate_interest` tier, anonymous callers by the `public` tier. Submodel internals are intentionally not enumerated in this specification. ## JSON Schema ```json { "type": "object", "description": "An Asset Administration Shell (AAS) v3.0 environment export of the passport, served as `application/aas+json`. Three top-level keys: `assetAdministrationShells` (asset identity — `urn:opendpp:aas:{passportId}` / `urn:opendpp:asset:{operatorId}:{productId}`, GS1 GLN-qualified specific asset ids), `submodels`, and `conceptDescriptions` (semantic concept records from the admin-curated registry, `urn:opendpp:concept:…`; empty array when the registry is empty). Submodels: a `GeneralProductInformation` submodel (`urn:opendpp:submodel:general:{passportId}`), a `ComplianceMetadata` submodel (`urn:opendpp:submodel:compliance`) mapping the passport metadata through the concept registry, and — whenever the issuing tenant's eIDAS signing key is provisioned (the normal case) — an `eidasVerificationSeal` submodel (`urn:opendpp:submodel:security-seal:{passportId}`) carrying `digitalSealHash`, `cryptographicSignature`, `pemPublicKey` and an optional `x509CertificateChain`. The seal submodel is present for EVERY access tier. Role filtering strips restricted and commercial owner-only elements from the `ComplianceMetadata` submodel before sending: owner credentials are filtered by their API-key role, grant holders by the `legitimate_interest` tier, anonymous callers by the `public` tier. Submodel internals are intentionally not enumerated in this specification.", "additionalProperties": true } ``` ## Used by - [resolvePublicPassport](/operations/resolvePublicPassport.md) (`GET /passport/{id}`) - [resolveGs1Gtin](/operations/resolveGs1Gtin.md) (`GET /01/{gtin14}`) - [resolveGs1Grai](/operations/resolveGs1Grai.md) (`GET /8003/{grai}`) ================================================================================ # FILE: /okf/schemas/AasEnvironmentInput.md ================================================================================ --- type: Schema title: AasEnvironmentInput description: An Asset Administration Shell (AAS) JSON Environment — the format produced by OpenDPP's AAS export of a passport. resource: https://opendpp-node.eu/openapi.json#/components/schemas/AasEnvironmentInput tags: - schema timestamp: 2026-06-17T00:00:00Z --- An Asset Administration Shell (AAS) JSON Environment — the format produced by OpenDPP's AAS export of a passport. MUST contain a submodel with `idShort: "ComplianceMetadata"` whose `submodelElements` (AAS `Property` elements and `SubmodelElementCollection`s) are parsed back into the passport metadata object; absence fails 400 `Ingestion Failed`. MAY contain an `eidasVerificationSeal` submodel (elements `digitalSealHash`, `cryptographicSignature`, `pemPublicKey`) — when present, the seal is verified against the tenant's SERVER-HELD eIDAS public key (the embedded `pemPublicKey` is never trusted as the verification key). Body limit 256 KiB. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `assetAdministrationShells` | array | no | AAS shells. | | `submodels` | array | yes | Must include the ComplianceMetadata submodel; may include eidasVerificationSeal. | | `conceptDescriptions` | array | no | — | ## JSON Schema ```json { "type": "object", "required": [ "submodels" ], "description": "An Asset Administration Shell (AAS) JSON Environment — the format produced by OpenDPP's AAS export of a passport. MUST contain a submodel with `idShort: \"ComplianceMetadata\"` whose `submodelElements` (AAS `Property` elements and `SubmodelElementCollection`s) are parsed back into the passport metadata object; absence fails 400 `Ingestion Failed`. MAY contain an `eidasVerificationSeal` submodel (elements `digitalSealHash`, `cryptographicSignature`, `pemPublicKey`) — when present, the seal is verified against the tenant's SERVER-HELD eIDAS public key (the embedded `pemPublicKey` is never trusted as the verification key). Body limit 256 KiB.", "properties": { "assetAdministrationShells": { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "AAS shells. The first shell's `assetInformation.specificAssetIds` entry with `name: \"productId\"` is the last-resort productId source (after `metadata.gtin`/`metadata.grai`/`metadata.productId`)." }, "submodels": { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Must include the `ComplianceMetadata` submodel; may include `eidasVerificationSeal`." }, "conceptDescriptions": { "type": "array", "items": { "type": "object", "additionalProperties": true } } }, "additionalProperties": true } ``` ## Used by - [ingestPassportFromAas](/operations/ingestPassportFromAas.md) (`POST /api/v1/passports/aas/ingest`) ================================================================================ # FILE: /okf/schemas/AasIngestCreated.md ================================================================================ --- type: Schema title: AasIngestCreated description: 201 envelope of POST /api/v1/passports/aas/ingest. resource: https://opendpp-node.eu/openapi.json#/components/schemas/AasIngestCreated tags: - schema timestamp: 2026-06-17T00:00:00Z --- 201 envelope of `POST /api/v1/passports/aas/ingest`. Returned for both newly created passports and in-place updates of existing UNSEALED passports. No webhook event is emitted by this endpoint. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | — | | `passportId` | string | yes | — | | `productId` | string | yes | — | | `isSealed` | boolean | yes | True when the environment embedded an eidasVerificationSeal submodel (the seal is then stored on the passport). | | `signatureVerified` | boolean | yes | True when the embedded seal verified against the tenant's server-held eIDAS public key. | ## JSON Schema ```json { "type": "object", "required": [ "success", "message", "passportId", "productId", "isSealed", "signatureVerified" ], "description": "201 envelope of `POST /api/v1/passports/aas/ingest`. Returned for both newly created passports and in-place updates of existing UNSEALED passports. No webhook event is emitted by this endpoint.", "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "const": "Digital Product Passport successfully ingested from AAS" }, "passportId": { "type": "string" }, "productId": { "type": "string" }, "isSealed": { "type": "boolean", "description": "True when the environment embedded an `eidasVerificationSeal` submodel (the seal is then stored on the passport)." }, "signatureVerified": { "type": "boolean", "description": "True when the embedded seal verified against the tenant's server-held eIDAS public key. Always false for unsealed documents. (A sealed-but-unverified document never reaches 201 — it fails 400.)" } } } ``` ## Used by - [ingestPassportFromAas](/operations/ingestPassportFromAas.md) (`POST /api/v1/passports/aas/ingest`) ================================================================================ # FILE: /okf/schemas/ApproveGrantRequest.md ================================================================================ --- type: Schema title: ApproveGrantRequest description: Approval body — only the final expiry is supplied; everything else comes from the original request. resource: https://opendpp-node.eu/openapi.json#/components/schemas/ApproveGrantRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- Approval body — only the final expiry is supplied; everything else comes from the original request. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `expiresAt` | string | yes | Required. | ## JSON Schema ```json { "type": "object", "description": "Approval body — only the final expiry is supplied; everything else comes from the original request.", "required": [ "expiresAt" ], "properties": { "expiresAt": { "type": "string", "format": "date-time", "description": "Required. Date parsing is lenient (some non-ISO values may be accepted) — ISO 8601 is strongly recommended. Must be in the future and at most 366 days from now. Replaces the request's provisional 90-day expiry." } } } ``` ## Used by - [approveGrantRequest](/operations/approveGrantRequest.md) (`POST /api/v1/grants/{id}/approve`) ================================================================================ # FILE: /okf/schemas/BatteryUnitCreateItem.md ================================================================================ --- type: Schema title: BatteryUnitCreateItem description: One unit to serialise. resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitCreateItem tags: - schema timestamp: 2026-06-17T00:00:00Z --- One unit to serialise. Validation is per-item: an invalid item is skipped (its error string collected) without failing the rest of the batch. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `serialNumber` | string | yes | Required. | | `manufacturedAt` | string,number | no | Optional. | | `status` | [BatteryUnitStatus](/schemas/BatteryUnitStatus.md) | no | Optional initial status. | | `predecessorUnitId` | string | no | Optional Art. | | `predecessorStatus` | string | no | Optional; only meaningful with predecessorUnitId. | ## JSON Schema ```json { "type": "object", "description": "One unit to serialise. Validation is per-item: an invalid item is skipped (its error string collected) without failing the rest of the batch.", "properties": { "serialNumber": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,64}$", "description": "Required. The battery's real physical serial (GS1 AI-21). Trimmed, then must match `^[A-Za-z0-9._-]{1,64}$`. Must be unique within the passport (duplicates are skipped with an item error)." }, "manufacturedAt": { "type": [ "string", "number" ], "description": "Optional. Any value accepted by JavaScript `Date` parsing (ISO 8601 recommended; epoch milliseconds also work). Invalid dates skip the item." }, "status": { "$ref": "#/components/schemas/BatteryUnitStatus", "description": "Optional initial status. Defaults to `IN_SERVICE`. Note: creating a unit directly with `RECYCLED` makes the public view a 410 tombstone but does NOT stamp `ceasedAt` (only the events-route transition does), so such a unit can still be referenced as a predecessor.", "default": "IN_SERVICE" }, "predecessorUnitId": { "type": "string", "description": "Optional Art. 77(7) linkage: id of an existing unit **in your tenant** (any passport) that this battery was repurposed/remanufactured from. A recycled predecessor (`ceasedAt` set) is refused — the check keys on `ceasedAt`, which only the events-route `RECYCLED` transition stamps. Atomically with creation, a `STATUS_CHANGE` event (`{status, successorUnitId, successorSerial}`) is appended to the predecessor and its status set to `predecessorStatus`." }, "predecessorStatus": { "type": "string", "enum": [ "REPURPOSED", "REMANUFACTURED", "REUSED" ], "default": "REPURPOSED", "description": "Optional; only meaningful with `predecessorUnitId`. The status the predecessor transitions to. Defaults to `REPURPOSED`." } }, "required": [ "serialNumber" ] } ``` ## Used by - schema [SerializeBatteryUnitsRequest](/schemas/SerializeBatteryUnitsRequest.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitCurrentState.md ================================================================================ --- type: Schema title: BatteryUnitCurrentState description: Latest recorded measurement of the unit (owner/grant tiers only). resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitCurrentState tags: - schema timestamp: 2026-06-17T00:00:00Z --- Latest recorded measurement of the unit (owner/grant tiers only). All measurement fields are `null` when the latest event did not carry them. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `stateOfHealth` | number,null | yes | State of health, percent (0-100). | | `cycleCount` | integer,null | yes | Cumulative full-equivalent charge cycles. | | `remainingCapacityAh` | number,null | yes | Measured remaining capacity in ampere-hours. | | `temperatureC` | number,null | yes | Observed temperature in degrees Celsius. | | `recordedAt` | string | yes | When the measurement was taken (client-supplied). | ## JSON Schema ```json { "type": "object", "description": "Latest recorded measurement of the unit (owner/grant tiers only). All measurement fields are `null` when the latest event did not carry them.", "required": [ "stateOfHealth", "cycleCount", "remainingCapacityAh", "temperatureC", "recordedAt" ], "properties": { "stateOfHealth": { "type": [ "number", "null" ], "description": "State of health, percent (0-100)." }, "cycleCount": { "type": [ "integer", "null" ], "description": "Cumulative full-equivalent charge cycles." }, "remainingCapacityAh": { "type": [ "number", "null" ], "description": "Measured remaining capacity in ampere-hours." }, "temperatureC": { "type": [ "number", "null" ], "description": "Observed temperature in degrees Celsius." }, "recordedAt": { "type": "string", "format": "date-time", "description": "When the measurement was taken (client-supplied)." } } } ``` ## Used by - schema [BatteryUnitJsonLd](/schemas/BatteryUnitJsonLd.md) - schema [PublicBatteryUnitJsonLd](/schemas/PublicBatteryUnitJsonLd.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitDeleteResponse.md ================================================================================ --- type: Schema title: BatteryUnitDeleteResponse description: BatteryUnitDeleteResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitDeleteResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | — | ## JSON Schema ```json { "type": "object", "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "const": "Battery unit deleted." } }, "required": [ "success", "message" ] } ``` ## Used by - [deleteBatteryUnit](/operations/deleteBatteryUnit.md) (`DELETE /api/v1/units/{id}`) ================================================================================ # FILE: /okf/schemas/BatteryUnitDynamicDataEvent.md ================================================================================ --- type: Schema title: BatteryUnitDynamicDataEvent description: One telemetry event in the JSON-LD dynamicData history (privileged view only). resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitDynamicDataEvent tags: - schema timestamp: 2026-06-17T00:00:00Z --- One telemetry event in the JSON-LD `dynamicData` history (privileged view only). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@type` | string | yes | — | | `eventType` | [BatteryUnitEventType](/schemas/BatteryUnitEventType.md) | yes | — | | `stateOfHealth` | number,null | yes | — | | `cycleCount` | integer,null | yes | — | | `remainingCapacityAh` | number,null | yes | — | | `temperatureC` | number,null | yes | — | | `payload` | object,array,null | yes | The persisted free-form payload — a JSON object or array (arrays pass the write-path typeof check), null when absent. | | `recordedAt` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "One telemetry event in the JSON-LD `dynamicData` history (privileged view only).", "properties": { "@type": { "type": "string", "const": "BatteryUnitEvent" }, "eventType": { "$ref": "#/components/schemas/BatteryUnitEventType" }, "stateOfHealth": { "type": [ "number", "null" ] }, "cycleCount": { "type": [ "integer", "null" ] }, "remainingCapacityAh": { "type": [ "number", "null" ] }, "temperatureC": { "type": [ "number", "null" ] }, "payload": { "type": [ "object", "array", "null" ], "description": "The persisted free-form payload — a JSON object **or array** (arrays pass the write-path `typeof` check), `null` when absent." }, "recordedAt": { "type": "string", "format": "date-time" } }, "required": [ "@type", "eventType", "stateOfHealth", "cycleCount", "remainingCapacityAh", "temperatureC", "payload", "recordedAt" ] } ``` ## Used by - schema [BatteryUnitJsonLd](/schemas/BatteryUnitJsonLd.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitEventListResponse.md ================================================================================ --- type: Schema title: BatteryUnitEventListResponse description: BatteryUnitEventListResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitEventListResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `count` | integer | yes | Equals events.length; never exceeds 500. | | `serialNumber` | string | yes | The unit's physical serial (GS1 AI-21 value). | | `events` | array<[BatteryUnitEventRow](/schemas/BatteryUnitEventRow.md)> | yes | Newest first by recordedAt, capped at the 500 most recent. | ## JSON Schema ```json { "type": "object", "properties": { "success": { "type": "boolean", "const": true }, "count": { "type": "integer", "minimum": 0, "maximum": 500, "description": "Equals `events.length`; never exceeds 500." }, "serialNumber": { "type": "string", "description": "The unit's physical serial (GS1 AI-21 value)." }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/BatteryUnitEventRow" }, "maxItems": 500, "description": "Newest first by `recordedAt`, capped at the 500 most recent." } }, "required": [ "success", "count", "serialNumber", "events" ] } ``` ## Used by - [listBatteryUnitEvents](/operations/listBatteryUnitEvents.md) (`GET /api/v1/units/{id}/events`) ================================================================================ # FILE: /okf/schemas/BatteryUnitEventNode.md ================================================================================ --- type: Schema title: BatteryUnitEventNode description: One append-only telemetry event (owner/grant tiers only). resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitEventNode tags: - schema timestamp: 2026-06-17T00:00:00Z --- One append-only telemetry event (owner/grant tiers only). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@type` | string | yes | — | | `eventType` | string | yes | — | | `stateOfHealth` | number,null | yes | Percent, 0-100. | | `cycleCount` | integer,null | yes | — | | `remainingCapacityAh` | number,null | yes | — | | `temperatureC` | number,null | yes | — | | `payload` | object,array,null | yes | Free-form additional telemetry/context supplied at ingestion. | | `recordedAt` | string | yes | When the measurement was taken (client-supplied at ingestion). | ## JSON Schema ```json { "type": "object", "description": "One append-only telemetry event (owner/grant tiers only).", "required": [ "@type", "eventType", "stateOfHealth", "cycleCount", "remainingCapacityAh", "temperatureC", "payload", "recordedAt" ], "properties": { "@type": { "type": "string", "const": "BatteryUnitEvent" }, "eventType": { "type": "string", "enum": [ "SOH_MEASUREMENT", "CHARGE_CYCLE", "STATUS_CHANGE", "NEGATIVE_EVENT", "OTHER" ] }, "stateOfHealth": { "type": [ "number", "null" ], "description": "Percent, 0-100." }, "cycleCount": { "type": [ "integer", "null" ] }, "remainingCapacityAh": { "type": [ "number", "null" ] }, "temperatureC": { "type": [ "number", "null" ] }, "payload": { "type": [ "object", "array", "null" ], "additionalProperties": true, "description": "Free-form additional telemetry/context supplied at ingestion. Ingestion accepts any JSON object OR array, so both shapes can appear here." }, "recordedAt": { "type": "string", "format": "date-time", "description": "When the measurement was taken (client-supplied at ingestion)." } } } ``` ## Used by - schema [PublicBatteryUnitJsonLd](/schemas/PublicBatteryUnitJsonLd.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitEventRow.md ================================================================================ --- type: Schema title: BatteryUnitEventRow description: One immutable per-unit telemetry record (raw persisted row). resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitEventRow tags: - schema timestamp: 2026-06-17T00:00:00Z --- One immutable per-unit telemetry record (raw persisted row). Append-only: no update or delete path exists. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | yes | — | | `batteryUnitId` | string | yes | — | | `tenantId` | string | yes | — | | `eventType` | [BatteryUnitEventType](/schemas/BatteryUnitEventType.md) | yes | — | | `stateOfHealth` | number,null | yes | State of health, percent. | | `cycleCount` | integer,null | yes | Cumulative full-equivalent cycles (truncated to an integer on write). | | `remainingCapacityAh` | number,null | yes | Measured remaining capacity, ampere-hours. | | `temperatureC` | number,null | yes | Observed temperature, °C. | | `payload` | object,array,null | yes | Free-form additional telemetry/context as persisted: a JSON object or array (both pass the server's typeof check and are stored verbatim); null when omitted or… | | `recordedAt` | string | yes | When the measurement was taken (client-supplied; server time when omitted). | | `createdAt` | string | yes | Immutable append timestamp (server-assigned). | ## JSON Schema ```json { "type": "object", "description": "One immutable per-unit telemetry record (raw persisted row). Append-only: no update or delete path exists.", "properties": { "id": { "type": "string" }, "batteryUnitId": { "type": "string" }, "tenantId": { "type": "string" }, "eventType": { "$ref": "#/components/schemas/BatteryUnitEventType" }, "stateOfHealth": { "type": [ "number", "null" ], "minimum": 0, "maximum": 100, "description": "State of health, percent." }, "cycleCount": { "type": [ "integer", "null" ], "minimum": 0, "description": "Cumulative full-equivalent cycles (truncated to an integer on write)." }, "remainingCapacityAh": { "type": [ "number", "null" ], "minimum": 0, "description": "Measured remaining capacity, ampere-hours." }, "temperatureC": { "type": [ "number", "null" ], "minimum": -273.15, "maximum": 10000, "description": "Observed temperature, °C." }, "payload": { "type": [ "object", "array", "null" ], "description": "Free-form additional telemetry/context as persisted: a JSON object **or array** (both pass the server's `typeof` check and are stored verbatim); `null` when omitted or when the submitted value was dropped (any non-object, non-array value)." }, "recordedAt": { "type": "string", "format": "date-time", "description": "When the measurement was taken (client-supplied; server time when omitted)." }, "createdAt": { "type": "string", "format": "date-time", "description": "Immutable append timestamp (server-assigned)." } }, "required": [ "id", "batteryUnitId", "tenantId", "eventType", "stateOfHealth", "cycleCount", "remainingCapacityAh", "temperatureC", "payload", "recordedAt", "createdAt" ] } ``` ## Used by - schema [RecordBatteryUnitEventResponse](/schemas/RecordBatteryUnitEventResponse.md) - schema [BatteryUnitEventListResponse](/schemas/BatteryUnitEventListResponse.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitEventType.md ================================================================================ --- type: Schema title: BatteryUnitEventType description: Per-unit dynamic-data event category (Annex XIII / Art. resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitEventType tags: - schema timestamp: 2026-06-17T00:00:00Z --- Per-unit dynamic-data event category (Annex XIII / Art. 77 telemetry). ## JSON Schema ```json { "type": "string", "enum": [ "SOH_MEASUREMENT", "CHARGE_CYCLE", "STATUS_CHANGE", "NEGATIVE_EVENT", "OTHER" ], "description": "Per-unit dynamic-data event category (Annex XIII / Art. 77 telemetry)." } ``` ## Used by - schema [BatteryUnitEventRow](/schemas/BatteryUnitEventRow.md) - schema [BatteryUnitDynamicDataEvent](/schemas/BatteryUnitDynamicDataEvent.md) - schema [RecordBatteryUnitEventRequest](/schemas/RecordBatteryUnitEventRequest.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitJsonLd.md ================================================================================ --- type: Schema title: BatteryUnitJsonLd description: "JSON-LD document for one serialised battery unit, privileged tenant view (isPrivileged=true): includes currentState + dynamicData telemetry (restricted to legitimate-interest holders/authorities on the public view, where a restrictedData m…" resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitJsonLd tags: - schema timestamp: 2026-06-17T00:00:00Z --- JSON-LD document for one serialised battery unit, **privileged tenant view** (`isPrivileged=true`): includes `currentState` + `dynamicData` telemetry (restricted to legitimate-interest holders/authorities on the public view, where a `restrictedData` marker appears instead — never on this endpoint). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@context` | array | yes | JSON-LD context: the shared https://opendpp-node.eu/contexts/dpp/v1 IRI plus an inline term map for the battery-unit vocabulary. | | `@type` | string | yes | — | | `@id` | string | yes | The unit's GS1 Digital Link URI (same value as digitalLinkUri). | | `id` | string | yes | — | | `serialNumber` | string | yes | — | | `digitalLinkUri` | string | yes | — | | `status` | [BatteryUnitStatus](/schemas/BatteryUnitStatus.md) | yes | — | | `manufacturedAt` | string,null | yes | — | | `repurposedFrom` | — | yes | Art. | | `successorUnits` | array<[BatteryUnitLineageRef](/schemas/BatteryUnitLineageRef.md)> | yes | Units repurposed/remanufactured from this one. | | `ofModel` | [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) | yes | The SKU/type-level passport this unit is an instance of. | | `currentState` | — | yes | — | | `dynamicData` | array<[BatteryUnitDynamicDataEvent](/schemas/BatteryUnitDynamicDataEvent.md)> | yes | Full telemetry history, newest first by recordedAt, capped at the 500 most recent events. | | `createdAt` | string | yes | — | | `updatedAt` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "JSON-LD document for one serialised battery unit, **privileged tenant view** (`isPrivileged=true`): includes `currentState` + `dynamicData` telemetry (restricted to legitimate-interest holders/authorities on the public view, where a `restrictedData` marker appears instead — never on this endpoint).", "properties": { "@context": { "type": "array", "description": "JSON-LD context: the shared `https://opendpp-node.eu/contexts/dpp/v1` IRI plus an inline term map for the battery-unit vocabulary.", "items": { "type": [ "string", "object" ] } }, "@type": { "type": "string", "const": "BatteryUnit" }, "@id": { "type": "string", "format": "uri", "description": "The unit's GS1 Digital Link URI (same value as `digitalLinkUri`)." }, "id": { "type": "string" }, "serialNumber": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,64}$" }, "digitalLinkUri": { "type": "string", "format": "uri" }, "status": { "$ref": "#/components/schemas/BatteryUnitStatus" }, "manufacturedAt": { "type": [ "string", "null" ], "format": "date-time" }, "repurposedFrom": { "oneOf": [ { "$ref": "#/components/schemas/BatteryUnitLineageRef" }, { "type": "null" } ], "description": "Art. 77(7) predecessor link. **Always `null` on `GET /api/v1/units/{id}`** — the authenticated handler does not load the lineage relation; use the public resolver `GET /unit/{id}` to see resolved lineage." }, "successorUnits": { "type": "array", "items": { "$ref": "#/components/schemas/BatteryUnitLineageRef" }, "description": "Units repurposed/remanufactured from this one. **Always `[]` on `GET /api/v1/units/{id}`** (relation not loaded; see `repurposedFrom`)." }, "ofModel": { "$ref": "#/components/schemas/PublicPassportJsonLd", "description": "The SKU/type-level passport this unit is an instance of. On this authenticated endpoint it is rendered in the **owner (privileged, unredacted) variant**: legitimate-interest-tier metadata keys and owner-only keys (e.g. `facilityDetails`) are NOT masked, unlike the anonymous public document this schema describes. The passport's own `@context` inline map always carries 9 fixed terms (DigitalProductPassport, economicOperator, manufacturingFacility, metadata, digitalSeal, signingPublicKey, status, archivedAt, retentionUntil) plus one dynamically generated term per metadata key." }, "currentState": { "oneOf": [ { "$ref": "#/components/schemas/BatteryUnitCurrentState" }, { "type": "null" } ] }, "dynamicData": { "type": "array", "items": { "$ref": "#/components/schemas/BatteryUnitDynamicDataEvent" }, "maxItems": 500, "description": "Full telemetry history, newest first by `recordedAt`, capped at the 500 most recent events." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "@context", "@type", "@id", "id", "serialNumber", "digitalLinkUri", "status", "manufacturedAt", "repurposedFrom", "successorUnits", "ofModel", "currentState", "dynamicData", "createdAt", "updatedAt" ] } ``` ## Used by - [getBatteryUnit](/operations/getBatteryUnit.md) (`GET /api/v1/units/{id}`) ================================================================================ # FILE: /okf/schemas/BatteryUnitLineageRef.md ================================================================================ --- type: Schema title: BatteryUnitLineageRef description: Public lineage pointer between battery units (Art. resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitLineageRef tags: - schema timestamp: 2026-06-17T00:00:00Z --- Public lineage pointer between battery units (Art. 77(7)). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `unitId` | string | yes | — | | `serialNumber` | string | yes | — | | `digitalLinkUri` | string | yes | — | | `unitUrl` | string | yes | Relative public unit URL: /unit/{unitId}. | ## JSON Schema ```json { "type": "object", "description": "Public lineage pointer between battery units (Art. 77(7)).", "required": [ "unitId", "serialNumber", "digitalLinkUri", "unitUrl" ], "properties": { "unitId": { "type": "string" }, "serialNumber": { "type": "string" }, "digitalLinkUri": { "type": "string", "format": "uri" }, "unitUrl": { "type": "string", "description": "Relative public unit URL: `/unit/{unitId}`." } } } ``` ## Used by - schema [BatteryUnitJsonLd](/schemas/BatteryUnitJsonLd.md) - schema [PublicBatteryUnitJsonLd](/schemas/PublicBatteryUnitJsonLd.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitListResponse.md ================================================================================ --- type: Schema title: BatteryUnitListResponse description: BatteryUnitListResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitListResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `count` | integer | yes | Number of items returned in THIS page (≤ limit). | | `productId` | string | yes | The passport's caller-supplied product identifier (GTIN-14 / GRAI / SKU). | | `units` | array<[BatteryUnitRow](/schemas/BatteryUnitRow.md)> | yes | All units of the passport, createdAt DESC. | | `page` | integer | yes | 1-based page number returned. | | `limit` | integer | yes | Effective page size (default 100, max 200). | | `total` | integer | yes | Total items matching across all pages. | | `totalPages` | integer | yes | Total number of pages (≥ 1). | ## JSON Schema ```json { "type": "object", "properties": { "success": { "type": "boolean", "const": true }, "count": { "type": "integer", "description": "Number of items returned in THIS page (≤ `limit`)." }, "productId": { "type": "string", "description": "The passport's caller-supplied product identifier (GTIN-14 / GRAI / SKU)." }, "units": { "type": "array", "items": { "$ref": "#/components/schemas/BatteryUnitRow" }, "description": "All units of the passport, `createdAt` DESC." }, "page": { "type": "integer", "description": "1-based page number returned." }, "limit": { "type": "integer", "description": "Effective page size (default 100, max 200)." }, "total": { "type": "integer", "description": "Total items matching across all pages." }, "totalPages": { "type": "integer", "description": "Total number of pages (≥ 1)." } }, "required": [ "success", "count", "productId", "units", "page", "limit", "total", "totalPages" ] } ``` ## Used by - [listBatteryUnits](/operations/listBatteryUnits.md) (`GET /api/v1/passports/{passportId}/units`) ================================================================================ # FILE: /okf/schemas/BatteryUnitRestrictedDataNotice.md ================================================================================ --- type: Schema title: BatteryUnitRestrictedDataNotice description: Marker replacing per-unit telemetry in anonymous (public-tier) responses, with a pointer for requesting legitimate-interest access (Reg. resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitRestrictedDataNotice tags: - schema timestamp: 2026-06-17T00:00:00Z --- Marker replacing per-unit telemetry in anonymous (public-tier) responses, with a pointer for requesting legitimate-interest access (Reg. (EU) 2023/1542, Annex XIII(2)-(4)). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `reason` | string | yes | — | | `reference` | string | yes | — | | `description` | string | yes | — | | `howToRequest` | string | yes | Relative URL /request-access?unit={unitId} where a legitimate-interest grant can be requested. | ## JSON Schema ```json { "type": "object", "description": "Marker replacing per-unit telemetry in anonymous (public-tier) responses, with a pointer for requesting legitimate-interest access (Reg. (EU) 2023/1542, Annex XIII(2)-(4)).", "required": [ "reason", "reference", "description", "howToRequest" ], "properties": { "reason": { "type": "string", "const": "LEGITIMATE_INTEREST_REQUIRED" }, "reference": { "type": "string", "const": "Regulation (EU) 2023/1542, Annex XIII(2)-(4)" }, "description": { "type": "string", "const": "Per-unit dynamic data (state of health, cycle counts, negative events, temperature) is accessible only to persons with a legitimate interest and to authorities." }, "howToRequest": { "type": "string", "description": "Relative URL `/request-access?unit={unitId}` where a legitimate-interest grant can be requested." } } } ``` ## Used by - schema [PublicBatteryUnitJsonLd](/schemas/PublicBatteryUnitJsonLd.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitRow.md ================================================================================ --- type: Schema title: BatteryUnitRow description: One physical serialised battery (raw persisted row — these routes declare no Fastify response schema, so all model fields are returned as-is). resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitRow tags: - schema timestamp: 2026-06-17T00:00:00Z --- One physical serialised battery (raw persisted row — these routes declare no Fastify response schema, so all model fields are returned as-is). A `BatteryUnit` is an individual instance of a SKU/type-level passport, carrying its real serial in GS1 AI-21. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | yes | — | | `serialNumber` | string | yes | The battery's real physical serial number (GS1 AI-21 value). | | `digitalLinkUri` | string | yes | Per-unit GS1 Digital Link: {BASE_URL}/{01|8003}/{productId}/21/{serialNumber} — AI 01 for GTIN (and non-GS1 SKUs), 8003 for GRAI. | | `passportId` | string | yes | The SKU/type-level passport this unit is an instance of. | | `tenantId` | string | yes | Owning tenant id (the demo tenant uses the fixed id tenant-demo-opendpp; regular tenants use UUIDs). | | `manufacturedAt` | string,null | yes | — | | `status` | [BatteryUnitStatus](/schemas/BatteryUnitStatus.md) | yes | — | | `ceasedAt` | string,null | yes | Stamped when the events endpoint transitions status to RECYCLED (Art. | | `predecessorUnitId` | string,null | yes | Art. | | `createdAt` | string | yes | — | | `updatedAt` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "One physical serialised battery (raw persisted row — these routes declare no Fastify response schema, so all model fields are returned as-is). A `BatteryUnit` is an individual instance of a SKU/type-level passport, carrying its real serial in GS1 AI-21.", "properties": { "id": { "type": "string" }, "serialNumber": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,64}$", "description": "The battery's real physical serial number (GS1 AI-21 value). 1–64 URL-safe characters; GS1 recommends ≤ 20." }, "digitalLinkUri": { "type": "string", "format": "uri", "description": "Per-unit GS1 Digital Link: `{BASE_URL}/{01|8003}/{productId}/21/{serialNumber}` — AI `01` for GTIN (and non-GS1 SKUs), `8003` for GRAI. Unique platform-wide." }, "passportId": { "type": "string", "description": "The SKU/type-level passport this unit is an instance of." }, "tenantId": { "type": "string", "description": "Owning tenant id (the demo tenant uses the fixed id `tenant-demo-opendpp`; regular tenants use UUIDs)." }, "manufacturedAt": { "type": [ "string", "null" ], "format": "date-time" }, "status": { "$ref": "#/components/schemas/BatteryUnitStatus" }, "ceasedAt": { "type": [ "string", "null" ], "format": "date-time", "description": "Stamped when the events endpoint transitions `status` to `RECYCLED` (Art. 77(8) cease-to-exist); never cleared afterwards, even if a later event changes `status` again. Non-null means the public unit view is a 410 tombstone and the unit is refused as a `predecessorUnitId`. Note: a unit *created* with initial status `RECYCLED` does NOT get `ceasedAt` stamped (the public view still tombstones on the status alone, but the predecessor refusal does not apply)." }, "predecessorUnitId": { "type": [ "string", "null" ], "description": "Art. 77(7) lineage: the original unit this battery was repurposed/remanufactured from (`null` for first-life units)." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "serialNumber", "digitalLinkUri", "passportId", "tenantId", "manufacturedAt", "status", "ceasedAt", "predecessorUnitId", "createdAt", "updatedAt" ] } ``` ## Used by - schema [SerializeBatteryUnitsResponse](/schemas/SerializeBatteryUnitsResponse.md) - schema [BatteryUnitListResponse](/schemas/BatteryUnitListResponse.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitSerialisationFailedError.md ================================================================================ --- type: Schema title: BatteryUnitSerialisationFailedError description: 400 body when every item in the serialisation batch failed. resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitSerialisationFailedError tags: - schema timestamp: 2026-06-17T00:00:00Z --- 400 body when **every** item in the serialisation batch failed. Note: `errors` is an array of plain strings and there is **no `message` field** (unlike the standard error triple). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `error` | string | yes | — | | `errors` | array | yes | — | ## JSON Schema ```json { "type": "object", "description": "400 body when **every** item in the serialisation batch failed. Note: `errors` is an array of plain strings and there is **no `message` field** (unlike the standard error triple).", "properties": { "success": { "type": "boolean", "const": false }, "error": { "type": "string", "const": "Serialisation Failed" }, "errors": { "type": "array", "items": { "type": "string" }, "minItems": 1 } }, "required": [ "success", "error", "errors" ] } ``` ## Used by - [serializeBatteryUnits](/operations/serializeBatteryUnits.md) (`POST /api/v1/passports/{passportId}/units`) ================================================================================ # FILE: /okf/schemas/BatteryUnitStatus.md ================================================================================ --- type: Schema title: BatteryUnitStatus description: Annex XIII battery-status vocabulary (Battery Reg. resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitStatus tags: - schema timestamp: 2026-06-17T00:00:00Z --- Annex XIII battery-status vocabulary (Battery Reg. (EU) 2023/1542). `RECYCLED` means the passport has ceased to exist (Art. 77(8)): the public unit view answers 410 Gone whenever `status` is `RECYCLED` **or** `ceasedAt` is set. Terminality is enforced via `ceasedAt`, which only the events-route transition stamps (and never clears): the predecessor-refusal check keys on `ceasedAt` alone, so a unit *created* with initial status `RECYCLED` (no `ceasedAt`) is already a public 410 tombstone yet can still be referenced as a predecessor; conversely, once `ceasedAt` is set the events endpoint will still accept later `status` values, but the 410 tombstone and the predecessor refusal persist. ## JSON Schema ```json { "type": "string", "enum": [ "IN_SERVICE", "DECOMMISSIONED", "RECALLED", "REPURPOSED", "REMANUFACTURED", "REUSED", "WASTE", "RECYCLED" ], "description": "Annex XIII battery-status vocabulary (Battery Reg. (EU) 2023/1542). `RECYCLED` means the passport has ceased to exist (Art. 77(8)): the public unit view answers 410 Gone whenever `status` is `RECYCLED` **or** `ceasedAt` is set. Terminality is enforced via `ceasedAt`, which only the events-route transition stamps (and never clears): the predecessor-refusal check keys on `ceasedAt` alone, so a unit *created* with initial status `RECYCLED` (no `ceasedAt`) is already a public 410 tombstone yet can still be referenced as a predecessor; conversely, once `ceasedAt` is set the events endpoint will still accept later `status` values, but the 410 tombstone and the predecessor refusal persist." } ``` ## Used by - schema [BatteryUnitRow](/schemas/BatteryUnitRow.md) - schema [BatteryUnitCreateItem](/schemas/BatteryUnitCreateItem.md) - schema [BatteryUnitJsonLd](/schemas/BatteryUnitJsonLd.md) - schema [RecordBatteryUnitEventRequest](/schemas/RecordBatteryUnitEventRequest.md) ================================================================================ # FILE: /okf/schemas/BatteryUnitTombstoneJsonLd.md ================================================================================ --- type: Schema title: BatteryUnitTombstoneJsonLd description: Art. resource: https://opendpp-node.eu/openapi.json#/components/schemas/BatteryUnitTombstoneJsonLd tags: - schema timestamp: 2026-06-17T00:00:00Z --- Art. 77(8) tombstone (HTTP 410): once a battery is recycled its passport has ceased to exist. This minimal record confirms the unit existed, that it was recycled and when, plus the (still living) model-passport link. Grants and owner credentials do not override the tombstone on the public URL; the underlying data is retained internally for the statutory retention window. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@context` | array<—> | yes | — | | `@type` | string | yes | — | | `@id` | string | yes | — | | `id` | string | yes | — | | `serialNumber` | string | yes | — | | `status` | string | yes | — | | `ceasedAt` | string,null | yes | When the unit's passport ceased to exist (stamped when the status transitioned to RECYCLED). | | `notice` | string | yes | — | | `ofModelUrl` | string,null | yes | Relative URL of the still-living SKU/type passport: /passport/{passportId}. | ## JSON Schema ```json { "type": "object", "description": "Art. 77(8) tombstone (HTTP 410): once a battery is recycled its passport has ceased to exist. This minimal record confirms the unit existed, that it was recycled and when, plus the (still living) model-passport link. Grants and owner credentials do not override the tombstone on the public URL; the underlying data is retained internally for the statutory retention window.", "additionalProperties": false, "required": [ "@context", "@type", "@id", "id", "serialNumber", "status", "ceasedAt", "notice", "ofModelUrl" ], "properties": { "@context": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "anyOf": [ { "type": "string" }, { "type": "object", "additionalProperties": { "type": "string" } } ] } }, "@type": { "type": "string", "const": "BatteryUnit" }, "@id": { "type": "string", "format": "uri" }, "id": { "type": "string" }, "serialNumber": { "type": "string" }, "status": { "type": "string", "const": "RECYCLED" }, "ceasedAt": { "type": [ "string", "null" ], "format": "date-time", "description": "When the unit's passport ceased to exist (stamped when the status transitioned to RECYCLED)." }, "notice": { "type": "string", "const": "This battery has been recycled. Its battery passport has ceased to exist (Regulation (EU) 2023/1542, Art. 77(8))." }, "ofModelUrl": { "type": [ "string", "null" ], "description": "Relative URL of the still-living SKU/type passport: `/passport/{passportId}`." } } } ``` ## Used by - [resolvePublicBatteryUnit](/operations/resolvePublicBatteryUnit.md) (`GET /unit/{id}`) ================================================================================ # FILE: /okf/schemas/CreateGrantRequest.md ================================================================================ --- type: Schema title: CreateGrantRequest description: Direct-issuance body. resource: https://opendpp-node.eu/openapi.json#/components/schemas/CreateGrantRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- Direct-issuance body. Over-length strings are silently truncated to the documented maximum, not rejected; unknown fields are ignored. The grant kind is always `LEGITIMATE_INTEREST` — there is no `kind`/`type` field. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `granteeName` | string | yes | Required (whitespace-only is rejected as missing). | | `granteeEmail` | string | no | Optional. | | `organization` | string | no | Optional. | | `purpose` | string | no | Optional stated legitimate interest. | | `scopeType` | string | yes | Required. | | `passportId` | string | no | Required when scopeType is PASSPORT; ignored otherwise. | | `batteryUnitId` | string | no | Required when scopeType is UNIT; ignored otherwise. | | `expiresAt` | string | yes | Required. | ## JSON Schema ```json { "type": "object", "description": "Direct-issuance body. Over-length strings are silently truncated to the documented maximum, not rejected; unknown fields are ignored. The grant kind is always `LEGITIMATE_INTEREST` — there is no `kind`/`type` field.", "required": [ "granteeName", "scopeType", "expiresAt" ], "properties": { "granteeName": { "type": "string", "minLength": 1, "maxLength": 160, "description": "Required (whitespace-only is rejected as missing). Truncated to 160 characters." }, "granteeEmail": { "type": "string", "format": "email", "maxLength": 254, "description": "Optional. Truncated to 254 characters, then must match a basic e-mail pattern (`x@y.z`) or the request fails with 400 `granteeEmail is invalid`. Stored as given (not lowercased)." }, "organization": { "type": "string", "maxLength": 200, "description": "Optional. Truncated to 200 characters." }, "purpose": { "type": "string", "maxLength": 2000, "description": "Optional stated legitimate interest. Truncated to 2000 characters." }, "scopeType": { "type": "string", "enum": [ "UNIT", "PASSPORT", "TENANT" ], "description": "Required. `UNIT` needs `batteryUnitId`; `PASSPORT` needs `passportId`; `TENANT` is workspace-wide. Any other value ⇒ 400." }, "passportId": { "type": "string", "maxLength": 64, "description": "Required when `scopeType` is `PASSPORT`; ignored otherwise. Must be a non-DRAFT passport in this workspace — a missing, cross-tenant, or `DRAFT` id (or omitting the field entirely) returns 404." }, "batteryUnitId": { "type": "string", "maxLength": 64, "description": "Required when `scopeType` is `UNIT`; ignored otherwise. Must be a battery unit in this workspace — a missing or cross-tenant id (or omitting the field entirely) returns 404." }, "expiresAt": { "type": "string", "format": "date-time", "description": "Required. Date parsing is lenient (some non-ISO values may be accepted) — ISO 8601 is strongly recommended. Must be in the future and at most 366 days from now." } } } ``` ## Used by - [createGrant](/operations/createGrant.md) (`POST /api/v1/grants`) ================================================================================ # FILE: /okf/schemas/DeleteOperatorResponse.md ================================================================================ --- type: Schema title: DeleteOperatorResponse description: DeleteOperatorResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/DeleteOperatorResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `archived` | boolean | yes | true = the operator was archived (soft-deleted; passports retained, restorable). | | `archivedPassports` | integer | no | Number of active passports archived alongside the operator. | ## JSON Schema ```json { "type": "object", "required": [ "success", "archived" ], "properties": { "success": { "type": "boolean", "const": true }, "archived": { "type": "boolean", "description": "`true` = the operator was archived (soft-deleted; passports retained, restorable). `false` = the operator was hard-deleted (it had no passports)." }, "archivedPassports": { "type": "integer", "minimum": 0, "description": "Number of active passports archived alongside the operator. Present only on the primary archive path (operator had passports); absent on hard deletes and on the foreign-key fallback archive." } } } ``` ## Used by - [deleteOperator](/operations/deleteOperator.md) (`DELETE /api/v1/operators/{id}`) ================================================================================ # FILE: /okf/schemas/DppJsonLdContextDocument.md ================================================================================ --- type: Schema title: DppJsonLdContextDocument description: "The fixed W3C JSON-LD context document served by GET /context/v1: maps DigitalProductPassport, economicOperator, metadata, digitalSeal, signingPublicKey and proof to https://opendpp-node.eu/ns/dpp#… IRIs, and createdAt/updatedAt to schema.…" resource: https://opendpp-node.eu/openapi.json#/components/schemas/DppJsonLdContextDocument tags: - schema timestamp: 2026-06-17T00:00:00Z --- The fixed W3C JSON-LD context document served by `GET /context/v1`: maps `DigitalProductPassport`, `economicOperator`, `metadata`, `digitalSeal`, `signingPublicKey` and `proof` to `https://opendpp-node.eu/ns/dpp#…` IRIs, and `createdAt`/`updatedAt` to schema.org `dateCreated`/`dateModified`. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@context` | object | yes | — | ## JSON Schema ```json { "type": "object", "description": "The fixed W3C JSON-LD context document served by `GET /context/v1`: maps `DigitalProductPassport`, `economicOperator`, `metadata`, `digitalSeal`, `signingPublicKey` and `proof` to `https://opendpp-node.eu/ns/dpp#…` IRIs, and `createdAt`/`updatedAt` to schema.org `dateCreated`/`dateModified`.", "additionalProperties": false, "required": [ "@context" ], "properties": { "@context": { "type": "object", "additionalProperties": { "type": "string" } } } } ``` ## Used by - [getJsonLdContext](/operations/getJsonLdContext.md) (`GET /context/v1`) ================================================================================ # FILE: /okf/schemas/EconomicOperatorNode.md ================================================================================ --- type: Schema title: EconomicOperatorNode description: Embedded economic-operator JSON-LD node (public in all tiers). resource: https://opendpp-node.eu/openapi.json#/components/schemas/EconomicOperatorNode tags: - schema timestamp: 2026-06-17T00:00:00Z --- Embedded economic-operator JSON-LD node (public in all tiers). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@type` | string | yes | — | | `id` | string | yes | — | | `name` | string | yes | — | | `regId` | string | yes | EORI number or official business-registry identifier (unique platform-wide), e.g. | | `role` | string | no | Operator role in the supply chain, e.g. | ## JSON Schema ```json { "type": "object", "description": "Embedded economic-operator JSON-LD node (public in all tiers).", "required": [ "@type", "id", "name", "regId" ], "properties": { "@type": { "type": "string", "const": "EconomicOperator" }, "id": { "type": "string" }, "name": { "type": "string" }, "regId": { "type": "string", "description": "EORI number or official business-registry identifier (unique platform-wide), e.g. `EU-DEFAULT-001`." }, "role": { "type": "string", "description": "Operator role in the supply chain, e.g. `MANUFACTURER`, `IMPORTER`, `RETAILER`. Always present in detail/resolution responses; absent from `GET /api/v1/passports` list items." } } } ``` ## Used by - schema [PassportListItem](/schemas/PassportListItem.md) - schema [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) ================================================================================ # FILE: /okf/schemas/Error.md ================================================================================ --- type: Schema title: Error description: Standard error body. resource: https://opendpp-node.eu/openapi.json#/components/schemas/Error tags: - schema timestamp: 2026-06-17T00:00:00Z --- Standard error body. Authenticated-API errors include `success: false`; some endpoints (and all public resolution errors) omit `success` and return only `error` + `message`. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | no | Always false when present. | | `error` | string | yes | Short error title (usually the HTTP reason phrase). | | `message` | string | yes | Human-readable explanation. | ## JSON Schema ```json { "type": "object", "description": "Standard error body. Authenticated-API errors include `success: false`; some endpoints (and all public resolution errors) omit `success` and return only `error` + `message`.", "required": [ "error", "message" ], "properties": { "success": { "type": "boolean", "description": "Always `false` when present. Omitted by public endpoints and some self-service endpoints." }, "error": { "type": "string", "description": "Short error title (usually the HTTP reason phrase).", "examples": [ "Bad Request", "Not Found", "Validation Failed" ] }, "message": { "type": "string", "description": "Human-readable explanation." } } } ``` ## Used by - [whoami](/operations/whoami.md) (`GET /api/v1/whoami`) - [serializeBatteryUnits](/operations/serializeBatteryUnits.md) (`POST /api/v1/passports/{passportId}/units`) - [recordBatteryUnitEvent](/operations/recordBatteryUnitEvent.md) (`POST /api/v1/units/{id}/events`) - [createFacility](/operations/createFacility.md) (`POST /api/v1/facilities`) - [getFacility](/operations/getFacility.md) (`GET /api/v1/facilities/{id}`) - [updateFacility](/operations/updateFacility.md) (`PUT /api/v1/facilities/{id}`) - [deleteFacility](/operations/deleteFacility.md) (`DELETE /api/v1/facilities/{id}`) - [revokeGrant](/operations/revokeGrant.md) (`DELETE /api/v1/grants/{id}`) - [registerOperator](/operations/registerOperator.md) (`POST /api/v1/operators`) - [updateOperator](/operations/updateOperator.md) (`PATCH /api/v1/operators/{id}`) - [deleteOperator](/operations/deleteOperator.md) (`DELETE /api/v1/operators/{id}`) - [restoreOperator](/operations/restoreOperator.md) (`POST /api/v1/operators/{id}/restore`) - [rotateTenantKeys](/operations/rotateTenantKeys.md) (`POST /api/v1/tenants/rotate-keys`) - [createPassport](/operations/createPassport.md) (`POST /api/v1/passports`) - [ingestPassportFromAas](/operations/ingestPassportFromAas.md) (`POST /api/v1/passports/aas/ingest`) - [getPassport](/operations/getPassport.md) (`GET /api/v1/passports/{id}`) - [updatePassport](/operations/updatePassport.md) (`PUT /api/v1/passports/{id}`) - [deleteDraftPassport](/operations/deleteDraftPassport.md) (`DELETE /api/v1/passports/{id}`) - [sealPassport](/operations/sealPassport.md) (`POST /api/v1/passports/{id}/seal`) - [updatePassportStatus](/operations/updatePassportStatus.md) (`PUT /api/v1/passports/{id}/status`) - [resolvePublicPassport](/operations/resolvePublicPassport.md) (`GET /passport/{id}`) - [resolveGs1Gtin](/operations/resolveGs1Gtin.md) (`GET /01/{gtin14}`) - [resolveGs1GtinSerial](/operations/resolveGs1GtinSerial.md) (`GET /01/{gtin14}/21/{serial}`) - [resolveGs1Grai](/operations/resolveGs1Grai.md) (`GET /8003/{grai}`) - [resolvePublicBatteryUnit](/operations/resolvePublicBatteryUnit.md) (`GET /unit/{id}`) - [getSealCaCertificate](/operations/getSealCaCertificate.md) (`GET /.well-known/opendpp-seal-ca.pem`) - [getPassportQrCode](/operations/getPassportQrCode.md) (`GET /api/v1/passports/{id}/qr`) - [getBatteryUnitQrCode](/operations/getBatteryUnitQrCode.md) (`GET /api/v1/units/{id}/qr`) - [registerTraceabilityEvent](/operations/registerTraceabilityEvent.md) (`POST /api/v1/events`) - [getEventLineage](/operations/getEventLineage.md) (`GET /api/v1/events/{id}/lineage`) - [auditEventLineage](/operations/auditEventLineage.md) (`POST /api/v1/events/{id}/audit`) - [listWebhookSubscriptions](/operations/listWebhookSubscriptions.md) (`GET /api/v1/webhooks/subscriptions`) - [createWebhookSubscription](/operations/createWebhookSubscription.md) (`POST /api/v1/webhooks/subscriptions`) - [updateWebhookSubscription](/operations/updateWebhookSubscription.md) (`PATCH /api/v1/webhooks/subscriptions/{id}`) - [deleteWebhookSubscription](/operations/deleteWebhookSubscription.md) (`DELETE /api/v1/webhooks/subscriptions/{id}`) - the shared error responses ================================================================================ # FILE: /okf/schemas/FacilityCreateRequest.md ================================================================================ --- type: Schema title: FacilityCreateRequest description: FacilityCreateRequest resource: https://opendpp-node.eu/openapi.json#/components/schemas/FacilityCreateRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `gln` | string | yes | GS1 GLN-13. | | `name` | string | yes | Facility name. | | `country` | string | yes | 2-letter ISO country code (case-insensitive on input; stored uppercase). | | `activity` | string | no | Optional activity, e.g. | | `streetAddress` | string | no | Optional street address (owner-only in public views). | | `city` | string | no | Optional city (owner-only in public views). | | `postalCode` | string | no | Optional postal code (owner-only in public views). | | `operatorId` | string | no | Optional owning Economic Operator id. | ## JSON Schema ```json { "type": "object", "required": [ "gln", "name", "country" ], "properties": { "gln": { "type": "string", "pattern": "^[0-9]{13}$", "description": "GS1 GLN-13. Trimmed, then validated: exactly 13 digits with a valid GS1 modulo-10 check digit. Unique platform-wide (409 on duplicate). Immutable after registration." }, "name": { "type": "string", "minLength": 1, "description": "Facility name. Must be a non-empty string; stored trimmed." }, "country": { "type": "string", "pattern": "^[A-Za-z]{2}$", "description": "2-letter ISO country code (case-insensitive on input; stored uppercase)." }, "activity": { "type": "string", "description": "Optional activity, e.g. \"Cell assembly\". Trimmed; empty/whitespace is stored as null." }, "streetAddress": { "type": "string", "description": "Optional street address (owner-only in public views). Trimmed; empty is stored as null." }, "city": { "type": "string", "description": "Optional city (owner-only in public views). Trimmed; empty is stored as null." }, "postalCode": { "type": "string", "description": "Optional postal code (owner-only in public views). Trimmed; empty is stored as null." }, "operatorId": { "type": "string", "description": "Optional owning Economic Operator id. Must be bound to your tenant workspace (403 otherwise). Empty/whitespace is treated as absent. Operator-scoped API keys may only use their own operator id (it is applied automatically when omitted)." } } } ``` ## Used by - [createFacility](/operations/createFacility.md) (`POST /api/v1/facilities`) ================================================================================ # FILE: /okf/schemas/FacilityCreatedEnvelope.md ================================================================================ --- type: Schema title: FacilityCreatedEnvelope description: FacilityCreatedEnvelope resource: https://opendpp-node.eu/openapi.json#/components/schemas/FacilityCreatedEnvelope tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | — | | `facility` | [FacilityRow](/schemas/FacilityRow.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "message", "facility" ], "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "const": "Facility registered successfully" }, "facility": { "$ref": "#/components/schemas/FacilityRow" } } } ``` ## Used by - [createFacility](/operations/createFacility.md) (`POST /api/v1/facilities`) ================================================================================ # FILE: /okf/schemas/FacilityDeletedEnvelope.md ================================================================================ --- type: Schema title: FacilityDeletedEnvelope description: FacilityDeletedEnvelope resource: https://opendpp-node.eu/openapi.json#/components/schemas/FacilityDeletedEnvelope tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success" ], "properties": { "success": { "type": "boolean", "const": true } } } ``` ## Used by - [deleteFacility](/operations/deleteFacility.md) (`DELETE /api/v1/facilities/{id}`) ================================================================================ # FILE: /okf/schemas/FacilityEnvelope.md ================================================================================ --- type: Schema title: FacilityEnvelope description: FacilityEnvelope resource: https://opendpp-node.eu/openapi.json#/components/schemas/FacilityEnvelope tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `facility` | [FacilityRow](/schemas/FacilityRow.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "facility" ], "properties": { "success": { "type": "boolean", "const": true }, "facility": { "$ref": "#/components/schemas/FacilityRow" } } } ``` ## Used by - [getFacility](/operations/getFacility.md) (`GET /api/v1/facilities/{id}`) - [updateFacility](/operations/updateFacility.md) (`PUT /api/v1/facilities/{id}`) ================================================================================ # FILE: /okf/schemas/FacilityListEnvelope.md ================================================================================ --- type: Schema title: FacilityListEnvelope description: FacilityListEnvelope resource: https://opendpp-node.eu/openapi.json#/components/schemas/FacilityListEnvelope tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `count` | integer | yes | Number of items returned in THIS page (≤ limit). | | `facilities` | array<[FacilityRow](/schemas/FacilityRow.md)> | yes | All facilities in the workspace, sorted by createdAt descending. | | `page` | integer | yes | 1-based page number returned. | | `limit` | integer | yes | Effective page size (default 100, max 200). | | `total` | integer | yes | Total items matching across all pages. | | `totalPages` | integer | yes | Total number of pages (≥ 1). | ## JSON Schema ```json { "type": "object", "required": [ "success", "count", "facilities", "page", "limit", "total", "totalPages" ], "properties": { "success": { "type": "boolean", "const": true }, "count": { "type": "integer", "description": "Number of items returned in THIS page (≤ `limit`)." }, "facilities": { "type": "array", "items": { "$ref": "#/components/schemas/FacilityRow" }, "description": "All facilities in the workspace, sorted by createdAt descending. Operator-scoped keys see only their operator's facilities." }, "page": { "type": "integer", "description": "1-based page number returned." }, "limit": { "type": "integer", "description": "Effective page size (default 100, max 200)." }, "total": { "type": "integer", "description": "Total items matching across all pages." }, "totalPages": { "type": "integer", "description": "Total number of pages (≥ 1)." } } } ``` ## Used by - [listFacilities](/operations/listFacilities.md) (`GET /api/v1/facilities`) ================================================================================ # FILE: /okf/schemas/FacilityRow.md ================================================================================ --- type: Schema title: FacilityRow description: A facility (GS1 GLN) master-data row, exactly as stored. resource: https://opendpp-node.eu/openapi.json#/components/schemas/FacilityRow tags: - schema timestamp: 2026-06-17T00:00:00Z --- A facility (GS1 GLN) master-data row, exactly as stored. Returned in full to the owning tenant. Public exposure in passport documents differs by format: the *JSON-LD* document exposes `id`, `gln`, `name`, `activity` and `country` of a linked facility; the *AAS* export emits only the GLN, name and country (`manufacturingFacilityGln`/`Name`/`Country`). `streetAddress`, `city` and `postalCode` are emitted only to the owning/bound tenant in both formats. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | yes | Facility id (UUID). | | `gln` | string | yes | GS1 Global Location Number — 13 digits with a valid GS1 modulo-10 check digit. | | `name` | string | yes | Facility display name (trimmed, non-empty). | | `activity` | string,null | yes | Free-text activity, e.g. | | `streetAddress` | string,null | yes | Street address. | | `city` | string,null | yes | City. | | `postalCode` | string,null | yes | Postal code. | | `country` | string | yes | 2-letter ISO 3166-1 alpha-2 country code, stored uppercase. | | `operatorId` | string,null | yes | Id of the owning Economic Operator, or null for a tenant-level facility. | | `tenantId` | string | yes | Owning tenant workspace id. | | `createdAt` | string | yes | — | | `updatedAt` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "A facility (GS1 GLN) master-data row, exactly as stored. Returned in full to the owning tenant. Public exposure in passport documents differs by format: the *JSON-LD* document exposes `id`, `gln`, `name`, `activity` and `country` of a linked facility; the *AAS* export emits only the GLN, name and country (`manufacturingFacilityGln`/`Name`/`Country`). `streetAddress`, `city` and `postalCode` are emitted only to the owning/bound tenant in both formats.", "required": [ "id", "gln", "name", "activity", "streetAddress", "city", "postalCode", "country", "operatorId", "tenantId", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "Facility id (UUID)." }, "gln": { "type": "string", "pattern": "^[0-9]{13}$", "description": "GS1 Global Location Number — 13 digits with a valid GS1 modulo-10 check digit. Unique platform-wide. Immutable after registration (it is the resolvable UFI)." }, "name": { "type": "string", "description": "Facility display name (trimmed, non-empty)." }, "activity": { "type": [ "string", "null" ], "description": "Free-text activity, e.g. \"Cell assembly\", \"Final manufacturing\", \"Recycling\". Public in JSON-LD; not emitted in the AAS export." }, "streetAddress": { "type": [ "string", "null" ], "description": "Street address. Owner-only: redacted from public JSON-LD and never emitted in AAS." }, "city": { "type": [ "string", "null" ], "description": "City. Owner-only: redacted from public JSON-LD and never emitted in AAS." }, "postalCode": { "type": [ "string", "null" ], "description": "Postal code. Owner-only: redacted from public JSON-LD and never emitted in AAS." }, "country": { "type": "string", "pattern": "^[A-Z]{2}$", "description": "2-letter ISO 3166-1 alpha-2 country code, stored uppercase. Public in both JSON-LD and AAS." }, "operatorId": { "type": [ "string", "null" ], "description": "Id of the owning Economic Operator, or null for a tenant-level facility. Set at creation; not updatable via PUT." }, "tenantId": { "type": "string", "description": "Owning tenant workspace id." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } } ``` ## Used by - schema [FacilityCreatedEnvelope](/schemas/FacilityCreatedEnvelope.md) - schema [FacilityListEnvelope](/schemas/FacilityListEnvelope.md) - schema [FacilityEnvelope](/schemas/FacilityEnvelope.md) ================================================================================ # FILE: /okf/schemas/FacilityUpdateRequest.md ================================================================================ --- type: Schema title: FacilityUpdateRequest description: Partial update. resource: https://opendpp-node.eu/openapi.json#/components/schemas/FacilityUpdateRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- Partial update. `gln` and `operatorId` are immutable — if present they are silently ignored, as is any unknown key. For `activity`/`streetAddress`/`city`/`postalCode` the *presence* of the key matters: the value is stringified and trimmed, and anything that trims to empty (null, "", or a whitespace-only string) clears the field to null — the same normalization as POST. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | string | no | New name. | | `activity` | string,null | no | New activity, or null/"" to clear. | | `streetAddress` | string,null | no | New street address, or null/"" to clear. | | `city` | string,null | no | New city, or null/"" to clear. | | `postalCode` | string,null | no | New postal code, or null/"" to clear. | | `country` | string | no | New 2-letter ISO country code (400 if a string that does not match; stored uppercase; non-string values are silently ignored). | ## JSON Schema ```json { "type": "object", "description": "Partial update. `gln` and `operatorId` are immutable — if present they are silently ignored, as is any unknown key. For `activity`/`streetAddress`/`city`/`postalCode` the *presence* of the key matters: the value is stringified and trimmed, and anything that trims to empty (null, \"\", or a whitespace-only string) clears the field to null — the same normalization as POST.", "properties": { "name": { "type": "string", "minLength": 1, "description": "New name. Applied only when a non-empty string; empty/whitespace or non-string values are silently ignored (the name can never be cleared)." }, "activity": { "type": [ "string", "null" ], "description": "New activity, or null/\"\" to clear. A whitespace-only string is stored as \"\" (see schema description)." }, "streetAddress": { "type": [ "string", "null" ], "description": "New street address, or null/\"\" to clear. A whitespace-only string is stored as \"\" (see schema description)." }, "city": { "type": [ "string", "null" ], "description": "New city, or null/\"\" to clear. A whitespace-only string is stored as \"\" (see schema description)." }, "postalCode": { "type": [ "string", "null" ], "description": "New postal code, or null/\"\" to clear. A whitespace-only string is stored as \"\" (see schema description)." }, "country": { "type": "string", "pattern": "^[A-Za-z]{2}$", "description": "New 2-letter ISO country code (400 if a string that does not match; stored uppercase; non-string values are silently ignored)." } } } ``` ## Used by - [updateFacility](/operations/updateFacility.md) (`PUT /api/v1/facilities/{id}`) ================================================================================ # FILE: /okf/schemas/FastifyDefaultBadRequest.md ================================================================================ --- type: Schema title: FastifyDefaultBadRequest description: Fastify's default 400 error body, returned when a syntactically malformed JSON request body is rejected by the framework before the handler runs (so none of the handler-built {success:false, ...} shapes apply). resource: https://opendpp-node.eu/openapi.json#/components/schemas/FastifyDefaultBadRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- Fastify's default 400 error body, returned when a syntactically malformed JSON request body is rejected by the framework **before the handler runs** (so none of the handler-built `{success:false, ...}` shapes apply). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `statusCode` | integer | yes | — | | `code` | string | no | Fastify error code, e.g. | | `error` | string | yes | — | | `message` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "Fastify's default 400 error body, returned when a syntactically malformed JSON request body is rejected by the framework **before the handler runs** (so none of the handler-built `{success:false, ...}` shapes apply).", "properties": { "statusCode": { "type": "integer", "const": 400 }, "code": { "type": "string", "description": "Fastify error code, e.g. `FST_ERR_CTP_INVALID_JSON_BODY`. May be absent." }, "error": { "type": "string", "const": "Bad Request" }, "message": { "type": "string" } }, "required": [ "statusCode", "error", "message" ] } ``` ## Used by - [serializeBatteryUnits](/operations/serializeBatteryUnits.md) (`POST /api/v1/passports/{passportId}/units`) - [recordBatteryUnitEvent](/operations/recordBatteryUnitEvent.md) (`POST /api/v1/units/{id}/events`) ================================================================================ # FILE: /okf/schemas/GrantDecisionResponse.md ================================================================================ --- type: Schema title: GrantDecisionResponse description: "Returned by deny and revoke: the updated grant, no token." resource: https://opendpp-node.eu/openapi.json#/components/schemas/GrantDecisionResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- Returned by deny and revoke: the updated grant, no token. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `grant` | [GrantRow](/schemas/GrantRow.md) | yes | — | ## JSON Schema ```json { "type": "object", "description": "Returned by deny and revoke: the updated grant, no token.", "required": [ "success", "grant" ], "properties": { "success": { "type": "boolean", "const": true }, "grant": { "$ref": "#/components/schemas/GrantRow" } } } ``` ## Used by - [denyGrantRequest](/operations/denyGrantRequest.md) (`POST /api/v1/grants/{id}/deny`) - [revokeGrant](/operations/revokeGrant.md) (`DELETE /api/v1/grants/{id}`) ================================================================================ # FILE: /okf/schemas/GrantIssuedResponse.md ================================================================================ --- type: Schema title: GrantIssuedResponse description: Returned by direct issuance (201) and request approval (200). resource: https://opendpp-node.eu/openapi.json#/components/schemas/GrantIssuedResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- Returned by direct issuance (201) and request approval (200). `token` is the raw capability token — shown ONCE here (and, on approval, in the grantee's inspection-link e-mail); only its SHA-256 hash is persisted. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `grant` | [GrantRow](/schemas/GrantRow.md) | yes | — | | `token` | string | yes | Legitimate-interest capability token (dpp_li_ + 32 lowercase hex). | ## JSON Schema ```json { "type": "object", "description": "Returned by direct issuance (201) and request approval (200). `token` is the raw capability token — shown ONCE here (and, on approval, in the grantee's inspection-link e-mail); only its SHA-256 hash is persisted.", "required": [ "success", "grant", "token" ], "properties": { "success": { "type": "boolean", "const": true }, "grant": { "$ref": "#/components/schemas/GrantRow" }, "token": { "type": "string", "pattern": "^dpp_li_[0-9a-f]{32}$", "description": "Legitimate-interest capability token (`dpp_li_` + 32 lowercase hex). Present it to public resolution endpoints as `Authorization: Bearer ` or `?grant=`. Treat like a password — it cannot be retrieved again." } } } ``` ## Used by - [createGrant](/operations/createGrant.md) (`POST /api/v1/grants`) - [approveGrantRequest](/operations/approveGrantRequest.md) (`POST /api/v1/grants/{id}/approve`) ================================================================================ # FILE: /okf/schemas/GrantListResponse.md ================================================================================ --- type: Schema title: GrantListResponse description: List envelope for GET /api/v1/grants (paginated). resource: https://opendpp-node.eu/openapi.json#/components/schemas/GrantListResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- List envelope for `GET /api/v1/grants` (paginated). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `count` | integer | yes | Number of items returned in THIS page (≤ limit). | | `page` | integer | yes | 1-based page number returned. | | `limit` | integer | yes | Effective page size (default 100, max 200). | | `total` | integer | yes | Total items matching across all pages. | | `totalPages` | integer | yes | Total number of pages (≥ 1). | | `grants` | array<[GrantRow](/schemas/GrantRow.md)> | yes | Grants for this page (≤ limit), ordered by status ascending then createdAt descending. | ## JSON Schema ```json { "type": "object", "description": "List envelope for `GET /api/v1/grants` (paginated).", "required": [ "success", "grants", "count", "page", "limit", "total", "totalPages" ], "properties": { "success": { "type": "boolean", "const": true }, "count": { "type": "integer", "description": "Number of items returned in THIS page (≤ `limit`)." }, "page": { "type": "integer", "description": "1-based page number returned." }, "limit": { "type": "integer", "description": "Effective page size (default 100, max 200)." }, "total": { "type": "integer", "description": "Total items matching across all pages." }, "totalPages": { "type": "integer", "description": "Total number of pages (≥ 1)." }, "grants": { "type": "array", "maxItems": 200, "items": { "$ref": "#/components/schemas/GrantRow" }, "description": "Grants for this page (≤ `limit`), ordered by `status` ascending then `createdAt` descending." } } } ``` ## Used by - [listGrants](/operations/listGrants.md) (`GET /api/v1/grants`) ================================================================================ # FILE: /okf/schemas/GrantRouteError.md ================================================================================ --- type: Schema title: GrantRouteError description: Error body used by the grants endpoints' route-level errors (400/403/404/409). resource: https://opendpp-node.eu/openapi.json#/components/schemas/GrantRouteError tags: - schema timestamp: 2026-06-17T00:00:00Z --- Error body used by the grants endpoints' route-level errors (400/403/404/409). Unlike the standard error envelope, it has NO `success` field. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `error` | string | yes | HTTP reason phrase, e.g. | | `message` | string | yes | Human-readable explanation. | ## JSON Schema ```json { "type": "object", "description": "Error body used by the grants endpoints' route-level errors (400/403/404/409). Unlike the standard error envelope, it has NO `success` field.", "required": [ "error", "message" ], "not": { "required": [ "success" ] }, "properties": { "error": { "type": "string", "description": "HTTP reason phrase, e.g. `Bad Request`, `Not Found`, `Conflict`, `Forbidden`." }, "message": { "type": "string", "description": "Human-readable explanation." } } } ``` ## Used by - [createGrant](/operations/createGrant.md) (`POST /api/v1/grants`) - [approveGrantRequest](/operations/approveGrantRequest.md) (`POST /api/v1/grants/{id}/approve`) - [denyGrantRequest](/operations/denyGrantRequest.md) (`POST /api/v1/grants/{id}/deny`) - [revokeGrant](/operations/revokeGrant.md) (`DELETE /api/v1/grants/{id}`) ================================================================================ # FILE: /okf/schemas/GrantRow.md ================================================================================ --- type: Schema title: GrantRow description: Tenant-facing projection of an access grant. resource: https://opendpp-node.eu/openapi.json#/components/schemas/GrantRow tags: - schema timestamp: 2026-06-17T00:00:00Z --- Tenant-facing projection of an access grant. The token hash, issuer user id, revoking actor and request IP are never exposed; raw capability tokens appear only in the one-time issuance/approval responses. All fields are always present (nullable ones serialize as `null`). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | yes | — | | `status` | string | yes | PENDING = undecided third-party request (no token exists yet); ACTIVE = usable token; DENIED = rejected request; REVOKED = soft-revoked. | | `kind` | string | yes | LEGITIMATE_INTEREST (dpp_li_… tokens, tenant-issued or approved from a request) or AUTHORITY (dpp_auth_… tokens, platform-issued for market surveillance; not t… | | `granteeName` | string | yes | — | | `granteeEmail` | string,null | yes | — | | `organization` | string,null | yes | — | | `purpose` | string,null | yes | The stated legitimate interest. | | `scopeType` | string | yes | What the token unlocks on the public resolvers: a single battery unit, a single passport, or the whole workspace. | | `passportId` | string,null | yes | Set for PASSPORT scope, and also for UNIT scope (the unit's parent passport). | | `batteryUnitId` | string,null | yes | Set only for UNIT scope. | | `issuerType` | string | yes | TENANT = issued directly via this API; REQUEST = submitted by a third party through the hosted request-access page; PLATFORM = platform-admin-issued (AUTHORITY… | | `issuerEmail` | string,null | yes | E-mail of the issuing user; null when issued by an API key or created from a public request. | | `decidedAt` | string,null | yes | When a PENDING request was approved/denied; null for direct issuances. | | `decidedBy` | string,null | yes | The deciding actor: a user e-mail, API_KEY_ when decided via an API key, or the literal unknown in degenerate authentication states. | | `expiresAt` | string | yes | Hard expiry; the public resolvers reject the token after this instant. | | `revokedAt` | string,null | yes | — | | `lastUsedAt` | string,null | yes | Last successful use on a public resolver (book-kept best-effort). | | `useCount` | integer | yes | Successful public-resolver uses (incremented best-effort). | | `createdAt` | string | yes | — | | `revocable` | boolean | yes | Computed: false for AUTHORITY grants (platform-managed), true otherwise. | ## JSON Schema ```json { "type": "object", "description": "Tenant-facing projection of an access grant. The token hash, issuer user id, revoking actor and request IP are never exposed; raw capability tokens appear only in the one-time issuance/approval responses. All fields are always present (nullable ones serialize as `null`).", "required": [ "id", "status", "kind", "granteeName", "granteeEmail", "organization", "purpose", "scopeType", "passportId", "batteryUnitId", "issuerType", "issuerEmail", "decidedAt", "decidedBy", "expiresAt", "revokedAt", "lastUsedAt", "useCount", "createdAt", "revocable" ], "properties": { "id": { "type": "string" }, "status": { "type": "string", "enum": [ "PENDING", "ACTIVE", "DENIED", "REVOKED" ], "description": "`PENDING` = undecided third-party request (no token exists yet); `ACTIVE` = usable token; `DENIED` = rejected request; `REVOKED` = soft-revoked." }, "kind": { "type": "string", "enum": [ "LEGITIMATE_INTEREST", "AUTHORITY" ], "description": "`LEGITIMATE_INTEREST` (`dpp_li_…` tokens, tenant-issued or approved from a request) or `AUTHORITY` (`dpp_auth_…` tokens, platform-issued for market surveillance; not tenant-revocable)." }, "granteeName": { "type": "string", "maxLength": 160 }, "granteeEmail": { "type": [ "string", "null" ], "maxLength": 254 }, "organization": { "type": [ "string", "null" ], "maxLength": 200 }, "purpose": { "type": [ "string", "null" ], "maxLength": 2000, "description": "The stated legitimate interest." }, "scopeType": { "type": "string", "enum": [ "UNIT", "PASSPORT", "TENANT" ], "description": "What the token unlocks on the public resolvers: a single battery unit, a single passport, or the whole workspace." }, "passportId": { "type": [ "string", "null" ], "description": "Set for `PASSPORT` scope, and also for `UNIT` scope (the unit's parent passport). `null` for `TENANT` scope." }, "batteryUnitId": { "type": [ "string", "null" ], "description": "Set only for `UNIT` scope." }, "issuerType": { "type": "string", "enum": [ "TENANT", "PLATFORM", "REQUEST" ], "description": "`TENANT` = issued directly via this API; `REQUEST` = submitted by a third party through the hosted request-access page; `PLATFORM` = platform-admin-issued (AUTHORITY grants)." }, "issuerEmail": { "type": [ "string", "null" ], "description": "E-mail of the issuing user; `null` when issued by an API key or created from a public request." }, "decidedAt": { "type": [ "string", "null" ], "format": "date-time", "description": "When a PENDING request was approved/denied; `null` for direct issuances." }, "decidedBy": { "type": [ "string", "null" ], "description": "The deciding actor: a user e-mail, `API_KEY_` when decided via an API key, or the literal `unknown` in degenerate authentication states." }, "expiresAt": { "type": "string", "format": "date-time", "description": "Hard expiry; the public resolvers reject the token after this instant. PENDING requests carry a provisional 90-day expiry that is replaced on approval." }, "revokedAt": { "type": [ "string", "null" ], "format": "date-time" }, "lastUsedAt": { "type": [ "string", "null" ], "format": "date-time", "description": "Last successful use on a public resolver (book-kept best-effort)." }, "useCount": { "type": "integer", "description": "Successful public-resolver uses (incremented best-effort)." }, "createdAt": { "type": "string", "format": "date-time" }, "revocable": { "type": "boolean", "description": "Computed: `false` for `AUTHORITY` grants (platform-managed), `true` otherwise." } } } ``` ## Used by - schema [GrantListResponse](/schemas/GrantListResponse.md) - schema [GrantIssuedResponse](/schemas/GrantIssuedResponse.md) - schema [GrantDecisionResponse](/schemas/GrantDecisionResponse.md) ================================================================================ # FILE: /okf/schemas/HealthStatus.md ================================================================================ --- type: Schema title: HealthStatus description: Health-check body of GET /health. resource: https://opendpp-node.eu/openapi.json#/components/schemas/HealthStatus tags: - schema timestamp: 2026-06-17T00:00:00Z --- Health-check body of `GET /health`. Carries the running build identity (`apiVersion`/`commit`/`builtAt`) in addition to the liveness fields. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `status` | string | yes | — | | `service` | string | yes | — | | `timestamp` | string | yes | Current server time, ISO 8601 UTC with milliseconds. | | `apiVersion` | string | yes | SemVer of the public API contract currently served (equals the OpenAPI document's info.version; its MAJOR equals the /api/v1 URL major). | | `commit` | string | yes | Short git commit SHA of the running build, or "unknown" when a build did not inject it. | | `builtAt` | string | yes | Build/deploy timestamp (ISO 8601 UTC), or "unknown" when a build did not inject it. | ## JSON Schema ```json { "type": "object", "description": "Health-check body of `GET /health`. Carries the running build identity (`apiVersion`/`commit`/`builtAt`) in addition to the liveness fields.", "additionalProperties": false, "required": [ "status", "service", "timestamp", "apiVersion", "commit", "builtAt" ], "properties": { "status": { "type": "string", "const": "OK" }, "service": { "type": "string", "const": "OpenDPP B2B Enterprise Engine" }, "timestamp": { "type": "string", "format": "date-time", "description": "Current server time, ISO 8601 UTC with milliseconds." }, "apiVersion": { "type": "string", "description": "SemVer of the public API contract currently served (equals the OpenAPI document's `info.version`; its MAJOR equals the `/api/v1` URL major).", "examples": [ "1.0.0" ] }, "commit": { "type": "string", "description": "Short git commit SHA of the running build, or `\"unknown\"` when a build did not inject it.", "examples": [ "a7a96d0", "unknown" ] }, "builtAt": { "type": "string", "description": "Build/deploy timestamp (ISO 8601 UTC), or `\"unknown\"` when a build did not inject it.", "examples": [ "2026-06-12T09:30:00Z", "unknown" ] } } } ``` ## Used by - [getHealth](/operations/getHealth.md) (`GET /health`) ================================================================================ # FILE: /okf/schemas/MaterialVocabularyListResponse.md ================================================================================ --- type: Schema title: MaterialVocabularyListResponse description: Envelope of GET /api/v1/materials. resource: https://opendpp-node.eu/openapi.json#/components/schemas/MaterialVocabularyListResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- Envelope of `GET /api/v1/materials`. Caveat: unlike most authenticated endpoints there is NO `success` field. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `materials` | array<[MaterialVocabularyRow](/schemas/MaterialVocabularyRow.md)> | yes | Active vocabulary entries, ordered by kind ascending then name ascending, capped at limit (max 1000). | ## JSON Schema ```json { "type": "object", "description": "Envelope of `GET /api/v1/materials`. Caveat: unlike most authenticated endpoints there is NO `success` field.", "required": [ "materials" ], "properties": { "materials": { "type": "array", "description": "Active vocabulary entries, ordered by `kind` ascending then `name` ascending, capped at `limit` (max 1000).", "items": { "$ref": "#/components/schemas/MaterialVocabularyRow" } } } } ``` ## Used by - [listMaterials](/operations/listMaterials.md) (`GET /api/v1/materials`) ================================================================================ # FILE: /okf/schemas/MaterialVocabularyRow.md ================================================================================ --- type: Schema title: MaterialVocabularyRow description: One entry of the platform-curated material vocabulary. resource: https://opendpp-node.eu/openapi.json#/components/schemas/MaterialVocabularyRow tags: - schema timestamp: 2026-06-17T00:00:00Z --- One entry of the platform-curated material vocabulary. Entries are unique per (`kind`, `name`). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | yes | — | | `name` | string | yes | Canonical display name, e.g. | | `kind` | string | yes | Vocabulary kind. | | `casNumber` | string,null | yes | Optional CAS registry number (chemicals/substances); null when not applicable. | | `description` | string,null | yes | Optional short note shown in the picker; null when unset. | ## JSON Schema ```json { "type": "object", "description": "One entry of the platform-curated material vocabulary. Entries are unique per (`kind`, `name`).", "required": [ "id", "name", "kind", "casNumber", "description" ], "properties": { "id": { "type": "string" }, "name": { "type": "string", "description": "Canonical display name, e.g. \"Organic Cotton\" or \"Lithium Iron Phosphate (LFP)\"." }, "kind": { "type": "string", "enum": [ "material", "fiber", "chemistry", "substance", "hazard", "crm" ], "description": "Vocabulary kind. `crm` = critical raw material." }, "casNumber": { "type": [ "string", "null" ], "description": "Optional CAS registry number (chemicals/substances); null when not applicable." }, "description": { "type": [ "string", "null" ], "description": "Optional short note shown in the picker; null when unset." } } } ``` ## Used by - schema [MaterialVocabularyListResponse](/schemas/MaterialVocabularyListResponse.md) ================================================================================ # FILE: /okf/schemas/MerkleTreeAttestationProof.md ================================================================================ --- type: Schema title: MerkleTreeAttestationProof description: "OpenDPP's own proof type — an eIDAS ADVANCED electronic seal: an ECDSA prime256v1 signature over a SHA-256 Merkle root of the key-sorted metadata (one leaf per top-level metadata key)." resource: https://opendpp-node.eu/openapi.json#/components/schemas/MerkleTreeAttestationProof tags: - schema timestamp: 2026-06-17T00:00:00Z --- OpenDPP's own proof type — an eIDAS ADVANCED electronic seal: an ECDSA prime256v1 signature over a SHA-256 Merkle root of the key-sorted metadata (one leaf per top-level metadata key). Deliberately NOT a W3C DataIntegrityProof / `ecdsa-jcs-2019` Verifiable Credential (no RFC 8785 JCS canonicalization). Verifiable offline: rebuild the Merkle root from `metadata` — substituting each `redactedLeaves` hash for its placeholder-masked key, and EXCLUDING any placeholder-masked key that has no `redactedLeaves` entry (such a key was never present in the sealed metadata; the serializer injects the owner-only placeholder unconditionally) — then verify `signatureValue` with `publicKeyPem`; the `x5c` chain validates against the platform seal CA (`GET /.well-known/opendpp-seal-ca.pem`) and the `rfc3161` token via `openssl ts -verify`. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@type` | array | yes | Always ["MerkleTreeAttestationProof"]. | | `type` | string | yes | — | | `signatureAlgorithm` | string | yes | — | | `created` | string | yes | Mirrors the passport's updatedAt. | | `proofPurpose` | string | yes | — | | `verificationMethod` | string | yes | https://opendpp-node.eu/passport/{passportId}#key-1. | | `signatureValue` | string | yes | Base64 ECDSA P-256/SHA-256 signature over the hex Merkle root string (same value as the document's digitalSeal). | | `publicKeyPem` | string | yes | PEM public key for verification (same value as the document's signingPublicKey). | | `x5c` | array | no | OPTIONAL (omitted when no chain was recorded at seal time). | | `rfc3161` | object | no | OPTIONAL (omitted when timestamping was off/unavailable at seal time). | | `merkleRoot` | string | yes | Hex SHA-256 Merkle root over the key-sorted metadata leaves. | | `redactedLeaves` | object | no | OPTIONAL — present only when at least one masked key actually exists in the underlying sealed metadata. | ## JSON Schema ```json { "type": "object", "description": "OpenDPP's own proof type — an eIDAS ADVANCED electronic seal: an ECDSA prime256v1 signature over a SHA-256 Merkle root of the key-sorted metadata (one leaf per top-level metadata key). Deliberately NOT a W3C DataIntegrityProof / `ecdsa-jcs-2019` Verifiable Credential (no RFC 8785 JCS canonicalization). Verifiable offline: rebuild the Merkle root from `metadata` — substituting each `redactedLeaves` hash for its placeholder-masked key, and EXCLUDING any placeholder-masked key that has no `redactedLeaves` entry (such a key was never present in the sealed metadata; the serializer injects the owner-only placeholder unconditionally) — then verify `signatureValue` with `publicKeyPem`; the `x5c` chain validates against the platform seal CA (`GET /.well-known/opendpp-seal-ca.pem`) and the `rfc3161` token via `openssl ts -verify`.", "required": [ "@type", "type", "signatureAlgorithm", "created", "proofPurpose", "verificationMethod", "signatureValue", "publicKeyPem", "merkleRoot" ], "properties": { "@type": { "type": "array", "items": { "type": "string", "const": "MerkleTreeAttestationProof" }, "description": "Always `[\"MerkleTreeAttestationProof\"]`." }, "type": { "type": "string", "const": "MerkleTreeAttestationProof" }, "signatureAlgorithm": { "type": "string", "const": "ECDSA-P256-SHA256-over-MerkleRoot" }, "created": { "type": "string", "format": "date-time", "description": "Mirrors the passport's `updatedAt`." }, "proofPurpose": { "type": "string", "const": "assertionMethod" }, "verificationMethod": { "type": "string", "format": "uri", "description": "`https://opendpp-node.eu/passport/{passportId}#key-1`." }, "signatureValue": { "type": "string", "description": "Base64 ECDSA P-256/SHA-256 signature over the hex Merkle root string (same value as the document's `digitalSeal`)." }, "publicKeyPem": { "type": "string", "description": "PEM public key for verification (same value as the document's `signingPublicKey`)." }, "x5c": { "type": "array", "items": { "type": "string" }, "description": "OPTIONAL (omitted when no chain was recorded at seal time). X.509 chain as base64 DER (no PEM armor), leaf first, binding the signing key to the tenant's legal identity; issued by the platform seal CA. Denormalised at seal time, so later key/cert rotations never retroactively change a proof." }, "rfc3161": { "type": "object", "description": "OPTIONAL (omitted when timestamping was off/unavailable at seal time). RFC 3161 trusted timestamp over SHA-256(merkleRoot) — an independent existed-at anchor from the configured TSA.", "required": [ "genTime", "token" ], "properties": { "genTime": { "type": [ "string", "null" ], "format": "date-time", "description": "TSA generation time." }, "token": { "type": "string", "description": "Base64 DER RFC 3161 TimeStampToken; verifies offline via `openssl ts -verify`." } } }, "merkleRoot": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "Hex SHA-256 Merkle root over the key-sorted metadata leaves." }, "redactedLeaves": { "type": "object", "additionalProperties": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "description": "OPTIONAL — present only when at least one masked key actually exists in the underlying sealed metadata. Maps each such metadata key to its TRUE hex leaf hash, so the Merkle root can be reconstructed from the redacted document. A masked key that was never present in the metadata (the owner-only key is placeholder-injected unconditionally for non-owner tiers) yields NO entry here — verifiers must exclude placeholder-valued keys without an entry when rebuilding the tree." } } } ``` ## Used by - schema [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) ================================================================================ # FILE: /okf/schemas/OperatorGetResponse.md ================================================================================ --- type: Schema title: OperatorGetResponse description: OperatorGetResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/OperatorGetResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `operator` | [OperatorRow](/schemas/OperatorRow.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "operator" ], "properties": { "success": { "type": "boolean", "const": true }, "operator": { "$ref": "#/components/schemas/OperatorRow" } } } ``` ## Used by - [getOperator](/operations/getOperator.md) (`GET /api/v1/operators/{id}`) ================================================================================ # FILE: /okf/schemas/OperatorListResponse.md ================================================================================ --- type: Schema title: OperatorListResponse description: OperatorListResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/OperatorListResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `count` | integer | yes | Number of operators returned (the list is not paginated). | | `operators` | array<[OperatorRow](/schemas/OperatorRow.md)> | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "count", "operators" ], "properties": { "success": { "type": "boolean", "const": true }, "count": { "type": "integer", "description": "Number of operators returned (the list is not paginated)." }, "operators": { "type": "array", "items": { "$ref": "#/components/schemas/OperatorRow" } } } } ``` ## Used by - [listOperators](/operations/listOperators.md) (`GET /api/v1/operators`) ================================================================================ # FILE: /okf/schemas/OperatorMinimalError.md ================================================================================ --- type: Schema title: OperatorMinimalError description: Minimal error envelope used by the operator/key self-service handlers — note the standard error key is ABSENT (unlike the shared Error schema). resource: https://opendpp-node.eu/openapi.json#/components/schemas/OperatorMinimalError tags: - schema timestamp: 2026-06-17T00:00:00Z --- Minimal error envelope used by the operator/key self-service handlers — note the standard `error` key is ABSENT (unlike the shared Error schema). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "Minimal error envelope used by the operator/key self-service handlers — note the standard `error` key is ABSENT (unlike the shared Error schema).", "required": [ "success", "message" ], "properties": { "success": { "type": "boolean", "const": false }, "message": { "type": "string" } } } ``` ## Used by - [registerOperator](/operations/registerOperator.md) (`POST /api/v1/operators`) - [updateOperator](/operations/updateOperator.md) (`PATCH /api/v1/operators/{id}`) - [deleteOperator](/operations/deleteOperator.md) (`DELETE /api/v1/operators/{id}`) - [restoreOperator](/operations/restoreOperator.md) (`POST /api/v1/operators/{id}/restore`) - [rotateTenantKeys](/operations/rotateTenantKeys.md) (`POST /api/v1/tenants/rotate-keys`) ================================================================================ # FILE: /okf/schemas/OperatorRow.md ================================================================================ --- type: Schema title: OperatorRow description: An economic-operator record (EconomicOperator). resource: https://opendpp-node.eu/openapi.json#/components/schemas/OperatorRow tags: - schema timestamp: 2026-06-17T00:00:00Z --- An economic-operator record (`EconomicOperator`). Operators are scoped to your workspace (each workspace keeps its own row for a given `regId`). Returned verbatim from the database (no field stripping); nullable fields are serialized as `null`. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | yes | Operator UUID. | | `name` | string | yes | Legal/display name of the operator. | | `regId` | string | yes | Official registration id (EORI number, VAT id, DUNS, or national business-registry id). | | `regIdScheme` | string,null | yes | Which kind of registration id regId is. | | `role` | string | yes | Supply-chain role, free text — e.g. | | `archivedAt` | string,null | yes | Soft-delete / cessation-of-trading marker. | | `createdAt` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "An economic-operator record (`EconomicOperator`). Operators are scoped to your workspace (each workspace keeps its own row for a given `regId`). Returned verbatim from the database (no field stripping); nullable fields are serialized as `null`.", "required": [ "id", "name", "regId", "regIdScheme", "role", "archivedAt", "createdAt" ], "properties": { "id": { "type": "string", "description": "Operator UUID." }, "name": { "type": "string", "description": "Legal/display name of the operator." }, "regId": { "type": "string", "description": "Official registration id (EORI number, VAT id, DUNS, or national business-registry id). Unique within your workspace and immutable after registration." }, "regIdScheme": { "type": [ "string", "null" ], "enum": [ "EORI", "VAT", "DUNS", "NATIONAL", "OTHER", null ], "description": "Which kind of registration id `regId` is. `null` = unspecified national/business id. When `EORI`, `regId` is guaranteed to satisfy the EORI syntax `^[A-Z]{2}[A-Za-z0-9]{1,15}$`." }, "role": { "type": "string", "description": "Supply-chain role, free text — e.g. `\"MANUFACTURER\"`, `\"IMPORTER\"`, `\"RETAILER\"`. Defaults to `\"MANUFACTURER\"` at registration." }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time", "description": "Soft-delete / cessation-of-trading marker. Non-null = the operator is archived (its passports are retained and still publicly resolvable)." }, "createdAt": { "type": "string", "format": "date-time" } } } ``` ## Used by - schema [RegisterOperatorResponse](/schemas/RegisterOperatorResponse.md) - schema [UpdateOperatorResponse](/schemas/UpdateOperatorResponse.md) - schema [OperatorListResponse](/schemas/OperatorListResponse.md) - schema [OperatorGetResponse](/schemas/OperatorGetResponse.md) ================================================================================ # FILE: /okf/schemas/PassportAasEnvironment.md ================================================================================ --- type: Schema title: PassportAasEnvironment description: IDTA Asset Administration Shell environment (returned when Accept contains application/aas+json), role-filtered for the caller's access tier. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportAasEnvironment tags: - schema timestamp: 2026-06-17T00:00:00Z --- IDTA Asset Administration Shell environment (returned when `Accept` contains `application/aas+json`), role-filtered for the caller's access tier. Identifiers use `urn:opendpp:*` forms: the shell id is `urn:opendpp:aas:` with idShort `AAS_` and `globalAssetId` `urn:opendpp:asset::` (plus `specificAssetIds` carrying the productId and, when a facility is assigned, its GLN). The environment always contains two submodels — `GeneralProductInformation` (`urn:opendpp:submodel:general:`) and `ComplianceMetadata` (`urn:opendpp:submodel:compliance`) — and an `eidasVerificationSeal` submodel is appended when the tenant has signing keys configured. Loose schema — the full AAS document structure is documented with the public resolution endpoints. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `assetAdministrationShells` | array | yes | — | | `submodels` | array | yes | — | | `conceptDescriptions` | array | yes | — | ## JSON Schema ```json { "type": "object", "description": "IDTA Asset Administration Shell environment (returned when `Accept` contains `application/aas+json`), role-filtered for the caller's access tier. Identifiers use `urn:opendpp:*` forms: the shell id is `urn:opendpp:aas:` with idShort `AAS_` and `globalAssetId` `urn:opendpp:asset::` (plus `specificAssetIds` carrying the productId and, when a facility is assigned, its GLN). The environment always contains two submodels — `GeneralProductInformation` (`urn:opendpp:submodel:general:`) and `ComplianceMetadata` (`urn:opendpp:submodel:compliance`) — and an `eidasVerificationSeal` submodel is appended when the tenant has signing keys configured. Loose schema — the full AAS document structure is documented with the public resolution endpoints.", "required": [ "assetAdministrationShells", "submodels", "conceptDescriptions" ], "properties": { "assetAdministrationShells": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "submodels": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "conceptDescriptions": { "type": "array", "items": { "type": "object", "additionalProperties": true } } }, "additionalProperties": true } ``` ## Used by - [getPassport](/operations/getPassport.md) (`GET /api/v1/passports/{id}`) ================================================================================ # FILE: /okf/schemas/PassportBulkFailure.md ================================================================================ --- type: Schema title: PassportBulkFailure description: 400 body of POST /api/v1/passports/bulk when EVERY row failed. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportBulkFailure tags: - schema timestamp: 2026-06-17T00:00:00Z --- 400 body of `POST /api/v1/passports/bulk` when EVERY row failed. Note: `errors` is an array of STRINGS (not objects) and there is NO `message` field. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `error` | string | yes | — | | `errors` | array | yes | One human-readable string per failed row, prefixed [SKU: ] where the productId was readable. | ## JSON Schema ```json { "type": "object", "required": [ "success", "error", "errors" ], "description": "400 body of `POST /api/v1/passports/bulk` when EVERY row failed. Note: `errors` is an array of STRINGS (not objects) and there is NO `message` field.", "properties": { "success": { "type": "boolean", "const": false }, "error": { "type": "string", "const": "Bulk Ingestion Failed" }, "errors": { "type": "array", "items": { "type": "string" }, "description": "One human-readable string per failed row, prefixed `[SKU: ]` where the productId was readable." } } } ``` ## Used by - [bulkIngestPassports](/operations/bulkIngestPassports.md) (`POST /api/v1/passports/bulk`) ================================================================================ # FILE: /okf/schemas/PassportBulkRequest.md ================================================================================ --- type: Schema title: PassportBulkRequest description: PassportBulkRequest resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportBulkRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `passports` | array<[PassportBulkRow](/schemas/PassportBulkRow.md)> | yes | 1–200 rows. | ## JSON Schema ```json { "type": "object", "required": [ "passports" ], "properties": { "passports": { "type": "array", "minItems": 1, "maxItems": 200, "items": { "$ref": "#/components/schemas/PassportBulkRow" }, "description": "1–200 rows. The bounds are enforced before any row is processed; violations return the default `{statusCode, code, error, message}` error body." } } } ``` ## Used by - [bulkIngestPassports](/operations/bulkIngestPassports.md) (`POST /api/v1/passports/bulk`) ================================================================================ # FILE: /okf/schemas/PassportBulkResult.md ================================================================================ --- type: Schema title: PassportBulkResult description: 201 partial-success envelope of POST /api/v1/passports/bulk. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportBulkResult tags: - schema timestamp: 2026-06-17T00:00:00Z --- 201 partial-success envelope of `POST /api/v1/passports/bulk`. Returned whenever at least one row was inserted, even if other rows failed. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | Template: "Bulk CSV ingestion finished. | | `insertedCount` | integer | yes | Number of rows actually inserted (= results.length). | | `results` | array | yes | — | | `errors` | array | no | Human-readable per-row failure strings, prefixed [SKU: ] (or "Missing or invalid productId in spreadsheet row"). | ## JSON Schema ```json { "type": "object", "required": [ "success", "message", "insertedCount", "results" ], "description": "201 partial-success envelope of `POST /api/v1/passports/bulk`. Returned whenever at least one row was inserted, even if other rows failed.", "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "description": "Template: \"Bulk CSV ingestion finished. Registered passports, skipped rows with errors.\"" }, "insertedCount": { "type": "integer", "minimum": 0, "description": "Number of rows actually inserted (= `results.length`)." }, "results": { "type": "array", "items": { "type": "object", "required": [ "productId", "digitalLinkUri" ], "properties": { "productId": { "type": "string" }, "digitalLinkUri": { "type": "string", "format": "uri", "description": "Generated GS1 Digital Link URI `https://opendpp-node.eu/{01|8003}/{productId}/21/{passportId}`." } } } }, "errors": { "type": "array", "items": { "type": "string" }, "description": "Human-readable per-row failure strings, prefixed `[SKU: ]` (or \"Missing or invalid productId in spreadsheet row\"). Present ONLY when at least one row failed — omitted otherwise." } } } ``` ## Used by - [bulkIngestPassports](/operations/bulkIngestPassports.md) (`POST /api/v1/passports/bulk`) ================================================================================ # FILE: /okf/schemas/PassportBulkRow.md ================================================================================ --- type: Schema title: PassportBulkRow description: One bulk-ingestion row. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportBulkRow tags: - schema timestamp: 2026-06-17T00:00:00Z --- One bulk-ingestion row. The HTTP layer only requires each row to be an object; rows missing `productId` or `metadata`, failing ESPR validation, referencing unbound operators/unknown facilities, or duplicating an existing `(productId, operatorId)` pair are SKIPPED and reported as strings in the response `errors[]` — they never fail the whole request (unless every row fails). Bulk rows do not support `draft` or `enrichment`, are always created with `status: "ACTIVE"`, skip the EPCIS traceability audit, and do NOT get `metadata.gtin`/`metadata.grai` auto-injected. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `productId` | string | no | GTIN-14 / GRAI / free-form SKU (required in practice; rows without it are skipped with an error string). | | `operatorId` | string | no | Optional EconomicOperator UUID bound to your workspace; defaults to the workspace's first bound operator. | | `facilityId` | string | no | Optional Facility UUID in your workspace; unknown ids skip the row. | | `metadata` | [PassportMetadataInput](/schemas/PassportMetadataInput.md) | no | — | ## JSON Schema ```json { "type": "object", "description": "One bulk-ingestion row. The HTTP layer only requires each row to be an object; rows missing `productId` or `metadata`, failing ESPR validation, referencing unbound operators/unknown facilities, or duplicating an existing `(productId, operatorId)` pair are SKIPPED and reported as strings in the response `errors[]` — they never fail the whole request (unless every row fails). Bulk rows do not support `draft` or `enrichment`, are always created with `status: \"ACTIVE\"`, skip the EPCIS traceability audit, and do NOT get `metadata.gtin`/`metadata.grai` auto-injected.", "properties": { "productId": { "type": "string", "description": "GTIN-14 / GRAI / free-form SKU (required in practice; rows without it are skipped with an error string)." }, "operatorId": { "type": "string", "description": "Optional EconomicOperator UUID bound to your workspace; defaults to the workspace's first bound operator. Operator-scoped API keys force their operator." }, "facilityId": { "type": "string", "description": "Optional Facility UUID in your workspace; unknown ids skip the row." }, "metadata": { "$ref": "#/components/schemas/PassportMetadataInput" } }, "additionalProperties": true } ``` ## Used by - schema [PassportBulkRequest](/schemas/PassportBulkRequest.md) ================================================================================ # FILE: /okf/schemas/PassportCreateRequest.md ================================================================================ --- type: Schema title: PassportCreateRequest description: PassportCreateRequest resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportCreateRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `productId` | string | yes | Product identifier: a GTIN-14 (exactly 14 digits with a valid GS1 mod-10 check digit — auto-copied to metadata.gtin), a GRAI (14-digit numeric asset id with va… | | `operatorId` | string | no | UUID of an EconomicOperator bound to your tenant workspace (403 if not bound). | | `facilityId` | string | no | Optional UUID of a Facility (GLN-backed Unique Facility Identifier) in your workspace; 400 if not found. | | `metadata` | [PassportMetadataInput](/schemas/PassportMetadataInput.md) | yes | — | | `draft` | boolean | no | When true: skips ALL ESPR/traceability validation, stores the passport with status: "DRAFT" (not publicly resolvable), and emits no webhook. | | `enrichment` | [PassportEnrichmentInput](/schemas/PassportEnrichmentInput.md) | no | — | ## JSON Schema ```json { "type": "object", "required": [ "productId", "metadata" ], "properties": { "productId": { "type": "string", "minLength": 1, "description": "Product identifier: a GTIN-14 (exactly 14 digits with a valid GS1 mod-10 check digit — auto-copied to `metadata.gtin`), a GRAI (14-digit numeric asset id with valid check digit + optional up to 16 alphanumeric serial chars, total 14–30 — auto-copied to `metadata.grai`), or a free-form SKU. Determines the GS1 Application Identifier (`01` vs `8003`) in the generated Digital Link URI. Whitespace-only values are rejected 400. Unique per economic operator (409 on duplicate)." }, "operatorId": { "type": "string", "description": "UUID of an EconomicOperator bound to your tenant workspace (403 if not bound). Defaults to your workspace's first bound operator. Operator-scoped API keys force their own operator (403 on mismatch)." }, "facilityId": { "type": "string", "description": "Optional UUID of a Facility (GLN-backed Unique Facility Identifier) in your workspace; 400 if not found." }, "metadata": { "$ref": "#/components/schemas/PassportMetadataInput" }, "draft": { "type": "boolean", "default": false, "description": "When true: skips ALL ESPR/traceability validation, stores the passport with `status: \"DRAFT\"` (not publicly resolvable), and emits no webhook. Publish later via a validated edit." }, "enrichment": { "$ref": "#/components/schemas/PassportEnrichmentInput" } } } ``` ## Used by - [createPassport](/operations/createPassport.md) (`POST /api/v1/passports`) ================================================================================ # FILE: /okf/schemas/PassportEnrichmentInput.md ================================================================================ --- type: Schema title: PassportEnrichmentInput description: Optional presentational (non-regulatory) marketing enrichment, stored OUTSIDE the ESPR-validated metadata and the Merkle seal; it never appears in the JSON-LD passport document. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportEnrichmentInput tags: - schema timestamp: 2026-06-17T00:00:00Z --- Optional presentational (non-regulatory) marketing enrichment, stored OUTSIDE the ESPR-validated metadata and the Merkle seal; it never appears in the JSON-LD passport document. Server-side it is whitelist-sanitized rather than rejected: unknown keys are dropped; `tagline` is trimmed and capped at 200 chars, `description` at 4000, image `caption` at 200, link `label` at 120; at most 24 `images` and 24 `links` are kept; URLs must be http, https, or mailto (any other scheme, e.g. `javascript:` or `data:`, is silently dropped). An enrichment that sanitizes down to nothing is stored as null. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `tagline` | string | no | Short marketing tagline (server-capped at 200 chars). | | `description` | string | no | Marketing description (server-capped at 4000 chars). | | `images` | array | no | Up to 24 kept. | | `links` | array | no | Up to 24 kept. | ## JSON Schema ```json { "type": "object", "description": "Optional presentational (non-regulatory) marketing enrichment, stored OUTSIDE the ESPR-validated metadata and the Merkle seal; it never appears in the JSON-LD passport document. Server-side it is whitelist-sanitized rather than rejected: unknown keys are dropped; `tagline` is trimmed and capped at 200 chars, `description` at 4000, image `caption` at 200, link `label` at 120; at most 24 `images` and 24 `links` are kept; URLs must be http, https, or mailto (any other scheme, e.g. `javascript:` or `data:`, is silently dropped). An enrichment that sanitizes down to nothing is stored as null.", "properties": { "tagline": { "type": "string", "description": "Short marketing tagline (server-capped at 200 chars)." }, "description": { "type": "string", "description": "Marketing description (server-capped at 4000 chars)." }, "images": { "type": "array", "description": "Up to 24 kept. Items without a valid http/https/mailto `url` are dropped.", "items": { "type": "object", "properties": { "url": { "type": "string", "format": "uri", "description": "http, https, or mailto only." }, "caption": { "type": "string", "description": "Server-capped at 200 chars." } } } }, "links": { "type": "array", "description": "Up to 24 kept. Items without a valid http/https/mailto `url` are dropped; a missing `label` defaults to the URL.", "items": { "type": "object", "properties": { "label": { "type": "string", "description": "Server-capped at 120 chars." }, "url": { "type": "string", "format": "uri", "description": "http, https, or mailto only." } } } } }, "additionalProperties": true } ``` ## Used by - schema [PassportCreateRequest](/schemas/PassportCreateRequest.md) - schema [PassportUpdateRequest](/schemas/PassportUpdateRequest.md) ================================================================================ # FILE: /okf/schemas/PassportIngestCreated.md ================================================================================ --- type: Schema title: PassportIngestCreated description: 201 envelope of POST /api/v1/passports. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportIngestCreated tags: - schema timestamp: 2026-06-17T00:00:00Z --- 201 envelope of `POST /api/v1/passports`. Only these four top-level keys are ever emitted. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | "Digital Product Passport successfully validated and ingested", or "Draft passport saved" when draft: true. | | `passport` | [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) | yes | The PUBLIC redacted JSON-LD passport document (unsealed at creation: digitalSeal/proof are null). | | `warnings` | array<[ValidationErrorItem](/schemas/ValidationErrorItem.md)> | yes | Non-blocking validation findings. | ## JSON Schema ```json { "type": "object", "required": [ "success", "message", "passport", "warnings" ], "description": "201 envelope of `POST /api/v1/passports`. Only these four top-level keys are ever emitted.", "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "description": "\"Digital Product Passport successfully validated and ingested\", or \"Draft passport saved\" when `draft: true`." }, "passport": { "$ref": "#/components/schemas/PublicPassportJsonLd", "description": "The PUBLIC redacted JSON-LD passport document (unsealed at creation: `digitalSeal`/`proof` are null). The owner-only metadata key `facilityDetails` is always replaced with the literal string \"[REDACTED - Privileged Access Required]\" — even in this creator-facing echo, and even when the submitted metadata did not contain it. For `category: \"batteries\"`, the restricted legitimate-interest keys `detailedPerformance`, `lifecycleAndInUse`, and `circularityAndDisassembly` are masked the same way when present." }, "warnings": { "type": "array", "items": { "$ref": "#/components/schemas/ValidationErrorItem" }, "description": "Non-blocking validation findings. Always present; empty array when none and always empty for drafts." } } } ``` ## Used by - [createPassport](/operations/createPassport.md) (`POST /api/v1/passports`) ================================================================================ # FILE: /okf/schemas/PassportListItem.md ================================================================================ --- type: Schema title: PassportListItem description: One JSON-LD passport document as it appears in GET /api/v1/passports list responses. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportListItem tags: - schema timestamp: 2026-06-17T00:00:00Z --- One JSON-LD passport document as it appears in `GET /api/v1/passports` list responses. Same shape as `PublicPassportJsonLd` with list-specific divergences imposed by the route's declared response serialization: `economicOperator` never carries `role`; `manufacturingFacility` is always `null`; the `@context` term map (second array element) is emptied to `{}`; and `proof` is emptied to `{}` on sealed items (`null` on unsealed) — fetch the single passport for the verifiable proof block. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@context` | array<—> | yes | Exactly two entries: the context URL https://opendpp-node.eu/contexts/dpp/v1 and an inline term map covering the 9 fixed DPP terms (DigitalProductPassport, eco… | | `@type` | string | yes | — | | `@id` | string | yes | The passport's canonical GS1 Digital Link URI (same value as digitalLinkUri). | | `id` | string | yes | Server-assigned passport UUID. | | `productId` | string | yes | Caller-supplied product identifier: a GTIN-14 (^[0-9]{14}$ with valid GS1 modulo-10 check digit), a GRAI (^[0-9]{14}[A-Za-z0-9]{0,16}$), or a free-form SKU. | | `digitalLinkUri` | string | yes | SKU/type-level GS1 Digital Link URI: {BASE_URL}/{01|8003}/{productId}/21/{passportId} (AI-21 carries the passport UUID at SKU level; individual units carry the… | | `digitalSeal` | string,null | yes | eIDAS ADVANCED electronic seal: base64 ECDSA prime256v1 (P-256) signature over the Merkle root of the key-sorted metadata. | | `signingPublicKey` | string,null | yes | PEM public key that verifies digitalSeal. | | `status` | string | yes | Passport lifecycle status (serialized as ACTIVE when unset). | | `archivedAt` | string,null | yes | Soft-delete marker (owner off-boarded / decommissioned). | | `retentionUntil` | string,null | yes | Minimum-availability deadline; the passport is never purged before this instant. | | `proof` | — | yes | {} when sealed, null when unsealed. | | `createdAt` | string | yes | — | | `updatedAt` | string | yes | — | | `economicOperator` | — | yes | The economic operator (manufacturer/importer/retailer) responsible for the product. | | `manufacturingFacility` | null | yes | Always null in list responses (facility nodes are only embedded on single-passport reads). | | `metadata` | object | yes | The ESPR category metadata, tier-masked: keys above the caller's tier hold the literal string [REDACTED - Privileged Access Required] instead of their value. | ## JSON Schema ```json { "type": "object", "description": "One JSON-LD passport document as it appears in `GET /api/v1/passports` list responses. Same shape as `PublicPassportJsonLd` with list-specific divergences imposed by the route's declared response serialization: `economicOperator` never carries `role`; `manufacturingFacility` is always `null`; the `@context` term map (second array element) is emptied to `{}`; and `proof` is emptied to `{}` on sealed items (`null` on unsealed) — fetch the single passport for the verifiable proof block.", "additionalProperties": true, "required": [ "@context", "@type", "@id", "id", "productId", "digitalLinkUri", "digitalSeal", "signingPublicKey", "status", "archivedAt", "retentionUntil", "proof", "createdAt", "updatedAt", "economicOperator", "manufacturingFacility", "metadata" ], "properties": { "@context": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "anyOf": [ { "type": "string" }, { "type": "object", "additionalProperties": { "type": "string" } } ] }, "description": "Exactly two entries: the context URL `https://opendpp-node.eu/contexts/dpp/v1` and an inline term map covering the 9 fixed DPP terms (`DigitalProductPassport`, `economicOperator`, `manufacturingFacility`, `metadata`, `digitalSeal`, `signingPublicKey`, `status`, `archivedAt`, `retentionUntil`) plus one generated term per metadata key (`https://opendpp-node.eu/contexts/dpp/v1#`)." }, "@type": { "type": "string", "const": "DigitalProductPassport" }, "@id": { "type": "string", "format": "uri", "description": "The passport's canonical GS1 Digital Link URI (same value as `digitalLinkUri`)." }, "id": { "type": "string", "description": "Server-assigned passport UUID." }, "productId": { "type": "string", "description": "Caller-supplied product identifier: a GTIN-14 (`^[0-9]{14}$` with valid GS1 modulo-10 check digit), a GRAI (`^[0-9]{14}[A-Za-z0-9]{0,16}$`), or a free-form SKU." }, "digitalLinkUri": { "type": "string", "format": "uri", "description": "SKU/type-level GS1 Digital Link URI: `{BASE_URL}/{01|8003}/{productId}/21/{passportId}` (AI-21 carries the passport UUID at SKU level; individual units carry their physical serial instead)." }, "digitalSeal": { "type": [ "string", "null" ], "description": "eIDAS ADVANCED electronic seal: base64 ECDSA prime256v1 (P-256) signature over the Merkle root of the key-sorted metadata. `null` when the passport has not been sealed." }, "signingPublicKey": { "type": [ "string", "null" ], "description": "PEM public key that verifies `digitalSeal`. `null` when unsealed." }, "status": { "type": "string", "enum": [ "DRAFT", "ACTIVE", "RECALLED", "DECOMMISSIONED" ], "description": "Passport lifecycle status (serialized as `ACTIVE` when unset). `DRAFT` is only ever visible to owner-tier callers — public/grant resolution of a draft returns 404." }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time", "description": "Soft-delete marker (owner off-boarded / decommissioned). Archived passports remain publicly resolvable (ESPR persistence duty)." }, "retentionUntil": { "type": [ "string", "null" ], "format": "date-time", "description": "Minimum-availability deadline; the passport is never purged before this instant." }, "proof": { "anyOf": [ { "type": "object", "additionalProperties": false, "description": "Always the empty object on sealed items — proof contents are stripped by the list serialization." }, { "type": "null" } ], "description": "`{}` when sealed, `null` when unsealed. The full `MerkleTreeAttestationProof` is only available on single-passport reads." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "economicOperator": { "anyOf": [ { "$ref": "#/components/schemas/EconomicOperatorNode" }, { "type": "null" } ], "description": "The economic operator (manufacturer/importer/retailer) responsible for the product. Public in all tiers." }, "manufacturingFacility": { "type": "null", "description": "Always `null` in list responses (facility nodes are only embedded on single-passport reads)." }, "metadata": { "type": "object", "additionalProperties": true, "description": "The ESPR category metadata, tier-masked: keys above the caller's tier hold the literal string `[REDACTED - Privileged Access Required]` instead of their value." } } } ``` ## Used by - schema [PassportListResponse](/schemas/PassportListResponse.md) ================================================================================ # FILE: /okf/schemas/PassportListResponse.md ================================================================================ --- type: Schema title: PassportListResponse description: Envelope of GET /api/v1/passports. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportListResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- Envelope of GET /api/v1/passports. The response is serialized against a declared response schema: top-level keys other than these four are stripped. There is NO total count. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | Always true on 200. | | `page` | integer | yes | Effective page after server-side clamping (min 1). | | `limit` | integer | yes | Effective page size after server-side clamping (default 10, max 100). | | `passports` | array<[PassportListItem](/schemas/PassportListItem.md)> | yes | — | ## JSON Schema ```json { "type": "object", "description": "Envelope of GET /api/v1/passports. The response is serialized against a declared response schema: top-level keys other than these four are stripped. There is NO total count.", "required": [ "success", "page", "limit", "passports" ], "properties": { "success": { "type": "boolean", "description": "Always true on 200." }, "page": { "type": "integer", "minimum": 1, "description": "Effective page after server-side clamping (min 1)." }, "limit": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Effective page size after server-side clamping (default 10, max 100)." }, "passports": { "type": "array", "items": { "$ref": "#/components/schemas/PassportListItem" } } } } ``` ## Used by - [listPassports](/operations/listPassports.md) (`GET /api/v1/passports`) ================================================================================ # FILE: /okf/schemas/PassportMetadataInput.md ================================================================================ --- type: Schema title: PassportMetadataInput description: The ESPR product metadata payload. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportMetadataInput tags: - schema timestamp: 2026-06-17T00:00:00Z --- The ESPR product metadata payload. For non-draft ingestion and for the validate-only endpoints, `category` is mandatory and must be one of the 9 ESPR categories; each category then mandates its own field set (e.g. textiles require `fiberComposition`, `careInstructions`, `size`; batteries require `batteryCategory`, `chemistry`, `electrochemicalCapacity`, `durability`, `recycledContentShare`, `carbonFootprint`). For five categories — textiles, batteries, electronics, chemicals, construction — the authoritative per-category JSON Schema (required fields, value constraints, field help) is served live at `GET /api/v1/schemas/{category}`; the other four (cosmetics, toys, iron-steel, aluminium) are validated by built-in server-side rules and `GET /api/v1/schemas/{category}` returns 404 for them. Cross-field rules are enforced on top: `materialComposition` (and textile `fiberComposition`) percentages must sum to 100 ±0.1, `originCountry` must be a real ISO 3166-1 alpha-2 code, textile hazardous-substance concentrations are checked against REACH ppm limits. A documented set of supplementary objects (e.g. `technicalProperties`, `environmentalFootprint`, `circularityAttributes`, `esgDueDiligence`, `detailedPerformance`) produce non-blocking `warnings` instead of `errors` when malformed. With `draft: true` (single ingestion only) validation is skipped entirely and any object is accepted. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `category` | string | no | ESPR product category; selects the validation rules. | | `originCountry` | string | no | ISO 3166-1 alpha-2 country code (validated against the full 249-code set). | ## JSON Schema ```json { "type": "object", "description": "The ESPR product metadata payload. For non-draft ingestion and for the validate-only endpoints, `category` is mandatory and must be one of the 9 ESPR categories; each category then mandates its own field set (e.g. textiles require `fiberComposition`, `careInstructions`, `size`; batteries require `batteryCategory`, `chemistry`, `electrochemicalCapacity`, `durability`, `recycledContentShare`, `carbonFootprint`). For five categories — textiles, batteries, electronics, chemicals, construction — the authoritative per-category JSON Schema (required fields, value constraints, field help) is served live at `GET /api/v1/schemas/{category}`; the other four (cosmetics, toys, iron-steel, aluminium) are validated by built-in server-side rules and `GET /api/v1/schemas/{category}` returns 404 for them. Cross-field rules are enforced on top: `materialComposition` (and textile `fiberComposition`) percentages must sum to 100 ±0.1, `originCountry` must be a real ISO 3166-1 alpha-2 code, textile hazardous-substance concentrations are checked against REACH ppm limits. A documented set of supplementary objects (e.g. `technicalProperties`, `environmentalFootprint`, `circularityAttributes`, `esgDueDiligence`, `detailedPerformance`) produce non-blocking `warnings` instead of `errors` when malformed. With `draft: true` (single ingestion only) validation is skipped entirely and any object is accepted.", "properties": { "category": { "type": "string", "enum": [ "textiles", "batteries", "electronics", "cosmetics", "toys", "iron-steel", "aluminium", "chemicals", "construction" ], "description": "ESPR product category; selects the validation rules. Required whenever validation runs (i.e. always, except `draft: true` single ingestion)." }, "originCountry": { "type": "string", "pattern": "^[A-Z]{2}$", "description": "ISO 3166-1 alpha-2 country code (validated against the full 249-code set)." } }, "additionalProperties": true } ``` ## Used by - schema [PassportCreateRequest](/schemas/PassportCreateRequest.md) - schema [PassportValidateOnlyRequest](/schemas/PassportValidateOnlyRequest.md) - schema [PassportBulkRow](/schemas/PassportBulkRow.md) ================================================================================ # FILE: /okf/schemas/PassportSealResponse.md ================================================================================ --- type: Schema title: PassportSealResponse description: 200 envelope of POST /api/v1/passports/{id}/seal. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportSealResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- 200 envelope of POST /api/v1/passports/{id}/seal. `digitalSeal` is duplicated inside `passport.digitalSeal` and `passport.proof.signatureValue`. The passport document is serialized at the PUBLIC redaction tier; masked keys keep their true leaf hashes in `proof.redactedLeaves`. Note: despite the message wording, this endpoint does not change the passport's `status`. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | Always true on 200. | | `message` | string | yes | — | | `digitalSeal` | string | yes | Base64 ECDSA P-256 (prime256v1) SHA-256 signature over the metadata Merkle root (eIDAS ADVANCED electronic seal — not a qualified seal, not a W3C DataIntegrity… | | `signingPublicKey` | string | yes | PEM-encoded public key of the tenant's signing key pair; verify the seal offline against proof.merkleRoot. | | `passport` | [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) | yes | — | ## JSON Schema ```json { "type": "object", "description": "200 envelope of POST /api/v1/passports/{id}/seal. `digitalSeal` is duplicated inside `passport.digitalSeal` and `passport.proof.signatureValue`. The passport document is serialized at the PUBLIC redaction tier; masked keys keep their true leaf hashes in `proof.redactedLeaves`. Note: despite the message wording, this endpoint does not change the passport's `status`.", "required": [ "success", "message", "digitalSeal", "signingPublicKey", "passport" ], "properties": { "success": { "type": "boolean", "description": "Always true on 200." }, "message": { "type": "string", "const": "Passport sealed with the tenant's eIDAS advanced electronic seal and published." }, "digitalSeal": { "type": "string", "description": "Base64 ECDSA P-256 (prime256v1) SHA-256 signature over the metadata Merkle root (eIDAS ADVANCED electronic seal — not a qualified seal, not a W3C DataIntegrityProof)." }, "signingPublicKey": { "type": "string", "description": "PEM-encoded public key of the tenant's signing key pair; verify the seal offline against `proof.merkleRoot`." }, "passport": { "$ref": "#/components/schemas/PublicPassportJsonLd" } } } ``` ## Used by - [sealPassport](/operations/sealPassport.md) (`POST /api/v1/passports/{id}/seal`) ================================================================================ # FILE: /okf/schemas/PassportStatusUpdateRequest.md ================================================================================ --- type: Schema title: PassportStatusUpdateRequest description: Body of PUT /api/v1/passports/{id}/status. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportStatusUpdateRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- Body of PUT /api/v1/passports/{id}/status. Only `status` is read; any other keys are ignored (there is no `reason` field). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `status` | string | yes | Target lifecycle state. | ## JSON Schema ```json { "type": "object", "description": "Body of PUT /api/v1/passports/{id}/status. Only `status` is read; any other keys are ignored (there is no `reason` field).", "required": [ "status" ], "additionalProperties": true, "properties": { "status": { "type": "string", "enum": [ "ACTIVE", "RECALLED", "DECOMMISSIONED" ], "description": "Target lifecycle state. DECOMMISSIONED starts the retention clock (retentionUntil = now + the configured retention period, default 15 years); ACTIVE reactivates (clears retentionUntil and archivedAt); RECALLED marks the product recalled. DRAFT is not a valid target — drafts are published via PUT /api/v1/passports/{id}." } } } ``` ## Used by - [updatePassportStatus](/operations/updatePassportStatus.md) (`PUT /api/v1/passports/{id}/status`) ================================================================================ # FILE: /okf/schemas/PassportStatusUpdateResponse.md ================================================================================ --- type: Schema title: PassportStatusUpdateResponse description: 200 envelope of PUT /api/v1/passports/{id}/status. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportStatusUpdateResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- 200 envelope of PUT /api/v1/passports/{id}/status. The passport document is serialized at the PUBLIC redaction tier. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | Always true on 200. | | `message` | string | yes | Passport status successfully updated to . | | `status` | string | yes | The passport's new lifecycle state. | | `passport` | [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) | yes | — | ## JSON Schema ```json { "type": "object", "description": "200 envelope of PUT /api/v1/passports/{id}/status. The passport document is serialized at the PUBLIC redaction tier.", "required": [ "success", "message", "status", "passport" ], "properties": { "success": { "type": "boolean", "description": "Always true on 200." }, "message": { "type": "string", "description": "`Passport status successfully updated to `." }, "status": { "type": "string", "enum": [ "ACTIVE", "RECALLED", "DECOMMISSIONED" ], "description": "The passport's new lifecycle state." }, "passport": { "$ref": "#/components/schemas/PublicPassportJsonLd" } } } ``` ## Used by - [updatePassportStatus](/operations/updatePassportStatus.md) (`PUT /api/v1/passports/{id}/status`) ================================================================================ # FILE: /okf/schemas/PassportUpdateRequest.md ================================================================================ --- type: Schema title: PassportUpdateRequest description: Body of PUT /api/v1/passports/{id}. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportUpdateRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- Body of PUT /api/v1/passports/{id}. Unknown keys are ignored. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `metadata` | object | yes | Full replacement ESPR metadata object. | | `draft` | boolean | no | true = save as draft: skips ESPR validation and forces status DRAFT (this also demotes an already-published passport back to DRAFT). | | `changeReason` | string | no | Free-text reason recorded on the version-history snapshot. | | `facilityId` | string,null | no | GLN-backed facility assignment. | | `enrichment` | — | no | Presentational marketing block stored OUTSIDE the ESPR-validated metadata and the Merkle seal. | ## JSON Schema ```json { "type": "object", "description": "Body of PUT /api/v1/passports/{id}. Unknown keys are ignored.", "required": [ "metadata" ], "additionalProperties": true, "properties": { "metadata": { "type": "object", "description": "Full replacement ESPR metadata object. `metadata.category` must be one of: textiles, batteries, electronics, cosmetics, toys, iron-steel, aluminium, chemicals, construction. Validated against the category's compliance rules unless `draft: true` (400 with `errors[]` on failure — no `warnings` on this route). A machine-readable JSON Schema is published at GET /api/v1/schemas/{category} for **textiles, batteries, electronics, chemicals and construction only** — cosmetics, toys, iron-steel and aluminium are validated by built-in rules and return 404 from that endpoint." }, "draft": { "type": "boolean", "default": false, "description": "true = save as draft: skips ESPR validation and forces status DRAFT (this also demotes an already-published passport back to DRAFT). false/absent = validated save; a DRAFT passport is published (status ACTIVE, emits the passport.ingested webhook)." }, "changeReason": { "type": "string", "description": "Free-text reason recorded on the version-history snapshot. Defaults to \"API Update\"." }, "facilityId": { "type": [ "string", "null" ], "description": "GLN-backed facility assignment. Omit to leave unchanged; null or \"\" detaches; a UUID attaches a facility that must be owned by your tenant (otherwise 400)." }, "enrichment": { "oneOf": [ { "$ref": "#/components/schemas/PassportEnrichmentInput" }, { "type": "null" } ], "description": "Presentational marketing block stored OUTSIDE the ESPR-validated metadata and the Merkle seal. Include the key (even null/{}) to overwrite; omit to leave unchanged. An empty result after sanitation clears the block." } } } ``` ## Used by - [updatePassport](/operations/updatePassport.md) (`PUT /api/v1/passports/{id}`) ================================================================================ # FILE: /okf/schemas/PassportUpdateResponse.md ================================================================================ --- type: Schema title: PassportUpdateResponse description: 200 envelope of PUT /api/v1/passports/{id}. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportUpdateResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- 200 envelope of PUT /api/v1/passports/{id}. The passport document is serialized at the PUBLIC redaction tier (owner-only/restricted metadata keys masked) even for the owner. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | Always true on 200. | | `message` | string | yes | "Draft published" when a validated save promoted a DRAFT to ACTIVE; the longer message otherwise. | | `passport` | [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) | yes | — | ## JSON Schema ```json { "type": "object", "description": "200 envelope of PUT /api/v1/passports/{id}. The passport document is serialized at the PUBLIC redaction tier (owner-only/restricted metadata keys masked) even for the owner.", "required": [ "success", "message", "passport" ], "properties": { "success": { "type": "boolean", "description": "Always true on 200." }, "message": { "type": "string", "enum": [ "Draft published", "Digital Product Passport successfully updated and history versioned" ], "description": "\"Draft published\" when a validated save promoted a DRAFT to ACTIVE; the longer message otherwise." }, "passport": { "$ref": "#/components/schemas/PublicPassportJsonLd" } } } ``` ## Used by - [updatePassport](/operations/updatePassport.md) (`PUT /api/v1/passports/{id}`) ================================================================================ # FILE: /okf/schemas/PassportUpdateValidationError.md ================================================================================ --- type: Schema title: PassportUpdateValidationError description: 400 ESPR validation failure body of PUT /api/v1/passports/{id}. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportUpdateValidationError tags: - schema timestamp: 2026-06-17T00:00:00Z --- 400 ESPR validation failure body of PUT /api/v1/passports/{id}. DIVERGENCE from POST /api/v1/passports: there is never a `warnings` array on this route. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `error` | string | yes | — | | `message` | string | yes | — | | `errors` | array<[ValidationErrorItem](/schemas/ValidationErrorItem.md)> | yes | Blocking validation errors. | ## JSON Schema ```json { "type": "object", "description": "400 ESPR validation failure body of PUT /api/v1/passports/{id}. DIVERGENCE from POST /api/v1/passports: there is never a `warnings` array on this route.", "required": [ "success", "error", "message", "errors" ], "properties": { "success": { "type": "boolean", "const": false }, "error": { "type": "string", "const": "Validation Failed" }, "message": { "type": "string", "const": "Dynamic metadata payload failed ESPR category compliance validation" }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/ValidationErrorItem" }, "description": "Blocking validation errors. `friendlyMessage` is localized via the `lang` query parameter / Accept-Language (default en)." } } } ``` ## Used by - [updatePassport](/operations/updatePassport.md) (`PUT /api/v1/passports/{id}`) ================================================================================ # FILE: /okf/schemas/PassportValidateOnlyError.md ================================================================================ --- type: Schema title: PassportValidateOnlyError description: 400 envelope of the validate-only endpoints. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportValidateOnlyError tags: - schema timestamp: 2026-06-17T00:00:00Z --- 400 envelope of the validate-only endpoints. Three variants: ESPR validation failure (`error: "Validation Failed"`, with `success`, `category`, `errors[]`, optional `warnings[]` — omitted when none; category-validity error items carry no `friendlyMessage`); whitespace-only `productId` (`error: "Bad Request"`, `category: "unknown"`, `errors: []`, no `warnings`); and structural request rejections (body-schema violations, malformed JSON), which return only `error` + `message`. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | no | — | | `error` | string | yes | — | | `message` | string | yes | — | | `category` | string | no | metadata.category echo, or "unknown" for structural failures. | | `errors` | array<[ValidationErrorItem](/schemas/ValidationErrorItem.md)> | no | — | | `warnings` | array<[ValidationErrorItem](/schemas/ValidationErrorItem.md)> | no | Omitted when there are no warnings. | ## JSON Schema ```json { "type": "object", "required": [ "error", "message" ], "description": "400 envelope of the validate-only endpoints. Three variants: ESPR validation failure (`error: \"Validation Failed\"`, with `success`, `category`, `errors[]`, optional `warnings[]` — omitted when none; category-validity error items carry no `friendlyMessage`); whitespace-only `productId` (`error: \"Bad Request\"`, `category: \"unknown\"`, `errors: []`, no `warnings`); and structural request rejections (body-schema violations, malformed JSON), which return only `error` + `message`.", "properties": { "success": { "type": "boolean", "const": false }, "error": { "type": "string", "enum": [ "Bad Request", "Validation Failed" ] }, "message": { "type": "string" }, "category": { "type": "string", "description": "`metadata.category` echo, or \"unknown\" for structural failures." }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/ValidationErrorItem" } }, "warnings": { "type": "array", "items": { "$ref": "#/components/schemas/ValidationErrorItem" }, "description": "Omitted when there are no warnings." } } } ``` ## Used by - [validatePassport](/operations/validatePassport.md) (`POST /api/v1/passports/validate-only`) - [validatePassportPublic](/operations/validatePassportPublic.md) (`POST /api/v1/passports/validate-only-public`) ================================================================================ # FILE: /okf/schemas/PassportValidateOnlyRequest.md ================================================================================ --- type: Schema title: PassportValidateOnlyRequest description: PassportValidateOnlyRequest resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportValidateOnlyRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `productId` | string | yes | Product identifier (GTIN-14 / GRAI / SKU). | | `operatorId` | string | no | Accepted by the body schema but IGNORED by the validate-only handlers. | | `metadata` | [PassportMetadataInput](/schemas/PassportMetadataInput.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "productId", "metadata" ], "properties": { "productId": { "type": "string", "minLength": 1, "description": "Product identifier (GTIN-14 / GRAI / SKU). Required and checked non-empty (whitespace-only → 400), but not otherwise used by the dry-run." }, "operatorId": { "type": "string", "description": "Accepted by the body schema but IGNORED by the validate-only handlers." }, "metadata": { "$ref": "#/components/schemas/PassportMetadataInput" } } } ``` ## Used by - [validatePassport](/operations/validatePassport.md) (`POST /api/v1/passports/validate-only`) - [validatePassportPublic](/operations/validatePassportPublic.md) (`POST /api/v1/passports/validate-only-public`) ================================================================================ # FILE: /okf/schemas/PassportValidateOnlyResult.md ================================================================================ --- type: Schema title: PassportValidateOnlyResult description: 200 envelope of the validate-only endpoints (only the declared keys are emitted). resource: https://opendpp-node.eu/openapi.json#/components/schemas/PassportValidateOnlyResult tags: - schema timestamp: 2026-06-17T00:00:00Z --- 200 envelope of the validate-only endpoints (only the declared keys are emitted). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | — | | `category` | string | yes | Echo of metadata.category (or "unknown" if absent). | | `errors` | array<[ValidationErrorItem](/schemas/ValidationErrorItem.md)> | yes | Always an empty array on 200. | | `warnings` | array<[ValidationErrorItem](/schemas/ValidationErrorItem.md)> | no | Non-blocking findings (e.g. | ## JSON Schema ```json { "type": "object", "required": [ "success", "message", "category", "errors" ], "description": "200 envelope of the validate-only endpoints (only the declared keys are emitted).", "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "const": "Passport metadata payload is 100% valid and ESPR category compliant" }, "category": { "type": "string", "description": "Echo of `metadata.category` (or \"unknown\" if absent)." }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/ValidationErrorItem" }, "maxItems": 0, "description": "Always an empty array on 200." }, "warnings": { "type": "array", "items": { "$ref": "#/components/schemas/ValidationErrorItem" }, "description": "Non-blocking findings (e.g. malformed supplementary objects like `circularityAttributes`). The key is OMITTED entirely when there are no warnings — it is never an empty array." } } } ``` ## Used by - [validatePassport](/operations/validatePassport.md) (`POST /api/v1/passports/validate-only`) - [validatePassportPublic](/operations/validatePassportPublic.md) (`POST /api/v1/passports/validate-only-public`) ================================================================================ # FILE: /okf/schemas/PublicBatteryUnitJsonLd.md ================================================================================ --- type: Schema title: PublicBatteryUnitJsonLd description: Public JSON-LD document for one individual serialised battery unit (Reg. resource: https://opendpp-node.eu/openapi.json#/components/schemas/PublicBatteryUnitJsonLd tags: - schema timestamp: 2026-06-17T00:00:00Z --- Public JSON-LD document for one individual serialised battery unit (Reg. (EU) 2023/1542 Art. 77(2)). The listed required keys are always present. EXACTLY ONE of two tier-dependent groups is added: anonymous (public) responses carry `restrictedData` (Annex XIII(2)-(4) notice) and OMIT `currentState`/`dynamicData` entirely; owner/grant (privileged) responses carry `currentState` (latest measurement or `null`) and `dynamicData` (up to 500 events, newest first) and omit `restrictedData`. The embedded `ofModel` passport is masked by the caller's tier like `GET /passport/{id}`. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@context` | array<—> | yes | The context URL https://opendpp-node.eu/contexts/dpp/v1 plus a fixed inline term map for the battery-unit terms. | | `@type` | string | yes | — | | `@id` | string | yes | The unit's GS1 Digital Link URI (AI-21 = the real physical serial). | | `id` | string | yes | — | | `serialNumber` | string | yes | The physical battery serial (the real GS1 AI-21 value; unique within its SKU/type passport). | | `digitalLinkUri` | string | yes | — | | `status` | string | yes | Annex XIII battery-status vocabulary. | | `manufacturedAt` | string,null | yes | — | | `repurposedFrom` | — | yes | Art. | | `successorUnits` | array<[BatteryUnitLineageRef](/schemas/BatteryUnitLineageRef.md)> | yes | Units re-placed on the market under a new passport derived from this one (empty array when none). | | `ofModel` | [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) | yes | The SKU/type-level passport this physical unit is an instance of, masked by the caller's tier. | | `restrictedData` | [BatteryUnitRestrictedDataNotice](/schemas/BatteryUnitRestrictedDataNotice.md) | no | Present ONLY in anonymous (public-tier) responses. | | `currentState` | — | no | Present ONLY in owner/grant-tier responses: the most recent recorded measurement, or null when the unit has no events. | | `dynamicData` | array<[BatteryUnitEventNode](/schemas/BatteryUnitEventNode.md)> | no | Present ONLY in owner/grant-tier responses: append-only telemetry history, newest first, capped at the 500 most recent events. | | `createdAt` | string | yes | — | | `updatedAt` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "Public JSON-LD document for one individual serialised battery unit (Reg. (EU) 2023/1542 Art. 77(2)). The listed required keys are always present. EXACTLY ONE of two tier-dependent groups is added: anonymous (public) responses carry `restrictedData` (Annex XIII(2)-(4) notice) and OMIT `currentState`/`dynamicData` entirely; owner/grant (privileged) responses carry `currentState` (latest measurement or `null`) and `dynamicData` (up to 500 events, newest first) and omit `restrictedData`. The embedded `ofModel` passport is masked by the caller's tier like `GET /passport/{id}`.", "additionalProperties": false, "required": [ "@context", "@type", "@id", "id", "serialNumber", "digitalLinkUri", "status", "manufacturedAt", "repurposedFrom", "successorUnits", "ofModel", "createdAt", "updatedAt" ], "properties": { "@context": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "anyOf": [ { "type": "string" }, { "type": "object", "additionalProperties": { "type": "string" } } ] }, "description": "The context URL `https://opendpp-node.eu/contexts/dpp/v1` plus a fixed inline term map for the battery-unit terms." }, "@type": { "type": "string", "const": "BatteryUnit" }, "@id": { "type": "string", "format": "uri", "description": "The unit's GS1 Digital Link URI (AI-21 = the real physical serial)." }, "id": { "type": "string" }, "serialNumber": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,64}$", "description": "The physical battery serial (the real GS1 AI-21 value; unique within its SKU/type passport)." }, "digitalLinkUri": { "type": "string", "format": "uri" }, "status": { "type": "string", "enum": [ "IN_SERVICE", "DECOMMISSIONED", "RECALLED", "REPURPOSED", "REMANUFACTURED", "REUSED", "WASTE", "RECYCLED" ], "description": "Annex XIII battery-status vocabulary. A `RECYCLED` (or ceased) unit is never served as a 200 — its URL answers 410 with the tombstone document instead." }, "manufacturedAt": { "type": [ "string", "null" ], "format": "date-time" }, "repurposedFrom": { "anyOf": [ { "$ref": "#/components/schemas/BatteryUnitLineageRef" }, { "type": "null" } ], "description": "Art. 77(7) lineage: the original unit this repurposed/remanufactured battery came from. The link itself is public." }, "successorUnits": { "type": "array", "items": { "$ref": "#/components/schemas/BatteryUnitLineageRef" }, "description": "Units re-placed on the market under a new passport derived from this one (empty array when none)." }, "ofModel": { "$ref": "#/components/schemas/PublicPassportJsonLd", "description": "The SKU/type-level passport this physical unit is an instance of, masked by the caller's tier." }, "restrictedData": { "$ref": "#/components/schemas/BatteryUnitRestrictedDataNotice", "description": "Present ONLY in anonymous (public-tier) responses." }, "currentState": { "anyOf": [ { "$ref": "#/components/schemas/BatteryUnitCurrentState" }, { "type": "null" } ], "description": "Present ONLY in owner/grant-tier responses: the most recent recorded measurement, or `null` when the unit has no events." }, "dynamicData": { "type": "array", "maxItems": 500, "items": { "$ref": "#/components/schemas/BatteryUnitEventNode" }, "description": "Present ONLY in owner/grant-tier responses: append-only telemetry history, newest first, capped at the 500 most recent events." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } } ``` ## Used by - [resolvePublicBatteryUnit](/operations/resolvePublicBatteryUnit.md) (`GET /unit/{id}`) ================================================================================ # FILE: /okf/schemas/PublicFacilityNode.md ================================================================================ --- type: Schema title: PublicFacilityNode description: Embedded manufacturing-facility JSON-LD node — the GS1 GLN-backed Unique Facility Identifier (UFI, EN 18219). resource: https://opendpp-node.eu/openapi.json#/components/schemas/PublicFacilityNode tags: - schema timestamp: 2026-06-17T00:00:00Z --- Embedded manufacturing-facility JSON-LD node — the GS1 GLN-backed Unique Facility Identifier (UFI, EN 18219). The five listed fields are public; `streetAddress`/`city`/`postalCode` appear ONLY in owner-tier responses (never via legitimate-interest grants). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@type` | string | yes | — | | `id` | string | yes | — | | `gln` | string | yes | GS1 GLN-13 with a valid modulo-10 check digit. | | `name` | string | yes | — | | `activity` | string,null | yes | What the facility does in the chain, e.g. | | `country` | string | yes | ISO 3166-1 alpha-2 country code. | | `streetAddress` | string,null | no | Owner tier only — omitted from public and grant-tier responses. | | `city` | string,null | no | Owner tier only. | | `postalCode` | string,null | no | Owner tier only. | ## JSON Schema ```json { "type": "object", "description": "Embedded manufacturing-facility JSON-LD node — the GS1 GLN-backed Unique Facility Identifier (UFI, EN 18219). The five listed fields are public; `streetAddress`/`city`/`postalCode` appear ONLY in owner-tier responses (never via legitimate-interest grants).", "required": [ "@type", "id", "gln", "name", "activity", "country" ], "properties": { "@type": { "type": "string", "const": "Facility" }, "id": { "type": "string" }, "gln": { "type": "string", "pattern": "^[0-9]{13}$", "description": "GS1 GLN-13 with a valid modulo-10 check digit." }, "name": { "type": "string" }, "activity": { "type": [ "string", "null" ], "description": "What the facility does in the chain, e.g. `cell assembly`." }, "country": { "type": "string", "description": "ISO 3166-1 alpha-2 country code." }, "streetAddress": { "type": [ "string", "null" ], "description": "Owner tier only — omitted from public and grant-tier responses." }, "city": { "type": [ "string", "null" ], "description": "Owner tier only." }, "postalCode": { "type": [ "string", "null" ], "description": "Owner tier only." } } } ``` ## Used by - schema [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) ================================================================================ # FILE: /okf/schemas/PublicPassportJsonLd.md ================================================================================ --- type: Schema title: PublicPassportJsonLd description: The public, redacted JSON-LD Digital Product Passport document (application/ld+json). resource: https://opendpp-node.eu/openapi.json#/components/schemas/PublicPassportJsonLd tags: - schema timestamp: 2026-06-17T00:00:00Z --- The public, redacted JSON-LD Digital Product Passport document (`application/ld+json`). All listed top-level keys are ALWAYS present (`null` where not applicable). Additionally, every key of the (masked) `metadata` object — except the reserved document keys (`@context`, `@type`, `@id`, `id`, `productId`, `digitalLinkUri`, `digitalSeal`, `signingPublicKey`, `status`, `archivedAt`, `retentionUntil`, `proof`, `createdAt`, `updatedAt`, `economicOperator`, `manufacturingFacility`, `metadata`) — is ALSO flattened onto the document root for direct semantic-graph querying (hence `additionalProperties: true`); flattened values are identical to the corresponding `metadata` values, including redaction placeholders. Tier-masked metadata keys are replaced (in both places) with the literal string `[REDACTED - Privileged Access Required]`. Masking by tier: anonymous public callers lose the per-category restricted keys (category `batteries`: `detailedPerformance`, `lifecycleAndInUse`, `circularityAndDisassembly` — masked only when actually present) AND the owner-only key `facilityDetails`; legitimate-interest/authority grant holders lose only `facilityDetails`; owner-tier responses are unmasked and additionally include the facility street address fields. Note: `facilityDetails` is placeholder-masked in EVERY non-owner response, even when the underlying metadata never contained the key — in that case it has no entry in `proof.redactedLeaves`. Each masked key that exists in the sealed metadata keeps its true Merkle leaf hash in `proof.redactedLeaves`, so the eIDAS seal stays verifiable offline after redaction (see `MerkleTreeAttestationProof` for the reconstruction rule). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@context` | array<—> | yes | Exactly two entries: the context URL https://opendpp-node.eu/contexts/dpp/v1 and an inline term map covering the 9 fixed DPP terms (DigitalProductPassport, eco… | | `@type` | string | yes | — | | `@id` | string | yes | The passport's canonical GS1 Digital Link URI (same value as digitalLinkUri). | | `id` | string | yes | Server-assigned passport UUID. | | `productId` | string | yes | Caller-supplied product identifier: a GTIN-14 (^[0-9]{14}$ with valid GS1 modulo-10 check digit), a GRAI (^[0-9]{14}[A-Za-z0-9]{0,16}$), or a free-form SKU. | | `digitalLinkUri` | string | yes | SKU/type-level GS1 Digital Link URI: {BASE_URL}/{01|8003}/{productId}/21/{passportId} (AI-21 carries the passport UUID at SKU level; individual units carry the… | | `digitalSeal` | string,null | yes | eIDAS ADVANCED electronic seal: base64 ECDSA prime256v1 (P-256) signature over the Merkle root of the key-sorted metadata. | | `signingPublicKey` | string,null | yes | PEM public key that verifies digitalSeal. | | `status` | string | yes | Passport lifecycle status (serialized as ACTIVE when unset). | | `archivedAt` | string,null | yes | Soft-delete marker (owner off-boarded / decommissioned). | | `retentionUntil` | string,null | yes | Minimum-availability deadline; the passport is never purged before this instant. | | `proof` | — | yes | Present (non-null) only when the passport is sealed (digitalSeal set). | | `createdAt` | string | yes | — | | `updatedAt` | string | yes | — | | `economicOperator` | — | yes | The economic operator (manufacturer/importer/retailer) responsible for the product. | | `manufacturingFacility` | — | yes | GLN-backed Unique Facility Identifier node (EN 18219), or null when no facility is linked. | | `metadata` | object | yes | The ESPR category metadata, tier-masked: keys above the caller's tier hold the literal string [REDACTED - Privileged Access Required] instead of their value. | ## JSON Schema ```json { "type": "object", "description": "The public, redacted JSON-LD Digital Product Passport document (`application/ld+json`). All listed top-level keys are ALWAYS present (`null` where not applicable). Additionally, every key of the (masked) `metadata` object — except the reserved document keys (`@context`, `@type`, `@id`, `id`, `productId`, `digitalLinkUri`, `digitalSeal`, `signingPublicKey`, `status`, `archivedAt`, `retentionUntil`, `proof`, `createdAt`, `updatedAt`, `economicOperator`, `manufacturingFacility`, `metadata`) — is ALSO flattened onto the document root for direct semantic-graph querying (hence `additionalProperties: true`); flattened values are identical to the corresponding `metadata` values, including redaction placeholders. Tier-masked metadata keys are replaced (in both places) with the literal string `[REDACTED - Privileged Access Required]`. Masking by tier: anonymous public callers lose the per-category restricted keys (category `batteries`: `detailedPerformance`, `lifecycleAndInUse`, `circularityAndDisassembly` — masked only when actually present) AND the owner-only key `facilityDetails`; legitimate-interest/authority grant holders lose only `facilityDetails`; owner-tier responses are unmasked and additionally include the facility street address fields. Note: `facilityDetails` is placeholder-masked in EVERY non-owner response, even when the underlying metadata never contained the key — in that case it has no entry in `proof.redactedLeaves`. Each masked key that exists in the sealed metadata keeps its true Merkle leaf hash in `proof.redactedLeaves`, so the eIDAS seal stays verifiable offline after redaction (see `MerkleTreeAttestationProof` for the reconstruction rule).", "additionalProperties": true, "required": [ "@context", "@type", "@id", "id", "productId", "digitalLinkUri", "digitalSeal", "signingPublicKey", "status", "archivedAt", "retentionUntil", "proof", "createdAt", "updatedAt", "economicOperator", "manufacturingFacility", "metadata" ], "properties": { "@context": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "anyOf": [ { "type": "string" }, { "type": "object", "additionalProperties": { "type": "string" } } ] }, "description": "Exactly two entries: the context URL `https://opendpp-node.eu/contexts/dpp/v1` and an inline term map covering the 9 fixed DPP terms (`DigitalProductPassport`, `economicOperator`, `manufacturingFacility`, `metadata`, `digitalSeal`, `signingPublicKey`, `status`, `archivedAt`, `retentionUntil`) plus one generated term per metadata key (`https://opendpp-node.eu/contexts/dpp/v1#`)." }, "@type": { "type": "string", "const": "DigitalProductPassport" }, "@id": { "type": "string", "format": "uri", "description": "The passport's canonical GS1 Digital Link URI (same value as `digitalLinkUri`)." }, "id": { "type": "string", "description": "Server-assigned passport UUID." }, "productId": { "type": "string", "description": "Caller-supplied product identifier: a GTIN-14 (`^[0-9]{14}$` with valid GS1 modulo-10 check digit), a GRAI (`^[0-9]{14}[A-Za-z0-9]{0,16}$`), or a free-form SKU." }, "digitalLinkUri": { "type": "string", "format": "uri", "description": "SKU/type-level GS1 Digital Link URI: `{BASE_URL}/{01|8003}/{productId}/21/{passportId}` (AI-21 carries the passport UUID at SKU level; individual units carry their physical serial instead)." }, "digitalSeal": { "type": [ "string", "null" ], "description": "eIDAS ADVANCED electronic seal: base64 ECDSA prime256v1 (P-256) signature over the Merkle root of the key-sorted metadata. `null` when the passport has not been sealed." }, "signingPublicKey": { "type": [ "string", "null" ], "description": "PEM public key that verifies `digitalSeal`. `null` when unsealed." }, "status": { "type": "string", "enum": [ "DRAFT", "ACTIVE", "RECALLED", "DECOMMISSIONED" ], "description": "Passport lifecycle status (serialized as `ACTIVE` when unset). `DRAFT` is only ever visible to owner-tier callers — public/grant resolution of a draft returns 404." }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time", "description": "Soft-delete marker (owner off-boarded / decommissioned). Archived passports remain publicly resolvable (ESPR persistence duty)." }, "retentionUntil": { "type": [ "string", "null" ], "format": "date-time", "description": "Minimum-availability deadline; the passport is never purged before this instant." }, "proof": { "anyOf": [ { "$ref": "#/components/schemas/MerkleTreeAttestationProof" }, { "type": "null" } ], "description": "Present (non-null) only when the passport is sealed (`digitalSeal` set)." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "economicOperator": { "anyOf": [ { "$ref": "#/components/schemas/EconomicOperatorNode" }, { "type": "null" } ], "description": "The economic operator (manufacturer/importer/retailer) responsible for the product. Public in all tiers." }, "manufacturingFacility": { "anyOf": [ { "$ref": "#/components/schemas/PublicFacilityNode" }, { "type": "null" } ], "description": "GLN-backed Unique Facility Identifier node (EN 18219), or `null` when no facility is linked. GLN, name, activity and country are public; street-address fields appear only in owner-tier responses." }, "metadata": { "type": "object", "additionalProperties": true, "description": "The ESPR category metadata, tier-masked: keys above the caller's tier hold the literal string `[REDACTED - Privileged Access Required]` instead of their value." } } } ``` ## Used by - [getPassport](/operations/getPassport.md) (`GET /api/v1/passports/{id}`) - [resolvePublicPassport](/operations/resolvePublicPassport.md) (`GET /passport/{id}`) - [resolveGs1Gtin](/operations/resolveGs1Gtin.md) (`GET /01/{gtin14}`) - [resolveGs1Grai](/operations/resolveGs1Grai.md) (`GET /8003/{grai}`) - schema [BatteryUnitJsonLd](/schemas/BatteryUnitJsonLd.md) - schema [PassportIngestCreated](/schemas/PassportIngestCreated.md) - schema [PassportUpdateResponse](/schemas/PassportUpdateResponse.md) - schema [PassportSealResponse](/schemas/PassportSealResponse.md) - schema [PassportStatusUpdateResponse](/schemas/PassportStatusUpdateResponse.md) - schema [PublicBatteryUnitJsonLd](/schemas/PublicBatteryUnitJsonLd.md) - schema [WebhookEnvelope](/schemas/WebhookEnvelope.md) ================================================================================ # FILE: /okf/schemas/RecordBatteryUnitEventRequest.md ================================================================================ --- type: Schema title: RecordBatteryUnitEventRequest description: One telemetry record. resource: https://opendpp-node.eu/openapi.json#/components/schemas/RecordBatteryUnitEventRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- One telemetry record. All measurements are optional and independently nullable; numeric ranges are enforced with 400 on violation. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `eventType` | [BatteryUnitEventType](/schemas/BatteryUnitEventType.md) | yes | — | | `stateOfHealth` | number,null | no | State of health, percent (0–100). | | `cycleCount` | number,null | no | Cumulative full-equivalent cycles. | | `remainingCapacityAh` | number,null | no | Remaining capacity, ampere-hours. | | `temperatureC` | number,null | no | Observed temperature, °C. | | `payload` | object,array,null | no | Free-form JSON with additional telemetry/context. | | `recordedAt` | string,number | no | When the measurement was taken. | | `status` | [BatteryUnitStatus](/schemas/BatteryUnitStatus.md) | no | Optional status transition, applied to the unit in the same transaction when it differs from the current status (works with any eventType; conventionally paire… | ## JSON Schema ```json { "type": "object", "description": "One telemetry record. All measurements are optional and independently nullable; numeric ranges are enforced with 400 on violation.", "properties": { "eventType": { "$ref": "#/components/schemas/BatteryUnitEventType" }, "stateOfHealth": { "type": [ "number", "null" ], "minimum": 0, "maximum": 100, "description": "State of health, percent (0–100)." }, "cycleCount": { "type": [ "number", "null" ], "minimum": 0, "maximum": 9007199254740991, "description": "Cumulative full-equivalent cycles. Fractional values are accepted but truncated to an integer before persisting." }, "remainingCapacityAh": { "type": [ "number", "null" ], "minimum": 0, "maximum": 9007199254740991, "description": "Remaining capacity, ampere-hours." }, "temperatureC": { "type": [ "number", "null" ], "minimum": -273.15, "maximum": 10000, "description": "Observed temperature, °C." }, "payload": { "type": [ "object", "array", "null" ], "description": "Free-form JSON with additional telemetry/context. Objects **and arrays** pass the server's `typeof` check and are persisted verbatim; any other value (string, number, boolean) — and an explicit `null` — is silently dropped and stored as `null`." }, "recordedAt": { "type": [ "string", "number" ], "description": "When the measurement was taken. Any value accepted by JavaScript `Date` parsing (ISO 8601 recommended; epoch milliseconds also work); 400 if unparseable. Defaults to the server's current time." }, "status": { "$ref": "#/components/schemas/BatteryUnitStatus", "description": "Optional status transition, applied to the unit in the same transaction when it differs from the current status (works with any `eventType`; conventionally paired with `STATUS_CHANGE`). Transitioning to `RECYCLED` stamps `ceasedAt` (if not already set; never cleared) and turns the public unit view into a 410 tombstone (Art. 77(8)); `status` itself is not locked afterwards — a later event may still set a different value — but `ceasedAt` persists, so the 410 and the predecessor refusal are permanent." } }, "required": [ "eventType" ] } ``` ## Used by - [recordBatteryUnitEvent](/operations/recordBatteryUnitEvent.md) (`POST /api/v1/units/{id}/events`) ================================================================================ # FILE: /okf/schemas/RecordBatteryUnitEventResponse.md ================================================================================ --- type: Schema title: RecordBatteryUnitEventResponse description: RecordBatteryUnitEventResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/RecordBatteryUnitEventResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | — | | `event` | [BatteryUnitEventRow](/schemas/BatteryUnitEventRow.md) | yes | — | ## JSON Schema ```json { "type": "object", "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "const": "Dynamic data recorded" }, "event": { "$ref": "#/components/schemas/BatteryUnitEventRow" } }, "required": [ "success", "message", "event" ] } ``` ## Used by - [recordBatteryUnitEvent](/operations/recordBatteryUnitEvent.md) (`POST /api/v1/units/{id}/events`) ================================================================================ # FILE: /okf/schemas/RegisterOperatorRequest.md ================================================================================ --- type: Schema title: RegisterOperatorRequest description: RegisterOperatorRequest resource: https://opendpp-node.eu/openapi.json#/components/schemas/RegisterOperatorRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | string | yes | Legal/display name. | | `regId` | string | yes | Official registration id (EORI, VAT, DUNS, or national registry id). | | `regIdScheme` | string,null | no | Optional declaration of what kind of id regId is. | | `role` | string | no | Supply-chain role, free text — e.g. | ## JSON Schema ```json { "type": "object", "required": [ "name", "regId" ], "properties": { "name": { "type": "string", "description": "Legal/display name. Ignored if your workspace already has an operator with this `regId` (the existing record is returned instead)." }, "regId": { "type": "string", "description": "Official registration id (EORI, VAT, DUNS, or national registry id). Unique within your workspace; immutable after registration. Fabricated `EORI-MOCK…` ids are rejected. When `regIdScheme` is `EORI`, must match `^[A-Z]{2}[A-Za-z0-9]{1,15}$` (e.g. `DE1234567890`)." }, "regIdScheme": { "type": [ "string", "null" ], "description": "Optional declaration of what kind of id `regId` is. Allowed values: `EORI`, `VAT`, `DUNS`, `NATIONAL`, `OTHER` — matched case-insensitively (uppercased server-side). Any other value is rejected with `400`. Omit or send `null` for an unspecified national/business id. Ignored when binding to an existing operator." }, "role": { "type": "string", "default": "MANUFACTURER", "description": "Supply-chain role, free text — e.g. `MANUFACTURER`, `IMPORTER`, `RETAILER`. Defaults to `MANUFACTURER`. Ignored when binding to an existing operator." } } } ``` ## Used by - [registerOperator](/operations/registerOperator.md) (`POST /api/v1/operators`) ================================================================================ # FILE: /okf/schemas/RegisterOperatorResponse.md ================================================================================ --- type: Schema title: RegisterOperatorResponse description: RegisterOperatorResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/RegisterOperatorResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | Always "Economic Operator supplier registered successfully" (also when an existing operator was bound rather than created). | | `operator` | [OperatorRow](/schemas/OperatorRow.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "message", "operator" ], "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "description": "Always `\"Economic Operator supplier registered successfully\"` (also when an existing operator was bound rather than created)." }, "operator": { "$ref": "#/components/schemas/OperatorRow" } } } ``` ## Used by - [registerOperator](/operations/registerOperator.md) (`POST /api/v1/operators`) ================================================================================ # FILE: /okf/schemas/RestoreOperatorResponse.md ================================================================================ --- type: Schema title: RestoreOperatorResponse description: RestoreOperatorResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/RestoreOperatorResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `restoredPassports` | integer | yes | Number of archived passports returned to the active catalogue (archivedAt and retentionUntil cleared). | ## JSON Schema ```json { "type": "object", "required": [ "success", "restoredPassports" ], "properties": { "success": { "type": "boolean", "const": true }, "restoredPassports": { "type": "integer", "minimum": 0, "description": "Number of archived passports returned to the active catalogue (`archivedAt` and `retentionUntil` cleared). Passports independently DECOMMISSIONED are not restored and not counted." } } } ``` ## Used by - [restoreOperator](/operations/restoreOperator.md) (`POST /api/v1/operators/{id}/restore`) ================================================================================ # FILE: /okf/schemas/RotateTenantKeysResponse.md ================================================================================ --- type: Schema title: RotateTenantKeysResponse description: RotateTenantKeysResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/RotateTenantKeysResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | Always "eIDAS Asymmetric Key Pair generated and rotated in secure DB custody successfully". | | `publicKey` | string | yes | The new ECDSA prime256v1 (P-256) public key, PEM-encoded (SPKI, -----BEGIN PUBLIC KEY----- block, trailing newline). | ## JSON Schema ```json { "type": "object", "required": [ "success", "message", "publicKey" ], "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "description": "Always `\"eIDAS Asymmetric Key Pair generated and rotated in secure DB custody successfully\"`." }, "publicKey": { "type": "string", "description": "The new ECDSA prime256v1 (P-256) public key, PEM-encoded (SPKI, `-----BEGIN PUBLIC KEY-----` block, trailing newline). The matching private key is held only AES-256-GCM-encrypted in the platform vault and is never returned." } } } ``` ## Used by - [rotateTenantKeys](/operations/rotateTenantKeys.md) (`POST /api/v1/tenants/rotate-keys`) ================================================================================ # FILE: /okf/schemas/SealCertificateReport.md ================================================================================ --- type: Schema title: SealCertificateReport description: "Present only for x5c-carrying proofs AND only when verification proceeds past the key-registration and operator-binding gates (the two policy verified: false responses omit it): the certified legal identity of the seal creator (eIDAS Art." resource: https://opendpp-node.eu/openapi.json#/components/schemas/SealCertificateReport tags: - schema timestamp: 2026-06-17T00:00:00Z --- Present only for x5c-carrying proofs AND only when verification proceeds past the key-registration and operator-binding gates (the two policy `verified: false` responses omit it): the certified legal identity of the seal creator (eIDAS Art. 36(1)(b)). On an unparseable chain only `chainValid: false` and `error` are present. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `subject` | string | no | Leaf-certificate subject (multi-line RDN string as produced by Node's X509Certificate, e.g. | | `issuer` | string | no | Leaf-certificate issuer RDN string. | | `validFrom` | string | no | X.509 textual date, e.g. | | `validTo` | string | no | X.509 textual date — NOT ISO 8601. | | `chainValid` | boolean | yes | True only when every chain link signature-verifies, every certificate is within its validity window, AND the top of the chain is anchored to this node's seal C… | | `keyMatchesProof` | boolean | no | True when the leaf SPKI equals the supplied publicKey (whitespace-insensitive), or when no explicit public key was supplied (the leaf key was used). | | `error` | string | no | Present only when the chain could not be parsed. | ## JSON Schema ```json { "type": "object", "description": "Present only for x5c-carrying proofs AND only when verification proceeds past the key-registration and operator-binding gates (the two policy `verified: false` responses omit it): the certified legal identity of the seal creator (eIDAS Art. 36(1)(b)). On an unparseable chain only `chainValid: false` and `error` are present.", "required": [ "chainValid" ], "properties": { "subject": { "type": "string", "description": "Leaf-certificate subject (multi-line RDN string as produced by Node's X509Certificate, e.g. `CN=OpenDPP Demo Eco Industries Seal`)." }, "issuer": { "type": "string", "description": "Leaf-certificate issuer RDN string." }, "validFrom": { "type": "string", "description": "X.509 textual date, e.g. `Jan 10 00:00:00 2026 GMT` — NOT ISO 8601." }, "validTo": { "type": "string", "description": "X.509 textual date — NOT ISO 8601." }, "chainValid": { "type": "boolean", "description": "True only when every chain link signature-verifies, every certificate is within its validity window, AND the top of the chain is anchored to this node's seal CA (fingerprint match or signature under the CA key)." }, "keyMatchesProof": { "type": "boolean", "description": "True when the leaf SPKI equals the supplied `publicKey` (whitespace-insensitive), or when no explicit public key was supplied (the leaf key was used)." }, "error": { "type": "string", "const": "Unparseable x5c certificate chain", "description": "Present only when the chain could not be parsed." } } } ``` ## Used by - schema [SealVerifyResponse](/schemas/SealVerifyResponse.md) ================================================================================ # FILE: /okf/schemas/SealTimestampReport.md ================================================================================ --- type: Schema title: SealTimestampReport description: "Present only when payload.proof.rfc3161.token was supplied AND verification proceeds past the key-registration and operator-binding gates (the two policy verified: false responses omit it)." resource: https://opendpp-node.eu/openapi.json#/components/schemas/SealTimestampReport tags: - schema timestamp: 2026-06-17T00:00:00Z --- Present only when `payload.proof.rfc3161.token` was supplied AND verification proceeds past the key-registration and operator-binding gates (the two policy `verified: false` responses omit it). Reports presence and the TSA-asserted genTime from the token's TSTInfo — full cryptographic TSR validation is the verifier's own step. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `present` | boolean | yes | — | | `genTime` | string,null | yes | TSA-asserted generation time (ISO 8601), or null when the token's TSTInfo could not be parsed. | | `note` | string | no | Present only when genTime is null. | ## JSON Schema ```json { "type": "object", "description": "Present only when `payload.proof.rfc3161.token` was supplied AND verification proceeds past the key-registration and operator-binding gates (the two policy `verified: false` responses omit it). Reports presence and the TSA-asserted genTime from the token's TSTInfo — full cryptographic TSR validation is the verifier's own step.", "required": [ "present", "genTime" ], "properties": { "present": { "type": "boolean", "const": true }, "genTime": { "type": [ "string", "null" ], "format": "date-time", "description": "TSA-asserted generation time (ISO 8601), or null when the token's TSTInfo could not be parsed." }, "note": { "type": "string", "const": "token present but TSTInfo could not be parsed", "description": "Present only when `genTime` is null." } } } ``` ## Used by - schema [SealVerifyResponse](/schemas/SealVerifyResponse.md) ================================================================================ # FILE: /okf/schemas/SealVerifyRequest.md ================================================================================ --- type: Schema title: SealVerifyRequest description: Verification request. resource: https://opendpp-node.eu/openapi.json#/components/schemas/SealVerifyRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- Verification request. Only `payload` is strictly required: `signature` and `publicKey` are extracted from `payload.proof` (proofValue/signatureValue, publicKeyPem, or the x5c leaf SPKI) when omitted, and the request fails 400 only if either is still missing after extraction. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `payload` | object | yes | The sealed passport document to verify — typically the JSON-LD passport document exactly as resolved from the public endpoints, or any {passportId, productId,… | | `signature` | string | no | Base64 ECDSA (P-256 / SHA-256) seal signature. | | `publicKey` | string | no | PEM (SPKI) public key of the sealing tenant. | ## JSON Schema ```json { "type": "object", "description": "Verification request. Only `payload` is strictly required: `signature` and `publicKey` are extracted from `payload.proof` (proofValue/signatureValue, publicKeyPem, or the x5c leaf SPKI) when omitted, and the request fails 400 only if either is still missing after extraction.", "required": [ "payload" ], "properties": { "payload": { "type": "object", "description": "The sealed passport document to verify — typically the JSON-LD passport document exactly as resolved from the public endpoints, or any `{passportId, productId, metadata, operator}` payload. Only the fields below are interpreted; all other properties are preserved and participate in the whole-payload signature fallback.", "properties": { "metadata": { "type": "object", "description": "The sealed metadata object — the SHA-256 Merkle tree is rebuilt over its top-level properties (every leaf recomputed from the actual values; caller-supplied redacted-leaf hashes are not accepted). When the `metadata` key is entirely ABSENT, the whole `payload` object is treated as the metadata for the Merkle phase; a present-but-non-object value skips the Merkle phase (only the whole-payload fallback runs)." }, "operator": { "type": "object", "description": "Declared economic operator. A present `regId` triggers the fail-closed operator-binding gate.", "properties": { "name": { "type": "string" }, "regId": { "type": "string", "description": "Operator registration id (e.g. EORI-style `EU-DEFAULT-001`). Must resolve to a registered Economic Operator bound to the signing tenant, or verification fails (`verified: false`)." } } }, "economicOperator": { "type": "object", "description": "Alternative location for the declared operator id — `economicOperator.regId` is checked when `operator.regId` is absent.", "properties": { "regId": { "type": "string" } } }, "proof": { "type": "object", "description": "Embedded W3C-style proof block. Sources for the signature, public key, certificate chain and RFC 3161 token when the top-level fields are omitted.", "properties": { "type": { "type": "string" }, "proofValue": { "type": "string", "description": "Base64 ECDSA seal — used as `signature` when the top-level field is absent." }, "signatureValue": { "type": "string", "description": "Legacy alias for `proofValue` (checked second)." }, "publicKeyPem": { "type": "string", "description": "PEM (SPKI) public key — used as `publicKey` when the top-level field is absent." }, "x5c": { "type": "array", "items": { "type": "string" }, "description": "X.509 certificate chain, base64 DER, LEAF FIRST. Enables the `certificate` report; the leaf's SPKI also serves as the public key when none is otherwise supplied." }, "rfc3161": { "type": "object", "properties": { "token": { "type": "string", "description": "RFC 3161 TimeStampToken (base64 DER CMS ContentInfo) — enables the `timestamp` report." } } } } } }, "additionalProperties": true }, "signature": { "type": "string", "description": "Base64 ECDSA (P-256 / SHA-256) seal signature. Optional when `payload.proof.proofValue` (or `signatureValue`) is present." }, "publicKey": { "type": "string", "description": "PEM (SPKI) public key of the sealing tenant. Optional when `payload.proof.publicKeyPem` or an `x5c` chain is present. CRLF line endings are normalized before matching." } } } ``` ## Used by - [verifyPassportSeal](/operations/verifyPassportSeal.md) (`POST /api/v1/audit/verify`) ================================================================================ # FILE: /okf/schemas/SealVerifyResponse.md ================================================================================ --- type: Schema title: SealVerifyResponse description: Always HTTP 200 once the request is well-formed. resource: https://opendpp-node.eu/openapi.json#/components/schemas/SealVerifyResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- Always HTTP 200 once the request is well-formed. `verified: false` covers both cryptographic failure and the two registration/binding policy failures — the policy failures add a `message` and OMIT `certificate`/`timestamp` even when an x5c chain or RFC 3161 token was supplied. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `verified` | boolean | yes | — | | `message` | string | no | Present only on the two policy failures: unregistered public key, or a declared operator not bound to the signing tenant. | | `certificate` | [SealCertificateReport](/schemas/SealCertificateReport.md) | no | — | | `timestamp` | [SealTimestampReport](/schemas/SealTimestampReport.md) | no | — | ## JSON Schema ```json { "type": "object", "description": "Always HTTP 200 once the request is well-formed. `verified: false` covers both cryptographic failure and the two registration/binding policy failures — the policy failures add a `message` and OMIT `certificate`/`timestamp` even when an x5c chain or RFC 3161 token was supplied.", "required": [ "success", "verified" ], "properties": { "success": { "type": "boolean", "const": true }, "verified": { "type": "boolean" }, "message": { "type": "string", "description": "Present only on the two policy failures: unregistered public key, or a declared operator not bound to the signing tenant.", "enum": [ "Cryptographic verification failed: The public key used to seal this passport is not registered to any authorized economic operator tenant on this node.", "Cryptographic verification failed: The economic operator declared in this passport is not a registered operator bound to the signing tenant." ] }, "certificate": { "$ref": "#/components/schemas/SealCertificateReport" }, "timestamp": { "$ref": "#/components/schemas/SealTimestampReport" } } } ``` ## Used by - [verifyPassportSeal](/operations/verifyPassportSeal.md) (`POST /api/v1/audit/verify`) ================================================================================ # FILE: /okf/schemas/SectorJsonSchemaDocument.md ================================================================================ --- type: Schema title: SectorJsonSchemaDocument description: A JSON Schema draft-07 document describing the ESPR metadata payload for one product category, served as application/schema+json. resource: https://opendpp-node.eu/openapi.json#/components/schemas/SectorJsonSchemaDocument tags: - schema timestamp: 2026-06-17T00:00:00Z --- A JSON Schema **draft-07** document describing the ESPR `metadata` payload for one product category, served as `application/schema+json`. Each known field is annotated server-side with a plain-English `description` from the platform's field-help registry (annotations are AJV-ignored; validation behavior is identical to the raw schema). The same schema (without annotations) validates `metadata` on `POST /api/v1/passports` and the validate-only endpoints. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `$schema` | string | yes | — | | `title` | string | yes | — | | `type` | string | yes | — | | `required` | array | yes | — | | `properties` | object | yes | — | ## JSON Schema ```json { "type": "object", "description": "A JSON Schema **draft-07** document describing the ESPR `metadata` payload for one product category, served as `application/schema+json`. Each known field is annotated server-side with a plain-English `description` from the platform's field-help registry (annotations are AJV-ignored; validation behavior is identical to the raw schema). The same schema (without annotations) validates `metadata` on `POST /api/v1/passports` and the validate-only endpoints.", "additionalProperties": true, "required": [ "$schema", "title", "type", "required", "properties" ], "properties": { "$schema": { "type": "string", "const": "http://json-schema.org/draft-07/schema#" }, "title": { "type": "string" }, "type": { "type": "string", "const": "object" }, "required": { "type": "array", "items": { "type": "string" } }, "properties": { "type": "object", "additionalProperties": true } } } ``` ## Used by - [getSectorSchema](/operations/getSectorSchema.md) (`GET /api/v1/schemas/{category}`) ================================================================================ # FILE: /okf/schemas/SectorVocabularyContext.md ================================================================================ --- type: Schema title: SectorVocabularyContext description: Per-category JSON-LD vocabulary context, returned by GET /api/v1/schemas/{category} when Accept contains application/ld+json. resource: https://opendpp-node.eu/openapi.json#/components/schemas/SectorVocabularyContext tags: - schema timestamp: 2026-06-17T00:00:00Z --- Per-category JSON-LD vocabulary context, returned by `GET /api/v1/schemas/{category}` when `Accept` contains `application/ld+json`. Fixed shape: `@vocab` is `https://w3id.org/opendpp/schemas/{category}#` plus mappings for `id`, `type`, `category`, `materialComposition`, `originCountry`, `facilityDetails` and `regulatoryCompliance`. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@context` | object | yes | — | ## JSON Schema ```json { "type": "object", "description": "Per-category JSON-LD vocabulary context, returned by `GET /api/v1/schemas/{category}` when `Accept` contains `application/ld+json`. Fixed shape: `@vocab` is `https://w3id.org/opendpp/schemas/{category}#` plus mappings for `id`, `type`, `category`, `materialComposition`, `originCountry`, `facilityDetails` and `regulatoryCompliance`.", "additionalProperties": false, "required": [ "@context" ], "properties": { "@context": { "type": "object", "additionalProperties": { "type": "string" } } } } ``` ## Used by - [getSectorSchema](/operations/getSectorSchema.md) (`GET /api/v1/schemas/{category}`) ================================================================================ # FILE: /okf/schemas/SerializeBatteryUnitsRequest.md ================================================================================ --- type: Schema title: SerializeBatteryUnitsRequest description: "Either a single unit object, or a batch wrapper {units: [...]}." resource: https://opendpp-node.eu/openapi.json#/components/schemas/SerializeBatteryUnitsRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- Either a single unit object, or a batch wrapper `{units: [...]}`. Precedence: when `units` is present **and is an array** it is used; otherwise the whole body is treated as one unit. (`anyOf`, not `oneOf`: a body carrying both a top-level `serialNumber` and a `units` array matches both shapes and is accepted by the server — `units` wins.) ## JSON Schema ```json { "description": "Either a single unit object, or a batch wrapper `{units: [...]}`. Precedence: when `units` is present **and is an array** it is used; otherwise the whole body is treated as one unit. (`anyOf`, not `oneOf`: a body carrying both a top-level `serialNumber` and a `units` array matches both shapes and is accepted by the server — `units` wins.)", "anyOf": [ { "$ref": "#/components/schemas/BatteryUnitCreateItem" }, { "type": "object", "properties": { "units": { "type": "array", "items": { "$ref": "#/components/schemas/BatteryUnitCreateItem" }, "minItems": 1, "maxItems": 200, "description": "1–200 units per request. An empty array is rejected with 400 `Bad Request`; more than 200 likewise." } }, "required": [ "units" ] } ] } ``` ## Used by - [serializeBatteryUnits](/operations/serializeBatteryUnits.md) (`POST /api/v1/passports/{passportId}/units`) ================================================================================ # FILE: /okf/schemas/SerializeBatteryUnitsResponse.md ================================================================================ --- type: Schema title: SerializeBatteryUnitsResponse description: Returned (201) when at least one unit was created. resource: https://opendpp-node.eu/openapi.json#/components/schemas/SerializeBatteryUnitsResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- Returned (201) when at least one unit was created. Partial success is possible: skipped items are reported in `errors` while `units` holds the created rows. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | E.g. | | `count` | integer | yes | Number of units actually created (equals units.length). | | `units` | array<[BatteryUnitRow](/schemas/BatteryUnitRow.md)> | yes | — | | `errors` | array | no | Present only when some items were skipped — one plain-English string per skipped item, generally prefixed []. | ## JSON Schema ```json { "type": "object", "description": "Returned (201) when at least one unit was created. Partial success is possible: skipped items are reported in `errors` while `units` holds the created rows.", "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "description": "E.g. `Serialised 2 individual unit(s)` or `Serialised 1 individual unit(s), skipped 1`." }, "count": { "type": "integer", "minimum": 1, "description": "Number of units actually created (equals `units.length`)." }, "units": { "type": "array", "items": { "$ref": "#/components/schemas/BatteryUnitRow" } }, "errors": { "type": "array", "items": { "type": "string" }, "description": "Present only when some items were skipped — one plain-English string per skipped item, generally prefixed `[]`." } }, "required": [ "success", "message", "count", "units" ] } ``` ## Used by - [serializeBatteryUnits](/operations/serializeBatteryUnits.md) (`POST /api/v1/passports/{passportId}/units`) ================================================================================ # FILE: /okf/schemas/ServiceVersion.md ================================================================================ --- type: Schema title: ServiceVersion description: Running API contract version and source build identity, returned by GET /api/v1/version. resource: https://opendpp-node.eu/openapi.json#/components/schemas/ServiceVersion tags: - schema timestamp: 2026-06-17T00:00:00Z --- Running API contract version and source build identity, returned by `GET /api/v1/version`. The `apiVersion` MAJOR is the safe thing to pin an integration or generated SDK to. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `apiVersion` | string | yes | SemVer of the public API contract currently served (equals the OpenAPI document's info.version; its MAJOR equals the /api/v1 URL major). | | `commit` | string | yes | Short git commit SHA of the running build, or "unknown" when a build did not inject it. | | `builtAt` | string | yes | Build/deploy timestamp (ISO 8601 UTC), or "unknown" when a build did not inject it. | ## JSON Schema ```json { "type": "object", "description": "Running API contract version and source build identity, returned by `GET /api/v1/version`. The `apiVersion` MAJOR is the safe thing to pin an integration or generated SDK to.", "additionalProperties": false, "required": [ "apiVersion", "commit", "builtAt" ], "properties": { "apiVersion": { "type": "string", "description": "SemVer of the public API contract currently served (equals the OpenAPI document's `info.version`; its MAJOR equals the `/api/v1` URL major).", "examples": [ "1.0.0" ] }, "commit": { "type": "string", "description": "Short git commit SHA of the running build, or `\"unknown\"` when a build did not inject it.", "examples": [ "a7a96d0", "unknown" ] }, "builtAt": { "type": "string", "description": "Build/deploy timestamp (ISO 8601 UTC), or `\"unknown\"` when a build did not inject it.", "examples": [ "2026-06-12T09:30:00Z", "unknown" ] } } } ``` ## Used by - [getApiVersion](/operations/getApiVersion.md) (`GET /api/v1/version`) ================================================================================ # FILE: /okf/schemas/TraceComplianceAuditResponse.md ================================================================================ --- type: Schema title: TraceComplianceAuditResponse description: TraceComplianceAuditResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/TraceComplianceAuditResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `eventId` | string | yes | Echo of the audited root event id. | | `compliant` | boolean | yes | — | | `errors` | array | yes | Human-readable violation strings, e.g. | | `auditedAt` | string | yes | — | | `certificate` | — | yes | Present only when compliant is true; null otherwise. | ## JSON Schema ```json { "type": "object", "required": [ "success", "eventId", "compliant", "errors", "auditedAt", "certificate" ], "properties": { "success": { "type": "boolean", "const": true }, "eventId": { "type": "string", "description": "Echo of the audited root event id." }, "compliant": { "type": "boolean" }, "errors": { "type": "array", "items": { "type": "string" }, "description": "Human-readable violation strings, e.g. `UFLPA Compliance Failure: Event [] contains raw materials originating from prohibited Xinjiang region ().` or `EUDR Compliance Failure: Farm coordinates [, ] intersect with deforested area plots.` Empty when compliant." }, "auditedAt": { "type": "string", "format": "date-time" }, "certificate": { "oneOf": [ { "$ref": "#/components/schemas/TraceComplianceCertificate" }, { "type": "null" } ], "description": "Present only when `compliant` is true; `null` otherwise." } } } ``` ## Used by - [auditEventLineage](/operations/auditEventLineage.md) (`POST /api/v1/events/{id}/audit`) ================================================================================ # FILE: /okf/schemas/TraceComplianceCertificate.md ================================================================================ --- type: Schema title: TraceComplianceCertificate description: TraceComplianceCertificate resource: https://opendpp-node.eu/openapi.json#/components/schemas/TraceComplianceCertificate tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `type` | string | yes | — | | `rootEventId` | string | yes | — | | `status` | string | yes | — | | `regulatoryStandards` | array | yes | Always ["EUDR-2026", "UFLPA-2026"]. | ## JSON Schema ```json { "type": "object", "required": [ "type", "rootEventId", "status", "regulatoryStandards" ], "properties": { "type": { "type": "string", "const": "TraceabilityComplianceCertificate" }, "rootEventId": { "type": "string" }, "status": { "type": "string", "const": "VERIFIED_COMPLIANT" }, "regulatoryStandards": { "type": "array", "items": { "type": "string", "enum": [ "EUDR-2026", "UFLPA-2026" ] }, "description": "Always `[\"EUDR-2026\", \"UFLPA-2026\"]`." } } } ``` ## Used by - schema [TraceComplianceAuditResponse](/schemas/TraceComplianceAuditResponse.md) ================================================================================ # FILE: /okf/schemas/TraceEventRegistered.md ================================================================================ --- type: Schema title: TraceEventRegistered description: 201 envelope of POST /api/v1/events. resource: https://opendpp-node.eu/openapi.json#/components/schemas/TraceEventRegistered tags: - schema timestamp: 2026-06-17T00:00:00Z --- 201 envelope of POST /api/v1/events. Note: `status: "success"` (string), not the usual `success: true` boolean. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `status` | string | yes | — | | `eventId` | string | yes | Server-generated event id. | | `untpVerified` | boolean | yes | — | ## JSON Schema ```json { "type": "object", "description": "201 envelope of POST /api/v1/events. Note: `status: \"success\"` (string), not the usual `success: true` boolean.", "required": [ "status", "eventId", "untpVerified" ], "properties": { "status": { "type": "string", "const": "success" }, "eventId": { "type": "string", "description": "Server-generated event id. Use it with `GET /api/v1/events/{id}/lineage` and `POST /api/v1/events/{id}/audit`." }, "untpVerified": { "type": "boolean", "const": true } } } ``` ## Used by - [registerTraceabilityEvent](/operations/registerTraceabilityEvent.md) (`POST /api/v1/events`) ================================================================================ # FILE: /okf/schemas/TraceLineageNode.md ================================================================================ --- type: Schema title: TraceLineageNode description: One node of the recursive upstream lineage DAG. resource: https://opendpp-node.eu/openapi.json#/components/schemas/TraceLineageNode tags: - schema timestamp: 2026-06-17T00:00:00Z --- One node of the recursive upstream lineage DAG. All keys are always present; `location`, `readPoint` and `issuerDid` are null when unset. `location` mirrors the stored `bizLocation`. A shared ancestor reached through multiple downstream paths appears once under each path (the DAG is expanded into a tree). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `eventId` | string | yes | — | | `eventType` | string | yes | — | | `bizStep` | string | yes | — | | `disposition` | string | yes | — | | `eventTime` | string | yes | — | | `epcs` | array | yes | EPC URIs parsed from the stored EPC list (degrades to [] when unparseable). | | `location` | string,null | yes | The event's stored bizLocation. | | `readPoint` | string,null | yes | — | | `isUntpCompliant` | boolean | yes | — | | `issuerDid` | string,null | yes | — | | `parents` | array<[TraceLineageNode](/schemas/TraceLineageNode.md)> | yes | Upstream parent events (recursive). | ## JSON Schema ```json { "type": "object", "description": "One node of the recursive upstream lineage DAG. All keys are always present; `location`, `readPoint` and `issuerDid` are null when unset. `location` mirrors the stored `bizLocation`. A shared ancestor reached through multiple downstream paths appears once under each path (the DAG is expanded into a tree).", "required": [ "eventId", "eventType", "bizStep", "disposition", "eventTime", "epcs", "location", "readPoint", "isUntpCompliant", "issuerDid", "parents" ], "properties": { "eventId": { "type": "string" }, "eventType": { "type": "string", "enum": [ "ObjectEvent", "AggregationEvent", "TransformationEvent", "AssociationEvent" ] }, "bizStep": { "type": "string" }, "disposition": { "type": "string" }, "eventTime": { "type": "string", "format": "date-time" }, "epcs": { "type": "array", "items": { "type": "string" }, "description": "EPC URIs parsed from the stored EPC list (degrades to `[]` when unparseable)." }, "location": { "type": [ "string", "null" ], "description": "The event's stored `bizLocation`." }, "readPoint": { "type": [ "string", "null" ] }, "isUntpCompliant": { "type": "boolean" }, "issuerDid": { "type": [ "string", "null" ] }, "parents": { "type": "array", "items": { "$ref": "#/components/schemas/TraceLineageNode" }, "description": "Upstream parent events (recursive). Empty array at the origin of the chain." } } } ``` ## Used by - schema [TraceLineageResponse](/schemas/TraceLineageResponse.md) ================================================================================ # FILE: /okf/schemas/TraceLineageResponse.md ================================================================================ --- type: Schema title: TraceLineageResponse description: TraceLineageResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/TraceLineageResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `lineage` | [TraceLineageNode](/schemas/TraceLineageNode.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "lineage" ], "properties": { "success": { "type": "boolean", "const": true }, "lineage": { "$ref": "#/components/schemas/TraceLineageNode" } } } ``` ## Used by - [getEventLineage](/operations/getEventLineage.md) (`GET /api/v1/events/{id}/lineage`) ================================================================================ # FILE: /okf/schemas/UntpEventCredential.md ================================================================================ --- type: Schema title: UntpEventCredential description: A UNTP/EPCIS 2.0 traceability event wrapped as a VC-shaped credential (a vendor proof, not a conformant W3C VC). resource: https://opendpp-node.eu/openapi.json#/components/schemas/UntpEventCredential tags: - schema timestamp: 2026-06-17T00:00:00Z --- A UNTP/EPCIS 2.0 traceability event wrapped as a VC-shaped credential (a vendor proof, not a conformant W3C VC). The only hard structural requirement is `credentialSubject`; a missing or unverifiable `proof` is rejected with the 400 `Cryptographic Verification Failed` body. Extra properties are permitted — the entire credential (with `proof.proofValue` blanked) is what the signature covers. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `@context` | array | no | — | | `id` | string | no | Credential id (e.g. | | `type` | array | no | — | | `issuer` | string | no | Issuer DID. | | `issuanceDate` | string | no | Fallback for the stored eventTime when credentialSubject.eventTime is absent. | | `credentialSubject` | [UntpEventCredentialSubject](/schemas/UntpEventCredentialSubject.md) | yes | — | | `proof` | [UntpEventProof](/schemas/UntpEventProof.md) | yes | — | ## JSON Schema ```json { "type": "object", "description": "A UNTP/EPCIS 2.0 traceability event wrapped as a VC-shaped credential (a vendor proof, not a conformant W3C VC). The only hard structural requirement is `credentialSubject`; a missing or unverifiable `proof` is rejected with the 400 `Cryptographic Verification Failed` body. Extra properties are permitted — the entire credential (with `proof.proofValue` blanked) is what the signature covers.", "required": [ "credentialSubject", "proof" ], "properties": { "@context": { "type": "array", "items": { "type": "string" } }, "id": { "type": "string", "description": "Credential id (e.g. `urn:uuid:...`). NOT used as the stored event id — the event primary key is always server-generated." }, "type": { "type": "array", "items": { "type": "string" } }, "issuer": { "type": "string", "description": "Issuer DID. Unless a trusted x5c chain is embedded, the verification key is resolved by EXACT match of the DID's trailing `:`-segment against registered tenant subdomains/company names — e.g. `did:web:opendpp-node.eu:demo` resolves the workspace with subdomain `demo`. For operator-scoped API keys the issuer DID must ALSO contain the bound operator's registration id somewhere in the string (the issuer is checked in preference to `credentialSubject.responsibleOperatorDid`), e.g. `did:web:opendpp-node.eu:EU-DEFAULT-001:demo`. Stored verbatim as the event's `issuerDid`." }, "issuanceDate": { "type": "string", "format": "date-time", "description": "Fallback for the stored `eventTime` when `credentialSubject.eventTime` is absent." }, "credentialSubject": { "$ref": "#/components/schemas/UntpEventCredentialSubject" }, "proof": { "$ref": "#/components/schemas/UntpEventProof" } } } ``` ## Used by - [registerTraceabilityEvent](/operations/registerTraceabilityEvent.md) (`POST /api/v1/events`) ================================================================================ # FILE: /okf/schemas/UntpEventCredentialSubject.md ================================================================================ --- type: Schema title: UntpEventCredentialSubject description: The EPCIS event payload. resource: https://opendpp-node.eu/openapi.json#/components/schemas/UntpEventCredentialSubject tags: - schema timestamp: 2026-06-17T00:00:00Z --- The EPCIS event payload. `eventType` is effectively required: it is persisted into a server-side enum, and a missing or unknown value is rejected at the persistence layer (surfacing as the 500 `Database Persistence Failed` body, not a 400). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | no | EPC identifier of the subject (e.g. | | `eventType` | string | yes | EPCIS 2.0 event type (server-side enum). | | `action` | string | no | EPCIS action (server-side enum). | | `bizStep` | string | no | CBV business step URI. | | `disposition` | string | no | CBV disposition URI. | | `readPoint` | string | no | Where the event was observed (e.g. | | `bizLocation` | string | no | Business location (SGLN URI, DID, or free identifier). | | `eventTime` | string | no | When the event occurred (anything new Date() parses). | | `epcList` | array | no | EPC URIs observed by the event. | | `parentEpc` | string | no | Parent EPC for AggregationEvent. | | `childEpcs` | array | no | Child EPCs for AggregationEvent (stored as JSON verbatim). | | `inputEpcList` | array | no | Input EPCs for TransformationEvent (stored as JSON verbatim). | | `outputEpcList` | array | no | Output EPCs for TransformationEvent (stored as JSON verbatim). | | `originLocation` | object | no | Geographic origin; used only to derive a default readPoint (geo:,) when readPoint is absent. | | `responsibleOperatorDid` | string | no | DID of the responsible economic operator. | ## JSON Schema ```json { "type": "object", "description": "The EPCIS event payload. `eventType` is effectively required: it is persisted into a server-side enum, and a missing or unknown value is rejected at the persistence layer (surfacing as the 500 `Database Persistence Failed` body, not a 400).", "required": [ "eventType" ], "properties": { "id": { "type": "string", "description": "EPC identifier of the subject (e.g. `urn:epc:id:sgtin:0950110153.0003.SN-2026-000123`). When `epcList` is not supplied as an array, the stored EPC list defaults to `[id]` (or `[]` if absent)." }, "eventType": { "type": "string", "enum": [ "ObjectEvent", "AggregationEvent", "TransformationEvent", "AssociationEvent" ], "description": "EPCIS 2.0 event type (server-side enum)." }, "action": { "type": "string", "enum": [ "ADD", "OBSERVE", "DELETE" ], "description": "EPCIS action (server-side enum). Optional — but MUST be absent (or null) on `TransformationEvent`, otherwise 400 `Schema Validation Error`. An unknown value is rejected at the persistence layer (500)." }, "bizStep": { "type": "string", "default": "urn:epcglobal:cbv:bizstep:receiving", "description": "CBV business step URI. Defaults to `urn:epcglobal:cbv:bizstep:receiving`." }, "disposition": { "type": "string", "default": "urn:epcglobal:cbv:disp:in_progress", "description": "CBV disposition URI. Defaults to `urn:epcglobal:cbv:disp:in_progress`." }, "readPoint": { "type": "string", "description": "Where the event was observed (e.g. `geo:41.1496,-8.6109`). When absent and `originLocation` is present, defaults to `geo:,`." }, "bizLocation": { "type": "string", "description": "Business location (SGLN URI, DID, or free identifier). When absent, defaults to `responsibleOperatorDid`." }, "eventTime": { "type": "string", "format": "date-time", "description": "When the event occurred (anything `new Date()` parses). Defaults to the credential `issuanceDate`, else the server clock." }, "epcList": { "type": "array", "items": { "type": "string" }, "description": "EPC URIs observed by the event. A non-array value is replaced with `[credentialSubject.id]` (or `[]`)." }, "parentEpc": { "type": "string", "description": "Parent EPC for AggregationEvent." }, "childEpcs": { "type": "array", "items": { "type": "string" }, "description": "Child EPCs for AggregationEvent (stored as JSON verbatim)." }, "inputEpcList": { "type": "array", "items": { "type": "string" }, "description": "Input EPCs for TransformationEvent (stored as JSON verbatim)." }, "outputEpcList": { "type": "array", "items": { "type": "string" }, "description": "Output EPCs for TransformationEvent (stored as JSON verbatim)." }, "originLocation": { "type": "object", "required": [ "latitude", "longitude" ], "properties": { "latitude": { "type": "number" }, "longitude": { "type": "number" }, "eudrPlotId": { "type": "string" } }, "description": "Geographic origin; used only to derive a default `readPoint` (`geo:,`) when `readPoint` is absent. `latitude`/`longitude` are required here for correct usage but NOT enforced server-side: an `originLocation` missing them is not rejected — the derived `readPoint` is then silently persisted as the malformed literal `geo:undefined,undefined`." }, "responsibleOperatorDid": { "type": "string", "description": "DID of the responsible economic operator. Fallback for `bizLocation`, and (only when `issuer` is absent) for the operator-scope check on operator-scoped API keys." } } } ``` ## Used by - schema [UntpEventCredential](/schemas/UntpEventCredential.md) ================================================================================ # FILE: /okf/schemas/UntpEventProof.md ================================================================================ --- type: Schema title: UntpEventProof description: Credential proof. resource: https://opendpp-node.eu/openapi.json#/components/schemas/UntpEventProof tags: - schema timestamp: 2026-06-17T00:00:00Z --- Credential proof. Verified with ECDSA P-256 / SHA-256 over OpenDPP's deterministic key-sorted JSON canonicalization of the credential with `proofValue` blanked (NOT RFC 8785 JCS — not a conformant W3C Data Integrity suite). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `type` | string | no | e.g. | | `created` | string | no | — | | `proofPurpose` | string | no | e.g. | | `verificationMethod` | — | no | Either a key-identifier string or an embedded object carrying an x5c certificate chain. | | `proofValue` | string | yes | Base64 ECDSA signature. | ## JSON Schema ```json { "type": "object", "description": "Credential proof. Verified with ECDSA P-256 / SHA-256 over OpenDPP's deterministic key-sorted JSON canonicalization of the credential with `proofValue` blanked (NOT RFC 8785 JCS — not a conformant W3C Data Integrity suite).", "required": [ "proofValue" ], "properties": { "type": { "type": "string", "description": "e.g. `DataIntegrityProof`." }, "created": { "type": "string", "format": "date-time" }, "proofPurpose": { "type": "string", "description": "e.g. `assertionMethod`." }, "verificationMethod": { "oneOf": [ { "type": "string", "description": "Public-key identifier / DID URL." }, { "$ref": "#/components/schemas/UntpVerificationMethod" } ], "description": "Either a key-identifier string or an embedded object carrying an `x5c` certificate chain." }, "proofValue": { "type": "string", "description": "Base64 ECDSA signature. Stored verbatim with the event." } } } ``` ## Used by - schema [UntpEventCredential](/schemas/UntpEventCredential.md) ================================================================================ # FILE: /okf/schemas/UntpVerificationMethod.md ================================================================================ --- type: Schema title: UntpVerificationMethod description: Embedded verification-method object. resource: https://opendpp-node.eu/openapi.json#/components/schemas/UntpVerificationMethod tags: - schema timestamp: 2026-06-17T00:00:00Z --- Embedded verification-method object. The `x5c` chain (base64 DER, leaf first) is honoured ONLY when the node has eIDAS trust anchors configured, every certificate is currently valid, each link verifies against the next, the top is anchored, and the leaf attests the credential issuer — otherwise it is ignored and the registered tenant key is used instead. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | no | — | | `type` | string | no | — | | `controller` | string | no | — | | `x5c` | array | no | X.509 certificate chain, base64 DER, leaf first. | ## JSON Schema ```json { "type": "object", "description": "Embedded verification-method object. The `x5c` chain (base64 DER, leaf first) is honoured ONLY when the node has eIDAS trust anchors configured, every certificate is currently valid, each link verifies against the next, the top is anchored, and the leaf attests the credential issuer — otherwise it is ignored and the registered tenant key is used instead.", "properties": { "id": { "type": "string" }, "type": { "type": "string" }, "controller": { "type": "string" }, "x5c": { "type": "array", "items": { "type": "string" }, "description": "X.509 certificate chain, base64 DER, leaf first." } } } ``` ## Used by - schema [UntpEventProof](/schemas/UntpEventProof.md) ================================================================================ # FILE: /okf/schemas/UpdateOperatorRequest.md ================================================================================ --- type: Schema title: UpdateOperatorRequest description: Both fields are optional. resource: https://opendpp-node.eu/openapi.json#/components/schemas/UpdateOperatorRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- Both fields are optional. Values must be non-empty strings after trimming; anything else (missing, non-string, whitespace-only) is silently ignored. `regId` and `regIdScheme` cannot be changed. An omitted body or an empty object `{}` is accepted and returns the unchanged row. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `name` | string | no | New display name (trimmed). | | `role` | string | no | New supply-chain role, free text (trimmed) — e.g. | ## JSON Schema ```json { "type": "object", "description": "Both fields are optional. Values must be non-empty strings after trimming; anything else (missing, non-string, whitespace-only) is silently ignored. `regId` and `regIdScheme` cannot be changed. An omitted body or an empty object `{}` is accepted and returns the unchanged row.", "properties": { "name": { "type": "string", "description": "New display name (trimmed)." }, "role": { "type": "string", "description": "New supply-chain role, free text (trimmed) — e.g. `MANUFACTURER`, `IMPORTER`, `RETAILER`." } } } ``` ## Used by - [updateOperator](/operations/updateOperator.md) (`PATCH /api/v1/operators/{id}`) ================================================================================ # FILE: /okf/schemas/UpdateOperatorResponse.md ================================================================================ --- type: Schema title: UpdateOperatorResponse description: UpdateOperatorResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/UpdateOperatorResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `operator` | [OperatorRow](/schemas/OperatorRow.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "operator" ], "properties": { "success": { "type": "boolean", "const": true }, "operator": { "$ref": "#/components/schemas/OperatorRow" } } } ``` ## Used by - [updateOperator](/operations/updateOperator.md) (`PATCH /api/v1/operators/{id}`) ================================================================================ # FILE: /okf/schemas/ValidationErrorItem.md ================================================================================ --- type: Schema title: ValidationErrorItem description: One field-level finding from ESPR category validation. resource: https://opendpp-node.eu/openapi.json#/components/schemas/ValidationErrorItem tags: - schema timestamp: 2026-06-17T00:00:00Z --- One field-level finding from ESPR category validation. `path` uses dot/bracket notation into the metadata object (e.g. `materialComposition[0].percentage`). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `path` | string | yes | Dot/bracket path of the offending metadata field. | | `message` | string | yes | Technical validation message. | | `friendlyMessage` | string | no | Localized, human-friendly explanation (language from ?lang= or Accept-Language; 28 languages, default en). | ## JSON Schema ```json { "type": "object", "description": "One field-level finding from ESPR category validation. `path` uses dot/bracket notation into the metadata object (e.g. `materialComposition[0].percentage`).", "required": [ "path", "message" ], "properties": { "path": { "type": "string", "description": "Dot/bracket path of the offending metadata field." }, "message": { "type": "string", "description": "Technical validation message." }, "friendlyMessage": { "type": "string", "description": "Localized, human-friendly explanation (language from `?lang=` or `Accept-Language`; 28 languages, default `en`)." } } } ``` ## Used by - [createPassport](/operations/createPassport.md) (`POST /api/v1/passports`) - [ingestPassportFromAas](/operations/ingestPassportFromAas.md) (`POST /api/v1/passports/aas/ingest`) - schema [PassportIngestCreated](/schemas/PassportIngestCreated.md) - schema [PassportValidateOnlyResult](/schemas/PassportValidateOnlyResult.md) - schema [PassportValidateOnlyError](/schemas/PassportValidateOnlyError.md) - schema [PassportUpdateValidationError](/schemas/PassportUpdateValidationError.md) ================================================================================ # FILE: /okf/schemas/WebhookDeliveriesResponse.md ================================================================================ --- type: Schema title: WebhookDeliveriesResponse description: WebhookDeliveriesResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookDeliveriesResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `count` | integer | yes | — | | `deliveries` | array<[WebhookDeliveryRow](/schemas/WebhookDeliveryRow.md)> | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "count", "deliveries" ], "properties": { "success": { "type": "boolean", "const": true }, "count": { "type": "integer" }, "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookDeliveryRow" } } } } ``` ## Used by - [listWebhookDeliveries](/operations/listWebhookDeliveries.md) (`GET /api/v1/webhooks/deliveries`) ================================================================================ # FILE: /okf/schemas/WebhookDeliveryRow.md ================================================================================ --- type: Schema title: WebhookDeliveryRow description: One outbox delivery record (event-level). resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookDeliveryRow tags: - schema timestamp: 2026-06-17T00:00:00Z --- One outbox delivery record (event-level). The payload is not included. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | yes | Outbox record id. | | `event` | string | yes | Event type, e.g. | | `status` | string | yes | Overall delivery state. | | `retryCount` | integer | yes | Failed attempts so far (0–5). | | `lastAttempt` | string,null | yes | Timestamp of the most recent attempt, or null if never attempted. | | `nextRetryAt` | string,null | yes | When the next retry is eligible (null if delivered or dead-lettered). | | `errorMessage` | string,null | yes | Joined per-endpoint error text from the last failed attempt, or null. | | `createdAt` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "One outbox delivery record (event-level). The payload is not included.", "required": [ "id", "event", "status", "retryCount", "lastAttempt", "nextRetryAt", "errorMessage", "createdAt" ], "properties": { "id": { "type": "string", "description": "Outbox record id." }, "event": { "type": "string", "description": "Event type, e.g. `passport.sealed`." }, "status": { "type": "string", "enum": [ "PENDING", "DELIVERED", "FAILED" ], "description": "Overall delivery state. FAILED after 5 exhausted attempts (dead-lettered)." }, "retryCount": { "type": "integer", "description": "Failed attempts so far (0–5)." }, "lastAttempt": { "type": [ "string", "null" ], "format": "date-time", "description": "Timestamp of the most recent attempt, or null if never attempted." }, "nextRetryAt": { "type": [ "string", "null" ], "format": "date-time", "description": "When the next retry is eligible (null if delivered or dead-lettered)." }, "errorMessage": { "type": [ "string", "null" ], "description": "Joined per-endpoint error text from the last failed attempt, or null." }, "createdAt": { "type": "string", "format": "date-time" } } } ``` ## Used by - schema [WebhookDeliveriesResponse](/schemas/WebhookDeliveriesResponse.md) ================================================================================ # FILE: /okf/schemas/WebhookEnvelope.md ================================================================================ --- type: Schema title: WebhookEnvelope description: The signed body of every webhook delivery. resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookEnvelope tags: - schema timestamp: 2026-06-17T00:00:00Z --- The signed body of every webhook delivery. `data` is the public (redacted) JSON-LD passport document; `type` is the concrete event name (also in the `X-OpenDPP-Event` header); `id` is the stable delivery id (also in the `X-OpenDPP-Delivery` header) and is CONSTANT across all retries of the same event, so deduplicate on it for exactly-once processing; `created` is the event time (stable across retries, distinct from the per-attempt `X-OpenDPP-Timestamp`). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | yes | Stable delivery id; mirrors the X-OpenDPP-Delivery header; constant across retries. | | `type` | string | yes | Concrete event type (also in the X-OpenDPP-Event header). | | `created` | string | yes | Event creation/enqueue time (stable across retries). | | `data` | [PublicPassportJsonLd](/schemas/PublicPassportJsonLd.md) | yes | The public (redacted) JSON-LD passport document — the same shape resolvers return. | ## JSON Schema ```json { "type": "object", "description": "The signed body of every webhook delivery. `data` is the public (redacted) JSON-LD passport document; `type` is the concrete event name (also in the `X-OpenDPP-Event` header); `id` is the stable delivery id (also in the `X-OpenDPP-Delivery` header) and is CONSTANT across all retries of the same event, so deduplicate on it for exactly-once processing; `created` is the event time (stable across retries, distinct from the per-attempt `X-OpenDPP-Timestamp`).", "required": [ "id", "type", "created", "data" ], "properties": { "id": { "type": "string", "description": "Stable delivery id; mirrors the `X-OpenDPP-Delivery` header; constant across retries." }, "type": { "type": "string", "enum": [ "passport.ingested", "passport.updated", "passport.sealed", "passport.recalled", "passport.status_updated" ], "description": "Concrete event type (also in the `X-OpenDPP-Event` header). Never the `*` filter value." }, "created": { "type": "string", "format": "date-time", "description": "Event creation/enqueue time (stable across retries)." }, "data": { "$ref": "#/components/schemas/PublicPassportJsonLd", "description": "The public (redacted) JSON-LD passport document — the same shape resolvers return." } } } ``` ================================================================================ # FILE: /okf/schemas/WebhookEventFilter.md ================================================================================ --- type: Schema title: WebhookEventFilter description: Subscribable event filter values. resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookEventFilter tags: - schema timestamp: 2026-06-17T00:00:00Z --- Subscribable event filter values. `*` matches every emitted event. `passport.status_updated` (decommission/reactivate) and `passport.updated` (in-place edit of a published passport) are now first-class subscribable filters. ## JSON Schema ```json { "type": "string", "enum": [ "passport.ingested", "passport.updated", "passport.sealed", "passport.recalled", "passport.status_updated", "*" ], "description": "Subscribable event filter values. `*` matches every emitted event. `passport.status_updated` (decommission/reactivate) and `passport.updated` (in-place edit of a published passport) are now first-class subscribable filters." } ``` ## Used by - schema [WebhookSubscriptionCreateRequest](/schemas/WebhookSubscriptionCreateRequest.md) - schema [WebhookSubscriptionRow](/schemas/WebhookSubscriptionRow.md) - schema [WebhookSubscriptionUpdateRequest](/schemas/WebhookSubscriptionUpdateRequest.md) ================================================================================ # FILE: /okf/schemas/WebhookSecretRotateResponse.md ================================================================================ --- type: Schema title: WebhookSecretRotateResponse description: WebhookSecretRotateResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookSecretRotateResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | — | | `subscription` | [WebhookSubscriptionWithSecret](/schemas/WebhookSubscriptionWithSecret.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "message", "subscription" ], "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string" }, "subscription": { "$ref": "#/components/schemas/WebhookSubscriptionWithSecret" } } } ``` ## Used by - [rotateWebhookSecret](/operations/rotateWebhookSecret.md) (`POST /api/v1/webhooks/subscriptions/{id}/rotate-secret`) ================================================================================ # FILE: /okf/schemas/WebhookSubscriptionCreateRequest.md ================================================================================ --- type: Schema title: WebhookSubscriptionCreateRequest description: WebhookSubscriptionCreateRequest resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookSubscriptionCreateRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `url` | string | yes | Absolute http(s) endpoint URL of your receiver (e.g. | | `events` | array<[WebhookEventFilter](/schemas/WebhookEventFilter.md)> | yes | Non-empty list of event filters. | ## JSON Schema ```json { "type": "object", "required": [ "url", "events" ], "properties": { "url": { "type": "string", "format": "uri", "description": "Absolute http(s) endpoint URL of your receiver (e.g. a PLM/ERP integration endpoint). DNS-resolved and SSRF-guarded at registration: malformed URLs, loopback, private (RFC 1918/CGNAT), link-local/cloud-metadata, multicast, and equivalent IPv6 ranges are rejected with 400. Redirects are never followed at delivery time." }, "events": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/WebhookEventFilter" }, "description": "Non-empty list of event filters. Any value outside the allowed set is rejected with 400." } } } ``` ## Used by - [createWebhookSubscription](/operations/createWebhookSubscription.md) (`POST /api/v1/webhooks/subscriptions`) ================================================================================ # FILE: /okf/schemas/WebhookSubscriptionCreateResponse.md ================================================================================ --- type: Schema title: WebhookSubscriptionCreateResponse description: WebhookSubscriptionCreateResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookSubscriptionCreateResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | — | | `subscription` | [WebhookSubscriptionWithSecret](/schemas/WebhookSubscriptionWithSecret.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "message", "subscription" ], "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "const": "Webhook subscription registered successfully" }, "subscription": { "$ref": "#/components/schemas/WebhookSubscriptionWithSecret" } } } ``` ## Used by - [createWebhookSubscription](/operations/createWebhookSubscription.md) (`POST /api/v1/webhooks/subscriptions`) ================================================================================ # FILE: /okf/schemas/WebhookSubscriptionDeleteResponse.md ================================================================================ --- type: Schema title: WebhookSubscriptionDeleteResponse description: WebhookSubscriptionDeleteResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookSubscriptionDeleteResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "message" ], "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "const": "Webhook subscription successfully deleted" } } } ``` ## Used by - [deleteWebhookSubscription](/operations/deleteWebhookSubscription.md) (`DELETE /api/v1/webhooks/subscriptions/{id}`) ================================================================================ # FILE: /okf/schemas/WebhookSubscriptionListResponse.md ================================================================================ --- type: Schema title: WebhookSubscriptionListResponse description: WebhookSubscriptionListResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookSubscriptionListResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `subscriptions` | array<[WebhookSubscriptionRow](/schemas/WebhookSubscriptionRow.md)> | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "subscriptions" ], "properties": { "success": { "type": "boolean", "const": true }, "subscriptions": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookSubscriptionRow" } } } } ``` ## Used by - [listWebhookSubscriptions](/operations/listWebhookSubscriptions.md) (`GET /api/v1/webhooks/subscriptions`) ================================================================================ # FILE: /okf/schemas/WebhookSubscriptionRow.md ================================================================================ --- type: Schema title: WebhookSubscriptionRow description: A webhook subscription row with the HMAC signing secret stripped (it is shown exactly once, in the 201 create response). resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookSubscriptionRow tags: - schema timestamp: 2026-06-17T00:00:00Z --- A webhook subscription row with the HMAC signing `secret` stripped (it is shown exactly once, in the 201 create response). ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `id` | string | yes | Subscription id. | | `tenantId` | string | yes | Owning workspace (tenant) id. | | `url` | string | yes | Receiver endpoint URL. | | `events` | array<[WebhookEventFilter](/schemas/WebhookEventFilter.md)> | yes | Event filters this subscription matches (validated at creation). | | `isActive` | boolean | yes | true while the subscription receives deliveries; only active subscriptions are delivered to. | | `createdAt` | string | yes | — | | `updatedAt` | string | yes | — | ## JSON Schema ```json { "type": "object", "description": "A webhook subscription row with the HMAC signing `secret` stripped (it is shown exactly once, in the 201 create response).", "required": [ "id", "tenantId", "url", "events", "isActive", "createdAt", "updatedAt" ], "properties": { "id": { "type": "string", "description": "Subscription id." }, "tenantId": { "type": "string", "description": "Owning workspace (tenant) id." }, "url": { "type": "string", "format": "uri", "description": "Receiver endpoint URL." }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEventFilter" }, "description": "Event filters this subscription matches (validated at creation)." }, "isActive": { "type": "boolean", "description": "`true` while the subscription receives deliveries; only active subscriptions are delivered to. Toggle with `PATCH /api/v1/webhooks/subscriptions/{id}` (`isActive`) to pause/resume without deleting." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } } ``` ## Used by - schema [WebhookSubscriptionWithSecret](/schemas/WebhookSubscriptionWithSecret.md) - schema [WebhookSubscriptionListResponse](/schemas/WebhookSubscriptionListResponse.md) - schema [WebhookSubscriptionUpdateResponse](/schemas/WebhookSubscriptionUpdateResponse.md) ================================================================================ # FILE: /okf/schemas/WebhookSubscriptionUpdateRequest.md ================================================================================ --- type: Schema title: WebhookSubscriptionUpdateRequest description: All fields optional; include only what you want to change. resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookSubscriptionUpdateRequest tags: - schema timestamp: 2026-06-17T00:00:00Z --- All fields optional; include only what you want to change. The secret is not editable here. ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `url` | string | no | New receiver URL. | | `events` | array<[WebhookEventFilter](/schemas/WebhookEventFilter.md)> | no | Replacement (non-empty) event-filter set. | | `isActive` | boolean | no | Pause (false) or resume (true) deliveries. | ## JSON Schema ```json { "type": "object", "description": "All fields optional; include only what you want to change. The secret is not editable here.", "properties": { "url": { "type": "string", "format": "uri", "description": "New receiver URL. Re-validated by the SSRF guard." }, "events": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/WebhookEventFilter" }, "description": "Replacement (non-empty) event-filter set." }, "isActive": { "type": "boolean", "description": "Pause (`false`) or resume (`true`) deliveries." } } } ``` ## Used by - [updateWebhookSubscription](/operations/updateWebhookSubscription.md) (`PATCH /api/v1/webhooks/subscriptions/{id}`) ================================================================================ # FILE: /okf/schemas/WebhookSubscriptionUpdateResponse.md ================================================================================ --- type: Schema title: WebhookSubscriptionUpdateResponse description: WebhookSubscriptionUpdateResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookSubscriptionUpdateResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `message` | string | no | Present when a change was applied ("Webhook subscription updated"); absent when the body had no recognized fields. | | `subscription` | [WebhookSubscriptionRow](/schemas/WebhookSubscriptionRow.md) | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "subscription" ], "properties": { "success": { "type": "boolean", "const": true }, "message": { "type": "string", "description": "Present when a change was applied (`\"Webhook subscription updated\"`); absent when the body had no recognized fields." }, "subscription": { "$ref": "#/components/schemas/WebhookSubscriptionRow" } } } ``` ## Used by - [updateWebhookSubscription](/operations/updateWebhookSubscription.md) (`PATCH /api/v1/webhooks/subscriptions/{id}`) ================================================================================ # FILE: /okf/schemas/WebhookSubscriptionWithSecret.md ================================================================================ --- type: Schema title: WebhookSubscriptionWithSecret description: The full subscription row as returned ONLY by the 201 create response — includes the HMAC-SHA256 signing secret. resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookSubscriptionWithSecret tags: - schema timestamp: 2026-06-17T00:00:00Z --- The full subscription row as returned ONLY by the 201 create response — includes the HMAC-SHA256 signing secret. ## JSON Schema ```json { "description": "The full subscription row as returned ONLY by the 201 create response — includes the HMAC-SHA256 signing secret.", "allOf": [ { "$ref": "#/components/schemas/WebhookSubscriptionRow" }, { "type": "object", "required": [ "secret" ], "properties": { "secret": { "type": "string", "pattern": "^whsec_[0-9a-f]{32}$", "description": "Server-generated HMAC-SHA256 signing key (`whsec_` + 32 lowercase hex chars). Shown ONCE — here and in the `rotate-secret` response only; the list endpoint strips it. The FULL string, including the `whsec_` prefix, is the HMAC key for delivery signatures. Rotate it with `POST /api/v1/webhooks/subscriptions/{id}/rotate-secret`." } } } ] } ``` ## Used by - schema [WebhookSubscriptionCreateResponse](/schemas/WebhookSubscriptionCreateResponse.md) - schema [WebhookSecretRotateResponse](/schemas/WebhookSecretRotateResponse.md) ================================================================================ # FILE: /okf/schemas/WebhookTestResult.md ================================================================================ --- type: Schema title: WebhookTestResult description: WebhookTestResult resource: https://opendpp-node.eu/openapi.json#/components/schemas/WebhookTestResult tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | The request was processed (NOT whether the receiver accepted it — see delivered). | | `event` | string | yes | The event type sent. | | `url` | string | yes | The receiver URL the test was sent to. | | `delivered` | boolean | yes | true if the receiver returned a 2xx within the 5s timeout. | | `statusCode` | integer,null | yes | The receiver's HTTP status, or null on a transport/SSRF error. | | `error` | string,null | yes | Transport/SSRF error message when delivery failed, else null. | ## JSON Schema ```json { "type": "object", "required": [ "success", "event", "url", "delivered", "statusCode", "error" ], "properties": { "success": { "type": "boolean", "const": true, "description": "The request was processed (NOT whether the receiver accepted it — see `delivered`)." }, "event": { "type": "string", "description": "The event type sent." }, "url": { "type": "string", "format": "uri", "description": "The receiver URL the test was sent to." }, "delivered": { "type": "boolean", "description": "`true` if the receiver returned a 2xx within the 5s timeout." }, "statusCode": { "type": [ "integer", "null" ], "description": "The receiver's HTTP status, or null on a transport/SSRF error." }, "error": { "type": [ "string", "null" ], "description": "Transport/SSRF error message when delivery failed, else null." } } } ``` ## Used by - [testWebhookSubscription](/operations/testWebhookSubscription.md) (`POST /api/v1/webhooks/subscriptions/{id}/test`) ================================================================================ # FILE: /okf/schemas/WhoamiResponse.md ================================================================================ --- type: Schema title: WhoamiResponse description: WhoamiResponse resource: https://opendpp-node.eu/openapi.json#/components/schemas/WhoamiResponse tags: - schema timestamp: 2026-06-17T00:00:00Z --- ## Schema | Property | Type | Required | Description | |----------|------|----------|-------------| | `success` | boolean | yes | — | | `tenant` | object | yes | — | | `auth` | object | yes | — | | `usage` | object | yes | — | ## JSON Schema ```json { "type": "object", "required": [ "success", "tenant", "auth", "usage" ], "properties": { "success": { "type": "boolean", "const": true }, "tenant": { "type": "object", "required": [ "id", "name", "subdomain", "tier", "subscriptionStatus" ], "properties": { "id": { "type": "string", "description": "Workspace (tenant) id." }, "name": { "type": "string", "description": "Workspace company name." }, "subdomain": { "type": [ "string", "null" ], "description": "Workspace subdomain (`.opendpp-node.eu`), or null if none is assigned." }, "tier": { "type": "string", "description": "Subscription tier (e.g. `pilot`, `micro`, `starter`, `growth`, `scale`, `enterprise`)." }, "subscriptionStatus": { "type": [ "string", "null" ], "description": "Billing status string (e.g. `active`, `past_due`). When not `active`, write operations may return `402`. No amounts or processor identifiers are exposed." } } }, "auth": { "type": "object", "required": [ "role", "permissions", "isApiKeySession", "operatorId" ], "properties": { "role": { "type": "string", "description": "The principal's role." }, "permissions": { "type": "array", "items": { "type": "string" }, "description": "Effective permission strings, re-derived server-side from the role (never trusted from the token). May include wildcards like `operator:*`." }, "isApiKeySession": { "type": "boolean", "description": "`true` when authenticated with an API key, `false` for a session JWT." }, "operatorId": { "type": [ "string", "null" ], "description": "The economic operator this credential is scoped to, or `null` for a workspace-wide key. A scoped key's writes and reads are restricted to this operator." } } }, "usage": { "type": "object", "required": [ "activePassports", "passportLimit" ], "properties": { "activePassports": { "type": "integer", "description": "Active (non-draft, non-archived) passports counted against the quota — operator-scoped for a scoped key." }, "passportLimit": { "type": [ "integer", "null" ], "description": "Tier passport quota, or `null` for an unlimited tier." } } } } } ``` ## Used by - [whoami](/operations/whoami.md) (`GET /api/v1/whoami`) ================================================================================ # FILE: /okf/webhooks/index.md ================================================================================ # Webhook events * [passport.ingested](passport.ingested.md) - Sent when a passport becomes active for the first time: a non-draft POST /api/v1/passports create, the first publish of a draft via PUT /ap… * [passport.recalled](passport.recalled.md) - Sent when PUT /api/v1/passports/{id}/status transitions a passport to RECALLED, transactionally with the status write. * [passport.sealed](passport.sealed.md) - Sent when a passport is sealed via POST /api/v1/passports/{id}/seal, transactionally with the seal write. * [passport.status_updated](passport.status_updated.md) - Sent when PUT /api/v1/passports/{id}/status performs a non-recall transition: decommissioning (DECOMMISSIONED, which also sets retentionUnt… * [passport.updated](passport.updated.md) - Sent when an already-published (non-draft) passport's content is edited in place via PUT /api/v1/passports/{id}, transactionally with the u… ================================================================================ # FILE: /okf/webhooks/passport.ingested.md ================================================================================ --- type: Webhook Event title: passport.ingested description: Passport created or first published resource: https://opendpp-node.eu/openapi.json#/webhooks/passport.ingested tags: - webhook - event timestamp: 2026-06-17T00:00:00Z --- Sent when a passport becomes active for the first time: a non-draft `POST /api/v1/passports` create, the **first publish** of a draft via `PUT /api/v1/passports/{id}`, or each successfully ingested row of `POST /api/v1/passports/bulk`. Draft creation does NOT emit it, non-publish updates and deletes emit nothing, and **AAS ingestion (`POST /api/v1/passports/aas/ingest`) emits no webhook events at all**. The payload is the freshly created passport: `status: "ACTIVE"` with `digitalSeal` and `proof` still `null` (unless a previously sealed draft is re-published). Emission is enqueued transactionally with the passport write (outbox pattern) and delivered asynchronously to every active subscription whose filter contains `passport.ingested` or `*`. **Delivery contract** (sender `User-Agent: OpenDPP-Webhook-Outbox/1.0`): - The body is a JSON **envelope** `{ id, type, created, data }`: `data` is the **public (redacted) JSON-LD passport document**, `type` is the event name (also in the `X-OpenDPP-Event` header), and `id` is the stable delivery id (also in the `X-OpenDPP-Delivery` header, constant across retries). Owner-tier fields (facility street address, restricted metadata values) are never included. The owner-only `facilityDetails` metadata key **always appears with the masked value `[REDACTED - Privileged Access Required]`** — in `metadata`, flattened at top level, and in the `@context` term map — even when the passport never set that key; restricted metadata keys likewise appear masked, with their true leaf hashes preserved in `proof.redactedLeaves` once sealed. - **Success = any HTTP 2xx** returned within the **5-second** timeout. Redirects are **never followed** — a 3xx counts as failure. The response body is ignored. - **Retries:** up to **5 delivery attempts** total. Failed attempts 1–4 schedule the next attempt with exponential backoff — ~1 min, 5 min, 30 min, then 2 h after the previous failure. The **5th failed attempt dead-letters the event** (no further deliveries) and the workspace receives an in-app notification. - **Per-subscription dedup:** endpoints that already returned 2xx are not re-POSTed when the same event is retried for sibling subscriptions. Still treat delivery as at-least-once (a 2xx the sender fails to record can re-deliver), but the **`X-OpenDPP-Delivery`** id is STABLE across retries, so deduplicate on it for exactly-once. - **Signature verification:** compute `HMAC-SHA256(secret, X-OpenDPP-Timestamp + "." + rawBody)` keyed with the FULL `whsec_…` secret (including the prefix), hex-encode lowercase, constant-time-compare with `X-OpenDPP-Signature` (bare hex — no `v1=`/`sha256=` prefix). Verify over the **raw body bytes** before any JSON parsing. Reject timestamps older than ~5 minutes; the timestamp and signature are **re-minted on every retry attempt**, so retried deliveries always carry a fresh, valid pair. ## Delivery Deliveries are HMAC-SHA256-signed `POST` requests with retry/backoff. Subscribe and manage endpoints via the [Webhooks](/tags/webhooks.md) tag. ## Payload Schema: [WebhookEnvelope](/schemas/WebhookEnvelope.md). ================================================================================ # FILE: /okf/webhooks/passport.recalled.md ================================================================================ --- type: Webhook Event title: passport.recalled description: Passport recalled resource: https://opendpp-node.eu/openapi.json#/webhooks/passport.recalled tags: - webhook - event timestamp: 2026-06-17T00:00:00Z --- Sent when `PUT /api/v1/passports/{id}/status` transitions a passport to `RECALLED`, transactionally with the status write. The payload is the public JSON-LD passport with `status: "RECALLED"` (seal/proof fields reflect whatever state the passport was in when recalled). Delivered to every active subscription whose filter contains `passport.recalled` or `*`. **Delivery contract** (sender `User-Agent: OpenDPP-Webhook-Outbox/1.0`): - The body is a JSON **envelope** `{ id, type, created, data }`: `data` is the **public (redacted) JSON-LD passport document**, `type` is the event name (also in the `X-OpenDPP-Event` header), and `id` is the stable delivery id (also in the `X-OpenDPP-Delivery` header, constant across retries). The owner-only `facilityDetails` metadata key **always appears with the masked value `[REDACTED - Privileged Access Required]`** (in `metadata`, flattened at top level, and in the `@context` term map) even when the passport never set that key; restricted metadata keys likewise appear masked. - **Success = any HTTP 2xx** within the **5-second** timeout. Redirects are **never followed** (3xx = failure). Response body ignored. - **Retries:** up to **5 delivery attempts** total. Failed attempts 1–4 schedule the next attempt ~1 min / 5 min / 30 min / 2 h after the previous failure; the **5th failed attempt dead-letters the event** and the workspace is notified in-app. - **Per-subscription dedup:** endpoints that already returned 2xx are not re-POSTed on retries; still treat delivery as at-least-once, but the **`X-OpenDPP-Delivery`** id is STABLE across retries, so deduplicate on it for exactly-once. - **Signature verification:** `HMAC-SHA256(secret, X-OpenDPP-Timestamp + "." + rawBody)` keyed with the FULL `whsec_…` secret, lowercase hex, constant-time compare against `X-OpenDPP-Signature` (bare hex, no scheme prefix). Verify over the **raw body bytes**; reject >~5 min timestamp skew; timestamp+signature are re-minted per retry attempt. ## Delivery Deliveries are HMAC-SHA256-signed `POST` requests with retry/backoff. Subscribe and manage endpoints via the [Webhooks](/tags/webhooks.md) tag. ## Payload Schema: [WebhookEnvelope](/schemas/WebhookEnvelope.md). ================================================================================ # FILE: /okf/webhooks/passport.sealed.md ================================================================================ --- type: Webhook Event title: passport.sealed description: Passport sealed with an eIDAS advanced seal resource: https://opendpp-node.eu/openapi.json#/webhooks/passport.sealed tags: - webhook - event timestamp: 2026-06-17T00:00:00Z --- Sent when a passport is sealed via `POST /api/v1/passports/{id}/seal`, transactionally with the seal write. The payload carries the populated `digitalSeal`, `signingPublicKey`, and the `proof` block: `merkleRoot` always; an `x5c` certificate chain binding the signing key to the tenant's legal identity **when the tenant's signing key has an issued chain**; an **optional** `rfc3161` trusted timestamp; and `redactedLeaves` hashes **when the passport carries masked metadata keys**. Delivered to every active subscription whose filter contains `passport.sealed` or `*`. **Delivery contract** (sender `User-Agent: OpenDPP-Webhook-Outbox/1.0`): - The body is a JSON **envelope** `{ id, type, created, data }`: `data` is the **public (redacted) JSON-LD passport document**, `type` is the event name (also in the `X-OpenDPP-Event` header), and `id` is the stable delivery id (also in the `X-OpenDPP-Delivery` header, constant across retries). The owner-only `facilityDetails` metadata key **always appears with the masked value `[REDACTED - Privileged Access Required]`** (in `metadata`, flattened at top level, and in the `@context` term map) even when the passport never set that key; restricted metadata keys likewise appear masked, with their true leaf hashes in `proof.redactedLeaves`. - **Success = any HTTP 2xx** within the **5-second** timeout. Redirects are **never followed** (3xx = failure). Response body ignored. - **Retries:** up to **5 delivery attempts** total. Failed attempts 1–4 schedule the next attempt ~1 min / 5 min / 30 min / 2 h after the previous failure; the **5th failed attempt dead-letters the event** and the workspace is notified in-app. - **Per-subscription dedup:** endpoints that already returned 2xx are not re-POSTed on retries; still treat delivery as at-least-once, but the **`X-OpenDPP-Delivery`** id is STABLE across retries, so deduplicate on it for exactly-once. - **Signature verification:** `HMAC-SHA256(secret, X-OpenDPP-Timestamp + "." + rawBody)` keyed with the FULL `whsec_…` secret, lowercase hex, constant-time compare against `X-OpenDPP-Signature` (bare hex, no scheme prefix). Verify over the **raw body bytes**; reject >~5 min timestamp skew; timestamp+signature are re-minted per retry attempt. ## Delivery Deliveries are HMAC-SHA256-signed `POST` requests with retry/backoff. Subscribe and manage endpoints via the [Webhooks](/tags/webhooks.md) tag. ## Payload Schema: [WebhookEnvelope](/schemas/WebhookEnvelope.md). ================================================================================ # FILE: /okf/webhooks/passport.status_updated.md ================================================================================ --- type: Webhook Event title: passport.status_updated description: Passport status changed (decommission or reactivation) resource: https://opendpp-node.eu/openapi.json#/webhooks/passport.status_updated tags: - webhook - event timestamp: 2026-06-17T00:00:00Z --- Sent when `PUT /api/v1/passports/{id}/status` performs a non-recall transition: decommissioning (`DECOMMISSIONED`, which also sets `retentionUntil` and starts the retention clock) or reactivation back to `ACTIVE`. Subscribe to it directly with the `passport.status_updated` filter (or `*`). **Delivery contract** (sender `User-Agent: OpenDPP-Webhook-Outbox/1.0`): - The body is a JSON **envelope** `{ id, type, created, data }`: `data` is the **public (redacted) JSON-LD passport document**, `type` is the event name (also in the `X-OpenDPP-Event` header), and `id` is the stable delivery id (also in the `X-OpenDPP-Delivery` header, constant across retries). The owner-only `facilityDetails` metadata key **always appears with the masked value `[REDACTED - Privileged Access Required]`** (in `metadata`, flattened at top level, and in the `@context` term map) even when the passport never set that key; restricted metadata keys likewise appear masked. - **Success = any HTTP 2xx** within the **5-second** timeout. Redirects are **never followed** (3xx = failure). Response body ignored. - **Retries:** up to **5 delivery attempts** total. Failed attempts 1–4 schedule the next attempt ~1 min / 5 min / 30 min / 2 h after the previous failure; the **5th failed attempt dead-letters the event** and the workspace is notified in-app. - **Per-subscription dedup:** endpoints that already returned 2xx are not re-POSTed on retries; still treat delivery as at-least-once, but the **`X-OpenDPP-Delivery`** id is STABLE across retries, so deduplicate on it for exactly-once. - **Signature verification:** `HMAC-SHA256(secret, X-OpenDPP-Timestamp + "." + rawBody)` keyed with the FULL `whsec_…` secret, lowercase hex, constant-time compare against `X-OpenDPP-Signature` (bare hex, no scheme prefix). Verify over the **raw body bytes**; reject >~5 min timestamp skew; timestamp+signature are re-minted per retry attempt. ## Delivery Deliveries are HMAC-SHA256-signed `POST` requests with retry/backoff. Subscribe and manage endpoints via the [Webhooks](/tags/webhooks.md) tag. ## Payload Schema: [WebhookEnvelope](/schemas/WebhookEnvelope.md). ================================================================================ # FILE: /okf/webhooks/passport.updated.md ================================================================================ --- type: Webhook Event title: passport.updated description: Passport edited in place resource: https://opendpp-node.eu/openapi.json#/webhooks/passport.updated tags: - webhook - event timestamp: 2026-06-17T00:00:00Z --- Sent when an already-published (non-draft) passport's content is edited in place via `PUT /api/v1/passports/{id}`, transactionally with the update write. This is **distinct from first publish** (that emits `passport.ingested`, not this event) and is **never** emitted for sealed passports — in-place edits of a sealed passport are rejected with `403`. The payload is the updated public JSON-LD passport with `status: "ACTIVE"`. Delivered to every active subscription whose filter contains `passport.updated` or `*`. **Delivery contract** (sender `User-Agent: OpenDPP-Webhook-Outbox/1.0`): - The body is a JSON **envelope** `{ id, type, created, data }`: `data` is the **public (redacted) JSON-LD passport document**, `type` is the event name (also in the `X-OpenDPP-Event` header), and `id` is the stable delivery id (also in the `X-OpenDPP-Delivery` header, constant across retries). The owner-only `facilityDetails` metadata key **always appears with the masked value `[REDACTED - Privileged Access Required]`** (in `metadata`, flattened at top level, and in the `@context` term map) even when the passport never set that key; restricted metadata keys likewise appear masked. - **Success = any HTTP 2xx** within the **5-second** timeout. Redirects are **never followed** (3xx = failure). Response body ignored. - **Retries:** up to **5 delivery attempts** total. Failed attempts 1–4 schedule the next attempt ~1 min / 5 min / 30 min / 2 h after the previous failure; the **5th failed attempt dead-letters the event** and the workspace is notified in-app. - **Per-subscription dedup:** endpoints that already returned 2xx are not re-POSTed on retries; still treat delivery as at-least-once, but the **`X-OpenDPP-Delivery`** id is STABLE across retries, so deduplicate on it for exactly-once. - **Signature verification:** `HMAC-SHA256(secret, X-OpenDPP-Timestamp + "." + rawBody)` keyed with the FULL `whsec_…` secret, lowercase hex, constant-time compare against `X-OpenDPP-Signature` (bare hex, no scheme prefix). Verify over the **raw body bytes**; reject >~5 min timestamp skew; timestamp+signature are re-minted per retry attempt. ## Delivery Deliveries are HMAC-SHA256-signed `POST` requests with retry/backoff. Subscribe and manage endpoints via the [Webhooks](/tags/webhooks.md) tag. ## Payload Schema: [WebhookEnvelope](/schemas/WebhookEnvelope.md). ================================================================================ # FILE: /okf/log.md ================================================================================ # Log ## 2026-06-17 **Update** — Knowledge bundle generated for OpenDPP Integration API v1.4.0. For the full per-version API contract history see [`/openapi.json`](https://opendpp-node.eu/openapi.json) and the project CHANGELOG. ================================================================================ # FILE: /okf/index.md ================================================================================ --- okf_version: "0.1" --- # OpenDPP Integration API — Knowledge Bundle > An OKF (Open Knowledge Format) projection of the public [OpenDPP Integration API](https://opendpp-node.eu/api-reference), for ingestion by AI agents and humans alike. Generated from [`/openapi.json`](https://opendpp-node.eu/openapi.json) (API v1.4.0). # Concepts * [Overview](overview.md) - What the OpenDPP Integration API does, how it is organised, and where to start. * [Log](log.md) - Generation history. # Subdirectories * [guides](guides/index.md) - Curated integration playbooks and references. * [tags](tags/index.md) - One overview per API domain. * [operations](operations/index.md) - One concept per endpoint. * [schemas](schemas/index.md) - One concept per request/response object. * [webhooks](webhooks/index.md) - Lifecycle events you can subscribe to.