FAIR Data Train — Visit Protocol

Editors' Draft,

This version:
https://specs.fairdatatrain.org/visit-protocol/
Issue Tracking:
GitHub
Editor:
(University of Twente and Leiden University Medical Center)
Protocol identifier:
https://w3id.org/fdt/visit-protocol/v1
Latest version:
https://specs.fairdatatrain.org/visit-protocol/
Part of:
FAIR Data Train — General Architecture, version 0.2
Contributors:
Kees Burger (Health RI), Rajaram Kaliyaperumal (Janssen Pharmaceuticals)
Feedback:
l.o.boninodasilvasantos@utwente.nl
Reference Implementation:
FAIRDataTeam/FAIRDataTrain · contracts: fdt-commons

Abstract

The FAIR Data Train Visit Protocol specifies the conversation between a Train Handler and a Data Station for one visit — one train, at one station, under one agreement. It covers how a visit is dispatched (by push, or by a station that accepts no inbound connection polling for it), how the station checks what arrived, how an agreement is negotiated on arrival and signed by both parties before anything runs, how every decision the station takes is reported as an event with a justification, how a result is inspected before it leaves and how state is handed from one visit to the next by location and digest and never by value. It defines the three message types — the visit descriptor, the visit event and the visit-result envelope — the two HTTP interfaces, the outcome vocabulary a completeness statement reports to a data consumer verbatim, and what a conforming station and a conforming Handler must do.

Status of this document

This is the editors' draft of version 1 of the FAIR Data Train Visit Protocol — the protocol between a Data Station and a Train Handler. It is provided for discussion and may change at any moment; its publication does not imply endorsement of its contents by any organization, and it should be cited only as work in progress. The protocol it describes is implemented and exercised end to end by the FAIR Data Train reference implementation; the JSON Schemas, OpenAPI documents and fixtures named in Appendix B are its normative artefacts, and where this text and those artefacts disagree, the artefacts are what an implementation is held to and this text has an error to be reported.

This document was created by the FAIR Data Team. Its source is maintained in the fdt-specs/fdt-specs.github.io repository and built with Bikeshed; comments and corrections are welcome as issues or pull requests there. The behaviour it specifies is exercised by the FAIR Data Train reference implementation, whose shared contracts (fdt-commons) are the normative artefacts this text describes.

1. Introduction

This section is non-normative.

In the FAIR Data Train the computation travels to the data. A train — a query, an API call, a script or a container — is sent to a Data Station that hosts the data, runs there under terms negotiated on arrival, and only what passes the station’s inspection leaves. The Train Handler is the consumer’s agent that sends the train and collects what comes back; the station is where the data controller’s conditions are enforced.

The Visit Protocol is the data plane of the FDT ecosystem: the conversation between a Handler and a station for one visit — one train, at one station, under one agreement (ADR-014, ADR-027 [FDT-ADR]). Every multi-station itinerary a Handler can run — a single visit, fan-out, a sequence, a discovery chain, iterative rounds, two-phase selection, composition — decomposes into visits, which is why this is deliberately the only interface a station has to implement to take part in any of them. The Handler owns the run; the station owns the visit.

The protocol is defined by its artefacts as much as by this text. Its three message types are JSON Schemas, its two interfaces are OpenAPI documents, its RDF payloads are validated by SHACL shapes, and the examples in this document are the published fixtures every implementation is tested against — not documents typed into prose. Appendix B: Normative artefacts names them, pinned to a commit.

1.1. Scope

This section is non-normative.

This specification covers, for one visit: dispatch in two directions (§ 6 Dispatch); what a station checks when a train arrives; negotiation on arrival, the derivation of the agreement and the signatures of both parties (§ 7 Negotiation on arrival); execution and the inspection of the result (§ 8 Execution and inspection); the hand-over of state between visits (§ 9 Onward state and hand-over); how every decision is reported (§ 10 Reporting); how a visit ends (§ 11 Outcomes, cancellation, retries, revocation); the messages (§ 12 Messages) and the interfaces (§ 13 Station API, § 14 Handler API — the station-facing surface); and the security model (§ 15 Security).

Three things are not in this protocol and live in their own contracts: the station’s metadata and catalogue ([FDT-ARCHITECTURE] §3.1.1, built on [FDP] and [VOCAB-DCAT-3]); the controller’s actions over their own data — authoring conditions, approving, revoking, suspending —​which reach a station through its console or an Individual Gateway; and the catalogues of the registry and the Train Depot, from which a train is dereferenced by IRI.

1.2. Reading this document

This section is non-normative.

Decisions behind the protocol are recorded as Architecture Decision Records [FDT-ADR] and are cited as ADR-nnn where the reasoning may matter to an implementer. Questions the protocol leaves open are listed in § 17 Open points with their register identifiers (Qnn); each has a stated default that implementations follow until it is decided.

Member names in message tables are written as camelCase JSON keys. Where a value is an IRI, it is a string in the JSON and an IRI in the vocabulary the schema maps it to.

2. Conformance

2.1. Conformance classes

Three conformance classes are defined. The key words MUST, MUST NOT, SHOULD and MAY in this document are to be interpreted as described in [RFC2119]. The requirements are stated in the body of this document and § 2.3 Criteria collects them as numbered criteria, so that an implementation can be checked against a list and a claim of conformance can say which item it rests on. Where a criterion and the body text differ, the body text and the artefacts of Appendix B: Normative artefacts govern and the criterion has an error to be reported.

Push-dispatch station

A Data Station that accepts inbound connections. It implements the Station API (§ 13 Station API) as a server, and meets every criterion of § 2.3.1 Every Data Station and § 2.3.2 A push-dispatch station.

Poll-dispatch station

A Data Station that accepts no inbound connection. It implements the poll half of the Handler API (§ 14 Handler API — the station-facing surface) as a client, reports by callback, and meets every criterion of § 2.3.1 Every Data Station and § 2.3.3 A poll-dispatch station.

Train Handler (conformance class)

The consumer-side party: the client of the Station API and the server of the Handler API. It meets every criterion of § 2.3.4 A Train Handler.

A station MAY be both a push-dispatch and a poll-dispatch station; its self-description says which modes it accepts and the Handler chooses per station (§ 6.2 Poll dispatch).

2.2. How the criteria are stated

Each criterion has a stable identifier, which a conformance claim and a test report cite; the requirement, with its key word; the section of this document that states it; and the way it is checked:

Schema

A JSON Schema or SHACL shape of Appendix B: Normative artefacts decides it. The fixtures named are the published examples the implementation must accept and the counter-examples it must refuse —​each counter-example with the message the shape carries, as tests/expectations.json in fdt-commons records it. python3 tests/validate.py runs every one of them.

Rule

A checker in fdt-commons/tools executes the rule over the fixtures named — the agreement derivation (derivation.py), result inspection (inspection.py), the canonical form (canonical.py) and payload digests (payloads.py). An implementation is held to the same verdict over the same fixtures.

Harness

The requirement is a behaviour observable only against a live counterpart — a station needs a Handler to dispatch to it, a Handler needs a station to answer — and no document decides it. These criteria are checked by a conformance harness that drives one side of the protocol and observes the other. That harness is not yet published; until it is, the reference implementation’s test suites are the only published evidence for these rows, and a third party’s claim over them is self-asserted (§ 2.5 Conformance statement).

The fixtures are in examples/ (Turtle, validated by shape), examples/invalid/ (Turtle counter-examples, each refused with its recorded message), examples/protocol/ (JSON, validated by schema) and examples/protocol/invalid/ (JSON counter-examples), all at the commit Appendix B: Normative artefacts pins. A criterion whose check names a shape that no fixture exercises would be a rule that can be deleted with every light still green, so every schema and rule row names at least one fixture.

2.3. Criteria

2.3.1. Every Data Station

These criteria bind a push-dispatch station and a poll-dispatch station alike. "Rejects" means the visit ends Rejected at the checkpoint named, with visit.rejected carrying reason and justification; "refuses" means Refused, the controller’s word, at NEG.

Id Requirement Stated in Checked by
STA-1 A station MUST publish a self-description stating its type, the interaction mechanisms it supports, its network memberships, the dispatch mode or modes it accepts and the protocol versions it implements, in which its dispatch endpoint is a dcat:DataService whose dct:conformsTo is https://w3id.org/fdt/visit-protocol/v1 and whose dcat:endpointURL is the base URL of the Station API. § 4 Parties and identifiers, § 6.2 Poll dispatch Schema — fdts:StationSelfDescriptionShape, fdts:MembershipShape, fdts:DataServiceShape (station-catalog.shapes.ttl) over the stations of network-and-stations.ttl. Harness for the live document.
STA-2 A station MUST verify the dispatching party’s credential against the identity providers it trusts for the visit’s network, and MUST answer 401 — not an outcome word — when the credential is missing or untrusted. § 15.1 Credentials Harness.
STA-3 At PEP 1 a station MUST validate the descriptor against the visit-descriptor schema and the embedded odrl:Request against fdts:RequestShape, and MUST reject a visit that fails either. § 7.1 Request or agreement, § 12.1 Visit descriptor Schema — visit-descriptor.schema.json over visit-descriptor-gd-m1.json, visit-descriptor-ttg-hop3.json; fdts:RequestShape refusing request-without-network.ttl, request-without-purpose.ttl, request-without-train.ttl.
STA-4 A station MUST reject at PEP 1 a descriptor whose consumer.party and whose request’s odrl:assignee name different parties. § 7.1 Request or agreement Harness. The consequence an agreement would carry is caught by Rule — derivation.py over agreement-binds-a-party-who-never-asked.ttl.
STA-5 Before anything runs a station MUST verify that payload.digest equals the fdt-o:artifactDigest of the train’s offer in its Depot, and that the payload bytes it holds hash to that digest; a mismatch rejects at PEP 1. § 12.1 Visit descriptor, § 15.2 Transport and integrity Rule — payloads.py check: visit-descriptor-ttg-hop1-wrong-digest.json is schema-valid on purpose and only the digest check can tell it from visit-descriptor-ttg-hop1.json.
STA-6 A station MUST reject at PEP 1 a visit whose train mechanism it does not support (415 under push). § 6.1 Push dispatch, § 5 Overview of a visit Harness.
STA-7 A station MUST reject a descriptor whose consumer.presentation carries assurance or exchange; it MUST grade the credential itself and record the grade in every event’s requester.assurance — presented-and-verified, with the exchange it established, or presented-only. § 15.1 Credentials, § 12.4 Presentation Schema — the ADR-044 mutations of tests/validate_protocol.py over visit-descriptor-gd-m1.json; visit.received and visit.rejected MUST carry requester (visit-event.schema.json). Harness for the grading.
STA-8 A station MUST NOT treat any claim of an exchanged token beyond its subject and actor as evidence in the negotiation, and MUST NOT record one. § 15.1 Credentials Harness — the evidence of the negotiation events.
STA-9 Event IRIs MUST be unique and MUST be minted by the station; sequence MUST be monotonic per visit and attempt. § 4 Parties and identifiers, § 12.2 Visit event Schema — visit-event.schema.json; tests/validate_protocol.py checks the published streams for one event per IRI. Harness for a live stream.
STA-10 With a request, a station MUST emit negotiation.requested first and MUST match the request against the controller’s offers for the visit’s network and the station owner’s policy, and against nothing else. § 7.1 Request or agreement Harness.
STA-11 A station MUST emit negotiation.matched only when the offer and the request are satisfiable, and it MUST carry a justification. § 7.2 Matched, pending, refused Schema — visit-event.schema.json (negotiation.matched requires justification); visit-events-gd-m1.json. Harness for the condition.
STA-12 Where a person must decide, a station MUST emit negotiation.pending-approval carrying an approval object that names the controller, the mode, the rule that imposed it, its recommendation and the facts it could not establish. Where a machine may not grant it MUST NOT refuse either: the visit goes from negotiation.requested to negotiation.pending-approval without a negotiation.matched. § 7.2 Matched, pending, refused Schema — visit-event.schema.json (negotiation.pending-approval requires approval); approval-by-controller-preference.json, approval-inconclusive-evaluation.json; refusing approval-in-an-evaluating-mode-with-no-recommendation.json, approval-that-names-a-rule-nothing-imposed.json. Harness for ADR-032.
STA-13 A refusal MUST be emitted as negotiation.refused with reason and justification and the state Refused, and MUST NOT be reported in any other word; a station’s own failure MUST NOT be reported as a refusal. § 7.2 Matched, pending, refused, § 11 Outcomes, cancellation, retries, revocation Schema — visit-events-gd-m1-refused.json, visit-events-gd-m1-failed.json; refusing visit-refusal-reported-as-a-failure.json, visit-failure-reported-as-a-refusal.json.
STA-14 A pending visit that is withdrawn because nobody decided MUST end TimedOut with negotiation.withdrawn, and MUST NOT be reported as Refused or Rejected. A station MAY apply its own timeout, stated as approval.expiresAt. § 7.2 Matched, pending, refused, § 11.1 Cancellation Harness.
STA-15 A station MUST derive the agreement from the offer, the request and the visit by the six rules of § 7.3 Derivation of the agreement: exactly the actions requested that the offer permits; every usage constraint, bounded by what was offered; the offer’s prohibitions and duties verbatim; one fdt-p:evidence per eligibility constraint; the three digests pinned; the offer’s assigner, the request’s assignee and the visit’s target and network. It MUST refuse when no action survives, naming the rule, and MAY narrow further but MUST NOT widen. § 7.3 Derivation of the agreement Rule — derivation.py check over every agreement in examples/, refusing agreement-grants-more-than-offered.ttl, agreement-binds-a-party-who-never-asked.ttl, agreement-changed-after-it-was-signed.ttl. Schema — fdts:AgreementShape refusing agreement-missing-assignee.ttl, agreement-without-an-authorisation-mode.ttl.
STA-16 Given an agreement IRI instead of a request, a station MUST verify that the agreement is active, that its target and assignee match the visit and that it was concluded at this station, and MUST reject at PEP 2 otherwise; on a retry it MUST reuse the agreement rather than negotiate anew. § 7.1 Request or agreement, § 6.4 Idempotency, attempts and withdrawal Harness.
STA-17 A station MUST sign the assigner’s side of every agreement it concludes: Ed25519 over the SHA-256 of the canonical form of the agreement with every fdt-p:signature excluded, keyId in the signer’s own published JWK set. Signing as the controller’s agent it MUST carry fdt-p:onBehalfOf; signing with the controller’s own key it MUST NOT; and it MUST refuse a signature naming as principal the party who made it. § 7.4.1 What is signed, § 7.4.2 Who signed, and for whom Rule — canonical.py against tests/canonical-vectors/; derivation.py refusing agents-signature-presented-as-the-partys.ttl. Schema — fdts:SignatureShape refusing agreement-signature-that-does-not-say-which-side.ttl.
STA-18 A station MUST NOT run anything under an agreement that does not carry the assignee’s signature, and MUST emit negotiation.active, naming the agreement, only once it has stored both signatures. § 7.4.3 The exchange, in two shapes, § 7.4.5 When the assignee does not sign Schema — fdts:AgreementShape refusing agreement-signed-by-the-assigner-only.ttl; Rule — derivation.py refusing agreement-only-the-station-ever-signed.ttl; visit-event.schema.json (negotiation.active names the agreement). Harness for the ordering.
STA-19 Verifying a signature, a station MUST hold the other party’s key from out-of-band configuration and MUST NOT take it from the document being verified; refusing one it MUST state exactly one of UnknownKey, DoesNotVerify, WrongParty, WrongDigest. § 7.4.4 Refusing a signature Schema — the 422 body of POST /visits/{visitId}/agreement/signature in station-visit-api.yaml. Harness for the four cases.
STA-20 At PEP 2 a station MUST check that the agreement is active and carries both signatures, that its target, train, network and assignee are the descriptor’s, that its usage terms admit the run now and that the station’s quotas allow it; it MUST emit visit.queued on success and reject otherwise. § 8 Execution and inspection Harness.
STA-21 A station MUST pass the descriptor’s parameters to the adapter unchanged, MUST emit visit.running, and MUST end a visit Failed with visit.failed carrying reason when the adapter or the infrastructure breaks. § 8 Execution and inspection Schema — visit-event.schema.json (visit.failed requires reason). Harness.
STA-22 At PEP 3 a station MUST perform the six checks of § 8 Execution and inspection in order and release nothing when one fails: the output schema is declared and resolves; it is closed; the result validates against it; the onward state class is admitted by fdt-p:deliverResult and by onward.allowedClasses; every duty holds, a k-anonymity duty over every value marked x-fdt-subjectCount, a count of 0 passing and a count of 1 to k−1 failing, and a duty the station cannot check rejecting; and the envelope names inspection.outputSchema. § 8 Execution and inspection Rule — inspection.py check over every envelope in examples/protocol/, refusing open-output-schema.json, result-the-schema-does-not-describe.json, cell-below-threshold.json, unenforceable-k-duty.json and accepting zero-cell-is-not-a-disclosure.json.
STA-23 RecordLevel state MUST NOT leave a station under any agreement; record-level content MUST NOT appear in an envelope’s result or artifacts, in an event’s evidence or in a justification. § 8 Execution and inspection, § 10.1 Events, § 12.3 Visit-result envelope Rule — inspection.py refusing record-level-onward.json; Schema — fdts:VisitShape refusing visit-record-level-onward.ttl. Harness for events.
STA-24 A station MUST NOT list a prohibition on what the consumer does after delivery (fdt-p:reidentify, odrl:sell) among the duties that passed inspection. § 8 Execution and inspection Harness — inspection.justification of a delivered envelope.
STA-25 Every decision a station takes and every state change of a visit MUST be emitted as a visit event conforming to the schema, with the conditional rules of § 12.2 Visit event: every decision event carries a justification (decision, rule, source, evidence), and every terminal event announces the state it ends in. § 10.1 Events, § 12.2 Visit event Schema — visit-event.schema.json over visit-events-gd-m1.json, visit-events-gd-m1-poll.json, visit-events-gd-m1-rejected.json, visit-events-gd-m1-revoked.json, visit-events-ttg-hop1.json. Harness for completeness.
STA-26 Every non-success event MUST carry the reason the consumer will see; the envelope MUST repeat it; and a Problem a station answers with MUST carry the same outcome word as the visit’s terminal event. § 11 Outcomes, cancellation, retries, revocation, § 12.5 VisitStatus and Problem Schema — visit-result.schema.json (every outcome but delivered requires reason), visit-result-gd-m1-refused.json, visit-result-gd-m1-rejected.json, visit-result-gd-m1-failed.json. Harness for the Problem.
STA-27 Once a visit is terminal its visit-result envelope MUST be available and MUST conform to the schema: state carries the outcome; result is empty under any outcome but delivered; a delivered visit carries agreement, deliveredAt and inspection; a result rejected at PEP 3 carries agreement and inspection; a visit that did not deliver says at which checkpoint it ended. § 10.2 Result and artefacts, § 12.3 Visit-result envelope Schema — visit-result.schema.json over visit-result-gd-m1.json, visit-result-gd-m1-refused.json, visit-result-gd-m1-rejected.json, visit-result-gd-m1-failed.json, visit-result-ttg-hop1.json.
STA-28 A station MUST keep the outcome vocabulary — Delivered, Refused, Rejected, TimedOut, Failed, Revoked — exact in its events, its envelopes, its logs and its user interfaces, and MUST NOT report Skipped about itself. § 11 Outcomes, cancellation, retries, revocation Schema — the state enumerations of visit-event.schema.json and visit-result.schema.json. Harness for logs and interfaces.
STA-29 On withdrawal a queued or pending visit MUST end at once with negotiation.withdrawn, a running visit MUST stop at its next checkpoint with visit.cancelled, either ending TimedOut; a visit already terminal MUST be unaffected. § 11.1 Cancellation Harness.
STA-30 When the controller takes the agreement out of force, a queued or running visit MUST stop at its next checkpoint with visit.revoked, naming the agreement and carrying the controller’s reason verbatim. A visit that arrives afterwards presenting a revoked agreement MUST be Refused at PEP 2, not Revoked. § 11.2 Revocation Schema — visit-events-gd-m1-revoked.json; refusing visit-event-revocation-that-names-no-agreement.json, visit-event-revocation-reported-as-refused.json. Harness for the arriving case.
STA-31 Receiving a hand-over, a station MUST check at PEP 1 that handOver.fromVisit appears in the descriptor’s precededBy before fetching anything, MUST require in the request a permission for fdt-p:acceptHandOver constrained by fdt-p:inboundStateClass, MUST fetch url over https only, and MUST hash what it got and compare it to digest. § 9.2 What arrives Schema — visit-descriptor.schema.json (a hand-over requires a lineage and the permission; url is https only) over visit-descriptor-ttg-hop3.json; fdts:RequestShape refusing request-accepting-a-hand-over-without-an-inbound-class.ttl. Harness for the membership itself, which no schema can decide, and for the digest.
STA-32 Handing state onward, a station MUST record the hand-over digest as onward.handedOverDigest and MUST hand Pseudonym-class state only to the station named in linkageStation, which performs the identifier translation. § 9.1 What leaves Schema — visit-result.schema.json (onward), visit-result-ttg-hop1.json. Harness.
STA-33 All traffic a station opens or accepts under this protocol MUST be over TLS. § 15.2 Transport and integrity Harness.

2.3.2. A push-dispatch station

Id Requirement Stated in Checked by
PUSH-1 A push-dispatch station MUST implement, as station-visit-api.yaml defines them, GET /, POST /visits, GET /visits/{visitId}, GET /visits/{visitId}/events, GET /visits/{visitId}/result, POST /visits/{visitId}/cancel, POST /visits/{visitId}/agreement/signature and GET /agreements/{agreementId}; and GET /visits/{visitId}/artifacts/{name} when it releases artefacts by reference. § 13 Station API Schema — station-visit-api.yaml. Harness.
PUSH-2 To POST /visits a station MUST answer 202 with a Location header and a VisitStatus when it took the visit; 200 with the current status on a replay of the same visit and attempt; 400 with a Problem when rejected at PEP 1; 401 when the credential is missing or untrusted; 403 with a Problem when refused; 409 when the visit IRI is known with a different, still-running attempt; and 415 when it does not support the mechanism. § 6.1 Push dispatch, § 4 Parties and identifiers Harness.
PUSH-3 After negotiation.matched a station MUST emit negotiation.awaiting-signature naming the agreement and carrying agreementDigest in its justification’s evidence; MUST serve the agreement at GET /agreements/{agreementId} to a party to it, with a Digest header; and MUST verify the posted AgreementSignature against the key it holds for the assignee, store it, emit negotiation.active and queue the run — answering 404 when nothing awaits a signature, 409 when already signed and 422 naming which of the four refusals. § 7.4.3 The exchange, in two shapes, § 13 Station API Schema — visit-events-gd-m1.json (nine events, the fifth negotiation.awaiting-signature). Harness.
PUSH-4 While the assignee has not signed, a station MUST record no outcome and MUST leave the visit at negotiation.awaiting-signature until it is withdrawn. § 7.4.5 When the assignee does not sign Harness. (Q27 default.)
PUSH-5 Under reporting mode callback a station MUST POST events to {callbackUrl}/events and PUT the envelope to {callbackUrl}/result, presenting callbackToken as a bearer token; under poll it MUST serve GET /visits/{visitId}/events?after=n returning the events with sequence greater than n, and GET /visits/{visitId}/result, answering 409 while the visit is not terminal. § 6.3 Reporting modes, § 10.1 Events, § 10.2 Result and artefacts Harness.
PUSH-6 To POST /visits/{visitId}/cancel a station MUST answer 200 with the visit’s current status, and MUST act as STA-29 says. § 11.1 Cancellation Harness.

2.3.3. A poll-dispatch station

Id Requirement Stated in Checked by
POLL-1 A poll-dispatch station MUST implement as a client, as handler-callback-api.yaml defines them, GET /stations/{stationId}/pending-visits, POST /visits/{visitId}/ack, POST /visits/{visitId}/events, PUT /visits/{visitId}/result and POST /visits/{visitId}/agreement; and PUT /visits/{visitId}/artifacts/{name} when it releases artefacts by reference. § 6.2 Poll dispatch, § 14 Handler API — the station-facing surface Schema — handler-callback-api.yaml. Harness.
POLL-2 For each descriptor it takes, a station MUST POST /visits/{visitId}/ack with its first event — visit.received — or with a Problem when rejected at PEP 1; answered 409, it MUST NOT start the visit. § 6.2 Poll dispatch Harness.
POLL-3 A poll-dispatch station MUST report by callback. § 6.3 Reporting modes Harness.
POLL-4 A poll-dispatch station MUST NOT emit negotiation.awaiting-signature. After negotiation.matched it MUST send one CountersignatureRequest to POST /visits/{visitId}/agreement, carrying the agreement IRI, the agreement itself as canonical N-Quads or Turtle — never a JSON projection — and the assigner’s signature; on 201 it MUST store both signatures and emit negotiation.active with signatureRoute: station-initiated and dispatchMode: poll in its justification’s evidence; on 422 it MUST proceed as PUSH-4 says, without the event. § 7.4.3 The exchange, in two shapes, § 12.6 Signature messages Schema — poll-countersign-ask-gd-m1.json, visit-events-gd-m1-poll.json (eight events, no negotiation.awaiting-signature); refusing poll-countersign-ask-without-the-assigners-signature.json, poll-countersign-ask-whose-document-is-not-the-one-signed.json, poll-countersign-ask-that-chooses-its-own-role.json. Harness.
POLL-5 Answered 410 to POST /visits/{visitId}/events, a station MUST stop the visit at its next checkpoint as STA-29 says. § 14 Handler API — the station-facing surface Harness.
POLL-6 A poll-dispatch station MUST authenticate at the Handler with the OpenID Connect client credentials of its operator. § 15.1 Credentials Harness.

2.3.4. A Train Handler

Id Requirement Stated in Checked by
HND-1 A Handler MUST mint the visit IRI of every visit and use it as the idempotency key: a retry of a Failed visit MUST re-dispatch the same visit IRI with attempt incremented by one and negotiation.agreement set to the existing agreement. § 4 Parties and identifiers, § 6.4 Idempotency, attempts and withdrawal Schema — visit-descriptor.schema.json (visit required). Harness for retries.
HND-2 Every descriptor a Handler composes MUST conform to the visit-descriptor schema: the protocol identifier; hop and phase; the train’s IRI, type and mechanism; payload.digest equal to the fdt-o:artifactDigest of the train’s Depot offer, with exactly one of content or url; exactly one of negotiation.request or negotiation.agreement; onward; reporting. § 12.1 Visit descriptor Schema — visit-descriptor.schema.json over visit-descriptor-gd-m1.json, visit-descriptor-ttg-hop1.json, visit-descriptor-ttg-hop3.json. Rule — payloads.py check for the digest.
HND-3 A Handler MUST embed an odrl:Request valid against fdts:RequestShape — the network, the train, the target, the assignee and the actions asked for with their constraints, a purpose among them — or the IRI of an existing active agreement; the descriptor’s consumer.party MUST be the request’s odrl:assignee. § 7.1 Request or agreement Schema — fdts:RequestShape over the requests of policies.ttl and trains-and-plans.ttl, refusing request-without-network.ttl, request-without-purpose.ttl, request-without-train.ttl. Harness for the party match.
HND-4 A Handler MUST visit under its own credential and MUST NOT forward a consumer’s. It MUST carry consumer.presentation verbatim in every descriptor, MUST carry exchangedToken only alongside it, and MUST NOT set assurance or exchange in a presentation it sends. § 15.1 Credentials, § 12.4 Presentation Schema — presentation.schema.json; the ADR-044 mutations of tests/validate_protocol.py. Harness for the credential used.
HND-5 A Handler MUST state approvalWait as an xsd:dayTimeDuration — never in months or years — and while a visit is pending MUST wait up to it and then either withdraw the visit with cause approval-timeout or keep waiting as the plan’s failure policy says; a visit so withdrawn MUST be recorded TimedOut. § 7.2 Matched, pending, refused, § 12.1 Visit descriptor Schema — the P1M mutation of tests/validate_protocol.py; fdts:FailurePolicyShape refusing plan-approval-wait-of-one-month.ttl, plan-approval-wait-typed-xsd-duration.ttl. Harness for the wait.
HND-6 Before signing, a Handler MUST compute the canonical digest of the agreement document it holds and check that it equals the digest stated; MUST verify the assigner’s signature over it against the key it holds for that party from out-of-band configuration; and MUST check the terms. It MUST NOT sign a document the station has not signed, a digest other than the one it computed, or a document whose assigner signature is for a party other than the agreement’s assigner; refusing, it MUST state exactly one of the four reasons. § 7.4.3 The exchange, in two shapes, § 7.4.4 Refusing a signature Rule — canonical.py against tests/canonical-vectors/; derivation.py check verifies every counter-signature in examples/ against the key it names and refuses agreement-changed-after-it-was-signed.ttl. Schema — refusing poll-countersign-ask-without-the-assigners-signature.json, poll-countersign-ask-whose-document-is-not-the-one-signed.json. Harness for the four cases.
HND-7 A Handler MUST sign the assignee’s side with the train owner’s key: to a push-dispatch station by posting an AgreementSignature — which carries no role — to POST /visits/{visitId}/agreement/signature; to a poll-dispatch station by serving POST /visits/{visitId}/agreement, answering 201 with a Countersignature, 409 when already countersigned, or 422 with a CountersignatureRefused. § 7.4.3 The exchange, in two shapes, § 12.6 Signature messages, § 14 Handler API — the station-facing surface Schema — poll-countersign-answer-gd-m1.json; fdts:AgreementShape refusing agreement-signed-by-the-assignee-only.ttl (a Handler that signs first). Harness.
HND-8 A Handler MUST accept at-least-once delivery of events, MUST de-duplicate them on id and order them on sequence, and MUST drive its view of the run from the event stream and the envelopes alone. § 10.1 Events Harness.
HND-9 A descriptor that carries handOver MUST name handOver.fromVisit in its own precededBy, MUST carry a request with a permission for fdt-p:acceptHandOver constrained by fdt-p:inboundStateClass, and MUST name the hand-over by https location and digest and never carry it. § 9.2 What arrives Schema — visit-descriptor.schema.json over visit-descriptor-ttg-hop3.json; fdts:RequestShape refusing request-accepting-a-hand-over-without-an-inbound-class.ttl. Harness for the membership, which no schema can decide.
HND-10 A Handler MUST hold Aggregate-class results, envelopes, events and the names of hand-overs only (ADR-015); Pseudonym-class state travels only from station to station. It MUST answer 422 to an envelope carrying record-level content or lacking its outcome or reason, and 409 to an uploaded artefact whose digest is not the envelope’s. § 9.2 What arrives, § 10.2 Result and artefacts, § 14 Handler API — the station-facing surface Harness.
HND-11 A Handler MUST assemble the run record and the completeness statement from envelopes and events only, reporting for every intended station its outcome in the seven words verbatim, its cause and the station’s stated reason; Skipped is the Handler’s own record and MUST carry one of NotSelected, NotEvaluated, Pruned, Dropped. § 11 Outcomes, cancellation, retries, revocation, § 12.3 Visit-result envelope Schema — visit-result.schema.json (a skipped visit carries skipCause and reason); run-state.schema.json over run-state-two-phase.json; fdts:RunShape refusing run-partial-without-completeness.ttl. Harness.
HND-12 A Handler MUST apply the plan’s failure policy and budget deadline through POST /visits/{visitId}/cancel, stating a reason and one of the causes approval-timeout, fail-run, stopped-by-person, deadline, replanned; it MUST NOT report a visit it withdrew as Refused or Rejected. § 6.4 Idempotency, attempts and withdrawal, § 11.1 Cancellation Harness.
HND-13 A Handler MUST dispatch query and API trains orchestrated and MUST NOT give them onward destination train. § 9.1 What leaves Schema — fdts:QueryTrainCoordinationShape refusing plan-query-train-choreographed.ttl. Harness for the descriptor.
HND-14 Serving GET /visits/{visitId}/plan, a Handler MUST disclose the run’s structure in full and never another station’s identity in any form — intendedStationCount and not the list — MUST say in redaction what the document withholds, and MUST answer 403 to a caller who does not hold the visit. § 12.7 Redacted plan, § 14 Handler API — the station-facing surface Schema — redacted-plan.schema.json over redacted-plan-ttg-hop1.json, refusing the seven redacted-plan-*.json counter-examples. Harness for 403.
HND-15 Everything a station can reach at a Handler MUST be scoped to that station’s own traffic, reachable with the per-visit callback token or the station operator’s credential; nothing a consumer’s console reads MUST be reachable with either. § 14 Handler API — the station-facing surface Harness.
HND-16 For a poll-dispatch station a Handler MUST serve GET /stations/{stationId}/pending-visits, honouring wait up to 60 s; MUST answer 409 to the acknowledgement of a visit it withdrew meanwhile and 410 to the events of a withdrawn visit. § 6.2 Poll dispatch, § 14 Handler API — the station-facing surface Harness.
HND-17 A Handler MUST keep the outcome vocabulary exact in code, logs and user interfaces, and a console MUST render presented-only and presented-and-verified as two states. § 11 Outcomes, cancellation, retries, revocation, § 15.1 Credentials Harness.
HND-18 All traffic a Handler opens or accepts under this protocol MUST be over TLS. § 15.2 Transport and integrity Harness.

2.4. Optional features

The following are permitted by this specification and required by no conformance class. A conformance statement says which of them an implementation provides.

Feature Who Stated in
Streaming events as Server-Sent Events on Accept: text/event-stream push-dispatch station § 10.1 Events
Batching several events of one visit in one callback station § 10.1 Events
visit.progress events station § 8 Execution and inspection
A station-side approval timeout, approval.expiresAt station § 7.2 Matched, pending, refused
Releasing artefacts by reference (artifacts[].url, or upload under poll dispatch) station § 10.2 Result and artefacts
Both dispatch modes station § 6.2 Poll dispatch
Long-polling the pending list with wait Handler § 14 Handler API — the station-facing surface
Carrying an RFC 8693 exchanged token Handler, where the network’s identity provider performs the exchange § 15.1 Credentials
Onward destination linkage and train Handler and station § 9.1 What leaves

2.5. Conformance statement

An implementation claims conformance by stating:

  1. the conformance class or classes claimed;

  2. the fdt-commons version and commit whose artefacts it was validated against (Appendix B: Normative artefacts names this document’s);

  3. which optional features of § 2.4 Optional features it provides;

  4. for every criterion checked by schema or rule, that it accepts every published fixture named and refuses every counter-example named with the message tests/expectations.json records — the reference implementation’s make check is one way to run them all;

  5. for every criterion checked by harness, the evidence it offers, and whether that evidence was produced by a harness a third party can run.

A statement that leaves item 5 to self-assertion is a valid statement and says so. The conformance harness and a statement template are owed by the reference implementation’s conformance work package and will be published beside this document; until they are, no implementation other than the reference implementation can make a third-party-verified claim, and this specification does not pretend otherwise.

3. Terminology

run

One execution of a plan — a train, its parameter values, an itinerary and a request template — by a Handler. A run is decomposed into visits.

hop, phase

A visit’s position in its itinerary: the hop counts outward from the run’s entry stations; the phase is the order of the itinerary phase it belongs to. Both are integers from 1.

attempt

A retry re-dispatches the same visit IRI with attempt incremented, under the same agreement. Attempts of one visit share its identity and its agreement.

agreement

The odrl:Agreement [ODRL-MODEL] a station derives from the controller’s offer and the train’s request for one visit, pinned by digest and signed by both parties. One per visit (ADR-014).

checkpoint

One of the points at which a station decides: PEP 1 (arrival), NEG (negotiation), PEP 2 (before execution), EXEC (execution), PEP 3 (result inspection) and OBL (post-execution obligations).

dispatch mode

How a visit reaches a station: push — the Handler posts it — or poll — the station fetches it from the Handler (ADR-004).

reporting mode

How a station’s events and envelope reach the Handler: callback — the station posts them to a per-visit URL — or poll — the Handler reads them from the station. Note that the two senses of poll are different parties reading in different directions; § 7.4 Signatures of both parties depends on keeping them apart.

onward state

What leaves a station other than the result delivered to the Handler: state of class Aggregate or Pseudonym carried to another station, directly in a choreographed train or through an identifier-translation hop. RecordLevel never leaves a station.

hand-over

State produced by an earlier visit of the same run and named, by location and digest, as the input of a later one.

outcome

The one word a visit ends in: Delivered, Refused, Rejected, TimedOut, Failed, Revoked, or — recorded by the Handler alone — Skipped (§ 11 Outcomes, cancellation, retries, revocation).

canonical form

Of a graph G: its [RDF-CANON] (URDNA2015) canonical form, serialised as N-Quads, the lines sorted, joined with \n, with a trailing \n, encoded UTF-8. Every digest a party signs or pins is "sha256:" followed by the lowercase hex SHA-256 of a canonical form.

4. Parties and identifiers

Party Role in the protocol Identified by
Train Handler Client of the Station API; server of the Handler API. Mints the visit IRI, embeds the request, signs the assignee’s side, applies the failure policy. Its IRI (consumer.handler); its own credential at the station (§ 15 Security); the consumer’s legal party as consumer.party.
Data Station Server of the Station API; client of the Handler API. Checks the arrival, negotiates, signs the assigner’s side, enforces the checkpoints, emits events, returns the envelope. Its IRI (self-description); the identity providers it trusts per network.
Train The subject of the visit: an executable asset in a Train Depot’s catalogue, with its creator’s offer and its payload digest (ADR-020). train.iri; its payload by sha256: digest.
Station hosting a translation or alignment service An ordinary station whose mechanism is identifier translation or vocabulary alignment; visited with this protocol under its own agreement. As any station.

Identifiers are IRIs. The visit IRI is the idempotency key of the whole exchange: a repeated push, a retry under the same agreement, and every event and envelope refer to it. A station MUST treat a repeated POST /visits with the same visit and attempt as a replay and answer with the visit’s current status. Event IRIs are minted by the station and MUST be unique; sequence MUST be monotonic per visit and attempt.

A station MUST advertise that it speaks this protocol in its self-description: its dispatch endpoint is a dcat:DataService whose dct:conformsTo is the protocol identifier https://w3id.org/fdt/visit-protocol/v1, and whose dcat:endpointURL is the base URL of the Station API.

5. Overview of a visit

This section is non-normative; the sections it points at are normative.

Sequence diagram of one visit under push dispatch: the Handler pushes a visit descriptor; the station checks it at PEP 1 and answers 202; it negotiates, signs the assigner's side and emits negotiation.awaiting-signature; the Handler fetches the agreement and posts its signature; the station emits negotiation.active, queues and runs the train, inspects the result at PEP 3 and delivers the envelope. Every decision is an event.
One visit, push-dispatched, callback-reported. Solid arrows are requests; open blue arrows are events.
  1. Dispatch. The Handler composes a visit descriptor and pushes it to the station, or a poll-dispatch station fetches it from the Handler (§ 6 Dispatch).

  2. PEP 1. The station verifies the caller’s credential against the identity providers it trusts for the visit’s network, validates the descriptor against its schema and the embedded ODRL request against fdts:RequestShape, checks that the train’s payload digest equals the one in the train’s depot offer, and checks that it supports the train’s mechanism. A failure rejects the visit; otherwise it emits visit.received.

  3. Negotiation. The station matches the request against the controller’s offer for the visit’s network and the owner’s policy. The outcome is a match, a case put to a person, or a refusal — which is the controller’s decision, never the station’s (§ 7 Negotiation on arrival).

  4. Signatures. The station derives the agreement, signs the assigner’s side and asks for the assignee’s; the Handler verifies the terms and that signature and signs the assignee’s side with the train owner’s key. Only then does the agreement become active, and only then does anything run (§ 7.4 Signatures of both parties).

  5. PEP 2 and execution. The station checks that the agreement is active and about this visit and that its quotas allow the run, queues it, and runs the payload through the adapter for the train’s mechanism.

  6. PEP 3. The station validates the result against the train’s declared output schema, checks every duty of the agreement on it, and decides what leaves and where (§ 8 Execution and inspection). A failure rejects the result; nothing leaves.

  7. Delivery. The visit-result envelope is pushed to the Handler’s callback URL or fetched by the Handler, and the visit ends in one of the outcomes of § 11 Outcomes, cancellation, retries, revocation.

Sequence diagram of one visit under poll dispatch: the station fetches its pending descriptors from the Handler and acknowledges one; after negotiating and signing it sends the agreement to the Handler in one round trip and receives the assignee's signature in the response; it pushes events and the envelope to the Handler. Every arrow is opened by the station.
The same visit for a station that accepts no inbound connection: every arrow is opened by the station, and the signature exchange is one round trip the station makes.

6. Dispatch

6.1. Push dispatch

Under push dispatch the Handler POSTs the visit descriptor to the station’s dispatch endpoint, read from the station’s self-description. The station MUST answer 202 Accepted with a Location header naming the visit’s status URL and a VisitStatus body when the visit was taken; 200 with the current status on an idempotent replay; 400 with a Problem when the visit is rejected at PEP 1; 401 when the credential is missing or untrusted; 403 with a Problem when the visit is refused at negotiation; 409 when the same visit IRI is known with a different, still-running attempt; and 415 when the station does not support the train’s mechanism.

6.2. Poll dispatch

A station that cannot accept inbound connections MUST instead call GET /stations/{stationId}/pending-visits on the Handler, which returns the descriptors waiting for it and MAY long-poll for up to wait seconds. For each descriptor it takes, the station MUST POST /visits/{visitId}/ack with its first event — visit.received — or with a Problem if the visit is rejected at PEP 1. An acknowledgement answered 409 means the Handler withdrew the visit meanwhile, and the station MUST NOT start it. Beyond the gateway, processing is identical to push dispatch (ADR-004).

A station advertises its dispatch mode or modes in its self-description; the Handler chooses per station.

6.3. Reporting modes

The descriptor’s reporting.mode selects how the station’s events and envelope reach the Handler. Under callback the station POSTs events to {callbackUrl}/events and PUTs the envelope to {callbackUrl}/result, presenting the descriptor’s callbackToken as a bearer token. Under poll the Handler reads GET /visits/{visitId}/events?after=n — or subscribes with Accept: text/event-stream — and fetches GET /visits/{visitId}/result. A poll-dispatch station MUST report by callback.

6.4. Idempotency, attempts and withdrawal

A Handler retrying a Failed visit MUST re-dispatch the same visit IRI with attempt incremented by one and negotiation.agreement set to the existing agreement; the station MUST reuse that agreement rather than negotiate anew. The Handler withdraws a visit with POST /visits/{visitId}/cancel, stating a reason and a cause from approval-timeout, fail-run, stopped-by-person, deadline or replanned; a queued or pending visit ends at once and a running one stops at its next checkpoint (§ 11.1 Cancellation).

7. Negotiation on arrival

7.1. Request or agreement

The descriptor’s negotiation member carries either an odrl:Request — JSON-LD, validated at PEP 1 against fdts:RequestShape — or the IRI of an existing active agreement (rounds of an iterative pattern, retries). With an agreement IRI the station MUST verify that the agreement is active, that its target and assignee match the visit, and that it was concluded at this station; otherwise the visit is Rejected at PEP 2.

With a request, the station MUST match it against the controller’s offers for the visit’s network (ADR-017) and the station owner’s policy, and MUST emit negotiation.requested first. The request names the network under fdt-p:underNetwork, the train under fdt-p:train, the target dataset or part, the assignee and the actions it asks for with their constraints, of which a purpose is required. The descriptor’s consumer.party and the request’s odrl:assignee MUST name the same party; a station MUST reject at PEP 1 a descriptor in which they differ, since the agreement binds the request’s assignee while every record a controller is shown names the descriptor’s consumer.

7.2. Matched, pending, refused

Matching has three outcomes.

Matched

The offer and the request are satisfiable and the station may conclude. It emits negotiation.matched with a justification and proceeds to derive and sign the agreement. negotiation.matched MUST be emitted only when the two are satisfiable: the state means exactly that, and maps to the Dataspace Protocol’s OFFERED or AGREED, neither of which describes a request nothing can satisfy.

Pending approval

A person must decide. A station emits negotiation.pending-approval carrying an approval object that names the controller who must decide, the mode that says why a person is deciding, the rule that imposed it, the station’s recommendation, the eligibility facts it could not establish, and its own expiresAt. The modes are HumanRequiredByRegulation (the network’s regime bars automated decisions), HumanByControllerPreference (the offer asks for a person), HumanByStationPolicy (the operator configured one) and HumanByInconclusiveEvaluation (the controller’s conditions did not answer — the request asks for something no permission addresses, or a fact could not be established; ADR-050). Where a machine may not grant, it may not refuse either (ADR-032): a visit whose evaluation recommends refusal in a network that bars automated decisions goes from negotiation.requested straight to negotiation.pending-approval, without a negotiation.matched, and the recommendation is what tells the two apart. A recommendation is not an outcome.

Refused

The controller’s standing conditions declined the request — conclusively, under a rule of the offer — or a person did. The station emits negotiation.refused with the stated reason and the justification, and the visit’s outcome is Refused. This is a governance decision by a named party and is never reported in any other word (§ 11 Outcomes, cancellation, retries, revocation).

Sequence diagram of a visit that waits for a person: negotiation.pending-approval names the controller, the rule, the recommendation and a timeout; then either a grant leads to the signature exchange and negotiation.active, a refusal leads to negotiation.refused with the controller's words, or the Handler's approval wait runs out and it cancels, leading to negotiation.withdrawn and TimedOut.
A visit that waits for a person, and its three endings

While a visit is pending, the Handler MUST wait up to the plan’s approvalWait and then either withdraw the visit (POST /visits/{visitId}/cancel, cause approval-timeout) or keep waiting as its failure policy says. The station MAY apply its own timeout (approval.expiresAt). A visit withdrawn because nobody decided ends TimedOut, and MUST NOT be reported as Refused or Rejected: nobody decided anything, and that is the fact to report.

7.3. Derivation of the agreement

An agreement that merely has the right shape is not an agreement anyone can audit. A station MUST derive the agreement from the offer O (restricted to the visit’s network), the request R and the visit V by the following rule, which fdt-commons executes over every published agreement and which a station’s own agreements are held to:

  1. Actions. The agreement grants exactly actions(R) ∩ actions(O). If no action survives there is no agreement and the outcome is Refused, naming the rule.

  2. Constraints. Every odrl:LeftOperand of the FDT profile has a fdt-p:constraintRole: Eligibility constraints (legal basis, consumer type, network, jurisdiction, train, mechanism) are established once at negotiation and become evidence, never terms; Usage constraints (purpose, onward state class, k-anonymity, retention, validity window) become terms re-checked at PEP 2 and PEP 3. For each granted action and each usage operand O's matching rule constrains, the agreement carries what was asked, bounded by what was offered: R's constraint if O admits it, otherwise O's. Every usage constraint of O's matching rule MUST appear, and every usage constraint R placed on itself MUST appear too.

  3. Prohibitions and duties of O are carried verbatim. Nothing in negotiation may drop or weaken one, and no approver may waive one.

  4. Evidence. For every eligibility constraint of O the agreement carries one fdt-p:evidence node: the fact, the established value, who attested it and when. The person who dispatched the train appears as a pseudonym the station can resolve in its own audit log; their identifier does not enter the agreement.

  5. Pinning. The agreement carries fdt-p:derivedFromOffer, fdt-p:derivedFromRequest, fdt-p:train, and the digests fdt-p:offerDigest, fdt-p:requestDigest and fdt-p:payloadDigest of each as it stood.

  6. Parties. The agreement’s odrl:assigner MUST be the offer’s assigner and its odrl:assignee MUST be the request’s assignee; odrl:target and fdt-p:underNetwork MUST be the visit’s; dspace:timestamp and fdt-p:authorisationMode MUST be present.

A station policy may narrow the result further; narrowing is always permitted and widening never is.

7.4. Signatures of both parties

The station writes the agreement and is a party to it; digests make a rewrite detectable by whoever kept the original, but prove nothing to a third party who kept nothing — a supervisory authority, or a controller asking later under what terms their data was touched. So each party signs (ADR-038), and an agreement carries one fdt-p:signature of each role, fdt-p:AssignerSignature and fdt-p:AssigneeSignature.

7.4.1. What is signed

The signed digest is the SHA-256 of the canonical form of the agreement’s concise bounded description **with every fdt-p:signature node excluded** — not only the signer’s own — so that both parties sign one string, neither signature covers the other, the order they were made in is not part of what was agreed, and neither party can invalidate the other’s by re-signing.

A signature is Ed25519 [RFC8032] (EdDSA) over the digest string as UTF-8, encoded base64url without padding. fdt-p:keyId is the IRI of the public key in the signer’s own published key set [RFC7517], so a verifier needs nothing from either party. Signing keys are not the bearer tokens of § 15 Security: a token proves who called, a signature proves who agreed.

7.4.2. Who signed, and for whom

fdt-p:signedBy names whose key it is. fdt-p:onBehalfOf names whose agreement it is, and MUST be present exactly when the two differ — when an agent signed.

signedBy onBehalfOf
The train owner signs (assignee) the owner; the key is published by their Handler absent
A data controller with their own key signs (assigner) the controller absent
The station signs for a controller who holds no key the station the controller

The controller signed and the station signed for the controller are different facts with different weight, and a record that let them be confused would be worth less than no signature, because a verifier would read the weaker as the stronger. A station signing as an agent MUST carry onBehalfOf; a party signing for themselves MUST NOT; a signature naming as its principal the party who made it MUST be refused — a party is not their own agent.

7.4.3. The exchange, in two shapes

The two-signature rule fixes the order: the station derives the terms and signs the assigner’s side; the Handler verifies the terms and that signature and signs the assignee’s side; the station stores the fully signed agreement and only then emits negotiation.active. Which party opens the call is decided by the dispatch mode and by nothing else.

Push-dispatched station. After negotiation.matched the station emits negotiation.awaiting-signature, carrying the agreement IRI and, in the justification’s evidence, agreementDigest — the digest to sign. The Handler fetches the agreement (GET /agreements/{agreementId}), computes the digest for itself from the document it read, checks that it equals the one stated, verifies the assigner’s signature over it, checks the terms, signs the digest with the train owner’s key and POSTs an AgreementSignature to POST /visits/{visitId}/agreement/signature. The station verifies it against the key it holds for that party, stores it, emits negotiation.active and queues the run. The visit stays at Checking throughout: nothing about it is waiting on a person.

Poll-dispatched station. The station has no inbound port, so it cannot be told to wait to be called; it emits no negotiation.awaiting-signature and instead opens one round trip of its own: POST /visits/{visitId}/agreement on the Handler, with a CountersignatureRequest carrying the agreement IRI, the agreement itself — as canonical N-Quads or Turtle, never a JSON projection, because the document is what the signature is checked against — and the assigner’s signature. The Handler applies exactly the checks above and answers 201 with the assignee’s Countersignature, or 422 with a CountersignatureRefused. The station then stores both signatures, emits negotiation.active and queues the run. Because no wait occurred, the stream MUST NOT contain negotiation.awaiting-signature; instead negotiation.active carries signatureRoute: station-initiated and dispatchMode: poll in its justification’s evidence, so which way the exchange went is stated rather than inferred from an absence.

In both shapes the Handler MUST NOT sign a document the station has not signed, MUST NOT sign a digest other than the one it computed from the document itself, and MUST NOT sign a document whose assigner signature is for a party other than the agreement’s assigner.

7.4.4. Refusing a signature

A verifier refusing a signature MUST say which of four things it found, because a verifier has to distinguish them and one status for all four would leave the first hunting a forgery that never happened:

Reason Meaning
UnknownKey The verifier holds no key for the signing party. A key-distribution problem; says nothing about the signature.
DoesNotVerify The signature is wrong.
WrongParty Valid cryptography by somebody who is not the party in that role, nor an agent naming them.
WrongDigest The digest the signature states is not the canonical digest of the document in hand: the two sides are not looking at the same bytes.

Keys are configured out of band, symmetrically: a station holds the public key of each train owner whose Handler may visit it; a Handler holds the public key of each station it dispatches to. A verifier MUST NOT take the key from the document it is verifying, which would establish only that the signer holds the key they nominated. A party for which no key is held is answered UnknownKey. Whether a network’s trust anchors may vouch for a station’s signing key is open (Q61).

7.4.5. When the assignee does not sign

An agreement only one party has signed is not an agreement, and nothing runs under one. If the assignee’s Handler cannot be reached or will not accept the terms, the visit does not proceed. The station records no outcome and the visit rests at negotiation.awaiting-signature until it is withdrawn, which is the behaviour a pending approval already has; the Handler reports its own run as failed when it stops waiting. What outcome the station should record is open (Q27). It is deliberately not Refused: a consumer walking away is not the controller refusing.

8. Execution and inspection

PEP 2. Before execution the station MUST check that the agreement is active and carries both signatures, that it is about this visit — its target, train, network and assignee match the descriptor’s — and that its usage terms admit the run now (the validity window) and that the station’s quotas allow it. A failure here rejects the visit at PEP 2, and the station MUST emit visit.queued on success.

EXEC. The adapter for train.mechanism executes the payload with the descriptor’s parameters, passed to the adapter unchanged; the station emits visit.running and MAY emit visit.progress. An error in the adapter or the infrastructure ends the visit Failed.

PEP 3. A train has run and whatever it produced is inside the station. Everything before this point decided whether the train could run; this decides whether what it produced may leave, and the first cannot answer the second: the same train under the same agreement produces a releasable result for one parameter and an unreleasable one for another. The station MUST perform these checks in order, and the first that fails ends the inspection with nothing released:

  1. The train declares an output schema (fdt-o:generatesOutput → dct:conformsTo, a JSON Schema [JSON-SCHEMA]) and it resolves.

  2. That schema is closed: every object sets additionalProperties: false and every array describes its items. Against an open schema, a result carrying an identifier on every row validates, and "the result conforms to the declared output" would be a statement about nothing.

  3. The result validates against it, with no relaxation.

  4. The onward state class being released is admitted by the agreement’s fdt-p:deliverResult permission and by the descriptor’s onward.allowedClasses. RecordLevel never leaves a station, whatever any agreement says.

  5. Every duty of the agreement holds on the result. A k-anonymity duty is applied to every value the declared output schema marks x-fdt-subjectCount: true; a count of 0 does not violate it and a count between 1 and k−1 does. A duty the station cannot check is not a duty that passes: if the agreement carries a k duty and the schema marks no subject count, the result is rejected with that stated. Prohibitions on what the consumer does after delivery (fdt-p:reidentify, odrl:sell) cannot be checked by inspecting a result; they bind the consumer through the agreement and the station MUST NOT list them as passed.

  6. The envelope names the schema it was checked against (inspection.outputSchema), so an auditor can re-run steps 2 and 3 without the station’s cooperation.

The inspection’s outcome is passed, redacted (cells suppressed to meet a threshold — a Delivered visit) or rejected (a Rejected visit, releasing nothing — not a partial result, not a count beyond what the justification states). The station emits visit.inspecting and then visit.delivered or visit.rejected at checkpoint PEP3, with the verdict’s evidence.

9. Onward state and hand-over

9.1. What leaves

The descriptor’s onward block is the plan’s request; PEP 3 applies the controller’s offer, which may be stricter. The destination is handler (the default: the envelope’s result and artifacts), linkage (Pseudonym-class state is handed to the station named in linkageStation — the one that performs the identifier translation — under the named join key; the envelope records the hand-over digest and that station verifies it on its own visit) or train (a choreographed script or container train carries state and a routing instruction; in version 1 the train returns to the Handler between hops, so the envelope’s routing.nextStations is what the Handler dispatches next, inspected at PEP 3 like any other output). Query and API trains MUST NOT use destination train.

9.2. What arrives

A later visit of the same run receives earlier state through the descriptor’s handOver block, which names the hand-over and never carries it: fromVisit, fromStation, url, mediaType, digest, stateClass, and optionally joinKey, conformsTo, duties and bytes. The Handler holds a name, which is what ADR-015 permits it to hold; the bytes stay at the station that produced them. The following rules apply:

Whether a station that cannot verify the bytes must refuse at PEP 1 or may run and report the result unverifiable (Q133, Q130), who may fetch a hand-over from the station holding it and under which credential (Q136), and how a hop with two inputs is expressed (Q135) are open; version 1 names the location and nothing else.

10. Reporting

10.1. Events

Every decision a station takes and every state change of a visit MUST be emitted as a visit event (ADR-027). The ordered event stream is the source of truth for the Handler’s view of the run and for the audit trail a controller reads; the Handler needs nothing else to drive its itinerary map and its completeness statement. Delivery is at least once: a Handler MUST de-duplicate on id and order on sequence. A station MAY batch events in one callback if all belong to the visit in the path.

Under reporting mode poll, GET /visits/{visitId}/events?after=n returns the events with sequence greater than n; with Accept: text/event-stream a station MAY stream new events as Server-Sent Events, each data: line one event object and id: the sequence.

Every decision event carries a justification: the decision, the rule that fired, the offer, agreement or station policy it came from, and evidence — counts, digests, cell counts, never record-level content.

10.2. Result and artefacts

The visit-result envelope is available once the visit is in a terminal state: under callback the station PUTs it to {callbackUrl}/result; under poll the Handler fetches GET /visits/{visitId}/result, which answers 409 while the visit is not yet terminal. For a non-success outcome the envelope is returned with an empty result, the outcome in state and the stated reason. Artefacts too large to inline are referenced by artifacts[].url on the station and fetched by the Handler, or — in poll mode — uploaded by the station with PUT /visits/{visitId}/artifacts/{name}, where the Handler MUST verify the digest against the envelope and answer 409 on a mismatch.

11. Outcomes, cancellation, retries, revocation

Every visit ends in exactly one terminal outcome, and the words are part of the contract (ADR-026): a run’s completeness statement reports them to a data consumer verbatim, and implementations MUST keep the vocabulary exact in code, logs and user interfaces.

Outcome Where Meaning Whose word
Delivered PEP 3 The result passed inspection and left; inspection.outcome is passed or redacted. —​
Refused NEG Access was declined by the controller’s standing conditions, or by a person asked to decide. A controller’s decision, with their stated reason, carried verbatim.
Rejected PEP 1, PEP 2, PEP 3 A checkpoint’s verdict on a document or a result: an invalid descriptor, a payload that is not the one offered, an agreement that is not this visit’s, a result a duty forbids. A checkpoint’s; nobody decided anything.
TimedOut NEG or EXEC The approval wait ran out, or the Handler cancelled the visit. Nobody decided.
Failed EXEC — or NEG, when a station broke carrying out a decision already taken Machinery broke. —​
Revoked PEP 2 or EXEC The controller took the agreement out of force while the visit was queued or running; the station stops at its next checkpoint. A controller’s act over an agreement they granted, with their reason.
Skipped never reaches a station The run did not ask this station: a phase condition excluded it (NotSelected), the first phase did not deliver (NotEvaluated), it was reachable only through a missing visit (Pruned), or the failure policy dropped it (Dropped). The Handler’s record. No station can report it about itself.
State diagram of a visit: Checking, PendingApproval, Queued, Running and Inspecting as non-terminal states; Delivered, Refused, Rejected, TimedOut, Failed and Revoked as terminal outcomes, each reached by the event that announces it.
The states of a visit at a station, and the event that enters each

Reporting a controller’s refusal as a rejection tells a consumer that their request failed a check when a person declined it; reporting a timeout as either puts a verdict in the mouth of somebody who never gave one; reporting a station’s own failure as a refusal attributes to a controller a decision they did not take. Every non-success event MUST carry the reason the consumer will see, and the envelope MUST repeat it.

11.1. Cancellation

POST /visits/{visitId}/cancel withdraws a visit: on approval timeout, on the run’s FailRun reaction, on a person stopping the run, at the budget deadline, or on replanning. A queued or pending visit ends immediately and the station emits negotiation.withdrawn; a running visit stops at its next checkpoint and the station emits visit.cancelled; either ends TimedOut. A visit already in a terminal state is unaffected, and the station answers 200 with its current status.

11.2. Revocation

A controller may take an agreement out of force while a visit under it is queued or running. The station MUST stop at its next checkpoint and emit visit.revoked, carrying the controller’s reason verbatim and naming the agreement, because a Handler applying the run’s onRevoked reaction needs it: outputs already derived from a revoked visit fall under that agreement’s duties. A visit that arrives afterwards presenting a revoked agreement was never running and is Refused at PEP 2, not Revoked; reporting it as Revoked would fire the run’s onRevoked over a visit that never began.

12. Messages

The three message types are JSON Schemas [JSON-SCHEMA] in Appendix B: Normative artefacts; the tables here describe them and the conditional rules each schema enforces. Members marked ✱ are required.

12.1. Visit descriptor

The visit descriptor is what a Handler hands to a station to start one visit. It is pushed to POST /visits or pulled by the station from the Handler’s pending list — the same object either way.

Member Type Meaning
protocol ✱ IRI https://w3id.org/fdt/visit-protocol/v1.
visit ✱ IRI The fdt-run:Visit, minted by the Handler; the idempotency key.
run ✱, plan ✱ IRI The run and the fdt-run:Plan the visit is part of. A station may dereference the redacted plan for a visit it holds (Handler API).
hop ✱, phase ✱ integer ≥ 1 The visit’s position in the itinerary.
attempt integer Retries reuse the visit IRI and increment this.
precededBy IRI[] Visits whose results led to this one. MUST name handOver.fromVisit where a hand-over is carried.
network IRI The FDT network the visit runs under; the station evaluates the controller’s offers for this network only.
consumer object party ✱ — the accountable legal party (odrl:assignee); agent — the person or service acting, as this descriptor claims; handler — the Handler instance; presentation — the credential presentation of § 12.4 Presentation, verbatim; exchangedToken — an RFC 8693 exchanged token where the network’s identity provider performs the exchange. A descriptor carrying exchangedToken MUST carry presentation; a descriptor’s presentation MUST NOT carry assurance or exchange (§ 15.1 Credentials).
train ✱ object iri ✱ — the fdt-o:Train in a depot catalogue; type ✱ — the concrete train class; mechanism ✱ — the interaction mechanism the station must support; version; coordination — Orchestrated or Choreographed.
payload ✱ object mediaType ✱; digest ✱ — MUST equal the fdt-o:artifactDigest in the train’s depot offer; and exactly one of content (inline, UTF-8) or url (where to fetch it; the station verifies the digest).
parameters object[] Each name ✱ and value ✱, optionally the declared parameter IRI. Values are the train’s declared parameters and are passed to the adapter unchanged.
negotiation ✱ object Exactly one of request — an odrl:Request as JSON-LD — or agreement — the IRI of an existing active agreement.
target IRI The hosted dataset, part or service the visit is about; also the request’s odrl:target.
onward ✱ object allowedClasses ✱ — classes of state the plan expects to leave; destination ✱ — handler, train or linkage; linkageStation, joinKey; outputSchema — the JSON Schema the train’s declared output conforms to.
handOver object (closed) fromVisit ✱, fromStation ✱, stateClass ✱ (Aggregate or Pseudonym), url ✱ (https only), mediaType ✱, digest ✱, bytes, joinKey, conformsTo, duties. See § 9.2 What arrives.
reporting ✱ object mode ✱ — callback or poll; callbackUrl and callbackToken for callback.
deadline date-time The run’s budget deadline; after it the Handler withdraws the visit.
approvalWait xsd:dayTimeDuration How long the Handler waits in pending approval before withdrawing. Days to seconds, never months: the value is the moment a controller’s pending case is withdrawn, and a month is 28 to 31 days depending on when it started.
labels object Free-form, shown in consoles.

Conditional rules the schema enforces: a descriptor carrying handOver MUST carry a non-empty precededBy and its request MUST contain a permission for fdt-p:acceptHandOver; a descriptor carrying exchangedToken MUST carry presentation; a Handler MUST NOT grade its own presentation.

The descriptor of hop 1 of the gene–disease run — the published fixture visit-descriptor-gd-m1.json.
{
  "protocol": "https://w3id.org/fdt/visit-protocol/v1",
  "visit": "https://example.org/fdt/visit/m1-v1",
  "run": "https://example.org/fdt/run/gd-m1-01",
  "plan": "https://example.org/fdt/plan/gene-disease-single",
  "hop": 1,
  "phase": 1,
  "attempt": 1,
  "network": "https://example.org/fdt/net/health-research-nl",
  "consumer": {
    "party": "https://example.org/fdt/party/eu-cardionet-like",
    "agent": "m.devries@cardionet.example",
    "handler": "https://handler.cardionet.example/fdt/v1",
    "presentation": {
      "form": "organisation-credential",
      "party": "https://example.org/fdt/party/eu-cardionet-like",
      "agent": "m.devries@cardionet.example",
      "issuer": "https://idp.example.org/ls-aai-like"
    },
    "exchangedToken": "eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCJ9.PLACEHOLDER.PLACEHOLDER"
  },
  "train": {
    "iri": "https://example.org/fdt/train/gene-disease",
    "type": "https://w3id.org/fdt/fdt-o#SPARQLTrain",
    "mechanism": "https://w3id.org/fdt/instances#SPARQL",
    "version": "1.3",
    "coordination": "Orchestrated"
  },
  "payload": {
    "mediaType": "application/sparql-query",
    "digest": "sha256:0f2ea318c57bde07b025561f2333bbf0b0cb7ce75ed867ca72020ff60a7890b9",
    "url": "https://depot.example.org/trains/gene-disease/1.3/query.rq"
  },
  "parameters": [
    {
      "name": "mode",
      "value": "genes-to-diseases",
      "parameter": "https://example.org/fdt/param/gd-mode"
    },
    {
      "name": "terms",
      "value": [
        "TTN",
        "MYH7",
        "LMNA"
      ],
      "parameter": "https://example.org/fdt/param/gd-terms"
    },
    {
      "name": "min_evidence",
      "value": 2,
      "parameter": "https://example.org/fdt/param/gd-min-evidence"
    },
    {
      "name": "limit",
      "value": 100,
      "parameter": "https://example.org/fdt/param/gd-limit"
    }
  ],
  "negotiation": {
    "request": {
      "@context": "http://www.w3.org/ns/odrl.jsonld",
      "@type": "Request",
      "@id": "https://example.org/fdt/request/gd-2026",
      "profile": "https://w3id.org/fdt/profile",
      "target": "https://example.org/fdt/dataset/ut-gene-disease",
      "assignee": "https://example.org/fdt/party/eu-cardionet-like",
      "https://w3id.org/fdt/profile#train": {
        "@id": "https://example.org/fdt/train/gene-disease"
      },
      "https://w3id.org/fdt/profile#underNetwork": {
        "@id": "https://example.org/fdt/net/health-research-nl"
      },
      "permission": [
        {
          "@type": "Permission",
          "action": "https://w3id.org/fdt/profile#runQuery",
          "constraint": [
            {
              "leftOperand": "purpose",
              "operator": "eq",
              "rightOperand": {
                "@id": "https://w3id.org/dpv#ResearchAndDevelopment"
              }
            }
          ]
        },
        {
          "@type": "Permission",
          "action": "https://w3id.org/fdt/profile#deliverResult",
          "constraint": [
            {
              "leftOperand": "https://w3id.org/fdt/profile#onwardStateClass",
              "operator": "eq",
              "rightOperand": {
                "@id": "https://w3id.org/fdt/profile#Aggregate"
              }
            }
          ]
        }
      ]
    }
  },
  "target": "https://example.org/fdt/dataset/ut-gene-disease",
  "onward": {
    "allowedClasses": [
      "Aggregate"
    ],
    "destination": "handler",
    "outputSchema": "https://example.org/fdt/schemas/gene-disease-output"
  },
  "reporting": {
    "mode": "callback",
    "callbackUrl": "https://handler.cardionet.example/fdt/v1/visits/m1-v1",
    "callbackToken": "<per-visit capability token>"
  },
  "deadline": "2026-09-19T00:00:00Z",
  "approvalWait": "PT0S",
  "labels": {
    "plan": "Cardiomyopathy genes at one station",
    "run": "M1"
  }
}

12.2. Visit event

A visit event is one progress or compliance event of a visit, emitted by the station at every checkpoint decision and state change.

Member Type Meaning
id ✱ IRI Globally unique, minted by the station.
visit ✱, run, station IRI What the event is about and who emitted it.
sequence ✱ integer Monotonic per visit and attempt.
attempt integer
time ✱ date-time The station’s time.
type ✱ enum One of the types in the table below.
state ✱ enum The fdt-run:VisitState after this event: Queued, Checking, PendingApproval, Running, Inspecting, Delivered, Skipped, Refused, TimedOut, Rejected, Failed, Revoked.
checkpoint enum PEP1, NEG, PEP2, EXEC, PEP3, OBL.
agreement IRI
progress number 0–1 For visit.progress.
message string One sentence for consoles.
reason string For refused, rejected, failed, revoked: the stated reason shown to the consumer. On visit.revoked it is the controller’s reason.
justification object decision — permit, deny, permit-with-duties or redact; rule; offer; agreement; stationPolicy; evidence — never record-level content; duties.
approval object (closed) For negotiation.pending-approval: controller ✱, expiresAt, mode, rule, recommendation (grant or refuse), missing, assumedRegime (§ 7.2 Matched, pending, refused).
resultUrl URL For visit.delivered under poll reporting.
dsp object The equivalent Dataspace Protocol state, informative (§ 16 Relationship to the Dataspace Protocol).
requester presentation Who the visit was dispatched by, as presented, graded by the station.
Checkpoint Event types State after
PEP 1 visit.received · visit.rejected Checking · Rejected
NEG negotiation.requested · negotiation.matched · negotiation.pending-approval · negotiation.awaiting-signature · negotiation.active · negotiation.refused · negotiation.withdrawn Checking · PendingApproval · Checking · Checking · Refused · TimedOut
PEP 2 visit.queued · visit.rejected · visit.revoked Queued · Rejected · Revoked
EXEC visit.running · visit.progress · visit.failed · visit.cancelled · visit.revoked Running · Running · Failed · TimedOut · Revoked
PEP 3 visit.inspecting · visit.delivered · visit.rejected Inspecting · Delivered · Rejected
OBL obligation.scheduled · obligation.fulfilled unchanged

Conditional rules the schema enforces: negotiation.refused and visit.rejected MUST carry reason and justification; visit.failed and visit.revoked MUST carry reason; visit.revoked, negotiation.awaiting-signature and negotiation.active MUST name the agreement; negotiation.pending-approval MUST carry approval; negotiation.matched and visit.delivered MUST carry justification; visit.received and visit.rejected MUST carry requester; and a terminal event MUST announce its own state — negotiation.refused → Refused, visit.rejected → Rejected, visit.delivered → Delivered, visit.failed → Failed, visit.revoked → Revoked.

Note: justification.decision has no value for nothing was decided. A visit.failed at NEG —​the controller granted and the station broke before it could act — carries deny today, the same word as a refusal, and type and state are what tell the two apart. Whether the field describes what the rules said or what happened to the visit, and therefore whether the enumeration is short by one value or two, is open (Q127).

The event stream of the same visit under push dispatch — visit-events-gd-m1.json. Nine events; nothing ran between the fourth and the fifth.
[
  {
    "id": "https://station.ut.example/fdt/v1/events/0001-m1v1",
    "visit": "https://example.org/fdt/visit/m1-v1",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 1,
    "attempt": 1,
    "time": "2026-09-12T09:03:04Z",
    "type": "visit.received",
    "state": "Checking",
    "checkpoint": "PEP1",
    "message": "Descriptor valid; payload digest matches the Depot offer for gene-disease 1.3; mechanism SPARQL supported and enabled",
    "requester": {
      "form": "organisation-credential",
      "party": "https://example.org/fdt/party/eu-cardionet-like",
      "agent": "m.devries@cardionet.example",
      "issuer": "https://idp.example.org/ls-aai-like",
      "assurance": "presented-and-verified",
      "exchange": {
        "issuer": "https://idp.example.org/ls-aai-like",
        "subject": "https://example.org/fdt/party/eu-cardionet-like",
        "actor": "m.devries@cardionet.example"
      }
    },
    "justification": {
      "decision": "permit",
      "rule": "PEP1: PayloadShape + digest == fdt-o:artifactDigest",
      "evidence": {
        "digest": "sha256:0f2ea318c57bde07b025561f2333bbf0b0cb7ce75ed867ca72020ff60a7890b9"
      }
    }
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0002-m1v1",
    "visit": "https://example.org/fdt/visit/m1-v1",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 2,
    "attempt": 1,
    "time": "2026-09-12T09:03:05Z",
    "type": "negotiation.requested",
    "state": "Checking",
    "checkpoint": "NEG",
    "message": "Matching request gd-2026 against 1 offer in network health-research-nl"
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0003-m1v1",
    "visit": "https://example.org/fdt/visit/m1-v1",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 3,
    "attempt": 1,
    "time": "2026-09-12T09:03:10Z",
    "type": "negotiation.matched",
    "state": "Checking",
    "checkpoint": "NEG",
    "message": "Offer ut-gene-disease-research matches: research purpose, network health-research-nl, aggregate delivery",
    "justification": {
      "decision": "permit",
      "rule": "NEG: every constraint of the matched permissions is satisfied by a fact the visit carries",
      "evidence": {
        "offer": "https://example.org/fdt/offer/ut-gene-disease-research",
        "purpose": "https://w3id.org/dpv#ResearchAndDevelopment",
        "network": "https://example.org/fdt/net/health-research-nl",
        "requiresManualApproval": false
      }
    }
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0004-m1v1",
    "visit": "https://example.org/fdt/visit/m1-v1",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 4,
    "attempt": 1,
    "time": "2026-09-12T09:03:11Z",
    "type": "negotiation.awaiting-signature",
    "state": "Checking",
    "checkpoint": "NEG",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "Agreement agr-m1-01 concluded without a human: the offer requires no manual approval and no constraint needed evidence the station lacks. Awaiting the assignee's signature (ADR-038)",
    "justification": {
      "decision": "permit-with-duties",
      "rule": "ADR-008 auto-approval",
      "agreement": "https://example.org/fdt/agreement/agr-m1-01",
      "duties": [
        "aggregate: at least 5 subjects per released cell"
      ],
      "evidence": {
        "agreementDigest": "sha256:a693463c971c9531ba74a8ba2f33efc64df38ee8da47397e2a07e304e706009f"
      }
    },
    "dsp": {
      "negotiationState": "AGREED"
    }
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0005-m1v1",
    "visit": "https://example.org/fdt/visit/m1-v1",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 5,
    "attempt": 1,
    "time": "2026-09-12T09:03:14Z",
    "type": "negotiation.active",
    "state": "Checking",
    "checkpoint": "NEG",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "Signed by both parties; the agreement is active",
    "justification": {
      "decision": "permit-with-duties",
      "rule": "NEG: both parties have signed (agreement-derivation.md §6)",
      "agreement": "https://example.org/fdt/agreement/agr-m1-01",
      "duties": [
        "aggregate: at least 5 subjects per released cell"
      ]
    },
    "dsp": {
      "negotiationState": "FINALIZED"
    }
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0006-m1v1",
    "visit": "https://example.org/fdt/visit/m1-v1",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 6,
    "attempt": 1,
    "time": "2026-09-12T09:03:12Z",
    "type": "visit.queued",
    "state": "Queued",
    "checkpoint": "PEP2",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "Agreement active and covers runQuery on the target; within quota"
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0007-m1v1",
    "visit": "https://example.org/fdt/visit/m1-v1",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 7,
    "attempt": 1,
    "time": "2026-09-12T09:03:15Z",
    "type": "visit.running",
    "state": "Running",
    "checkpoint": "EXEC",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "SPARQL adapter executing the train payload against the station graph"
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0008-m1v1",
    "visit": "https://example.org/fdt/visit/m1-v1",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 8,
    "attempt": 1,
    "time": "2026-09-12T09:04:20Z",
    "type": "visit.inspecting",
    "state": "Inspecting",
    "checkpoint": "PEP3",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "Inspecting 5 cells against the train's declared output schema and the agreement's aggregation duty (k=5)"
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0009-m1v1",
    "visit": "https://example.org/fdt/visit/m1-v1",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 9,
    "attempt": 1,
    "time": "2026-09-12T09:04:22Z",
    "type": "visit.delivered",
    "state": "Delivered",
    "checkpoint": "PEP3",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "Aggregate delivered to the Handler; 5 cells; no record-level field present",
    "justification": {
      "decision": "permit-with-duties",
      "rule": "PEP3: every cell counts at least 5 subjects; result validates against the train's declared output schema",
      "agreement": "https://example.org/fdt/agreement/agr-m1-01",
      "evidence": {
        "cells": 5,
        "min_subject_count": 14,
        "outputSchema": "https://example.org/fdt/schemas/gene-disease-output"
      }
    },
    "resultUrl": "https://station.ut.example/fdt/v1/visits/m1-v1/result",
    "dsp": {
      "transferState": "COMPLETED"
    }
  }
]
The same visit under poll dispatch — visit-events-gd-m1-poll.json. Eight events: no negotiation.awaiting-signature, and negotiation.active states the route the exchange took.
[
  {
    "id": "https://station.ut.example/fdt/v1/events/0001-m1v4",
    "visit": "https://example.org/fdt/visit/m1-v4",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 1,
    "attempt": 1,
    "time": "2026-09-12T09:03:04Z",
    "type": "visit.received",
    "state": "Checking",
    "checkpoint": "PEP1",
    "message": "Descriptor valid; payload digest matches the Depot offer for gene-disease 1.3; mechanism SPARQL supported and enabled",
    "requester": {
      "form": "organisation-credential",
      "party": "https://example.org/fdt/party/eu-cardionet-like",
      "agent": "m.devries@cardionet.example",
      "issuer": "https://idp.example.org/ls-aai-like",
      "assurance": "presented-and-verified",
      "exchange": {
        "issuer": "https://idp.example.org/ls-aai-like",
        "subject": "https://example.org/fdt/party/eu-cardionet-like",
        "actor": "m.devries@cardionet.example"
      }
    },
    "justification": {
      "decision": "permit",
      "rule": "PEP1: PayloadShape + digest == fdt-o:artifactDigest",
      "evidence": {
        "digest": "sha256:0f2ea318c57bde07b025561f2333bbf0b0cb7ce75ed867ca72020ff60a7890b9"
      }
    }
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0002-m1v4",
    "visit": "https://example.org/fdt/visit/m1-v4",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 2,
    "attempt": 1,
    "time": "2026-09-12T09:03:05Z",
    "type": "negotiation.requested",
    "state": "Checking",
    "checkpoint": "NEG",
    "message": "Matching request gd-2026 against 1 offer in network health-research-nl"
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0003-m1v4",
    "visit": "https://example.org/fdt/visit/m1-v4",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 3,
    "attempt": 1,
    "time": "2026-09-12T09:03:10Z",
    "type": "negotiation.matched",
    "state": "Checking",
    "checkpoint": "NEG",
    "message": "Offer ut-gene-disease-research matches: research purpose, network health-research-nl, aggregate delivery",
    "justification": {
      "decision": "permit",
      "rule": "NEG: every constraint of the matched permissions is satisfied by a fact the visit carries",
      "evidence": {
        "offer": "https://example.org/fdt/offer/ut-gene-disease-research",
        "purpose": "https://w3id.org/dpv#ResearchAndDevelopment",
        "network": "https://example.org/fdt/net/health-research-nl",
        "requiresManualApproval": false
      }
    }
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0004-m1v4",
    "visit": "https://example.org/fdt/visit/m1-v4",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 4,
    "attempt": 1,
    "time": "2026-09-12T09:03:14Z",
    "type": "negotiation.active",
    "state": "Checking",
    "checkpoint": "NEG",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "Signed by both parties; the agreement is active. This station is poll-dispatched, so it obtained the assignee's signature itself in one round trip (POST /visits/{visitId}/agreement) rather than waiting to be reached",
    "justification": {
      "decision": "permit-with-duties",
      "rule": "NEG: both parties have signed (agreement-derivation.md §6)",
      "agreement": "https://example.org/fdt/agreement/agr-m1-01",
      "duties": [
        "aggregate: at least 5 subjects per released cell"
      ],
      "evidence": {
        "signatureRoute": "station-initiated",
        "dispatchMode": "poll",
        "agreementDigest": "sha256:a693463c971c9531ba74a8ba2f33efc64df38ee8da47397e2a07e304e706009f"
      }
    },
    "dsp": {
      "negotiationState": "FINALIZED"
    }
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0005-m1v4",
    "visit": "https://example.org/fdt/visit/m1-v4",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 5,
    "attempt": 1,
    "time": "2026-09-12T09:03:12Z",
    "type": "visit.queued",
    "state": "Queued",
    "checkpoint": "PEP2",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "Agreement active and covers runQuery on the target; within quota"
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0006-m1v4",
    "visit": "https://example.org/fdt/visit/m1-v4",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 6,
    "attempt": 1,
    "time": "2026-09-12T09:03:15Z",
    "type": "visit.running",
    "state": "Running",
    "checkpoint": "EXEC",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "SPARQL adapter executing the train payload against the station graph"
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0007-m1v4",
    "visit": "https://example.org/fdt/visit/m1-v4",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 7,
    "attempt": 1,
    "time": "2026-09-12T09:04:20Z",
    "type": "visit.inspecting",
    "state": "Inspecting",
    "checkpoint": "PEP3",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "Inspecting 5 cells against the train's declared output schema and the agreement's aggregation duty (k=5)"
  },
  {
    "id": "https://station.ut.example/fdt/v1/events/0008-m1v4",
    "visit": "https://example.org/fdt/visit/m1-v4",
    "run": "https://example.org/fdt/run/gd-m1-01",
    "station": "https://example.org/fdt/station/ut",
    "sequence": 8,
    "attempt": 1,
    "time": "2026-09-12T09:04:22Z",
    "type": "visit.delivered",
    "state": "Delivered",
    "checkpoint": "PEP3",
    "agreement": "https://example.org/fdt/agreement/agr-m1-01",
    "message": "Aggregate delivered to the Handler; 5 cells; no record-level field present",
    "justification": {
      "decision": "permit-with-duties",
      "rule": "PEP3: every cell counts at least 5 subjects; result validates against the train's declared output schema",
      "agreement": "https://example.org/fdt/agreement/agr-m1-01",
      "evidence": {
        "cells": 5,
        "min_subject_count": 14,
        "outputSchema": "https://example.org/fdt/schemas/gene-disease-output"
      }
    },
    "resultUrl": "https://station.ut.example/fdt/v1/visits/m1-v4/result",
    "dsp": {
      "transferState": "COMPLETED"
    }
  }
]

12.3. Visit-result envelope

The visit-result envelope is what a station returns for one visit after result inspection. The same object is the evaluation scope over which a Handler evaluates a plan’s conditions (ADR-025), so a Handler evaluates phase conditions and next-hop selectors directly on what arrives.

Member Type Meaning
visit ✱, station ✱, train ✱ IRI
hop ✱, phase ✱, attempt integer
run, protocol IRI
agreement IRI Required when delivered, and when rejected at PEP 3.
state ✱ enum delivered, skipped, refused, timed-out, rejected, failed, revoked — the outcome, in the envelope’s spelling.
checkpoint enum Where the terminal decision was taken. Required for every outcome but delivered.
deliveredAt date-time Required when delivered.
inspection object outcome ✱ — passed, redacted or rejected; onwardStateClass; outputSchema — the schema PEP 3 validated against; justification — in words a controller can read. Required when delivered and when rejected at PEP 3.
result ✱ object The train’s declared aggregate output, conforming to inspection.outputSchema. MUST be empty under any outcome but delivered.
skipCause enum NotSelected, NotEvaluated, Pruned, Dropped. Required when skipped.
reason string The stated reason for a non-success outcome. Required for every outcome but delivered.
events URL Where the full event stream can be read.
artifacts object[] Files released by PEP 3: name ✱, mediaType ✱, digest ✱, bytes ✱, url (https only — a location, never a container), inline (the honest way to put content in an envelope), conformsTo. Never record-level.
onward object stateClass ✱ (Aggregate, Pseudonym, None), destination ✱, linkageStation, joinKey, handedOverDigest, duties.
routing object Choreography only: nextStations, carriedState (digest), carriedStateClass.
obligations object[] Post-execution duties the station scheduled and the consumer must honour: duty ✱, dueAt, agreement.
provenance URL The PROV-O [PROV-O] record of this visit.
requester presentation Who the visit was dispatched by, as presented.

Conditional rules the schema enforces: a delivered visit carries agreement, deliveredAt and inspection; a result rejected at PEP 3 carries agreement and inspection; every non-success outcome carries reason; a skipped visit carries skipCause and reason; only a delivered visit carries a result; a visit that did not deliver says where it ended.

The envelope of the same visit — visit-result-gd-m1.json. Five cells, the smallest counting 14 subjects against a duty requiring 5; the numbers came out of the query, not out of prose.
{
  "protocol": "https://w3id.org/fdt/visit-protocol/v1",
  "visit": "https://example.org/fdt/visit/m1-v1",
  "run": "https://example.org/fdt/run/gd-m1-01",
  "station": "https://example.org/fdt/station/ut",
  "hop": 1,
  "phase": 1,
  "attempt": 1,
  "train": "https://example.org/fdt/train/gene-disease",
  "agreement": "https://example.org/fdt/agreement/agr-m1-01",
  "state": "delivered",
  "deliveredAt": "2026-09-12T09:04:22Z",
  "inspection": {
    "outcome": "passed",
    "onwardStateClass": "Aggregate",
    "outputSchema": "https://example.org/fdt/schemas/gene-disease-output",
    "justification": "Result validates against the train's declared output schema; 5 cells, smallest counts 14 subjects, k=5 required by the agreement's odrl:aggregate duty; no record-level field present."
  },
  "result": {
    "rows": [
      {
        "gene_symbol": "TTN",
        "disease": "https://example.org/fdt/gd/disease/dilated-cardiomyopathy",
        "disease_label": "Dilated cardiomyopathy",
        "evidence_count": 6,
        "subject_count": 62
      },
      {
        "gene_symbol": "MYH7",
        "disease": "https://example.org/fdt/gd/disease/hypertrophic-cardiomyopathy",
        "disease_label": "Hypertrophic cardiomyopathy",
        "evidence_count": 5,
        "subject_count": 40
      },
      {
        "gene_symbol": "LMNA",
        "disease": "https://example.org/fdt/gd/disease/dilated-cardiomyopathy",
        "disease_label": "Dilated cardiomyopathy",
        "evidence_count": 4,
        "subject_count": 23
      },
      {
        "gene_symbol": "MYH7",
        "disease": "https://example.org/fdt/gd/disease/dilated-cardiomyopathy",
        "disease_label": "Dilated cardiomyopathy",
        "evidence_count": 3,
        "subject_count": 18
      },
      {
        "gene_symbol": "LMNA",
        "disease": "https://example.org/fdt/gd/disease/conduction-disease",
        "disease_label": "Cardiac conduction disease",
        "evidence_count": 2,
        "subject_count": 14
      }
    ],
    "cell_count": 5,
    "min_subject_count": 14
  },
  "onward": {
    "stateClass": "Aggregate",
    "destination": "handler"
  },
  "events": "https://station.ut.example/fdt/v1/visits/m1-v1/events",
  "provenance": "https://station.ut.example/fdt/v1/visits/m1-v1/prov"
}

12.4. Presentation

The presentation records what a component saw of the credential a visit was dispatched under, verbatim — because a component that normalised every form into "the agent" would write down something nobody said, and which form was presented is often what an audit is about. One schema, recorded in the descriptor, every event and the envelope.

Member Meaning
form ✱ organisation-credential — the subject is the party and the credential names the human agent acting for it (RFC 8693’s act.sub); person-credential — the subject is the person and a named federation claim relates them to a party; configured-credential — configured out of band (development deployments); no-credential — authentication switched off; credential-not-read — the visit was turned away before any credential could be read.
party, agent, affiliation, issuer The legal party bound, the person or service that acted, the affiliation’s value and who signed the credential.
affiliationClaim For a person’s credential, which claim the affiliation was read from — required, because the claim’s name is a deployment’s configuration and not pinned by this protocol (Q30), and two deployments reading two different claims must be distinguishable in the first record either writes.
assurance Set by the station only: presented-only or presented-and-verified (§ 15.1 Credentials).
exchange What a verified exchanged token established and nothing else: issuer, subject, actor. Closed.
assuranceNote, note Why the weaker state applies; why a degenerate form was accepted.

12.5. VisitStatus and Problem

A VisitStatus is the station’s answer to a push, a replay, a cancellation, a signature and GET /visits/{visitId}: visit ✱, attempt, state ✱, agreement, sequence ✱ (of the latest event), approval (controller, expiresAt), progress, events and result URLs, and reason for a non-success outcome.

A Problem is an RFC 9457 problem details object [RFC9457] — type ✱, title ✱, status ✱, detail, instance — extended with the FDT outcome (Refused, Rejected or Failed), the checkpoint, and the justification. A station answering with a Problem MUST put the same outcome word in it that the visit’s terminal event carries.

12.6. Signature messages

An AgreementSignature (posted by the Handler to a push-dispatch station) and a PartySignature (carried in both directions of the poll-dispatch exchange) are one construction: signedBy ✱, onBehalfOf (present exactly when it differs from signedBy), keyId ✱, algorithm ✱ (EdDSA), signedDigest ✱ (sha256: + 64 hex), value ✱ (base64url, unpadded). Neither carries a role: the endpoint or the position in the exchange fixes it, and a role a sender could choose would let the party holding the pen present the other side’s signature as its own.

A CountersignatureRequest is what a poll-dispatch station sends: agreement ✱ (IRI), document ✱ (mediaType ✱ — application/n-quads in canonical form or text/turtle —​and content ✱, the serialised graph), and assignerSignature ✱. A Countersignature is the answer: agreement ✱ and signature ✱. A CountersignatureRefused carries reason ✱ from the four words of § 7.4.4 Refusing a signature and a detail.

The poll-dispatch exchange for the same agreement — poll-countersign-ask-gd-m1.json (the document’s N-Quads abbreviated here only in prose; the fixture is complete) and poll-countersign-answer-gd-m1.json.
{
  "agreement": "https://example.org/fdt/agreement/agr-m1-01",
  "signature": {
    "signedBy": "https://example.org/fdt/party/eu-cardionet-like",
    "keyId": "https://handler.cardionet.example/fdt/v1/keys/1",
    "algorithm": "EdDSA",
    "signedDigest": "sha256:a693463c971c9531ba74a8ba2f33efc64df38ee8da47397e2a07e304e706009f",
    "value": "TtmuJ0hgTbt4K7UE-o7f3vZEqvJoFD7CFSW8OEmHEUNQ8pJy-2TlXun9jB515wzHMeT3L3UbBDSo14gtezIWAw"
  }
}

12.7. Redacted plan

The redacted plan a station may read for a visit it holds (Handler API) shows the run’s structure in full and never another station’s identity: plan ✱, visit ✱, pattern ✱, coordination ✱, phases ✱, failurePolicy ✱, stateCarriage, budget, stopCondition, intendedStationCount ✱ — the count is disclosed, the list is not, in any form — branch ✱ (this station’s own, with nothing withheld) and redaction ✱, by which the document says of itself what it is not and that it is the Handler’s unverified account (ADR-047).

13. Station API

Normative definition: station-visit-api.yaml (Appendix B: Normative artefacts). Base path: the station’s dispatch endpoint. Authentication: § 15 Security.

Operation Purpose Responses
GET / Self-description (JSON-LD): type, mechanisms, memberships, dispatch modes, protocol versions, links to metadata and catalogue. 200
GET /catalogue The DCAT catalogue — datasets, parts, offers, distributions, what each conforms to. Publishing it grants nothing. 200 (Turtle, JSON-LD) · 406
GET /shapes The SHACL shapes the station’s data conforms to — what makes station selection answerable. 200 · 406
POST /visits Push a visit (§ 6.1 Push dispatch). Body: visit descriptor. 202 + Location · 200 (replay) · 400 Rejected · 401 · 403 Refused · 409 · 415
GET /visits/{visitId} Current VisitStatus. 200 · 404
GET /visits/{visitId}/events Ordered events with sequence > after; SSE with Accept: text/event-stream. 200
GET /visits/{visitId}/result The visit-result envelope once terminal. 200 · 409 not yet terminal
GET /visits/{visitId}/artifacts/{name} An artefact released by PEP 3; Digest header carries its SHA-256. 200
POST /visits/{visitId}/cancel Withdraw or stop (§ 11.1 Cancellation). Body: reason ✱, cause. 200 current status
POST /visits/{visitId}/agreement/signature The assignee signs (§ 7.4.3 The exchange, in two shapes, push shape). Body: AgreementSignature. 200 active and queued · 404 nothing awaiting a signature · 409 already signed · 422 refused, naming which of the four
GET /agreements/{agreementId} An agreement the caller is party to, as JSON-LD or Turtle, with a Digest header; read-only. 200 · 403 · 404

visitId is the URL-safe form of the visit IRI; percent-encoding of the full IRI is the default and a station documents its encoding.

14. Handler API — the station-facing surface

Normative definition: handler-callback-api.yaml (Appendix B: Normative artefacts). This document is defined by its audience: everything a station reads or writes at a Handler is declared here, and nothing a consumer’s console reads is — a station operator must never be able to see a whole run, because a run is other controllers' decisions about other people’s data. Everything here is reachable with the per-visit callback token or the station operator’s own credential and is scoped to one station’s own traffic.

Operation Purpose Responses
GET /stations/{stationId}/pending-visits Poll dispatch: the descriptors waiting for this station; wait long-polls up to 60 s. 200 (possibly empty)
POST /visits/{visitId}/ack The station has taken the visit. Body: the first event or a Problem. 204 · 409 withdrawn meanwhile — do not start
POST /visits/{visitId}/events Push one or more events; at-least-once. 204 · 404 · 410 withdrawn — stop at the next checkpoint
PUT /visits/{visitId}/result Deliver the envelope; idempotent per visit and attempt. 204 · 422 does not conform (record-level content, missing outcome or reason)
PUT /visits/{visitId}/artifacts/{name} Upload an artefact (poll mode); digest verified against the envelope. 204 · 409 digest mismatch
GET /visits/{visitId}/plan The redacted plan for this visit (§ 12.7 Redacted plan). 200 · 403 caller does not hold this visit · 404
POST /visits/{visitId}/agreement Poll-dispatch signature exchange: the station hands over the agreement it derived and signed; the Handler signs the other side in the same response. Body: CountersignatureRequest. 201 Countersignature · 404 · 409 already countersigned · 422 CountersignatureRefused

15. Security

15.1. Credentials

Version 1 uses OpenID Connect bearer tokens [OPENID-CONNECT-CORE]. A station MUST verify a visit’s token against the identity providers it trusts for the visit’s network (ADR-017), and MUST answer 401 — not an outcome word — when the token is missing or untrusted, since nothing has been decided about a caller whose identity was not established.

The Handler visits under its own credential (ADR-044). Nothing forwards a consumer’s credential to a station: a token minted for a Handler’s audience and presented to a station would be a confused deputy. Who dispatched the run reaches the station two ways, and the station records which it got:

A station MUST record which of the two it got in every event’s requester.assurance —​presented-and-verified, with the exchange it established, or presented-only. Grading is the station’s act and nobody else’s: a descriptor whose presentation carries assurance or exchange MUST be rejected, because a Handler writing verified about a credential presented to it would be certifying its own assertion. A network without exchange is recorded, not broken, and a console MUST render the two states as two states.

Callbacks use the per-visit capability token in the descriptor. A poll-dispatch station authenticates at the Handler with the OIDC client credentials of its operator, trusted by the Handler for the network.

15.2. Transport and integrity

All traffic between the parties MUST be over TLS, and the schemas say so wherever a location is written down: handOver.url and artifacts[].url are https only. Integrity is by sha256: digests end to end — the train’s offer, the descriptor, the arrival check, the agreement’s three pins, the released artefacts and the hand-over. Signing keys are published as JWK sets by their owners and distributed out of band (§ 7.4.4 Refusing a signature).

Version 2 is planned to replace bearer tokens with credential presentations and to bind negotiation to the Dataspace Protocol (§ 16 Relationship to the Dataspace Protocol).

16. Relationship to the Dataspace Protocol

This section is non-normative.

The agreement and visit lifecycles map onto the Dataspace Protocol’s Contract Negotiation and Transfer Process [DSP]; in version 1 the mapping is documentation and a version 2 binding would implement it (ADR-019). The mapping is lossy in one place and the loss must not survive the crossing: DSP ends badly in exactly one way — TERMINATED with a reason — while this protocol distinguishes Refused, Rejected, TimedOut, Failed and Revoked and keeps Skipped outside the protocol altogether. A binding carries this protocol’s outcome word in every TERMINATED reason, verbatim; a binding that emits TERMINATED without it has implemented the part of the mapping that DSP could express.

Agreement state DSP Contract Negotiation
requested REQUESTED
matched OFFERED, or straight to AGREED (there are no counter-offers)
pending approval a provider-side wait before AGREED; DSP has no state for a person deciding
awaiting signature AGREED — the provider has sent the agreement and the consumer has not yet verified
active VERIFIED → FINALIZED
never concluded TERMINATED, carrying Refused, TimedOut or Rejected as the case is
suspended, revoked, expired, fulfilled after FINALIZED; DSP 2025-1 does not model them, and revocation after FINALIZED is a gap to raise
Visit state DSP Transfer Process
Queued, Checking, PendingApproval REQUESTED (SUSPENDED if already started)
Running, Inspecting STARTED
Delivered COMPLETED
Refused, Rejected, TimedOut, Failed, Revoked TERMINATED, reason carrying the word
Skipped no Transfer Process is ever created: nothing was requested of this station

Not covered by DSP and therefore this protocol’s own: progress and compliance events, checkpoint decisions and justifications, pending approval, onward-state hand-over and provenance.

17. Open points

This section is non-normative.

The following are recorded in the reference implementation’s question register with a default every implementation follows until the question is decided.

Appendix A: Examples

This appendix is non-normative.

Every example in this document is a published fixture of fdt-commons, copied verbatim from the commit named in Appendix B: Normative artefacts; the reference implementation’s validators check each against its schema and the station’s and Handler’s test suites run against them. The parties in them are fictional. Besides those shown inline: visit-events-gd-m1-refused.json and visit-result-gd-m1-refused.json (a controller’s refusal — five events, ending Refused with the controller’s words), visit-events-gd-m1-failed.json and visit-result-gd-m1-failed.json (a station that broke after a grant — the same five events, ending Failed, and the collision Q127 is about), visit-descriptor-ttg-hop3.json (a descriptor carrying a hand-over, with the lineage rule of § 9.2 What arrives satisfied) and poll-countersign-ask-gd-m1.json (the full CountersignatureRequest, document included).

The envelope of a refused visit — visit-result-gd-m1-refused.json: an empty result, the outcome, where it ended, and the controller’s reason verbatim.
{
  "protocol": "https://w3id.org/fdt/visit-protocol/v1",
  "visit": "https://example.org/fdt/visit/m1-v6",
  "station": "https://example.org/fdt/station/ut",
  "hop": 1,
  "phase": 1,
  "attempt": 1,
  "train": "https://example.org/fdt/train/gene-disease",
  "state": "refused",
  "checkpoint": "NEG",
  "result": {},
  "events": "https://station.ut.example/fdt/v1/visits/https%3A%2F%2Fexample.org%2Ffdt%2Fvisit%2Fm1-v6/events",
  "run": "https://example.org/fdt/run/gd-m1-01",
  "requester": {
    "form": "person-credential",
    "party": "https://example.org/fdt/party/eu-cardionet-like",
    "agent": "m.devries@cardionet.example",
    "affiliation": "https://example.org/fdt/party/eu-cardionet-like",
    "issuer": "https://idp.example.org/ls-aai-like",
    "affiliationClaim": "affiliation",
    "assurance": "presented-only",
    "assuranceNote": "this station verifies no RFC 8693 exchanged token (nothing in this ecosystem performs the exchange yet), so the acting person named above stands on the Handler's assertion and was not verified here (ADR-044 \u00a75: recorded, not broken)"
  },
  "reason": "This cohort is under embargo until the consortium's own analysis is published, and the committee does not release it early for any purpose."
}

Appendix B: Normative artefacts

The following artefacts in fdt-commons [FDT-COMMONS] are normative for this version, pinned to commit eaa339c1c6749997bf93fe00bf9cc967291ce3cb (VERSION 0.75.0). Where this document and an artefact disagree, the artefact governs and this document has an error to be reported.

Artefact Identifier Location
Visit descriptor https://w3id.org/fdt/schemas/visit-descriptor.schema.json schemas/visit-descriptor.schema.json
Visit event https://w3id.org/fdt/schemas/visit-event.schema.json schemas/visit-event.schema.json
Visit-result envelope https://w3id.org/fdt/schemas/visit-result.schema.json schemas/visit-result.schema.json
Presentation https://w3id.org/fdt/schemas/presentation.schema.json schemas/presentation.schema.json
Redacted plan — schemas/redacted-plan.schema.json
Station API OpenAPI 3.1 [OPENAPI], 1.1.0-draft.1 protocol/openapi/station-visit-api.yaml
Handler API, station-facing OpenAPI 3.1, 1.1.0-draft.1 protocol/openapi/handler-callback-api.yaml
ODRL profile and shapes https://w3id.org/fdt/profile vocab/fdt-profile.ttl, shapes/policy.shapes.ttl (fdts:RequestShape, fdts:AgreementShape, fdts:SignatureShape)
Run vocabulary https://w3id.org/fdt/run# vocab/fdt-run.ttl
Agreement derivation (rule and checker) — protocol/agreement-derivation.md, tools/derivation.py
Result inspection (rule and checker) — protocol/result-inspection.md, tools/inspection.py
Dispatch credential — protocol/dispatch-credential.md
Canonical form — tools/canonical.py, cross-checked against the RDF Dataset Canonicalization reference implementation
DSP mapping — mapping/state-machines.md
Fixtures — examples/protocol/

The https://w3id.org/fdt/… identifiers are the names these artefacts carry in the contracts. Their redirects are not yet published; until they are, the locations above are where the artefacts resolve.

Appendix C: Change history

This appendix is non-normative.

16 October 2026 — state classes, not "non-personal"

What may reach a Handler is described by state class — Aggregate, Pseudonym, RecordLevel — and no longer as "non-personal": an aggregate can be personal and a record need not be, and the contracts never used the word. The rule itself (ADR-015) is unchanged.

16 October 2026 — no station types

The protocol never had a station type for identifier translation, but it spoke of "a linkage station" as though it did. It now says what it meant: an ordinary station hosting a translation service, named in the descriptor’s linkageStation (§ 9.1 What leaves). The member names linkage and linkageStation are contract names and are unchanged.

16 October 2026 — conformance criteria

§ 2 Conformance gains the numbered criteria of § 2.3 Criteria — STA for every station, PUSH and POLL for the two dispatch classes, HND for the Handler — each naming the section that states it and the fixture, checker or harness that checks it; the optional features; and the form of a conformance statement. GET /agreements/{agreementId} is now listed among the operations a push-dispatch station implements, which the signature exchange already required. No normative requirement changed; one indicative sentence (§ 4 Parties and identifiers, the self-description advertisement) now carries the key word it was always read with.

16 October 2026 — version 1, editors' draft

First publication as a specification, written against fdt-commons 0.75.0 and the running reference implementation. Relative to the draft-1 protocol note of 12 September 2026 inside fdt-commons: both parties sign the agreement (ADR-038) and the exchange has a poll-dispatch shape (Q52); the dispatch credential is graded (ADR-044); a station may read the redacted plan (ADR-047); hand-over has an inbound direction, named by location and digest (Q124), with the lineage rule and https-only locations; the agreement lifecycle has an awaiting signature state; and the derivation rule binds the agreement’s parties to the offer’s and the request’s.

Index

Terms defined by this specification

References

Normative References

[JSON-SCHEMA]
Austin Wright; et al. JSON Schema: A Media Type for Describing JSON Documents. 10 June 2022. Internet-Draft. URL: https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema
[ODRL-MODEL]
Renato Iannella; Serena Villata. ODRL Information Model 2.2. URL: https://w3c.github.io/poe/model/
[OPENID-CONNECT-CORE]
N. Sakimura; et al. OpenID Connect Core 1.0 incorporating errata set 2. 15 December 2023. Final. URL: https://openid.net/specs/openid-connect-core-1_0.html
[RDF-CANON]
Gregg Kellogg; Dave Longley; Dan Yamamoto. RDF Dataset Canonicalization. URL: https://w3c.github.io/rdf-canon/spec/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[RFC7517]
M. Jones. JSON Web Key (JWK). May 2015. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc7517/
[RFC8032]
S. Josefsson; I. Liusvaara. Edwards-Curve Digital Signature Algorithm (EdDSA). January 2017. Informational. URL: https://www.rfc-editor.org/info/rfc8032/
[RFC8693]
M. Jones; et al. OAuth 2.0 Token Exchange. January 2020. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc8693/
[RFC9457]
M. Nottingham; E. Wilde; S. Dalal. Problem Details for HTTP APIs. July 2023. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc9457/

Non-Normative References

[DSP]
Eclipse Dataspace Working Group. Dataspace Protocol 2025-1. 2025. URL: https://eclipse-dataspace-protocol-base.github.io/DataspaceProtocol/2025-1/
[FDP]
FAIR Data Point Working Group. FAIR Data Point specification. Working Draft. URL: https://specs.fairdatapoint.org/
[FDT-ADR]
FAIR Data Team. FAIR Data Train — Architecture Decision Records. URL: https://github.com/FAIRDataTeam/FAIRDataTrain/tree/master/ReferenceImplementation/docs/adr
[FDT-ARCHITECTURE]
Luiz Olavo Bonino da Silva Santos. FAIR Data Train — General Architecture. 16 October 2026. Editors' Draft. URL: https://specs.fairdatatrain.org/
[FDT-COMMONS]
FAIR Data Team. fdt-commons — shared contracts of the FAIR Data Train ecosystem. URL: https://github.com/FAIRDataTeam/fdt-commons
[OPENAPI]
Darrell Miller; et al. OpenAPI Specification. URL: https://www.openapis.org/
[PROV-O]
Timothy Lebo; Satya Sahoo; Deborah McGuinness. PROV-O: The PROV Ontology. 30 April 2013. REC. URL: https://www.w3.org/TR/prov-o/
[VOCAB-DCAT-3]
Simon Cox; et al. Data Catalog Vocabulary (DCAT) - Version 3. URL: https://w3c.github.io/dx-dcat/TR/