Skip to content

EU registry types: what is still unverified against the specification #98

Open

Description

@LKSNDRTMLKV

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/v1 prefix is observed on the registry's own web client, not read from a specification;
  • api_version "1.0" is provisional;
  • token_endpoint models 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

  1. added
    enhancementNew feature or request
    complianceRegulatory/compliance correctness issue
    questionFurther information is requested
    on Aug 6, 2026
  2. LKSNDRTMLKV commented on Sep 16, 2026

    @LKSNDRTMLKV
    MemberAuthor

    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:

    v1.01 — 28 July 2026 v1.02 — 24 August 2026
    Unique Product Identifier "a mandatory value conforming to a URL format compliant with JTC 24 standards. Max length is 50 chars." "…Max length is 2000 chars."

    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:

    • Nobody misread anything. The number moved. The fix was re-reading a document, not re-arguing an analysis.
    • 2000 is not settled either. It changed once, silently, inside a month, in a document with no OJ number, no ELI and no consolidation. The only evidence it moved is that line on page 1. Anything built on it should say which version it read.

    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:

    /api/v1/dpp-registration-requests
    

    This issue records /registrations as 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-requests and /verification/restart-requests beneath it.

    The /api/v1 prefix 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:

    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.

    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;
    • a replayed key returns 409 with subCode: "CONFLICT_IDEMPOTENCY_KEY_ALREADY_USED";
    • error bodies carry a traceId;
    • the client keeps the key on 409 and rotates it on other outcomes — i.e. a conflict is treated as "this already succeeded", not "retry differently".

    So the mechanism is vindicated and its location is wrong: we carry it in the envelope body. The error envelope's subCode / traceId shape 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:

    • a submission may carry up to 100 DPPs, as an XML or JSON file, up to 1 GB;
    • "When submitting multiple DPPs, if a single DPP has an error, all the DPPs in the same submission will be rejected.";
    • duplicate identifiers within one submission are a named error;
    • each submission is assigned a correlation ID, and outcomes are reported per DPP beneath it.

    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.

    RegistryStatusCode is Pending / 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.html fallback, 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.

  3. LKSNDRTMLKV commented on Sep 17, 2026

    @LKSNDRTMLKV
    MemberAuthor

    Status pass, 2026-09-17 — checked against main, not against this issue

    Re-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

    ProofOfRegistration models the Art. 9(2) content, the Art. 9(4) window, and the
    seal 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:

    • The ninety days are the registry's retention window, not the document's
      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.
    • Art. 9(2) says the proof "shall contain at least" the listed data. The
      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 mandatory
    value 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-link
    and 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. RegistryBasis makes it data (#301, #302): ENDPOINT_BASIS and
    basis_of per wire constant, and EuRegistryErrorKind::http_status returns the
    status with its basis. A test claiming conformance can now assert it exercised
    nothing Assumed, which was previously unassertable and therefore untrue by
    accident.


    Still standing, unchanged

    • The payload and envelope shapes are ours. Field names, nesting, and the
      envelope's existence are our design, not a transcribed schema.
    • request_id as an idempotency key is unverified — and now known to be not
      the registry's key: that rides as the Idempotency-Key header, observed
      2026-09-16, with a 409 and CONFLICT_IDEMPOTENCY_KEY_ALREADY_USED on replay.
      The envelope field is ours and independently useful; it de-duplicates nothing
      at the registry.
    • token_endpoint is structurally wrong and marked wrong in place. Registry
      identity is eIDAS-based (Arts. 4–5), not a bearer exchange.
    • RegistryStatusCode is an unverified vocabulary.
    • Commodity code ranges remain work-not-waiting, still pending a delegated act
      that names them.
    • TransferNotification semantics 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_SUBMISSION and
    MAX_SUBMISSION_BYTES as constants, and models EuRegistryEnvelope with a
    single 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: EuRegistryResponse carries one registry_id, which
    is 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.

  4. added this to the v0.21.0 milestone on Sep 17, 2026
  5. removed this from the v0.21.0 milestone on Oct 5, 2026
  6. LKSNDRTMLKV commented on Oct 20, 2026

    @LKSNDRTMLKV
    MemberAuthor

    Three unobserved facts that behaviour now leans on, 2026-10-20

    #486 adds typed outcomes to RegistrySyncPort: TransferReport for notify_transfer, and SubmissionLookup for the new find_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_TEMPLATE is Assumed: 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. Unrecognised is 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 is Unrecognised unless it matches something we guessed.

    2. Whether the transfer route honours Idempotency-Key at all

    The header is Observed on registration and enrolment POSTs, with a 409 and CONFLICT_IDEMPOTENCY_KEY_ALREADY_USED on replay. Nothing records it on a transfer notification. TransferReport rests on it: three of its answers mean "send it again", and an adapter sends the handover's stable transfer_id as the key so that a resend is recognised as the first attempt and answered AlreadyHeld.

    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 sort on the task list

    TASKS_PATH takes page, size and sort; the syntax of sort, and the order the list comes back in when none is given, is not recorded. find_submission has 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 sort value the dashboard sends. If they can be read together, one pass over the client's bundle settles all three.

    Until then, ENDPOINT_BASIS already says Assumed for the transfer route, which covers the first two, and the third belongs in the doc comment on TASKS_PATH, which currently says only that the parameter exists.

  7. LKSNDRTMLKV commented on Oct 20, 2026

    @LKSNDRTMLKV
    MemberAuthor

    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.

    1. The transfer route's reply: cannot be settled from this source. The client has no call that moves responsibility for a passport, and none of the words a handover would use appear in it. TRANSFER_PATH_TEMPLATE stays Assumed, and so does STATUS_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 by Unrecognised.
    2. Whether the transfer route honours Idempotency-Key: same answer. The header is set by one interceptor, on POSTs 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's 409 with CONFLICT_IDEMPOTENCY_KEY_ALREADY_USED is 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.
    3. The dashboard's sort: answered. <field>,<direction>, and the client asks for submittedAt,desc on every read until a column is chosen, so a new submission is on the first page. The offered columns are submittedAt, type, displayName and status; pages count from 0. Now TASKS_SORT_NEWEST_FIRST, Observed. A read that sends no sort is still not observed.

    One thing to keep an eye on: the task's type field is type among the sortable columns and taskType in 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.

  8. added
    epic/registryEU registry loop: registration, transfers, handles, the local stand-in
    on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    complianceRegulatory/compliance correctness issueenhancementNew feature or requestepic/registryEU registry loop: registration, transfers, handles, the local stand-inquestionFurther information is requestedsurface/registryMoves EU registry enrolment or the unique product identifier

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions