Repository navigation
Description
The EU digital product passport registry became operational on 20 July 2026, with operating rules in Commission Implementing Regulation (EU) 2026/1778 (in force 6 August 2026). The parts of dpp-registry that the regulation text fixes have been brought in line with it. This issue records the parts it does not fix — the wire contract — so that a reader knows which types are grounded and which are still our own invention.
This matters more here than in a normal crate: these types are published, and a released version cannot be unpublished. Anything shipped as though it were the registry's contract is vendored downstream and outlives the mistake.
What the regulation grounds, and what it does not
Grounded, and now modelled: the model/batch/item registration level and its linking rules (Art. 8(1), 8(4), 8(5)); the operator identifier carrying its scheme rather than a guess; the commodity code as a structural type; the back-up link; and the distinction between a registration that was accepted for validation and one that has been ruled on.
Not grounded — everything below.
The payload and envelope shapes are ours, not the registry's
RegistrationPayload and EuRegistryEnvelope are a reasonable reading of what Art. 8 requires to be registered. They are not transcribed from a published schema. Field names, nesting, and the envelope's existence at all are our design.
EuRegistryEnvelope::request_id is treated as an idempotency key — the request is now minted once and replayed unchanged on retries, which is correct behaviour if the registry uses it that way. Whether it does is unverified.
TransferNotification is in the same position, with the added uncertainty that the registry's transfer semantics are not described in the material available.
RegistryEndpoint carries a confirmed host and guessed everything else
The production and test hosts are the Commission's published ones. Beneath them:
- resource paths are invented;
- the
/api/v1prefix is observed on the registry's own web client, not read from a specification; api_version"1.0"is provisional;token_endpointmodels an OAuth2 token exchange, and registry identity is not that — a verified economic operator proves identity with a qualified electronic seal or a qualified electronic attestation of attributes (IR 2026/1778 Arts. 4–5). The field is kept only so consumers compile, and is marked as wrong in place.
RegistryStatusCode is an unverified vocabulary
The five codes are a plausible set, not a transcribed one. One variant has already been removed as unreachable; the remainder should be checked against the specification rather than against our own mapping.
There is no proof-of-registration type
Art. 9 entitles an operator to a proof of registration: a secure electronic document, downloadable, valid 90 calendar days with regeneration, and carrying an electronic time stamp. Nothing models it. Adding storage and lifecycle without a specified retrieval contract would be an empty box, so this waits.
Two items that are work, not waiting
Commodity code ranges
CommodityCode validates structure only — HS-6, CN-8 or TARIC-10. Art. 8(7)(d) has the registry check the code against "the permitted ranges for this product group", and those ranges live in the applicable delegated act. Once an act names them, they belong beside the other per-sector regulatory facts rather than hard-coded here.
The identifier length question
The registry's operator-facing form states a 50-character maximum for the unique product identifier. A GS1 Digital Link of the shape this workspace builds — https://<host>/01/<gtin-14>/21/<20-char serial> — is 65 characters, and 77 with a batch segment. Even a minimal host stays over.
If 50 turns out to be the API's limit and not the form's, the carrier URL shape has to change, which reaches dpp-digital-link and every printed label derived from it. This is worth resolving early: it is cheap to answer now and expensive to discover during integration.
How to close these
Every item above is closed the same way: obtain the registry's published API specification and diff it against these types. Until then the honest position is what the code already states — hosts confirmed, contract provisional, and the pins left visible in place rather than quietly dropped.
Activity
LKSNDRTMLKV commentedon Sep 16, 2026
Re-checked against the Commission's published material, 2026-09-16
Four of the items here move. One of them retires a risk this issue records as expensive, and the way it retires is worth reading carefully, because the analysis in this issue was correct when written — the Commission changed the constraint afterwards.
Everything below is either quoted from the registry's User Guide for Economic Operators or observed in the registry's own unauthenticated web bundle. Neither is a specification, and the distinction is the whole point of this issue — see the caveat at the end.
🔴 The 50-character identifier question is answered: it is now 2000
The guide carries a version line on page 1. Comparing the version this issue was written against with the one published since:
So the concern recorded here was accurate: 50 really was stated, for the UPI specifically. It has since been raised by a factor of forty.
A GS1 Digital Link of the shape this workspace builds is 65 characters, and 77 with a batch segment. Against 2000 that is not close to a constraint, so the carrier URL shape does not have to change, and neither does anything derived from it. That closes the item this issue flags as "cheap to answer now and expensive to discover during integration" — it was worth answering, and the answer is the good one.
Two things not to take from this:
The guide also ties the UPI to "JTC 24 standards", which points at the CEN/CENELEC DPP series rather than at a registry-specific format.
The registration endpoint has a different name
The registry's web client holds its endpoints as constants. The registration one is:
This issue records
/registrationsas invented, and it is — but it is also now known to be wrong, rather than merely unverified. Enrolment sits at/api/v1/enrolment-applications, with/verification/signed-declaration-uploads,/verification/submission-requestsand/verification/restart-requestsbeneath it.The
/api/v1prefix is confirmed again by the same source. The status and transfer paths remain entirely unobserved — no constant for either exists in the bundle, so nothing here grounds them.Idempotency is real, and it is a header
This issue says of
EuRegistryEnvelope::request_id:It does use one — as an HTTP header, not a payload field:
Idempotency-Key, carrying a UUID, set by an interceptor on POSTs to enrolment and registration;subCode: "CONFLICT_IDEMPOTENCY_KEY_ALREADY_USED";traceId;So the mechanism is vindicated and its location is wrong: we carry it in the envelope body. The error envelope's
subCode/traceIdshape is not modelled at all.Registration is a batch operation, and it is all-or-nothing
Not in this issue at all, and it reaches further than the types:
We model one passport per request. That is not merely a different shape — all-or-nothing batching changes what a retry means and what a partial failure does to a queue, so it reaches the consuming platform's outbox and drain, not only these types.
The status vocabulary is on the wrong axis
Observed request outcomes are
PROCESSING→SUCCESS/FAILURE, keyed by the correlation ID above.RegistryStatusCodeisPending / Registered / Rejected / SuspendedByAuthority / Deactivated. That is a record status — what became of a registered DPP. The observed vocabulary is a submission status — what became of a request that may carry a hundred of them. Both plausibly exist; we have one enum doing both jobs.Still blocked, and one item that got worse
No API specification is published. Re-checked: no OpenAPI document is served at any conventional path — every one returns the web client's
index.htmlfallback, so a 200 there means nothing at all. The XML/JSON submission templates the guide describes exist only behind the authenticated UI.Registration cannot presently succeed for anyone. Stated twice in the guide: "Successful registration of DPPs for batteries is not currently available, as the semantic catalogue for this product group has not yet been defined." There is a matching error string, "Semantic validation failed due to its unavailability."
That last point weakens a plan that was looking attractive: reading the Art. 12 semantic repository and diffing our schemas against it. The repository is still mandatory under Art. 11(3), still checked at registration under Art. 8(7)(a), and still free and open to any user under Art. 12(5)–(7) — but if the battery catalogue is undefined, our first product group may have nothing to diff against yet. Worth confirming it has content before committing to that work.
What this does not change
Everything above comes from a user guide and a web client. Neither is the contract. A web client is one consumer's behaviour at one moment, and it can change without notice — as the guide just demonstrated by changing a number by a factor of forty between two versions.
So this does not convert any of these types from ours to the registry's. It converts four of them from unverified guesses into dated observations, which is a better place to stand and is still not conformance. The honest position stated at the bottom of this issue is unchanged: obtain the published API specification and diff it. What has changed is that we now know four specific things our current types get wrong, rather than suspecting they might.
LKSNDRTMLKV commentedon Sep 17, 2026
Status pass, 2026-09-17 — checked against
main, not against this issueRe-read while assessing what a mock registry needs from core. Three sections
here are now stale, one of them answered outright. Everything else stands.
✅ "There is no proof-of-registration type" — no longer true
ProofOfRegistrationmodels the Art. 9(2) content, the Art. 9(4) window, and theseal as an opaque blob (#304). This issue said adding it "would be an empty box"
without a retrieval contract; that was half right — the retrieval call is still
missing and still blocked, but Art. 9(2) states the content directly, so the
shape never depended on the specification. Only getting hold of one does.
Two things the article corrected while it was modelled:
validity. Art. 9(4) governs what the Commission makes available; a
downloaded proof does not stop being evidence on day 91 and its seal goes on
verifying. This issue's "valid 90 calendar days" reads the wrong way round.
five points are a floor, not the document.
✅ "The identifier length question" — answered, and the answer is a warning
This issue calls it "cheap to answer now and expensive to discover during
integration", which was right. It is answered:
MAX_PRODUCT_IDENTIFIER_CHARS = 2000, from User Guide v1.02 — "a mandatoryvalue conforming to a URL format compliant with JTC 24 standards. Max length is
2000 chars." The carrier URL shape does not have to change.
dpp-digital-linkand every printed label are unaffected.
🚨 But v1.01 (2026-07-28) said 50. The Commission moved the constraint by a
factor of forty inside a month, silently, in a document carrying no OJ number and
no consolidation. Treat 2000 as current rather than settled — that is on the
constant, in place.
✅ The observed/invented split is machine-readable now
This issue's framing — "a reader knows which types are grounded and which are
still our own invention" — was prose-only, so nothing that acts on those values
could read it.
RegistryBasismakes it data (#301, #302):ENDPOINT_BASISandbasis_ofper wire constant, andEuRegistryErrorKind::http_statusreturns thestatus with its basis. A test claiming conformance can now assert it exercised
nothing
Assumed, which was previously unassertable and therefore untrue byaccident.
Still standing, unchanged
envelope's existence are our design, not a transcribed schema.
request_idas an idempotency key is unverified — and now known to be notthe registry's key: that rides as the
Idempotency-Keyheader, observed2026-09-16, with a 409 and
CONFLICT_IDEMPOTENCY_KEY_ALREADY_USEDon replay.The envelope field is ours and independently useful; it de-duplicates nothing
at the registry.
token_endpointis structurally wrong and marked wrong in place. Registryidentity is eIDAS-based (Arts. 4–5), not a bearer exchange.
RegistryStatusCodeis an unverified vocabulary.that names them.
TransferNotificationsemantics remain undescribed in available material.Newly found, and filed
#305 — a submission carries up to 100 passports and fails as a whole, and no
type can hold more than one. This crate documents the multi-passport submission
in its own module docs, declares
MAX_PASSPORTS_PER_SUBMISSIONandMAX_SUBMISSION_BYTESas constants, and modelsEuRegistryEnvelopewith asingle payload — so both caps are unenforceable and the all-or-nothing rule has
nothing to apply across. Art. 8(10)'s per-product registration identifier
falls out of the same gap:
EuRegistryResponsecarries oneregistry_id, whichis also an invented name for something Art. 8(8) defines.
That is the one thing blocking a faithful mock. Everything else a mock needs is
present and was verified against
main.LKSNDRTMLKV commentedon Oct 20, 2026
Three unobserved facts that behaviour now leans on, 2026-10-20
#486 adds typed outcomes to
RegistrySyncPort:TransferReportfornotify_transfer, andSubmissionLookupfor the newfind_submission. An adapter written against them (one exists as odal-node/dpp-engine#484) makes decisions that are only as sound as three things this issue already lists as unverified. None of the three is new. What is new is that code now acts on them, so each is written down here with what it costs if the guess is wrong.1. The reply vocabulary of the transfer route
TRANSFER_PATH_TEMPLATEisAssumed: no constant for it exists in the registry's web client, and IR 2026/1778 Art. 6a says transfers happen without saying how they are notified.TransferReport(Accepted,AlreadyHeld,Processing,Refused,Unrecognised) is therefore our vocabulary for a reply nobody has seen.That is safe in one direction only.
Unrecognisedis the variant for a reply the adapter cannot place, and it keeps the notification open instead of closing it, so a mapping that is wrong errs towards resending, never towards declaring a handover done. What it cannot do is make a correct reading of a real reply. Until one is observed, every reply isUnrecognisedunless it matches something we guessed.2. Whether the transfer route honours
Idempotency-Keyat allThe header is
Observedon registration and enrolment POSTs, with a 409 andCONFLICT_IDEMPOTENCY_KEY_ALREADY_USEDon replay. Nothing records it on a transfer notification.TransferReportrests on it: three of its answers mean "send it again", and an adapter sends the handover's stabletransfer_idas the key so that a resend is recognised as the first attempt and answeredAlreadyHeld.If the route ignores the header, a resend after a lost answer is a second notification, not a replay. The record would receive the same handover twice. That is the cost of the guess being wrong, and it is a worse failure than the one the typed report was built to remove.
3. The syntax of
sorton the task listTASKS_PATHtakespage,sizeandsort; the syntax ofsort, and the order the list comes back in when none is given, is not recorded.find_submissionhas to find a submission's task by paging that list, because no route taking a correlation identifier was seen. An adapter that does not know the order reads a long list from both ends, interleaved, since a fresh task is at the front of a newest-first list and at the back of an oldest-first one.Knowing the syntax would let a caller ask for newest-first and read one end, and would make the page budget mean something. Not knowing it costs reads, not correctness. It is the cheapest of the three to answer: it needs one observed request.
What would close them
Each is a single observation of the registry's own web client or a response from it, not a specification: the request the client makes when it transfers a record and what answers; whether that request carries the header; the
sortvalue the dashboard sends. If they can be read together, one pass over the client's bundle settles all three.Until then,
ENDPOINT_BASISalready saysAssumedfor the transfer route, which covers the first two, and the third belongs in the doc comment onTASKS_PATH, which currently says only that the parameter exists.LKSNDRTMLKV commentedon Oct 20, 2026
The three unobserved facts, re-read 2026-10-20 (#493)
Followed up on the previous comment by reading the registry's web client once more, in both its acceptance and its production build (32 script files each, following lazy-loaded chunks until none were new). Their API surface is identical: twenty-one calls.
TRANSFER_PATH_TEMPLATEstaysAssumed, and so doesSTATUS_PATH_TEMPLATE: nothing in the client reads a registered record either. Until the registry publishes something or a signed-in session is observed doing it,TransferReport's mapping is a guess made safe byUnrecognised.Idempotency-Key: same answer. The header is set by one interceptor, onPOSTs whose URL contains the enrolment path or the registration path, and nothing else. It keeps a key while the body is unchanged and mints a new one when it changes. A replayed key's409withCONFLICT_IDEMPOTENCY_KEY_ALREADY_USEDis handled by keeping the key, as already recorded. Nothing says a transfer route would honour one, so an adapter that sends it there is relying on a guess.sort: answered.<field>,<direction>, and the client asks forsubmittedAt,descon every read until a column is chosen, so a new submission is on the first page. The offered columns aresubmittedAt,type,displayNameandstatus; pages count from0. NowTASKS_SORT_NEWEST_FIRST,Observed. A read that sends nosortis still not observed.One thing to keep an eye on: the task's type field is
typeamong the sortable columns andtaskTypein the detail view, so which one the list rows carry is not visible from the code.Still blocked, and unchanged: the payload and envelope shapes,
RegistryStatusCode's vocabulary, and the other items above. The registry's responses are shown only to a signed-in session, so none of them is reachable this way.