Skip to content

About

Digital Product Passport API (EN 18219/18221/18222/18223) — public snapshot

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

DPP Service — Digital Product Passport API

Rails 7 API-only service implementing the Digital Product Passport endpoints defined by the CEN/CENELEC JTC 24 standards. The REST contract is derived from EN 18222:2026 (APIs for product passport lifecycle management), with payloads from EN 18223:2026 (system interoperability / semantic model).

ℹ️ Public snapshot. This repository is a snapshot of an internal development repository, published without history and refreshed periodically. Pull requests here cannot be merged — please open an issue instead, or contact OwnYourData.

⚠️ Six of the eight JTC 24 standards were published as EN …:2026 and are cited in the Official Journal; EN 18239 and EN 18246 are still drafts and are cited as prEN. The payload, the method set and the identifier schemes here follow the published editions. Two deviations are deliberate and named where they occur: the element path is a path of element identifiers rather than RFC 9535 JSONPath (EN 18222:2026 8.1), and the two collection operations are an addition beyond the published method set. The semanticId base URI https://jtc24/... is still a placeholder.

Running it

docs/Standalone.md is the guide — how to set the service up so it stores Digital Product Passports in its own database and runs without any external infrastructure beyond a PostgreSQL server.

bundle install
bin/rails db:create db:migrate db:seed
bin/rails server

Swagger UI at http://localhost:3000/api-docs, rendered from the OpenAPI 3.0 description in docs/openapi.yaml.

Hosted instance

OwnYourData operates a public instance at https://dpp-service.ownyourdata.eu. That deployment does not keep passport documents in its own database: it stores them in a hosting pod operated by the data intermediary DID FlexCo, which then also serves them publicly. The service keeps only an index and the storage configuration.

That mode is out of scope for this snapshot — it needs a pod provisioned by the intermediary and credentials issued by them. Everything documented here is the stand-alone path, which is fully functional on its own.

The hosted instance runs with DPP_AUTH_MODE=did: writing requires a bearer token that is a self-issued JWT, signed with the document key of the issuer's did:oyd, and only the DID that created a passport may change it. Reading is public and needs no token. A stand-alone instance starts in permissive mode, where the signature is not checked — see docs/Standalone.md.

did:oyd is one of the DID methods EN 18219:2026 admits, specified in the OYD DID Method and implemented by the oydid gem and CLI (source). The identifier is the hash of its own DID document, so it is self-certifying: what the specification calls calculate_hash and retrieve_log is what oydid read --show-verification recomputes step by step. The service uses it for the passport identifier and for actor identities; nothing in the API is specific to it, and docs/Identifiers.md says which identifier is which.

Documentation

Two demonstrations of the same lifecycle, one from each side. Start with whichever matches how you would use it.

Demo What it shows
docs/DPP-Service-Demo.md the API, on the command line — a passport created, corrected, handed to a custodian and ended, as curl calls to paste
docs/DPP-Manager-Demo.md the application, with screenshots — the same lifecycle as an economic operator sees it, in the DPP Manager on their own computer

The rest:

File What it covers
docs/Standalone.md running the service on your own, without the intermediary's pod
docs/EXAMPLES.md the walkthrough: issuing a token, then every endpoint as copy-paste curl calls
docs/examples-lightbulb.md one complete worked example (an LED lamp) with full request and response payloads
docs/Identifiers.md the six things called "identifier" in the DPP context, how they relate, and which of them ends up on the data carrier
docs/Integrity.md how the identifier, the version chain and the payload commitment fit together, and what a reader can verify without trusting anyone
docs/AccessClasses.md the vocabulary of access classes (public, legitimate interest, authority), the access policy that assigns one class to every attribute of a passport, how a policy is registered and attached to a passport, access grants and revoking them, what each reader gets on the read paths, and the access log
bin/verify_dpp scan a carrier link, resolve the passport, recompute the hash: bundle exec ruby bin/verify_dpp <carrier URL>
bin/attest_payload the holder side of the same coin: hash what the custodian delivers and sign the entry with your own document key: bundle exec ruby bin/attest_payload <carrier URL> --keys <file>
docs/verification/ the record of the run that moved a live passport between two custodians without touching the printed carrier
docs/openapi.yaml the machine-readable contract: an OpenAPI 3.0 description of every endpoint, which the service also serves through a Swagger UI at /api-docs

Endpoints (EN 18222:2026, Tables 16–18)

Method (standard) HTTP Path
ReadDPPById GET /dpp/v1/dpps/:dpp_id
CreateDPP POST /dpp/v1/dpps
UpdateDPPById (RFC 7396) PATCH /dpp/v1/dpps/:dpp_id
DeleteDPPById DELETE /dpp/v1/dpps/:dpp_id
ReadDPPByProductId GET /dpp/v1/dppsByProductId/:product_id
ReadDPPVersionByIdAndDate GET /dpp/v1/dppsByIdAndDate/:dpp_id?date=
ReadDPPIdsByProductIds POST /dpp/v1/dppsByProductIds
RegisterProductDPP POST /dpp/v1/registerDPP
ReadDataElement GET /dpp/v1/dpps/:dpp_id/elements/*element_path
UpdateDataElement PATCH /dpp/v1/dpps/:dpp_id/elements/*element_path

Beyond the standard's methods:

HTTP Path
change of custodian POST /dpp/v1/dpps/:dpp_id/custody
read the mandate held for a passport GET /dpp/v1/dpps/:dpp_id/delegation
hand over a fresh mandate POST /dpp/v1/dpps/:dpp_id/delegation
service discovery (service DID, audience, reader DIDs, authority issuers) GET /.well-known/dpp-service
read a collection directly GET /dpp/v1/dpps/:dpp_id/collections/:element_id
update a collection directly PATCH /dpp/v1/dpps/:dpp_id/collections/:element_id
register the access policy of a product type PUT /dpp/v1/access-policies
list the gaps of a passport's access policy GET /dpp/v1/dpps/:dpp_id/access-policy
register an access grant PUT /dpp/v1/access-grants
list the access grants one has issued GET /dpp/v1/access-grants
revoke an access grant one has issued DELETE /dpp/v1/access-grants/:jti
read the entries of the access log that concern one GET /dpp/v1/access-log

A collection is a subclass of DataElement (EN 18223:2026 4.1.2.4) and is reachable through the element path, so EN 18222:2026 Table 18 defines ReadDataElement and UpdateDataElement only. The two collection operations exist because addressing a top-level collection by name is convenient, not because the standard asks for them.

custody moves a pod-backed passport to the custodian named by a new X-DPP-Storage mandate. The identifiers do not change, which is the point: the product identifier is what was printed. The previous custodian keeps serving unless release_previous=true is passed, because retiring it is bounded by the DNS time-to-live in the operator's zone. docs/verification/ records a run of this against two live deployments.

delegation concerns the mandate under which the service reaches the custodian: it has a lifetime, the passport does not. GET says which mandate is held, POST replaces it with a fresh one; neither moves the passport. The rules are in docs/Delegation.md. /.well-known/dpp-service names the DID an economic operator delegates to, and for each pod the reader DID under which the intermediary registers the service as protected statutory reader.

Identifiers used as a single path segment (:dpp_id, :product_id) must be URL-encoded by the client. *element_path is a glob holding an absolute element path: the elementId of each level, separated by /.

How the standards map to the code

Standard Topic Where in the app
EN 18222:2026 API methods, status codes, Result object routes, app/controllers, ApiStatus
EN 18223:2026 DPP semantic model (payload) app/models/dpp.rb, docs/openapi.yaml
EN 18216:2026 Transport and formats (TLS, JSON) force_ssl; TLS 1.2 and 1.3 at the ingress; response content types
prEN 18239 Access rights and security TokenAuthenticatable; rate limit in ApplicationController; cross-origin policy in config/initializers/cors.rb; access classes and policies in AccessClass, AccessPolicy, config/vocab/; access grants in AccessGrant; reading per role in PassportAccess; access log in AccessLog
EN 18221:2026 Storage, archiving, versioning DppVersion, Dpp#archive_current_version!
EN 18219:2026 Unique identifiers, W3C DID DidOyd, did:oyd and did:web
prEN 18246 Authenticity and integrity DID document as anchor, PayloadCommitment, PayloadIntegrity, bin/verify_dpp
DIN DKE SPEC 99100 Battery passport attributes db/seeds.rb example content

Errors return a Result object (statusCode plus message[]) per EN 18222:2026 Tables 12–15, mapped in app/controllers/concerns/api_status.rb.

Tests

mkdir -p storage
RAILS_ENV=test bin/rails db:prepare
bundle exec rspec

Open items before production use

  1. Access log kept inside the database (prEN 18239). Signature verification, owner binding, access classes, per-attribute access policies, access grants, revoking them and reading per role are implemented, and every access to the controlled part is logged (docs/AccessClasses.md). Entries are only ever added and PostgreSQL refuses to change them, but the log is not anchored outside the database: someone who can change the database directly can change it unnoticed. Grants for authority are taken only from the issuers listed in DPP_AUTHORITY_ISSUERS.
  2. EU registry client (EN 18222:2026 §5). registerDPP checks the entry and answers 502 (registry_not_connected); nothing is registered. The Commission publishes no API specification for the EU DPP Registry.
  3. Content negotiation (EN 18216:2026 §5). JSON is implemented; XML, JSON-LD and HTML renderers are not.
  4. Data dictionary (EN 18223:2026 §4.3). The element-path resolver walks the document structurally instead of validating against a product-group dictionary.
  5. Data integrity (prEN 18246). The content is bound to the identifier: every change appends a signed log entry carrying payloadHash, the hash of the payload as the custodian delivers it, and bin/verify_dpp checks that from the public web alone. Where the holder keeps the document key, the service can only report the expected hash and leave the passport pending — it cannot sign in their place. The holder does that themselves, with bin/attest_payload or the DPP Manager, and the service picks the change up when the passport is next read. What remains open is the trust anchor: it is self-managed, 4.8.2 and 5.2.2 ask for an accredited one, so electronic attestation of attributes (EAA) is not covered.

Citing this

CITATION.cff in the repository root carries the metadata; each release is archived on Zenodo under a concept DOI that always resolves to the latest version.

License

Apache 2.0 License 2026 - OwnYourData.eu

About

Digital Product Passport API (EN 18219/18221/18222/18223) — public snapshot

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages