Skip to content

Protocol Specification

Authorization and disclosure semantics for personal data — record model, selection request, grant, manifest, and resource server interface.

1. Introduction

PDPP is an authorization and disclosure protocol for personal data. It defines how a user authorizes an application to access specific data from a data source that holds their records, and how a resource server enforces that authorization.

The protocol specifies:

  • A record model for representing personal data as flat relational streams
  • A selection request format, the structured request for consent a client submits during authorization (RFC 9396 envelope)
  • A grant object representing user-approved, parameterized consent
  • A source declaration describing the consent, record, selection, and query surface exposed by either a connector-backed or provider-native source
  • A resource server interface for serving records under grant enforcement

Most source platforms do not yet expose a PDPP interface natively. Collection is the bridge for those sources: it brings their data into a resource server so the protocol's consent and enforcement layers can govern access to it. The informative PDPP Collection Profile describes that bridge as builder guidance; it is not a conformance requirement. A connector conforms to PDPP by producing a source declaration valid under Section 5 and serving its data through a resource server conforming to Section 8. No particular collection method is required. The core protocol is useful without it: a resource server holding pre-collected data can serve that data under grant enforcement with no collection machinery involved, and data may also reach it via regulatory data exports, manual import, or platform-native APIs. The consent and enforcement layers defined in this specification (Sections 5-8) are agnostic to the collection method.

Any implementation satisfying the role conformance criteria in Section 9 is PDPP-compliant. This specification does not depend on any specific network, token, ledger, infrastructure provider, hosted service, centralized registry lookup, or deployment of this repository. URI identifiers name sources, purposes, clients, and resources; they do not make the example registries in this document runtime dependencies. Consent integrity comes from the resolved grant and the exact source declaration snapshot retained by the authorization server.

Interoperable core sections

Sections 4-8 define the protocol surfaces that implementations evaluate independently.

SectionGovernsOther layers
Section 4: Record ModelPortable record envelopes, stream identity, primary keys, blob references, resource references, stream semantics, and incremental-sync metadata.Source collection, connector execution, and storage-engine choices.
Section 5: Source DeclarationCommon source identity, consent, record, selection, and query capabilities used by connector-backed and provider-native sources, and the conditions under which an authorization server accepts a declaration.Connector acquisition and execution mechanics.
Section 6: Selection RequestWhat a client asks an authorization server to approve, plus declaration-backed validation and consent rendering before a grant is issued.Product-specific consent flows, screen layouts, and hosted authorization-server deployments.
Section 7: GrantThe immutable consent artifact and the constraints a resource server enforces for a token-bound client.Grant database schema, signed-token format, hosted registries, and deployment topology.
Section 8: Resource Server InterfaceThe interoperable record-query and blob-fetch interface under grant enforcement, and the protected resource metadata a resource server publishes about itself.Authorization-server deployment, storage backend, collection runtime, operator dashboard, and hosted service choices.

Relationship to existing standards

StandardRelationship
OAuth 2.0 (RFC 6749)PDPP is a profile of OAuth 2.0, carrying selection requests in RFC 9396 authorization_details. The grant is issued as the result of an OAuth authorization flow.
RFC 9396 (RAR)PDPP uses the authorization_details envelope for selection requests. The type URI is https://pdpp.dev/data-access.
RFC 6750 (Bearer Token)PDPP transports both owner tokens and client tokens as RFC 6750 Bearer Tokens on the wire. The resource server distinguishes token kind via pdpp_token_kind in the introspection response, not by token syntax.
RFC 7662 (Token Introspection)PDPP uses authenticated RFC 7662 token introspection where the authorization server and resource server are separated, so the resource server can resolve grant-bound tokens. Co-located deployments may use a local equivalent.
RFC 9728 (Protected Resource Metadata)PDPP resource servers publish RFC 9728 protected resource metadata, so a client discovers the authorization server, the query base, and the supported token kinds from the resource itself rather than from prior configuration. Core defines four pdpp_-prefixed additional members (Section 8); the extension profiles define capabilities.
OAuth 2.0 Dynamic Client Registration (RFC 7591)PDPP reuses the RFC 7591 client metadata vocabulary (client_name, logo_uri, policy_uri, and similar fields) for the consent display. A dynamic client registration endpoint is a deployment choice and is required only where deployments need it; Core functions without it.
Client ID Metadata Documents (CIMD, IETF OAuth WG draft)A client identifier that is itself an https URL the authorization server fetches to obtain RFC 7591-shaped client metadata, with no prior registration handshake. Control of the URL's domain is the trust root. CIMD is how deployed MCP clients present themselves: the MCP authorization specification revision 2025-11-25 states that authorization servers and clients SHOULD support CIMD and MAY support RFC 7591 dynamic client registration, which is retained for backward compatibility. Core treats a validated CIMD document as one source of validated binding metadata (Section 6) and its verified domain as a trust signal; the fetch and validation obligations belong to the OAuth binding rather than to Core.
SMART on FHIRFollows the domain-profile-over-OAuth pattern PDPP adopts: OAuth handles authorization, and the profile adds a domain data model, consent semantics, and a conformance regime. SMART on FHIR reached ubiquity through regulatory adoption of SMART-on-FHIR-patterned API requirements (the ONC Cures Act rule).
UK Open BankingAlso follows the domain-profile-over-OAuth pattern PDPP adopts: OAuth handles authorization, and the profile adds a domain data model, consent semantics, and a conformance regime. UK Open Banking reached ubiquity through the CMA's Open Banking mandate for the largest UK banks.
UMA 2.0 (Kantara)UMA is important prior art for PDPP's user-managed, standing, revocable access model, particularly where an outside party seeks access to user-controlled resources. PDPP's authorization protocol derives directly from OAuth 2.0 and RFC 9396.
GNAP (RFC 9635)GNAP is an IETF authorization protocol that revisits OAuth-style delegation with a new protocol design. Several design decisions are directly relevant to PDPP: (1) interaction modes beyond browser redirects (relevant to nonstandard authorization interaction patterns); (2) request continuation for multi-step consent negotiation (relevant to optional streams); (3) key-bound grants instead of bearer tokens (stronger security for ongoing personal data access); (4) built-in grant management with revocation and rotation (relevant to continuous access mode). PDPP v0.1 uses OAuth 2.0 + RFC 9396. A future version should evaluate whether GNAP is a better foundation. PDPP's entity-scoped client_display already follows GNAP's pattern of carrying client display metadata inline in the request. For key-bound tokens specifically, DPoP (RFC 9449) offers an OAuth-native path to GNAP-style sender-constrained tokens and is a candidate optional hardening profile for v0.2.
SolidSolid takes the full re-architecture approach: personal data moves into user-controlled pods with RDF/Linked Data semantics, which requires source platforms to adopt the model or users to migrate off-platform. PDPP instead layers on existing OAuth infrastructure and bootstraps data supply through the Collection Profile, without requiring source platforms to adopt anything.
Data Transfer Project (DTI)PDPP and DTI are complementary. The Data Transfer Project handles transfer mechanics, and DTI's stated position is that there is "no silver bullet" for portability: multiple approaches coexist. DTI's Data Trust Registry (post-pilot, 2026) addresses who is trusted: it vets services seeking access to platforms' portability interfaces so that platforms can rely on shared trust signals. PDPP addresses what was consented and how it is enforced (the grant and the resource server interface); a trust registry and PDPP's consent semantics compose rather than compete. The two protocols can chain. See Appendix B.
Airbyte / SingerPDPP borrows the RECORD/STATE checkpoint pattern for incremental sync. This record and state-checkpoint lineage informs the Collection Profile companion specification; it appears here for reader orientation and is informative for Core.
GDPRPDPP implements data minimization through stream and field selection. It also carries machine-readable purpose declarations (purpose_code) that support consent display, local policy, and implementation-defined audit or transparency mechanisms, with an explicit protocol-level consent rule for ai_training. The internal version history required for incremental sync may support implementations that choose to expose historical access features to users. Whether such exposure is required is outside the scope of this specification. This alignment is informative only and is not a required v0.1 capability.
DMAThe continuous access mode enables ongoing portability aligned with the DMA's requirements. Article 6(9) requires effective portability with continuous and real-time access to the end user's data; PDPP's continuous grants and incremental sync map to that requirement. This alignment is informative only and is not a required v0.1 capability.
EU Data Act (Regulation 2023/2854)The Data Act's Article 5(1) flow, where a user directs a data holder to make data available to a third party, is the same triangle as PDPP's owner, source, and client. Article 2(12) defines the user as a natural or legal person, matching PDPP's subject-neutral owner. It applies from 12 September 2025, requires access by design for connected products placed on the market after 12 September 2026, and names no protocol.

Why a profile rather than a new authorization framework

PDPP is a data-portability profile, not a general authorization framework. It rides OAuth 2.0 and RFC 9396 because that is what is deployed, and it adds what neither UMA nor GNAP defines: a portable record model, purpose-bound durable grants, a declared collection and source layer, and a query surface with grant enforcement. UMA is prior art for the user-managed standing access model and GNAP is a candidate future foundation; the table above records both. A GNAP transport binding remains possible later without changing the record model, the grant, or the resource server interface, because none of those depend on the authorization protocol that produced the grant.

Relationship to sector-specific consent regimes

Note (non-normative). PDPP is a floor, not a ceiling. A source already operating under a stricter or more specific consent regime — open banking, or a health regime built on FHIR — does not lose or weaken any obligation of that regime by also being reachable through PDPP. Where the two differ, the stricter requirement governs that source. Nothing in this specification authorizes a disclosure that the source's own regime forbids, and a PDPP grant is not a substitute for a consent that regime requires. This is a scoping statement rather than a conformance criterion: PDPP defines no test for which of two regimes is stricter, and conformance to this specification is assessed against this specification alone. Compatibility profiles that map a sector regime's consent vocabulary onto PDPP's are a possible future addition and are not defined in v0.1.


2. Terminology and Actors

Actors

ActorDefinition
UserThe person whose data is being accessed. Owns the data, approves grants, may revoke.
ClientAn application or AI agent requesting user data. Identified by client_id. In OAuth terms, this is the client.
Data SourceAny external system from which a user's data originates: a consumer platform, a SaaS application, a device, a local archive, a financial institution, or other system.

The owner is the authenticated subject whose records the source holds. Typically this is a person; the protocol itself is subject-neutral, and an organization that authenticates as the account holder participates identically.

Protocol roles

These roles may be co-located in a single deployment (e.g., a personal server acting as both authorization server and resource server) or separated. The spec defines the interfaces between roles, not the deployment topology.

RoleResponsibility
Authorization ServerIssues and manages grants. Validates selection requests against retained source declaration snapshots. Tracks grant lifecycle (active, expired, revoked).
Resource ServerStores records as flat relational streams. Serves records to clients filtered by grant parameters.

The PDPP Collection Profile defines a third role:

RoleResponsibility
Connector RuntimeRuns connectors. Writes collected records to the resource server. Manages incremental sync state.

In many deployments, a single personal server fills all three roles. The spec uses "personal server" when referring to a combined deployment, and the specific role name when the distinction matters.

Token resolution: User-facing authorization flows are deployment-specific and are not normatively specified in v0.1. However, when the AS and RS are deployed separately, the AS-to-RS token-resolution contract is normative: the RS MUST authenticate to the RFC 7662 introspection endpoint and resolve the complete grant enforcement context from its response. The RS MUST enforce the request from that response and MUST NOT make a second AS lookup. For co-located deployments, a local equivalent (shared database or function call) is acceptable. Self-contained JWTs may be used as an optimization but MUST NOT be the sole revocation mechanism (see Section 10).

Trust registry queries

Core refers to a trust registry as a source of requester identity metadata and a positive trust signal at consent, as an external mechanism supporting retention accountability, and as a deferred concern. Core relies on two abstract queries: whether a client identifier is recognized and authorized, and whether a source declaration authority is accepted. Neither query is a wire protocol; an authorization server MAY answer locally or through a remote service.

A registry answer is an assertion that local authorization-server policy evaluates; it is not itself a decision. A conveyed answer identifies the subject it concerns, the role, action, or scope the status qualifies, the status, the governance framework under which the named issuer conferred it by URI, the issuer or trust-anchor identifier, and the validity window as valid_from and valid_until. Without the subject and the issuer, an answer cannot be attributed or replayed against the right party; without the role or scope, a status does not say what the subject is authorized to do.

An authorization server records the trust signal it relied on — subject, role or scope, status, governance-framework URI, issuer or trust-anchor identifier, valid_from, valid_until, and the time of lookup — on its acceptance record or resulting grant. The lookup time matters because a status may be withdrawn later, and the record has to show what was true when the server relied on it.

Core does not define the registry, transport, recognition mechanism, or withdrawal propagation. A server that consults no registry remains conformant because registry answers inform local policy and never replace the owner’s grant. ToIP’s Trust Registry Query Protocol is an intended future profile binding, not a Core requirement.

Data concepts

TermDefinition
GrantAn immutable consent artifact specifying what data a client may access, under what constraints.
StreamA named collection of records with a schema, primary key, and optional cursor field. Stream names are source-local (e.g., messages). The fully qualified identifier is an ordered pair (source.id, stream_name), used in cross-source references and storage. Example: ("https://registry.pdpp.dev/connectors/spotify", "top_artists").
RecordA single data object within a stream.
ConnectorA program that collects data from a data source, used when data is collected rather than served natively. One of possibly several producers of a source's streams. Defined in the Collection Profile.
Source DeclarationA source's versioned declaration of its identity, publisher, streams, schemas, consent surface, selection capabilities, and Resource Server query capabilities. It does not define connector acquisition or execution.
Selection RequestA client's request for specific data, expressed as RFC 9396 authorization_details.
ViewAn optional named field set a source declaration may define for a stream, composed from fields declared in the stream schema. When a client requests by view name, the resulting grant records the resolved field list, which is authoritative. Declared views are advisory; the authorization server is authoritative for views used in consent UI and issued grants.

Requirements Language

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in BCP 14 [RFC 2119] [RFC 8174] when, and only when, they appear in all capitals, as shown here.

This document is normative except where content is explicitly marked as an example, a note, or otherwise non-normative.

This is the only normative document for PDPP Core v0.1 conformance. A profile defines no Core requirement; a future profile may define a separate named conformance claim only if its own status says so. The PDPP Collection Profile and PDPP Source Declaration Discovery and Trust are informative and define no conformance requirements of their own.


3. System Architecture

Every PDPP deployment shares the same authorization core: a user grants a client consent, and the authorization server issues a grant. The resource server that then serves records under that grant is not shown here; how it is populated and operated is deployment-specific (see below).

What differs between deployments is how the resource server that fulfills the grant is populated and operated. This is not a closed set: a resource server may hold pre-collected data with no collection machinery involved, or receive data via regulatory export, manual import, or platform-native APIs (Section 1). The two examples below illustrate the ends of that spectrum; the PDPP Collection Profile is one fulfillment mechanism, not the only one.

Example A: source-native fulfillment. The data source operates its own authorization and resource servers directly; there is no separate collection step.

Example B: personal-server fulfillment. A connector runtime, governed by the Collection Profile, collects data from the source and syncs it into a resource server the user controls.

Core defines no ingest path. Core specifies the authorization server, the resource server, and what passes between them and a client. It does not specify how records reach the resource server, and conformance to Core does not depend on any particular answer. Examples A and B are illustrations, not a classification: a resource server holding data placed there by regulatory export, manual import, a provider's own write path, or a mechanism not yet described is equally within Core. The PDPP Collection Profile describes one such mechanism, connector collection, and describes it for builders rather than as a conformance requirement.

On-behalf-of access is two ordinary grants, not one delegated grant. At the upstream platform, the owner authorizes the personal server to read the owner's data. At the personal server, the owner separately authorizes a downstream client to read owner-held data. These are independent grants, each issued and revocable by the owner. A personal server MAY disclose owner-held data under the fresh downstream grant; it does not pass the platform grant or a downstream PDPP token to the other relationship.

PDPP therefore defines no delegated-grant, sub-grant, or grant-chaining construct, and none is needed to build this. The same shape is already how account aggregation works in deployed systems: an open banking aggregator holds the user’s consent at each bank and its own consent from the user, rather than subdividing one authorization. Where a binding needs to exchange one credential for another across the two relationships, OAuth Token Exchange (RFC 8693) is the existing mechanism, and it is a concern of the binding rather than of Core.

Only live passthrough is bounded by the upstream grant: when the personal server does not hold a copy and reads from the platform to fulfill a downstream request, it MUST stay within the upstream grant. This limit does not constrain a fresh grant for owner-held data. A downstream PDPP token is never forwarded upstream.

Onward transfer is prohibited. A recipient MUST NOT transfer its token or let an ungranted party exercise it.

A different topology — the owner running their own authorization server in front of a platform’s data, so that the platform holds the data but the owner’s server decides who may read it — is not introduced by this specification. It is the arrangement UMA describes, it is not the arrangement above, and it is recorded as deferred rather than left ambiguous.

Protocol layering

PDPP separates three concerns that other systems conflate:

  1. Authorization: the user's consent about what is disclosed, to whom, and under what constraints. This is the grant. It is the portable core of PDPP.

  2. Disclosure: the records the resource server returns given a valid grant. This is the resource server query API.

  3. Collection: how data gets into the resource server in the first place. This is the Collection Profile. It is one answer to this question; pre-loaded data, manual imports, and other mechanisms are equally valid.

The grant and query API are the normative core. Collection is a companion mechanism.

Ingest and sync-state are Collection Profile concerns. The core protocol defines the query API (disclosure) and grant semantics. The Collection Profile defines record ingest and sync-state management endpoints for implementations that claim Collection Profile support.


4. Record Model

Personal data is represented as flat relational streams. This enables streaming, pagination, incremental sync, and compatibility with DTI canonical data models.

Streams

A stream is a named collection of records with a consistent schema. Examples: playlists, messages, sleep_sessions. A stream's shape (its fields and schema) is declared in the source declaration; see Section 5: Source Declaration.

Stream semantics

Each stream has one of two semantic types:

SemanticsMeaningExamplesResource server behavior
append_onlyRecords are immutable events. New records are added; existing records are never modified.messages, transactions, play_events, workoutsInsert only. Duplicate keys are idempotent.
mutable_stateRecords represent current state of an entity. Records may be updated or deleted.profile, settings, playlist_items, follow_listsUpsert by primary key. Resource server maintains version history for incremental sync.

Approximately 95% of personal data by volume is append_only. The remaining 5% is mutable_state. Mutable state records (profiles, preferences, relationships) are often the highest-value context for AI agents.

Incremental sync for mutable streams

For mutable_state streams, the resource server maintains internal version history to support incremental sync queries. This is an implementation detail: the protocol surface is a standard cursor-based query that returns records changed since a given cursor position (see Section 8). The version history is not exposed as a separate stream.

A client that has previously synced a mutable_state stream queries for changes by passing its last cursor. The resource server returns only records whose state has changed since that cursor, within the client's grant-authorized field projection. If no authorized fields changed on a record, that record does not appear in the response.

This design ensures that a client authorized for fields A and B cannot infer that field C changed, even if C was modified after the client’s last sync. The response is a function of the grant, not of the full record state.

Snapshot model: changes_since returns the full current state of each record whose grant-authorized projection changed since the cursor position, plus tombstones for deletions. It does not return field-level diffs. The client receives a complete record object for any record that changed.

Cursor expiry: Resource servers MAY expire historical version data after a retention period. If a client's cursor has expired, the resource server MUST return HTTP 410 Gone with error code cursor_expired. The client MUST perform a full re-sync to re-establish its baseline.

Two distinct cursor spaces: cursor/next_cursor are pagination tokens within a single query execution; changes_since/next_changes_since are incremental sync tokens across sessions. A client MUST NOT use a next_cursor value as a changes_since parameter; they are different token spaces and will produce a protocol error if confused. The terminal page of a changes_since result MUST include a next_changes_since field. Paginating an incremental sync: pass changes_since on the first request, follow next_cursor for subsequent pages within that session, then store next_changes_since from the terminal page for the next session.

Tombstones: When a record is deleted from a mutable_state stream, the resource server MUST include a tombstone entry in incremental sync responses for clients whose cursor predates the deletion. Tombstone format:

{
  "object": "record",
  "id": "canonical-key-string",
  "stream": "playlists",
  "deleted": true,
  "deleted_at": "2026-04-01T10:00:00Z",
  "emitted_at": "2026-04-01T10:00:01Z"
}

Tombstones use the same object: "record" envelope as regular response records, with deleted: true. The id field is the canonical key string (see RECORD envelope, Compound key encoding below). Both deleted_at and emitted_at are required on tombstone objects. No data field is present on tombstones.

A tombstone signals that a record left the stream. For subset or derived streams this means membership removal; it does not assert that the source record was deleted. See Derived subset streams (Section 12) for the non-normative discussion of this stream shape.

deleted_at represents the time the record was deleted in the source system, if known; otherwise the time the RS processed the deletion directive. If the source system deletion time is unknown, the RS SHOULD use the emitted_at value of the delete directive as deleted_at.

Non-normative note (GDPR Article 15): The version history maintained internally by the resource server to support mutable_state incremental sync may support implementations that choose to expose historical access features to users. Whether such exposure is required is outside the scope of this specification. This alignment is non-normative and is not a required v0.1 capability.

Split rule

When modeling data from a source, apply this rule:

  • Separate stream if: has its own stable ID, unbounded cardinality, changes independently, or consumers query it independently.
  • Nested within a record if: small, bounded, and only meaningful as part of the parent.

Example: conversations and messages are separate streams (messages are unbounded, have their own IDs, change independently). A message's content_blocks array can be nested (bounded, only meaningful within the message).

This is a common data modeling judgment call. The rule above is a guide, not a formula; declaration publishers exercise discretion.

The RECORD envelope

RECORD is the universal data envelope. It is used in the Collection Profile and is the canonical shape for records stored in the resource server.

{
  "stream": "messages",
  "key": "msg_abc123",
  "data": {
    "id": "msg_abc123",
    "conversation_id": "conv_xyz",
    "role": "user",
    "content": "What is the weather like?",
    "source_created_at": "2026-03-28T15:00:00Z"
  },
  "emitted_at": "2026-03-28T15:01:00Z"
}
FieldTypeRequiredDescription
streamstringyesStream name
keystring or string[]yesPrimary key value. Array for compound keys; order matches the SourceDeclaration primary_key.
dataobjectyesRecord payload conforming to the stream schema.
emitted_atISO 8601yesWhen the record was emitted by its producer (processing time, not source time).
openumnoupsert (default) or delete. This field is a directive to the resource server and is not stored as part of the record data.

Compound key encoding: When primary_key has multiple fields, key is an array of values in the order declared by the SourceDeclaration primary_key. The canonical string form of a compound key is the minified JSON array of key values (e.g., ["user_123","2026-04-01"]). Each primary-key component MUST be serialized as a string in the canonical encoding. Non-string primary-key field values (e.g., integers, dates) MUST be converted to their string representation before encoding. URL path parameters and resources[] entries use percent-encoded canonical string form. The resource_ref.record_id field retains native string | string[] type.

Record identity: For any record, the values of the data fields named by the stream's primary_key MUST match the values in the key envelope field (in order). A resource server or profile-defined write interface MUST reject a record before storage when those values disagree. The Collection Profile defines the HTTP error for its ingest endpoint.

Timestamps

Two timestamp concepts appear in records:

  • emitted_at: when the producer emitted the record. Always present on the RECORD envelope. Producer-generated.
  • Source timestamps: when the event occurred or the resource was created or modified on the source platform. These are fields within data, declared in the stream schema. The spec reserves two standard field names: source_created_at and source_updated_at. Connector authors SHOULD use these names when the platform provides them, rather than inventing platform-specific names.

Foreign keys

Streams reference each other via foreign key fields in data:

{
  "stream": "conversations",
  "key": "conv_xyz",
  "data": {
    "id": "conv_xyz",
    "title": "Weather chat",
    "source_created_at": "2026-03-28T14:00:00Z"
  }
}
{
  "stream": "messages",
  "key": "msg_abc123",
  "data": {
    "id": "msg_abc123",
    "conversation_id": "conv_xyz",
    "content": "...",
    "source_created_at": "2026-03-28T15:00:00Z"
  }
}

The SourceDeclaration declares primary_key per stream. Foreign key relationships are declared in its relationships field (see Section 5).

Binary data (blob_ref)

Binary data (photos, videos, audio, documents) is not inlined in records. The record contains metadata and a blob_ref:

{
  "stream": "media",
  "key": "media_123",
  "data": {
    "id": "media_123",
    "caption": "Sunset",
    "media_type": "image",
    "source_created_at": "2026-03-28T10:00:00Z",
    "blob_ref": {
      "blob_id": "blob_media_123",
      "mime_type": "image/jpeg",
      "size_bytes": 2048000,
      "sha256": "a1b2c3..."
    }
  }
}

mime_type MUST be a valid IANA media type (see IANA Media Types). Record producers emit blob_ref without a fetch_url. The resource server injects fetch_url at read time when serving records via the query API.

Cross-stream references (resource_ref)

When a record references a record in a different stream on the same resource server, use a resource_ref. This is a within-subject, within-server pointer. Cross-user or cross-server references are out of scope in v0.1.

{
  "stream": "tag_assignments",
  "key": "assign_1",
  "data": {
    "tag_id": "tag_sunset",
    "target": {
      "source_id": "https://registry.pdpp.dev/connectors/instagram",
      "stream": "media",
      "record_id": "media_123"
    }
  }
}

5. Source Declaration

Each source publishes a SourceDeclaration describing its identity, publisher, consent surface, record semantics, selection capabilities, and Resource Server query capabilities. Connector-backed and provider-native sources use the same Core shape. The declaration defines what can be consented to. The resolved grant defines what was approved.

SourceDeclaration structure

{
  "protocol_version": "0.1.0",
  "source": {
    "kind": "connector",
    "id": "https://registry.pdpp.dev/connectors/spotify"
  },
  "declaration_version": "2026-08-11",
  "publisher": {
    "id": "https://registry.pdpp.dev/"
  },
  "display": {
    "name": "Spotify"
  },
  "selection_presets": [
    {
      "id": "listening_history",
      "label": "Listening history",
      "streams": [
        { "name": "top_artists" },
        { "name": "play_events" }
      ]
    }
  ],
  "streams": [
    {
      "name": "top_artists",
      "description": "Most-listened artists over time",
      "display": {
        "label": "Your top artists",
        "detail": "Artist names, genres, and popularity scores. No listening timestamps or play counts."
      },
      "semantics": "mutable_state",
      "schema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "genres": { "type": "array", "items": { "type": "string" } },
          "popularity": { "type": "integer" },
          "source_updated_at": { "type": "string", "format": "date-time" }
        },
        "required": ["id", "name"]
      },
      "primary_key": ["id"],
      "cursor_field": "source_updated_at",
      "consent_time_field": "source_updated_at",
      "selection": {
        "fields": true,
        "resources": false
      },
      "query": {
        "range_filters": {
          "source_updated_at": ["gte", "gt", "lte", "lt"]
        }
      },
      "views": [
        {
          "id": "basic",
          "label": "Artist names and genres",
          "fields": ["id", "name", "genres"]
        },
        {
          "id": "full",
          "label": "Full artist data",
          "fields": ["id", "name", "genres", "popularity", "source_updated_at"]
        }
      ]
    },
    {
      "name": "play_events",
      "description": "Individual track play events",
      "semantics": "append_only",
      "schema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "track_id": { "type": "string" },
          "played_at": { "type": "string", "format": "date-time" },
          "duration_ms": { "type": "integer" }
        },
        "required": ["id", "track_id", "played_at"]
      },
      "primary_key": ["id"],
      "cursor_field": "played_at",
      "consent_time_field": "played_at",
      "selection": {
        "fields": true,
        "resources": false
      }
    }
  ],
  "extensions": {}
}

SourceDeclaration fields

FieldDescription
protocol_versionVersion of the PDPP SourceDeclaration schema. This contract requires exactly 0.1.0.
sourceExactly { kind, id }. kind is connector or provider_native; id is the absolute URI authorization identity for the source's data surface.
declaration_versionOpaque, non-empty revision identifier for this source declaration. It is not the connector software version and has no implied ordering.
publisher.idAbsolute URI identifying the declaration publisher. It is an attribution claim, not an authenticated identity. The authorization server MUST treat publisher.id as authenticated only where an accepted channel or configured mapping binds that publisher to the declaration; absent that binding it MUST NOT support source acceptance, redirect policy, attribution, or any other trust decision.
display.nameHuman-readable source name for consent UIs. It is display metadata, not source identity.
selection_presetsOptional preset selections. The authorization server expands a selected preset into explicit stream terms before issuing a grant.
streams[].nameUnique non-empty stream name, source-local. * is request-only and is not a declaration stream name.
streams[].descriptionOptional short human-readable summary of the stream's contents (e.g., "Most-listened artists over time"). Not consent-surface metadata; see streams[].display for the fields the AS renders during consent.
streams[].displayOptional consent-surface metadata. See Stream display metadata.
streams[].semanticsappend_only or mutable_state.
streams[].schemaJSON Schema for the record's data field. primary_key and cursor_field MUST reference fields declared here.
streams[].primary_keyFields that uniquely identify a record within the stream.
streams[].cursor_fieldField used for logical record ordering in cursor-based reads and incremental sync. List reads sort by (cursor_field, primary_key), with null or absent cursor values sorting after present values. A cursor is an opaque token the server issues, encoding a logical sort position in the stream.
streams[].consent_time_fieldThe temporal consent boundary: the field against which time_range is evaluated. Absent means time_range is not applicable to this stream. MUST reference a field declared in the schema.
streams[].selectionWhich selection parameters this stream supports (fields, resources). Time-range capability is derived from consent_time_field presence; absent means not time-range-capable. The AS MUST reject grants that request time_range on a stream without a consent_time_field, or that request an unsupported selection parameter.
streams[].viewsNamed field projections the declaration publisher suggests. Advisory; the AS is authoritative. Each view has id, label, and fields (top-level field names only).
streams[].relationshipsDeclared foreign key relationships to other streams. Structural graph metadata only; does not by itself make a relation expandable in the read API. Expandability is declared separately as a query capability; see expand[].
streams[].queryStream-specific query capability declaration. range_filters declares range-queryable fields and operators. expand declares expandable relations and per-relation limits. search and aggregations preserve source-neutral capability declarations used by companion query profiles; their operation semantics and conformance are not defined by Core.
extensionsOptional object keyed by absolute profile URI. Core preserves or ignores an unknown extension value and does not parse it. An operation that explicitly invokes an unsupported profile is rejected. An extension cannot redefine or weaken Core semantics.

The public SourceDeclaration JSON Schema declares JSON Schema 2020-12. Each streams[].schema uses that dialect when $schema is absent. If $schema is present, it MUST equal https://json-schema.org/draft/2020-12/schema. This dialect choice does not by itself guarantee identical validator behavior. The AS MUST meta-validate each embedded stream schema before accepting the declaration. Embedded $ref and $dynamicRef values MUST be local fragment references. A declaration MUST NOT make consent interpretation depend on a mutable remote schema.

source.id is the authorization identity. In a retained declaration or grant, source.kind records the provenance class that the AS accepted from its declaration authority. It is not authorization equality, a runtime type, or a Collection Profile claim, and it never selects serving or acquisition runtime. A connector declaration with no Collection extension remains valid Core input. The declaration never contains owner-specific account or instance handles. Those appear on each stream in the selection request and resolved grant.

Stream display metadata

Streams MAY include a display object with human-readable metadata for the consent UI. This metadata is authored by the accepted declaration publisher, not the requesting client.

FieldTypeDescription
display.labelstringShort human-readable name shown in the consent card (e.g., "Who you follow"). If absent, the AS SHOULD display streams[].description or fall back to the stream name.
display.detailstringConsent-oriented description of what data is included and, where relevant, what is excluded (e.g., "Usernames and account IDs of accounts you follow. No DMs, profile details, or follower lists."). If absent, the AS MAY generate a description from the stream schema, or display no detail.

Authorship principle: display.label and display.detail describe the data itself, not the requester's purpose. They are attributed to the accepted declaration publisher. The requesting client MUST NOT override or supplement these descriptions in the selection request. Publisher attribution is authenticated only under the conditions in SourceDeclaration fields.

{
  "name": "following_accounts",
  "description": "Accounts the user follows",
  "display": {
    "label": "Who you follow",
    "detail": "Usernames and account IDs of accounts you follow. No DMs, profile details, or follower lists."
  },
  "semantics": "mutable_state",
  "schema": { "..." : "..." }
}

The consent_time_field is the field on each record that the resource server evaluates time_range against. It represents the stream's temporal consent boundary: when the user consents to "data from the last 6 months," the consent_time_field is the field that determines whether a given record falls within that window.

The consent_time_field may be the same field as cursor_field, but they serve different purposes and MUST be declared separately:

  • cursor_field governs incremental sync mechanics (which records to fetch since the last run).
  • consent_time_field governs consent-time filtering (which records fall within the authorized time window).

For many append_only streams, both fields will be the same (e.g., played_at for play events). For some mutable_state streams they may differ: a playlists stream might use source_updated_at as the cursor (for efficient incremental sync) but source_created_at as the consent_time_field (because the user's consent to "playlists from the last 6 months" most naturally means playlists they created in that period, not playlists they edited).

The consent_time_field MUST be rendered in human-readable consent UX. A grant with time_range: { since: "2026-01-01" } on the playlists stream should be presented as "playlists created on or after January 1, 2026," not just "playlists in time_range."

Streams that cannot define a stable consent_time_field simply omit it. The absence of consent_time_field is the normative signal that the stream does not support time-range filtering.

Relationships

{
  "name": "conversations",
  "relationships": [
    {
      "name": "messages",
      "stream": "messages",
      "foreign_key": "conversation_id",
      "cardinality": "has_many"
    }
  ]
}
FieldDescription
nameRelation name (used in expand[] on the query API).
streamThe related stream name.
foreign_keyThe field on the related stream that references this stream's primary key.
cardinalityhas_many or has_one.

Views

Views are named field projections that the authorization server may define for a stream, composed from fields declared in the stream schema. Views are the unit of consent when a client requests access by view name rather than by explicit field list. A view names a field set within a single stream, while a selection preset expands to stream-level selections at request scope.

Declaration publishers MAY suggest views. These suggestions are advisory. The authorization server is authoritative for views used in consent UI and issued grants. The AS MUST NOT define a view that includes fields absent from the retained SourceDeclaration schema for the relevant stream.

View evolution: Grants are bound to the resolved field set at issuance time: fields in the StreamGrant is authoritative, not the view name. View evolution (adding new fields to a view) never silently widens existing grants. Re-consent is required before a client can access new fields, even if those fields are subsequently added to a named view the client already has a grant for.

Authority and registry: Views defined under pdpp.dev URI namespaces are controlled by PDPP maintainers via a public change process. Implementations MUST treat unrecognized view URIs as opaque identifiers.

Versioning and snapshots

declaration_version is an opaque exact revision identifier. Core does not assign semantic-version ordering or compatibility meaning to it. A connector software release can leave the declaration unchanged, and a declaration can change without a connector release.

The authorization server retains the exact declaration snapshot used to validate the request and render consent. It uses that same snapshot for narrowing, issuance, and retained consent evidence. The issued grant records the declaration revision and every resolved authorization fact. A version label alone is not a snapshot.

Adding a field, view, stream, or capability to a later declaration never widens an existing grant. Removing or changing a currently served capability may make a request technically unsupported, but it does not reinterpret what the user approved. The Resource Server enforces the resolved grant without a current declaration lookup. Current serving metadata may only route, describe current capabilities, narrow, or reject.

Declaration acceptance

An authorization server accepts a source declaration only through explicit owner or operator onboarding, an installed catalog, an accepted registry entry, or explicit local provisioning. A client MUST NOT introduce a new source authority or declaration URI during authorization.

For a provider_native source, source.id MUST be identical to the protected-resource identifier the authorization server has already accepted for that resource. The authorization server MUST reject any mismatch before consent or grant issuance.

publisher.id is an unauthenticated claim unless an accepted channel or configured mapping binds that publisher to the declaration. Without such a binding, the authorization server MUST NOT rely on publisher.id for source acceptance, attribution, redirect policy, or any other trust decision.

An accepted revision is keyed by its accepted authority binding, source.id, and declaration_version. Different parsed content under an accepted key is equivocation: the authorization server MUST reject it and retain the previously accepted content, and MUST NOT infer ordering or freshness from declaration_version.

When retrieving a declaration, the authorization server MUST use HTTPS without ambient credentials, MUST validate every redirect target and the final URL against its accepted declaration pointer and network policy, MUST reject a declaration that requires automatic retrieval of a remote schema, and MUST fail closed when any check fails. It MUST validate the destination address against its network policy immediately before each connection attempt, including each redirect hop, and MUST connect only to an address from that validated result; an address accepted for an earlier attempt does not authorize a later resolution. The declaration location is not the source identity. Section 10 states the retrieval limits an authorization server sets by local judgment.

Declaration display values are untrusted input. An authorization server MUST render declaration display values safely for the output context, by context-appropriate escaping, by sanitization, or by any construction that guarantees the value cannot be interpreted as markup, script, or a control sequence in that context. The requirement is on the outcome; this specification does not mandate one technique. Current declaration capabilities MUST NOT widen an issued grant.


6. Selection Request

A client requests specific personal data by including authorization_details in an OAuth 2.0 authorization request, following RFC 9396.

{
  "response_type": "code",
  "client_id": "music_recommendations",
  "redirect_uri": "https://app.example.com/callback",
  "scope": "openid",
  "client_display": {
    "name": "Concert Finder",
    "uri": "https://concertfinder.example.com",
    "logo_uri": "https://concertfinder.example.com/logo.png",
    "policy_uri": "https://concertfinder.example.com/privacy",
    "tos_uri": "https://concertfinder.example.com/terms"
  },
  "authorization_details": [
    {
      "type": "https://pdpp.dev/data-access",
      "source": { "id": "https://registry.pdpp.dev/connectors/spotify" },
      "purpose_code": "https://pdpp.dev/purpose/personalization",
      "purpose_description": "Recommend concerts based on your listening history",
      "access_mode": "single_use",
      "streams": [
        {
          "name": "top_artists",
          "necessity": "required",
          "instance_ids": ["spotify-account-a"],
          "time_range": { "since": "2025-09-28T00:00:00Z" }
        },
        {
          "name": "play_events",
          "necessity": "optional"
        }
      ],
      "client_claims": {
        "commitments": ["Data used only for concert recommendations"]
      }
    }
  ]
}

Client display metadata

The top-level client_display object carries inline client display metadata for the requesting application. PDPP reuses the human-readable client metadata model from OAuth 2.0 Dynamic Client Registration (RFC 7591 Section 2.2), but transports it inline in the authorization request rather than requiring a dynamic client registration endpoint.

Inside client_display, PDPP drops the client_ prefix from client_name and client_uri because the enclosing object is already client-scoped. The metadata model is otherwise aligned with RFC 7591.

FieldTypeRequiredStatusDescription
client_display.namestringyesInline client metadataInline equivalent of RFC 7591 client_name. Human-readable application name.
client_display.uriURInoInline client metadataInline equivalent of RFC 7591 client_uri. The client's homepage.
client_display.logo_uriURInoInline client metadataRFC 7591 logo_uri. URL to a square image representing the client.
client_display.policy_uriURInoInline client metadataRFC 7591 policy_uri. URL for the client's privacy policy.
client_display.tos_uriURInoInline client metadataRFC 7591 tos_uri. URL for the client's terms of service.

client_display is entity-scoped: it describes the client, not a specific authorization request. It appears at the top level of the authorization request, outside authorization_details.

client_display is an inline carrier, not necessarily the AS's final rendered identity record. The AS MAY replace or augment inline values with locally registered metadata, validated binding metadata, validated software-statement metadata, or trust-registry metadata.

Validated binding metadata is client metadata the AS obtained and verified through the mechanism that binds the client to the authorization protocol in use, rather than metadata the client asserted inline in this request. Under the OAuth binding it is the metadata a client ID metadata document or a dynamic registration record supplies, after the binding's own validation succeeded. Core does not define how a binding validates it; Core defines only that validated binding metadata outranks inline client_display, because the AS checked it and the client did not merely assert it.

URL-hosted client identity. A client ID metadata document is valid for this rule when three conditions hold: it is syntactically valid per the client ID metadata document specification the OAuth binding names; the authorization server retrieved it over HTTPS from the client_id URL itself; and the client_id inside the document is identical to the URL it was retrieved from. That last check is what stops a document from claiming to be a different client.

Two obligations follow, and they are separate. The interoperability obligation: a conforming authorization server MUST NOT reject a valid client ID metadata document solely because the client is not preregistered. The local-authority obligation, which the first does not weaken: the server MAY still deny authorization, rate-limit the client, or require a registry-derived trust or admission result, under local policy and for any reason other than the absence of preregistration. A conformance test therefore exercises two distinct outcomes — an unregistered valid document that is accepted as an identity, and a policy denial that is not a rejection of the identity form.

This is a PDPP interoperability decision, not a claim of standards-wide MUST consensus. The client ID metadata document draft states no obligation to support dynamic onboarding, and MCP’s authorization specification stops at SHOULD; PDPP requires more than either.

Metadata resolution and rendering obligations:

  1. The AS MUST resolve requester identity metadata from the best available source. Source precedence is local registration or trust-registry metadata, then validated software-statement metadata if supported, then validated binding metadata, then inline client_display, then client_id fallback.
  2. If the resolved metadata contains a display name, the AS MUST display it to the user during consent. If no display name is available, the AS MUST display client_id as the requester identity.
  3. If the resolved metadata contains policy_uri or tos_uri, the AS MAY display them as secondary links or disclosures.
  4. If the server has a positive trust signal for the client (e.g., verified domain control, trust registry membership), it MUST render that status distinctly (e.g., a “verified” badge). If it has no positive trust signal, it MUST treat the client as unverified and SHOULD display an “unverified app” indicator.
  5. Domain control as a trust signal. Where the binding identifies a client by a URL it controls, and the AS both retrieved that client's metadata from that URL over HTTPS and confirmed the retrieved document identifies the same client, the AS has verified that the client controls that domain. The AS MAY treat verified domain control as a positive trust signal under obligation 4, and when it does it MUST name the verified domain rather than assert an unqualified verification (for example "Verified domain: example.com", not "Verified app"). Domain control establishes only that the operator of that domain published this client's metadata. It is not an assertion about the client's conduct, its data practices, or any review by the AS operator, and the AS MUST NOT present it as one.
  6. The AS MUST treat logo_uri as untrusted content until it has been accepted under local policy. It MUST NOT fetch and render a client-supplied remote logo in the consent UI unless the client is verified or the asset has been proxied, cached, and approved under local policy. For unverified clients, the AS SHOULD generate a monogram from the resolved display name.
  7. If neither resolved metadata nor inline client_display provides a display name, the consent UI SHOULD clearly indicate that the client has not provided display metadata.

Pre-registered public client discovery

An authorization server MAY support clients that are public and pre-registered by the deployment rather than dynamically registered. Dynamic public-client self-registration is the preferred discoverable path when registration_endpoint is advertised. When AS metadata advertises pre_registered_public in pdpp_registration_modes_supported, the reference publishes pdpp_pre_registered_public_clients so agents and third-party clients can discover usable fallback or example client_id values without an out-of-band walkthrough.

Each pdpp_pre_registered_public_clients entry contains client_id, client_name, and token_endpoint_auth_method. These entries are public client metadata, not authority to access data. The user grant remains the authorization boundary, and the field MUST NOT contain secrets, access tokens, owner-scoped clients, dynamically registered clients, or private registration state.

Client claims

The optional client_claims object within each authorization_details entry carries client-authored, non-enforceable statements about the specific authorization being requested. These are request-scoped, not entity-scoped: a client may make different commitments for different authorization requests.

FieldTypeRequiredStatusDescription
client_claims.commitmentsstring[]noAttributed client claimFree-text policy commitments relevant to this request (e.g., "Data used only for this study").

Trust boundary: Client claims are self-asserted and unverifiable by the server. The AS MUST render client_claims content separately from protocol-enforced grant terms and MUST attribute it to the client (e.g., "[client name] says:"). The AS MUST NOT render client claims in the same visual register as protocol-enforced grant terms, structured policy declarations, or declaration-authored data descriptions.

If rendered on the final owner review surface, client_claims MUST be normalized and bound exactly, with client attribution, into the immutable final approval artifact and review revision. Retained consent evidence MUST preserve that binding. They are material consent context, not grant rights. They remain outside authorization equality, the resolved grant, introspection rights, and RS enforcement.

Relationship to purpose_description: purpose_description is a first-class request field describing what the authorization is for. It is part of the authorization semantics the user reviews. client_claims.commitments are supplementary promises that are not reducible to structured protocol fields. Both are client-authored, but purpose_description is the primary purpose statement while commitments are additional assurances.

Commitments that ARE machine-readable: Structured grant fields (e.g., retention.max_duration, access_mode) SHOULD be rendered by the AS as server-generated display text (e.g., "Deleted within 90 days", "Ongoing access until you revoke it"). Clients SHOULD NOT duplicate machine-readable constraints as free-text commitments. If a commitment duplicates a structured field, the structured field is authoritative.

PDPP uses three primary semantic classes across selection requests and grants:

  • Protocol-enforced constraints: Values the AS and/or RS actually validate or enforce, such as stream selection, field projection, time_range, resources, and access_mode.
  • Structured policy declarations: Machine-readable statements that matter for consent, local policy, and implementation-defined audit or transparency mechanisms, but are not generally self-enforcing at the protocol layer. In v0.1 this includes purpose_code, purpose_description, and retention, with one explicit exception: https://pdpp.dev/purpose/ai_training adds a protocol-level consent requirement.
  • Attributed client claims: Client-authored statements that may matter to the user but are not protocol facts. In v0.1 this is client_claims.

client_display is a separate category: requester identity metadata used to identify who is asking, not a grant constraint. Inline values may be client-asserted, but the AS renders them under its own resolution and trust policy.

PDPP does not standardize consent screen layout, visual design, or copywriting. It does normatively constrain semantic rendering. A conformant AS MUST preserve the distinction between protocol-enforced terms, structured policy declarations, declaration-authored data descriptions, and client-authored claims. It MUST NOT flatten these categories into a single undifferentiated consent surface.

Request-level parameters

ParameterTypeRequiredStatusDescription
sourceobjectyesProtocol-enforcedSource binding. id is required and is the stable absolute URI authorization identity for the data surface. A request carries id alone: provenance is derived by the authorization server from the accepted declaration, not asserted by the client.
purpose_codeURIyesStructured policy declarationMachine-readable purpose (absolute URI). See Appendix A for the initial registry. The AS MUST accept any syntactically valid absolute-URI purpose code. For unrecognized codes, the AS MUST display purpose_description if present, or the raw URI if not, and MUST NOT reject the request solely because the purpose code is unrecognized. Consent properties associated with purpose codes in the registry are advisory, not protocol-enforced, with the exception of https://pdpp.dev/purpose/ai_training (see below).
purpose_descriptionstringnoStructured policy declarationHuman-readable purpose, displayed to the user during consent. Clients SHOULD provide this field. When present, the AS MUST display it. For standard purpose codes, the AS MAY display a human-readable label from the registry when purpose_description is absent.
access_modeenumyesProtocol-enforcedsingle_use or continuous. See Section 7.
retentionobjectnoStructured policy declarationRequested retention constraints: { max_duration, on_expiry }.
streamsStreamRequest[]yes (unless selection_preset is used)Protocol-enforcedRequested streams with per-stream parameters.
selection_presetstringnoProtocol-enforced at issuance timeReference to a SourceDeclaration preset, as an alternative to explicit streams.
client_claimsobjectnoAttributed client claimClient-authored, non-enforceable claims about this request. See Client claims.

Source kinds

These are the two provenance classes an authorization server derives and carries forward. They are defined here because this is where a reader meets the source binding, not because a client sends one.

source.kindMeaning
"connector"The declaration authority represents a connector-backed source. source.id is its public source URI, not a local package name, connector key, storage namespace, account identifier, or runtime binding. A connector declaration remains Core-valid without Collection Profile data.
"provider_native"The declaration authority represents the provider's own PDPP data surface. source.id is normally the OAuth protected-resource identifier for that surface, not merely the provider's legal-entity URI.

A selection request does not carry source.kind. The authorization server derives the provenance class from the declaration it accepted for source.id, and records it in consent evidence and any issued grant, where a client reads it back through introspection. A client whose policy depends on provenance therefore reads it from the issued grant rather than asserting an expectation in the request; Section 9 states that as a client requirement. The OAuth/RAR binding returns RFC 9396 invalid_authorization_details for invalid authorization details.

The AS MUST obtain explicit affirmative user consent before issuing any grant with purpose_code value https://pdpp.dev/purpose/ai_training. This is the sole purpose code with a mandatory consent requirement at the protocol level.

Stream selection parameters

Per-stream, within the streams array. All are optional except name.

ParameterTypeStatusDescription
namestringProtocol-enforcedStream name, or * for all streams (resolved at consent time against the retained SourceDeclaration).
necessityenumConsent-flow control at issuance timerequired (default) or optional. Optional streams are presented as user choices during consent.
instance_idsstring[]Protocol-enforcedOptional opaque owner-instance handles for this stream. Handles are scoped to issuer, subject, source.id, and stream. Omission never means fan-in (reading across more than one connected instance of the same source). The AS resolves exactly one eligible handle or requires an explicit owner choice.
time_range.sinceISO 8601Protocol-enforcedEarliest data to include (inclusive, >=), evaluated against the stream's consent_time_field.
time_range.untilISO 8601Protocol-enforcedLatest data to include (exclusive, <), evaluated against the stream's consent_time_field. A hard cap: applies to future resources as well as past ones.
viewstringProtocol-enforced at issuance timeNamed view defined by the authorization server. Mutually exclusive with fields in a request; both MUST NOT be present simultaneously. AS returns 400 invalid_request if both are present.
fieldsstring[]Protocol-enforcedField allowlist. Schema-required fields are always included regardless of this list. In v0.1, restricted to top-level field names only. Mutually exclusive with view.
resourcesstring[]Protocol-enforcedSpecific record IDs to authorize. Values are canonical key strings: minified JSON array for compound keys, plain string for simple keys. The AS validates arity and type against the retained declaration's primary_key at grant issuance. The RS filters by exact primary-key match.

Note on fields: At consent resolution, schema-required fields are always included in the resolved field set, regardless of the requested field list, because a record missing its schema-required fields is not a valid record of that stream; the per-stream consent floor is its required fields.

Note on time_range: time_range is only valid for streams that declare a consent_time_field. The authorization server MUST reject selection requests that specify time_range on a stream without that field. Its presence in the retained declaration is the authoritative signal that a stream is time-range-capable.

A wildcard entry MUST be the only entry in streams. Otherwise stream names MUST be unique within the request.

Note on streams vs selection_preset: Exactly one is required. Source validation fails if both or neither are present. The OAuth/RAR binding maps this failure to RFC 9396 invalid_authorization_details.

Selection presets

Source declarations may define selection presets. A client can reference a preset instead of constructing explicit stream selections:

{
  "type": "https://pdpp.dev/data-access",
  "source": { "id": "https://registry.pdpp.dev/connectors/instagram" },
  "purpose_code": "https://pdpp.dev/purpose/personalization",
  "access_mode": "single_use",
  "selection_preset": "social_summary"
}

The authorization server expands the preset from the retained snapshot into explicit streams and fields before final owner review and issuance. Each selection preset MUST NOT contain the same stream name more than once. Duplicate stream names make the declaration invalid. They are not deferred to grant issuance.

Every field in the issued grant is derived from either the selection request, client registration, or authorization server policy. The grant never contains values whose source is ambiguous.


7. Grant

A grant is an immutable consent artifact. It is the output of the authorization flow.

The authorization server issues an access token bound to the grant. The client uses the access token (not the raw grant) to authenticate with the resource server. The resource server resolves the token to the grant and enforces its constraints on every request. Grant lifecycle (active, expired, revoked) is tracked by the authorization server, not stored in the grant itself.

{
  "version": "0.1.0",
  "grant_id": "grt_8f72a1b3",
  "issued_at": "2026-04-06T15:00:00Z",
  "subject": { "id": "user_abc123" },
  "client": { "client_id": "music_recommendations" },
  "source": { "kind": "connector", "id": "https://registry.pdpp.dev/connectors/spotify" },
  "source_declaration": { "version": "2026-08-11" },
  "purpose_code": "https://pdpp.dev/purpose/personalization",
  "purpose_description": "Recommend concerts based on your listening history",
  "access_mode": "single_use",
  "streams": [
    {
      "name": "top_artists",
      "instance_ids": ["spotify-account-a"],
      "fields": ["id", "name", "genres", "popularity", "source_updated_at"],
      "time_constraint": {
        "field": "source_updated_at",
        "since": "2025-09-28T00:00:00Z"
      }
    }
  ],
  "retention": {
    "max_duration": "P1Y",
    "on_expiry": "delete"
  },
  "expires_at": "2027-04-06T00:00:00Z"
}

Grant fields

The following field table is normative. Section 13's TypeScript types are a non-normative convenience mirror; on conflict, this table wins.

FieldTypeRequiredStatusDescription
versionstringyesProtocol metadataTracks the version of this specification's grant schema (not a URL; there is no external schema document to resolve). This contract requires exactly 0.1.0.
grant_idstringyesProtocol metadataUnique identifier.
issued_atISO 8601yesProtocol metadataWhen the grant was issued.
subjectobjectyesIdentity bindingExactly { id }. The subject.id is an opaque string, unique within the issuing AS's namespace. No format constraint is imposed.
clientobjectyesIdentity bindingExactly { client_id } or { client_id, client_display }. client_display, when retained, is the requester identity metadata resolved by the AS, not unverified inline input.
sourceobjectyesProtocol-enforcedExact { kind, id } retained from the accepted SourceDeclaration. id is authorization identity; kind is provenance metadata.
source_declarationobjectyesProtocol evidence{ version } records the opaque revision of the exact declaration snapshot used for consent and issuance. It is evidence metadata, not a live lookup authority.
purpose_codeURIyesStructured policy declarationMachine-readable purpose (see Appendix A).
purpose_descriptionstringnoStructured policy declarationHuman-readable purpose.
access_modeenumyesProtocol-enforcedsingle_use or continuous.
streamsStreamGrant[]yesProtocol-enforcedGranted streams. Always expanded; no wildcards. See StreamGrant fields table below.
selection_presetstringnoInformationalWhich SourceDeclaration preset was selected. The resolved streams and fields remain authoritative.
retentionobjectnoStructured policy declarationPolicy commitment by the data recipient (see below).
expires_atISO 8601noProtocol-enforcedGrant expiry. Absent means no expiry.

StreamGrant fields

FieldTypeRequiredStatusDescription
namestringyesProtocol-enforcedUnique stream name within the grant. Always concrete; no wildcards in issued grants.
instance_idsstring[]yesProtocol-enforcedUnique non-empty opaque instance handles scoped to issuer, subject, source ID, and this stream. Multiple handles authorize fan-in only when explicitly listed. Example: two connected Gmail accounts are two instances of the same source kind, each with its own handle.
fieldsstring[]yesProtocol-enforcedUnique non-empty resolved field allowlist, authoritative for RS enforcement. Top-level field names only.
time_constraintobjectnoProtocol-enforcedFrozen { field, since?, until? } resolved from the retained declaration. field is required and at least one bound is present. since is inclusive; until is exclusive.
resourcesstring[]noProtocol-enforcedAuthorized record IDs in canonical key string encoding. Absent means all records.

Request-only conveniences such as wildcard names, view, omitted fields, and omitted instance handles are fully resolved before final owner review and issuance. They are not continuing authority in the grant. Selection provenance may be retained at grant level through selection_preset; the concrete stream rows remain authoritative.

Before the final approval surface is shown, the AS MUST resolve omitted instance_ids to exact eligible instance handles or require an explicit owner choice. The final approval artifact MUST include the exact resolved instance_ids, stream names, fields, resources, temporal field, since, until, purpose, retention, client identity, and grant expiry. If client_claims are rendered during final review, the final approval artifact and review revision MUST also bind the normalized exact claims with client attribution. Retained consent evidence MUST preserve that binding. The approval mutation MUST bind to an immutable review revision or digest over the authorization decision fields. client_claims MUST remain outside the resolved grant and RS enforcement. If instance eligibility or the reviewed revision becomes stale before approval, the AS MUST reject approval and require a new review.

Time concepts

The grant carries three orthogonal time-related concepts that must not be conflated:

ConceptFieldsMeaning
Grant validity periodissued_at, expires_atHow long the authorization itself is active.
Data temporal scopestreams[].time_constraintWhich records the client is authorized to see and which record field defines that boundary.
Access patternaccess_modeWhether the grant can be exercised once or continuously.

A grant can be short-lived (expires in 1 hour) but cover all historical data (no time_constraint). A grant can be long-lived but cover only data from the last 6 months (time_constraint.since). A grant can be single_use but cover a large historical window. These combinations are all valid and distinct.

Version layering

Three independent version axes exist in PDPP. They MUST NOT be conflated:

AxisFieldMeaning
Grant schema versiongrant.versionVersion of the PDPP grant schema. RS MUST reject grants with unsupported major versions, returning 400 unsupported_version.
Source declaration revisiongrant.source_declaration.versionIdentifies the exact retained declaration snapshot used for consent and issuance. It is opaque evidence metadata. The RS enforces the resolved grant and does not fetch that revision for authorization.
HTTP API contract versionPDPP-Version request headerVersion of the RS HTTP API contract. RS returns 400 unsupported_version if the requested version is not supported. If the header is absent, the RS uses the current stable version and returns the selected version in the response header (see Section 8).

The current persisted-authorization-state reader MUST reject any persisted authorization state whose version or shape it cannot validate against a supported contract before its caller continues introspection or route handling. The reader MUST NOT reconstruct missing authorization or binding facts from current configuration. A deployment that cannot support or explicitly migrate such state MUST require fresh consent.

Access modes

ModeBehavior
single_useThe grant is consumed at first token issuance. The AS marks the grant consumed atomically with issuance of the first client access token. The AS MUST reject subsequent attempts to issue new client access tokens against the same consumed grant. The RS honors all tokens issued against the grant until token expiry or revocation. The client MAY retry or resume pagination using the same access token. Failure to complete retrieval before token expiry does not un-consume the grant.
continuousThe grant is fulfilled repeatedly. The client may query the resource server incrementally over time. Active until expiry or revocation.

Time constraint semantics

The selection request's time_range is resolved against the retained stream consent_time_field into the grant's time_constraint. The grant freezes that field with the bounds. The filter is:

record[time_constraint.field] >= time_constraint.since  (if since is present)
record[time_constraint.field] <  time_constraint.until  (if until is present)

time_constraint.until is a hard cap. It applies equally to records that existed at grant issuance and to records created afterward. A continuous grant with time_constraint.until set to a past date is valid: it is a historical-only grant that will never disclose new records. This is not an error.

For continuous grants without time_constraint.until, future records in a granted stream are included as they are collected, provided the frozen field falls within any since constraint. Stream names and fields are frozen at consent time; future stream types or fields require a new grant.

Standing authorization

Grants freeze stream names, instance handles, fields, and any time constraint at consent time. Within that resolved scope, future records are included for continuous grants. Future stream types or fields are not included; they require a new grant.

Grant narrowing

Grant narrowing (reducing the scope of an existing grant) is not supported in v0.1. Scope reduction is achieved via revoke-and-reissue: the client revokes the existing grant and the user issues a new, narrower grant. Authorization server UIs SHOULD model this flow as revocation followed by a new grant request.

Records from revoked grants

Revocation stops future access only. Records already delivered to the client before revocation are governed by the grant's retention policy and applicable legal obligations. PDPP does not retroactively reach into client-side data stores.

Retention

Retention is a structured policy declaration and policy commitment by the data recipient (the client). PDPP does not technically enforce retention. Enforcement is through legal agreements or contractual obligations; a trust registry supports admission and accountability for a retention commitment rather than enforcing it, because Core defines no compliance-evidence query a registry could answer. This is consistent with how OAuth 2.0 treats scope compliance: the protocol makes the commitment legible and machine-readable; external mechanisms enforce it.

{
  "max_duration": "P6M",
  "on_expiry": "delete"
}
FieldTypeDescription
max_durationISO 8601 durationHow long the client may retain collected data.
on_expiryenumdelete or anonymize. Note: archive is not supported in v0.1.

Examples

Specific data, single use:

{
  "version": "0.1.0",
  "grant_id": "grt_001",
  "issued_at": "2026-04-06T15:00:00Z",
  "subject": { "id": "user_abc" },
  "client": { "client_id": "concert_app" },
  "source": { "kind": "connector", "id": "https://registry.pdpp.dev/connectors/spotify" },
  "source_declaration": { "version": "2026-08-11" },
  "purpose_code": "https://pdpp.dev/purpose/personalization",
  "access_mode": "single_use",
  "streams": [
    {
      "name": "top_artists",
      "instance_ids": ["spotify-account-a"],
      "fields": ["id", "name", "genres", "popularity", "source_updated_at"],
      "time_constraint": {
        "field": "source_updated_at",
        "since": "2025-09-28T00:00:00Z"
      }
    }
  ]
}

Continuous access for an AI agent:

{
  "version": "0.1.0",
  "grant_id": "grt_002",
  "issued_at": "2026-04-06T15:00:00Z",
  "subject": { "id": "user_abc" },
  "client": { "client_id": "personal_agent" },
  "source": { "kind": "connector", "id": "https://registry.pdpp.dev/connectors/openai" },
  "source_declaration": { "version": "2026-08-11" },
  "purpose_code": "https://pdpp.dev/purpose/agent_context",
  "access_mode": "continuous",
  "streams": [
    {
      "name": "conversations",
      "instance_ids": ["openai-account-a"],
      "fields": ["id", "title", "source_created_at", "source_updated_at"]
    },
    {
      "name": "messages",
      "instance_ids": ["openai-account-a"],
      "fields": ["id", "conversation_id", "role", "content", "source_created_at"]
    }
  ]
}

Health data with field selection and retention:

{
  "version": "0.1.0",
  "grant_id": "grt_003",
  "issued_at": "2026-04-06T15:00:00Z",
  "subject": { "id": "user_abc" },
  "client": { "client_id": "sleep_analysis" },
  "source": { "kind": "connector", "id": "https://registry.pdpp.dev/connectors/oura" },
  "source_declaration": { "version": "2026-08-11" },
  "purpose_code": "https://pdpp.dev/purpose/analytics",
  "access_mode": "single_use",
  "streams": [
    {
      "name": "sleep_sessions",
      "instance_ids": ["oura-account-a"],
      "fields": ["day", "total_sleep_duration", "sleep_score"],
      "time_constraint": {
        "field": "day",
        "since": "2026-01-01T00:00:00Z",
        "until": "2026-04-01T00:00:00Z"
      }
    }
  ],
  "retention": { "max_duration": "P90D", "on_expiry": "delete" }
}

8. Resource Server Interface

The resource server stores records and serves them to clients filtered by grants. This section is normative: a compliant resource server must implement this interface for cross-deployment interoperability.

Grant enforcement

On every request, the resource server:

  1. Resolves the access token through authenticated RFC 7662 introspection or a local equivalent for co-located deployments. Positive introspection results MUST NOT be cached longer than min(token_exp, 60 seconds).
  2. Determines pdpp_token_kind from the introspection response, then branches:
    • Client: requires an active resolved authorization context (active: true, a resolved grant). Verifies that the requested stream appears in the grant's streams list. Selects records only from the explicitly granted instance_ids and enforces the grant's time_constraint, fields, and resources constraints.
    • Owner: enforces subject, source, connection, and operation scope derived from the introspection response. Does not require or synthesize a client grant — an owner token carries none.
  3. If all checks pass, returns records filtered accordingly.
  4. If any check fails, returns a structured error (see Errors below).

For owner-token current-capability reads, the effective filter is the permitted owner request filter alone: an owner token carries no grant, so there is no grant filter to intersect. Request filters can only narrow the current owner read and cannot widen it.

In v0.1, client-token reads do not have request-time predicate filters (see List records below); the resource server enforces the frozen grant constraints and rejects a client request-time predicate filter rather than evaluating it. A future client-filter capability may define intersection semantics.

The RS MUST NOT re-validate authorization against the current SourceDeclaration. All enforcement constraints are in the resolved grant. Current serving metadata MAY route a granted instance, describe current schemas or query capabilities, or reject a request that cannot currently be served. It MUST NOT widen or reinterpret a stream, instance, field, time field, bound, or resource key.

Token type distinction: The format of the access token is opaque to the Resource Server. The RS MUST determine the token's properties (including pdpp_token_kind) solely from the introspection response, never from token syntax.

Token introspection

For separated AS/RS deployments, the RS MUST authenticate to the AS introspection endpoint as required by RFC 7662. The introspection response combines standard RFC 7662 fields with PDPP-defined extensions:

FieldTypeDescription
activebooleanWhether the token is currently valid.
pdpp_token_kindstring"owner" or "client".
subject_idstringThe subject (user) identifier.
grant_idstringThe associated grant identifier. Present for client tokens.
client_idstringThe client identifier. Present for client tokens.
expintegerOptional expiry timestamp (Unix epoch). Omitted when the token has no expiration.
authorization_detailsarrayThe approved RFC 9396 detail for a client token. It carries the resolved grant enforcement constraints defined in Section 7.

The introspection response MUST contain the complete context needed to enforce the request. The separated RS MUST enforce only from that response and MUST NOT make a second AS lookup while handling the request. A co-located AS and RS MAY resolve the same context through a local equivalent.

Token kind extensibility: This specification defines owner and client. Deployments MAY introduce additional token kinds in companion profiles. A resource server that receives a pdpp_token_kind value it does not recognize MUST treat the token as unauthorized for all operations defined in this specification.

Positive introspection results MUST NOT be cached longer than min(token_exp, 60 seconds). Self-contained JWTs (e.g., signed JWTs) are allowed as an optimization but MUST NOT be the sole revocation mechanism; the RS MUST still be able to check active status through introspection or local equivalent.

Authentication

Two authentication boundaries exist:

Owner operations: Authorization: Bearer <owner_token>. Core owner tokens are scoped to a single subject's data store and support the Core owner and self-export operations defined here. The RS MUST derive the subject_id from the introspection response and MUST reject any request attempting to access data outside that subject's scope. Companion profiles may define additional owner-authenticated operations. How the owner obtains this token is out of scope (device code flow, API key, or any other mechanism).

Client operations (query records, list streams, fetch blobs): Authorization: Bearer <access_token>. Access tokens are bound to a specific grant. Both token types use RFC 6750 Bearer Token format. The RS distinguishes them via pdpp_token_kind in the introspection response.

Self-export: An owner holding a valid owner token MAY query their own data using the standard client query endpoints without a client grant. This is the v0.1 self-export mechanism and does not require a separate grant. Conformant Core RS implementations SHOULD support this capability (see Section 9 conformance item 13).

Protected resource metadata

A resource server MUST publish OAuth 2.0 Protected Resource Metadata as defined in RFC 9728. RFC 9728 Section 3 fixes the document's location: the well-known URI string /.well-known/oauth-protected-resource is inserted into the resource identifier between the host component and any path or query component.

A client that reaches the resource server without a usable access token learns that location from the response itself. On a 401, the resource server MUST include a WWW-Authenticate: Bearer challenge as RFC 6750 Section 3 requires, MUST set error="invalid_token" when a token was presented and rejected, and MUST include the resource_metadata parameter RFC 9728 Section 5.1 defines, carrying the URL of this document. This is the bootstrap path: without it a client holding no token has no specified way to discover where to authenticate.

The resource member is the resource server's own identifier, as RFC 9728 Section 2 requires. For a provider_native source it is the same identifier as the declaration's source.id, which is the binding Section 5 already requires an authorization server to check before consent. A resource server that serves several sources publishes one metadata document per resource identifier rather than one document listing them.

PDPP defines four additional members. RFC 9728 Section 2 permits additional parameters and RFC 9728 Section 3.2 requires a reader to ignore any parameter it does not understand, so a generic OAuth client is unaffected by their presence. Each name carries the pdpp_ prefix to keep it distinct from a future registered parameter; RFC 9728 does not itself prescribe a naming convention.

MemberMeaning
pdpp_core_query_baseThe base path the Section 8 endpoint paths extend, so a client composes a record query without assuming a version segment.
pdpp_token_kinds_supportedThe pdpp_token_kind values this resource server accepts, drawn from the kinds Section 8 defines.
pdpp_self_export_supportedWhether an owner token may read the owner's own data through the client query endpoints without a client grant.
pdpp_provider_connect_versionThe PDPP version this resource server's interface implements, which a client would otherwise learn only from the PDPP-Version negotiation on a first request.

resource_name is RFC 9728's own member for a human-readable resource name, not a PDPP extension; a resource server SHOULD publish it because a consent surface has no other name to display for the resource.

RFC 9728 Section 2 makes authorization_servers OPTIONAL, and notes that in some deployments the set of authorization servers is not enumerable. A PDPP resource server MUST publish authorization_servers when that set is enumerable, so a client can reach the issuer without prior configuration. When the set is not enumerable, the resource server MUST omit the member rather than publish a partial list, and the client obtains the issuer from the grant it already holds or from the deployment's own onboarding; Core defines no other discovery path for that case.

The capabilities member is defined by the extension profiles that advertise into it, not by Core. Core neither requires it nor constrains its contents, and a resource server that implements no extension omits it.

Endpoints

List streams

GET /v1/streams
Authorization: Bearer <access_token>

Returns streams with record counts, scoped by token kind: for a client token, the streams present in the resolved authorization context; for an owner token, the streams in the subject-scoped data store the owner token is scoped to.

Response:

{
  "object": "list",
  "data": [
    {
      "object": "stream",
      "name": "conversations",
      "record_count": 2196,
      "last_updated": "2026-04-06T15:01:00Z",
      "freshness": {
        "captured_at": "2026-04-06T15:01:00Z",
        "status": "current",
        "last_attempted_at": "2026-04-06T15:01:00Z"
      }
    },
    {
      "object": "stream",
      "name": "messages",
      "record_count": 48302,
      "last_updated": "2026-04-06T15:01:00Z",
      "freshness": {
        "captured_at": "2026-04-06T15:01:00Z",
        "status": "current",
        "last_attempted_at": "2026-04-06T15:01:00Z"
      }
    }
  ]
}

Get stream metadata

GET /v1/streams/{stream}
Authorization: Bearer <access_token>

A client-token caller may fetch metadata only for a stream present in its resolved authorization context. An owner-token caller may fetch metadata for streams in the subject’s data store the owner token is scoped to. Once access is authorized, the response body is actor-specific:

  • Owner token: the metadata document is returned whole — full current schema, query capabilities, views, and relationships — rather than field-projected by any grant. An owner token carries no grant, so there is nothing to project against.
  • Client token: the response is a closed projection derived from the resolved authorization context: only the granted stream's explicitly granted fields, and only immutable/frozen grant facts. Current query, view, relationship, filter, expansion, and aggregation capabilities MUST NOT appear unless that capability is explicitly part of a future frozen grant vocabulary. Current metadata MAY report availability/freshness or reject an unavailable operation, but MUST NOT make the grant appear broader or semantically different than what was issued. A source declaration change made after the grant was issued (e.g., a new field) MUST NOT become visible through this endpoint for that grant.

Owner-token response (full current metadata):

{
  "object": "stream_metadata",
  "name": "top_artists",
  "schema": { },
  "primary_key": ["id"],
  "cursor_field": "source_updated_at",
  "consent_time_field": "source_updated_at",
  "selection": {
    "fields": true,
    "resources": false
  },
  "query": {
    "range_filters": {
      "source_updated_at": ["gte", "gt", "lte", "lt"]
    },
    "expand": [
      { "name": "messages", "default_limit": 10, "max_limit": 50 }
    ]
  },
  "freshness": {
    "captured_at": "2026-04-06T15:01:00Z",
    "status": "current",
    "last_attempted_at": "2026-04-06T15:01:00Z"
  },
  "views": [
    { "id": "basic", "label": "Artist names and genres", "fields": ["id", "name", "genres"] }
  ],
  "relationships": [
    { "name": "messages", "stream": "messages", "foreign_key": "conversation_id", "cardinality": "has_many" }
  ]
}

Client-token response (closed projection of a grant frozen to fields id, name, source_updated_at; no range filters, views, or relationships were granted):

{
  "object": "stream_metadata",
  "name": "top_artists",
  "schema": {
    "properties": {
      "id": { },
      "name": { },
      "source_updated_at": { }
    }
  },
  "primary_key": ["id"],
  "cursor_field": "source_updated_at",
  "consent_time_field": "source_updated_at",
  "selection": {
    "fields": true,
    "resources": false
  },
  "query": { },
  "freshness": {
    "captured_at": "2026-04-06T15:01:00Z",
    "status": "current",
    "last_attempted_at": "2026-04-06T15:01:00Z"
  },
  "views": [],
  "relationships": []
}

Freshness metadata

A resource server MAY attach a freshness object to stream listings, stream metadata, and record-list responses.

Freshness is server-observed disclosure metadata, not a grant constraint. It reports what the server knows about the recency of the underlying data relevant to the response. It does not guarantee that the source has not changed since captured_at, and it does not widen or narrow access rights.

FieldTypeDescription
captured_atISO 8601 or nullTime of the most recent successful collection or source confirmation that could have affected the response. null if unknown.
statusenumcurrent, stale, or unknown. stale means the server believes the stored data may no longer reflect source state based on local collection policy or failed refresh attempts.
last_attempted_atISO 8601 or nullTime of the most recent attempted refresh relevant to the response, if tracked.

List records

GET /v1/streams/{stream}/records
Authorization: Bearer <access_token>

Returns records from a stream, filtered by the grant and any additional request parameters.

Query parameters:

ParameterTypeDescription
limitintegerRecords per page. Default 25, max 100. A request for more than 100 is clamped to 100 and the response carries a non-fatal limit_clamped warning (see below), not an error.
cursorstringOpaque pagination token from a previous response. Clients MUST NOT parse or construct cursor tokens.
orderenumdesc (default) or asc.
filter[{field}] and filter[{field}][op]stringOwner-token current-capability filters only. Client-token requests MUST reject exact and range forms in v0.1.
viewstringOwner-token current-capability request for records projected to a named view. Client-token records requests MUST reject view; clients use explicit fields or the field projection already frozen into the grant. Mutually exclusive with fields.
fieldscomma-separatedSparse fieldset. Schema-required fields are always included. In v0.1, restricted to top-level field names only. Mutually exclusive with view.
expand[]stringOwner-token current-capability request to expand a relation declared under query.expand. Depth is 1. Expanded relations appear under the expanded key on the parent record. Client-token requests MUST reject this parameter in v0.1.
expand_limit[{relation}]integerOwner-token current-capability limit for an expanded has_many relation. Valid only for relations declared under query.expand; defaults and limits come from that declaration. Client-token requests MUST reject this parameter in v0.1.
changes_sincestringOpaque incremental-sync token from a previous session (distinct token space from cursor). Returns only records whose grant-authorized projection changed since that cursor, plus tombstones for deletions. Use next_changes_since from the terminal page to seed the next session. Returns HTTP 410 Gone with error code cursor_expired if the cursor has expired.

The durable client-token base query surface in v0.1 is: limit, cursor, order, fields, changes_since, and blob fetch. Exact and range filter[...], expand[], and expand_limit[...] parameters are not part of the client-token surface. Owner-token current-capability reads MAY support exact and declared range filters, view, and declared expansion; those reads consult current serving metadata. Advanced stream-specific query power MUST be declared in stream metadata under query.

Unknown query parameters and unsupported query shapes MUST be rejected with HTTP 400 and MUST NOT be silently ignored.

Non-fatal warnings: A list response MAY carry a meta.warnings[] array reporting non-fatal lossiness that the server resolved without failing the request. Each entry has a stable code and a human-readable message; clients SHOULD branch on code, not on message text. A limit above the maximum is the canonical case: the RS returns the bounded page and a limit_clamped warning rather than silently dropping the excess or returning an error. Clients page forward with the returned cursor instead of expecting a larger page. Warnings are not errors and MUST NOT change the HTTP status.

Client-token requests that contain any exact or range filter[...] parameter MUST be rejected with HTTP 400 invalid_request before the RS consults current SourceDeclaration or serving metadata. This rejection applies regardless of whether the field or operator would otherwise be declared. Owner-token current-capability reads MAY accept exact filters on declared top-level scalar fields and range filters explicitly declared by current serving metadata. Unknown fields, non-scalar fields, and unsupported range shapes return HTTP 400. Owner subject, source, and connection scope are enforced independently. An owner token has no client grant field projection.

Client-token requests that contain expand[] or expand_limit[...] MUST be rejected with HTTP 400 invalid_request before the RS consults current SourceDeclaration or serving metadata. A v0.1 resolved grant does not freeze relationship identity, target stream, foreign-key join semantics, cardinality, or expansion limits. Current relationship metadata therefore cannot interpret client grant rights. Owner-token current-capability reads MAY use declared expansion against current serving metadata.

For owner-token current-capability reads, range filters (gte, gt, lte, lt) apply only to fields declared in query.range_filters. Nested paths, arrays, OR grammar, and full-text search are not part of v0.1.

For owner-token current-capability reads, expansion is declaration-driven. A relation is structurally present if listed under relationships, but it is only expandable if declared under query.expand. expand_limit[{relation}] is only valid for declared has_many relations.

Stable sort: Records are sorted by (cursor_field, primary_key) for cursor safety. Null or absent cursor_field values sort after present values.

Page cursors are direction-bound: a client MUST follow a next_cursor with the same order value that produced it. To change direction, the client MUST restart pagination without a cursor. Resource servers MUST reject order-mismatched page cursors as invalid_cursor.

Incremental sync for mutable streams: Pass changes_since to retrieve only records changed since a previous sync. The resource server returns changed records within the grant's authorized field projection. If a record was deleted, a tombstone entry is included. If the cursor has expired (HTTP 410 Gone with error code cursor_expired), the client MUST perform a full re-sync.

Eligibility for changes_since MUST be computed on the grant-authorized projection, not on the unprojected record. Returning a record whose authorized projection is unchanged is a protocol violation because it leaks that hidden fields changed.

If a changes_since response is paginated, all pages in that session MUST be anchored to the same session horizon selected on the first page. New writes arriving after page 1 MUST NOT appear in later pages of that same session; they surface in the next session via the terminal-page next_changes_since.

Invalid owner filter: An owner-token current-capability filter on an unknown, non-scalar, or unsupported field/operator returns HTTP 400 invalid_request or unknown_field, as applicable. Client-token predicate filters are rejected earlier under the v0.1 client-filter rule.

Expansion: A client-token expansion request is rejected with 400 invalid_request before declaration lookup. For an owner-token current-capability read, requesting an undeclared relation returns 400 invalid_expand. Expansion never widens the current owner read scope.

Response:

{
  "object": "list",
  "url": "/v1/streams/conversations/records",
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMy0yNVQxODoyMjoxMVoiLCJpZCI6ImNvbnZfMDFKUVc4TTJSNyJ9",
  "next_changes_since": "eyJjaGFuZ2VzX3NpbmNlIjoiMjAyNi0wNC0wNlQxNTowMTowMFoifQ",
  "freshness": {
    "captured_at": "2026-04-06T15:01:00Z",
    "status": "current",
    "last_attempted_at": "2026-04-06T15:01:00Z"
  },
  "data": [
    {
      "object": "record",
      "id": "conv_01JQW8M2R7",
      "stream": "conversations",
      "data": {
        "id": "conv_01JQW8M2R7",
        "title": "Trip planning",
        "source_created_at": "2026-03-25T18:22:11Z"
      },
      "emitted_at": "2026-04-06T15:01:00Z"
    }
  ]
}

The terminal page of a changes_since request (i.e., has_more: false) MUST include next_changes_since.

Get a single record

GET /v1/streams/{stream}/records/{id}
Authorization: Bearer <access_token>

Returns a single record by primary key. The {id} path parameter is the percent-encoded canonical key string. Owner-token current-capability reads support expand[]; client-token requests reject it in v0.1 before declaration lookup.

Delete a record (owner-authenticated)

DELETE /v1/streams/{stream}/records/{id}
Authorization: Bearer <owner_token>

Permanently removes a record from the stream. The RS may implement this as a tombstone internally. Returns 204 No Content on success. The {id} path parameter is the percent-encoded canonical key string.

Get a blob

GET /v1/blobs/{blob_id}
Authorization: Bearer <access_token>

The resource server authorizes blob access by verifying that:

  1. The grant includes a stream containing a record that references this blob_id.
  2. The referencing record passes all grant filters.
  3. The blob_ref field is included in the grant's authorized field projection.

A blob_id alone does not grant access. The client MUST have discovered the blob through an authorized record.

Direct response MUST include:

  • Content-Type (IANA media type)
  • Content-Length if known
  • Cache-Control: private, no-store
  • Accept-Ranges: bytes if range requests are supported

Redirect response (HTTP 302) MUST include:

  • Location header pointing to a short-lived signed URL (valid for at least 60 seconds)
  • Cache-Control: no-store

A stale or unknown blob_id returns 404 blob_not_found.

HEAD is supported for size checks. Range headers are recommended for large files.

Errors

Every non-2xx response returns a structured error:

{
  "error": {
    "type": "permission_error",
    "code": "grant_stream_not_allowed",
    "message": "Grant does not include stream 'messages'.",
    "param": "expand[0]",
    "request_id": "req_01JQXA3N9Y"
  }
}

Clients MUST treat unrecognized error codes as opaque and fall back to the actual HTTP status code and applicable response headers.

Authority and forward compatibility.

  1. The actual HTTP status code and applicable response headers are authoritative for generic HTTP semantics, including success or failure, authentication challenges, redirection, and retry timing.
  2. A recognized error.type or error.code MAY refine PDPP-specific category, presentation, or recovery behavior only when its defined semantics are compatible with the actual status code and headers.
  3. An absent, unknown, malformed, or status-incompatible type or code is opaque and MUST NOT override the actual status code or relevant headers.
  4. Unknown identifiers MUST NOT cause parse failure.

Clients MAY retain unknown identifiers for diagnostics, subject to local size limits, safe rendering/escaping, and privacy policy.

This makes a future error code safe to introduce: an older client keeps handling the response by status and headers, whatever the new code or type says.

CodeHTTP StatusTypeMeaning
invalid_cursor400invalid_request_errorCursor token is malformed or unrecognized.
invalid_request400invalid_request_errorMalformed request parameter or mutually exclusive parameters.
invalid_expand400invalid_request_errorRelation is not declared as expandable.
unknown_field400invalid_request_errorRequested field not in stream schema.
unsupported_version400invalid_request_errorPDPP-Version header specifies unsupported version, or grant references unsupported schema version.
authentication_error401authentication_errorMissing or invalid access token.
authorization_state.unsupported_legacy_shape401authentication_errorPersisted authorization state does not match a supported shape. Fresh consent is required when no migration applies.
field_not_granted403permission_errorRequested client field exceeds the grant's authorized field projection.
insufficient_scope403permission_errorExpansion requests a stream not in the grant.
grant_stream_not_allowed403permission_errorStream not in grant.
grant_time_range_exceeded403permission_errorRequest filters exceed the grant's frozen time_constraint.
grant_expired403permission_errorGrant has expired.
grant_revoked403permission_errorGrant has been revoked.
grant_invalid403permission_errorResolved grant is malformed or cannot be served without changing its authorization meaning.
blob_not_found404not_found_errorblob_id is unknown or stale.
not_found404not_found_errorStream or record not found.
cursor_expired410gone_errorchanges_since cursor is too old; full re-sync required.
rate_limit_exceeded429rate_limit_errorToo many requests. Includes Retry-After header.
api_error500api_errorInternal server error.

API versioning

API version is specified via header:

PDPP-Version: 2026-04-06

If the PDPP-Version header is absent, the RS uses the current stable version and returns the selected version in the PDPP-Version response header. If the requested version is not supported, the RS returns 400 unsupported_version.

Every response includes a Request-Id header for debugging.


9. Conformance

This section defines what it means to implement each PDPP role. Conformance claims should reference this section.

Conformance is role- and behavior-based. A conformant implementation is not required to use any particular vendor-hosted service, token, chain, centralized registry operator, domain, or repository deployment.

Authorization Server conformance

A conformant authorization server:

  1. Accepts selection requests using the RFC 9396 authorization_details envelope with type: "https://pdpp.dev/data-access".
  2. Validates selection requests against one retained SourceDeclaration snapshot: rejects unknown streams, unsupported selection parameters (e.g., time_range on a stream without consent_time_field), and unrecognized selection presets.
  3. Issues grants that conform to the grant schema defined in Section 7 (normative field tables). All grant fields are derived from the selection request, client registration, or AS policy.
  4. Expands wildcards and selection presets into explicit stream names, fields, per-stream instance handles, resources, and frozen time constraints before issuing the grant.
  5. Produces a binding-neutral Source validation failure when a request contains both or neither of streams and selection_preset. The OAuth/RAR binding maps it to RFC 9396 invalid_authorization_details.
  6. MUST NOT reject a purpose_code solely because it is not in the PDPP registry. For unrecognized codes, displays purpose_description if present, or the raw URI. MAY reject a purpose_code based on local policy.
  7. Renders requester identity metadata, declaration-authored data descriptions, structured policy declarations, and client-authored claims as semantically distinct categories during consent. MUST attribute client_claims to the client and MUST NOT present them as protocol-enforced terms. If client_claims are rendered during final review, binds the normalized exact claims into the immutable final approval artifact and review revision, and preserves that binding in retained consent evidence, without adding them to the resolved grant or RS enforcement.
  8. Tracks grant lifecycle (active, expired, revoked). Reflects revocation immediately in introspection responses (active: false).
  9. Issues access tokens bound to specific grants. Access tokens include the PDPP introspection extension fields.
  10. For single_use grants, consumes the grant atomically with first client-token issuance and rejects subsequent attempts to issue new client access tokens against that grant.
  11. Validates stream/field/view/resource-id shape at grant issuance.
  12. MUST NOT define a view including fields absent from the retained SourceDeclaration schema.
  13. Resolves view names to field lists at issuance time; stores resolved fields in the StreamGrant. Client-token record reads reject query-time view in v0.1. Owner-token current-capability reads MAY resolve current views.
  14. Obtains explicit affirmative user consent before issuing grants with purpose_code: "https://pdpp.dev/purpose/ai_training".
  15. Resolves omitted instance IDs before the final approval surface. Binds exact resolved instances and all final decision fields to an immutable review revision or digest. Rejects stale approval if eligibility or the reviewed revision changes before approval.
  16. Retains one exact SourceDeclaration snapshot through request validation, consent display, narrowing, issuance, and consent evidence. A later current declaration never substitutes for it.
  17. Returns 400 unsupported_version if PDPP-Version header specifies an unsupported version.
  18. For a separated AS and RS, authenticates the RS at the RFC 7662 introspection endpoint and returns the complete grant enforcement context in one response.
  19. Consumes each OAuth authorization code atomically on its first successful redemption. Rejects every later redemption with invalid_grant and does not issue another token.
  20. Issues refresh tokens only for continuous grants, or for a grant package only when every child grant is continuous. It rotates refresh tokens by family. Reuse of a superseded token revokes the family and every family-linked access token, returns invalid_grant, and requires fresh authorization.
  21. Rejects unsupported persisted authorization state before introspection or request handling. Does not reconstruct missing facts from current configuration and requires fresh consent when no migration applies.

Resource Server conformance

A conformant Core RS:

  1. Implements the query endpoints defined in Section 8: list streams, get stream metadata, list records, get a single record, get a blob, delete a record (owner-authenticated).
  2. Enforces grant constraints on every client request: stream membership, explicit instance handles, frozen time_constraint, fields allowlist, and resources filter.
  3. In a separated deployment, resolves access tokens through authenticated RFC 7662 introspection, enforces only from that response, and makes no second AS lookup while handling the request. A co-located deployment may use a local equivalent. Caches positive results no longer than min(token_exp, 60 seconds).
  4. Distinguishes owner tokens from client tokens via pdpp_token_kind.
  5. For owner tokens, computes the effective filter as the permitted owner request filter alone (there is no grant filter). For client tokens in v0.1, rejects request-time predicate filters and enforces the frozen grant constraints.
  6. Returns structured errors as defined in Section 8 (unified error table).
  7. Supports incremental sync via changes_since for mutable_state streams, including tombstone entries, omission of records whose grant-authorized projection did not change, and HTTP 410 with error code cursor_expired on cursor expiry.
  8. Returns next_changes_since on the terminal page of every changes_since response.
  9. Rejects client-token exact and range filter[...] parameters with 400 invalid_request before consulting current declaration metadata; owner-token current-capability reads MAY retain declared filter behavior.
  10. Rejects unknown query parameters and unsupported query shapes with 400 instead of silently ignoring them.
  11. Implements the PDPP-Version header negotiation.
  12. Scopes owner token access to a single subject's data store; derives subject_id from introspection response.
  13. SHOULD support owner-authenticated access to the /v1/streams/{stream}/records query endpoints without a client grant, allowing the data subject to export their own data directly (self-export).
  14. For owner-token stream-metadata reads, returns the full current stream metadata within the owner’s subject/source/connection scope, including current query, view, and relationship capabilities.
  15. For client-token stream-metadata reads, returns only a projection derived from the resolved authorization context: the granted stream and its explicitly granted fields, and immutable/frozen grant facts. MUST NOT include current view, relationship, filter, expansion, or aggregation capability unless that capability is explicitly part of a future frozen grant vocabulary, and MUST NOT surface a source-declaration change made after grant issuance.
  16. Publishes RFC 9728 protected resource metadata at the location RFC 9728 Section 3 derives from its resource identifier, carrying resource, the four pdpp_-prefixed members defined in Section 8, and authorization_servers when its issuer set is enumerable. Returns a WWW-Authenticate: Bearer challenge on 401 per RFC 6750 Section 3, carrying the RFC 9728 resource_metadata parameter.

Collection Resource Server, runtime, and connector conformance are separate claims defined in the PDPP Collection Profile.

Client conformance

A conformant client:

  1. Submits selection requests using the RFC 9396 authorization_details envelope.
  2. Uses access tokens (not raw grants) to authenticate with the resource server.
  3. Treats cursor and changes_since tokens as opaque and from distinct token spaces. MUST NOT use a next_cursor value as a changes_since parameter.
  4. Stores next_changes_since from the terminal page of a changes_since response for use in the next sync session.
  5. Respects HTTP 410 cursor_expired responses by performing a full re-sync rather than retrying with the expired cursor.
  6. Honors retention commitments declared in the grant.
  7. Treats unrecognized error codes as opaque, falling back to the exact HTTP status code and applicable response headers rather than failing on an unknown code. Takes the actual status code and headers as the authoritative outcome; uses a recognized error.type only to refine category or presentation when compatible with that outcome; ignores an absent, unrecognized, or status-incompatible type for control flow; and never fails to parse on an unknown code or type.
  8. Where local policy depends on source provenance, MUST read source.kind from the issued grant and apply that policy before first use of the records. A client MUST NOT assume a provenance class it did not read from the grant. A client with no provenance-dependent policy has nothing to check.

Conformance test suite

A formal conformance test suite is planned but is not defined in v0.1. This is out of scope for the current version.


10. Security Considerations

Declaration retrieval hygiene

An authorization server SHOULD enforce configured response-size, time, and redirect-depth limits when retrieving a declaration, and SHOULD resolve DNS freshly for each connection attempt. These limits are set by local judgment: they do not alter the declaration or grant semantics that peers must share, and a peer cannot observe which values a server chose.

Two related properties are not tuning choices and are stated in Section 5 as requirements. The safety of what a server renders is a shared invariant. So is validating the destination address against network policy immediately before each connection attempt: a stale validation is what a rebinding attack exploits, so the freshness of the DNS result above is a performance and hygiene preference while the timing of the address check is a security requirement.

Token security

PDPP defines two token kinds at the resource server boundary: owner tokens and client tokens. Both use RFC 6750 Bearer Token format on the wire. The RS distinguishes them via pdpp_token_kind in the introspection response, not by token syntax.

For separated AS/RS deployments, the RS MUST authenticate to the AS introspection endpoint (RFC 7662) and enforce only from its response. It MUST NOT make a second AS lookup while handling the request. For co-located deployments, a local equivalent (shared database lookup or function call) is acceptable. Self-contained JWTs are allowed as an optimization but MUST NOT be the sole revocation mechanism.

Positive introspection results MUST NOT be cached longer than min(token_exp, 60 seconds). This bounds the propagation window for revocation.

An access token issued with or from a refresh-token family MUST be linked to that family and MUST have a short, token-specific expiration no later than the family or grant expiration. A token response MUST derive expires_in from the access token's persisted expiration. It MUST omit expires_in when the access token has no expiration. An RFC 7662 response MUST likewise omit exp when no expiration exists.

Every successful OAuth token response that contains an access token or refresh token MUST include Cache-Control: no-store and Pragma: no-cache before the response is serialized. This applies to authorization-code, refresh-token, and device-code exchanges, including package-scoped variants.

An authorization code MUST be consumed atomically on its first successful redemption. A later redemption, including one with the same valid PKCE verifier, MUST return invalid_grant and MUST NOT issue another token.

When an authorization server issues refresh tokens for a continuous grant, each token MUST belong to a family and MUST rotate after successful use. The AS MUST atomically supersede the presented token and issue one active successor. Reuse of any superseded token, including a retry after a lost successful response, MUST revoke the token family and every access token linked to that family, return invalid_grant, and require fresh authorization. Introspection MUST report every family-linked access token inactive after the replay is detected. An AS MUST NOT issue refresh tokens for a single_use grant. It MUST NOT issue one for a grant package unless every child grant is continuous. On upgrade, an implementation MUST NOT infer family linkage for an existing bearer. Any live refresh family without persisted bearer linkage MUST be revoked together with its grant- or package-bound bearer tokens and MUST require fresh authorization. This behavior follows RFC 9700, Section 4.14.2.

Sender-constrained tokens (non-normative): Bearer tokens (RFC 6750) are the v0.1 baseline. Deployments handling sensitive standing access SHOULD consider sender-constrained tokens, which bind a token to a client-held key so that possession of the token alone is not sufficient to use it. DPoP (RFC 9449) and mutual-TLS certificate binding (RFC 8705) are both compatible with PDPP's introspection-based design. A formal optional hardening profile is a candidate for a future version.

Grant integrity

The grant is designed to be signable. The subject and client fields support future JWS/JWT signatures. Implementations MUST treat grants as tamper-sensitive. Grant signing and a formal token format are deferred to a future version; the current design is compatible with adding them without breaking changes.

Large authorization_details payloads may exceed URL length limits. Production deployments SHOULD use Pushed Authorization Requests (PAR, RFC 9126).

Credential handling

INTERACTION_RESPONSE messages in the Collection Profile may contain passwords and OTP codes. Runtimes MUST NOT log or persist credential data. See the PDPP Collection Profile for details.

Connector trust

In the Collection Profile, connectors receive credentials via the INTERACTION channel. A malicious connector could exfiltrate credentials. Production deployments SHOULD mitigate this by sandboxing connector processes (restricting network egress), using connectors from trusted registries only, or having the runtime authenticate on behalf of the connector and pass only session tokens. A formal connector trust model is deferred.

Trust boundary responsibilities

RoleResponsibilities
Authorization ServerValidates purpose-code syntax and local policy; authenticates user; preserves semantic distinctions on the consent surface; validates stream/field/view/resource-id shape at grant issuance; resolves views to field lists; issues access tokens; maintains grant lifecycle.
Resource ServerValidates token via introspection; for client requests, enforces stream, instance, fields, frozen time constraints, and resources against the resolved grant; for owner requests, enforces subject/source/connection/operation scope without requiring a grant; never reinterprets authorization from a current declaration.
ClientSubmits well-formed selection requests; uses access tokens; terminates on revocation; honors retention commitments.

Revocation

There is no push revocation channel in v0.1. Revocation propagation is bounded by the introspection cache TTL (maximum 60 seconds). The AS MUST reflect revocation immediately in introspection responses (active: false). A client will receive a 403 grant_revoked response no later than 60 seconds after revocation.

Upon receiving any 403 grant_revoked response, the client MUST stop further requests against that grant. Companion profiles define how their active work is terminated.

Revocation stops future access only. Data already delivered to the client before revocation is governed by the grant's retention policy and applicable legal obligations.

Revocation is not deletion. v0.1 does not define an active erasure signal or downstream deletion callback.


11. Privacy Considerations

Data minimization

Stream-level and field-level selection implements the GDPR principle of data minimization. Clients SHOULD request only the data they need for their stated purpose. Authorization servers SHOULD display the specific fields and streams being requested during consent.

Purpose limitation

The purpose_code URI enables purpose declaration, consent display, registration policy, and implementation-defined audit or transparency mechanisms. Authorization servers MAY restrict client registrations to specific purpose codes.

Auditability and transparency boundary

PDPP core defines the authorization, grant, and disclosure semantics that make auditing and transparency possible. This includes stable identifiers and state transitions such as grant_id, client_id, subject_id, purpose_code, stream and resource identifiers, timestamps, and grant lifecycle states.

PDPP core does not define a local audit-log schema, storage model, retention period for operational logs, or a user-facing disclosure-history interface. Implementations MAY maintain local records of grant issuance, disclosure, sync, token use, and revocation under local policy.

If interoperable audit or transparency events are standardized in the future, they SHOULD be defined in a separate companion profile rather than by extending the core grant or query semantics.

Retention

The retention field is a structured policy declaration and policy commitment by the data recipient. PDPP does not technically enforce retention. Enforcement is through legal agreements or contractual obligations, with a trust registry supporting admission and accountability rather than enforcement. This is an intentional design choice, consistent with how OAuth 2.0 treats scope compliance.


12. Scope and Boundaries

In scope (v0.1)

  • Parameterized grants for user-owned data
  • Flat relational streams with declared schemas
  • Binary data references (blob_ref)
  • Cross-stream references within a single subject (resource_ref)
  • SourceDeclaration shared by connector-backed and provider-native sources
  • Resource server query API with cursor-based pagination and grant enforcement
  • Incremental sync for mutable_state streams via changes_since
  • Tombstones for deleted records
  • Owner-authenticated user erasure (DELETE /v1/streams/{stream}/records/{id})
  • Self-export via owner token (SHOULD-level Core RS conformance, see Section 9 item 13)
  • Conformance definitions for all roles

Out of scope (v0.1)

ConcernStatus
Authorization server interfaceIntrospection endpoint contract defined here; full AS interface informational only in v0.1
Ingest and sync-state endpointsDefined by the Collection Profile; not required for Core RS
Conformance test suitePlanned but not defined in v0.1
Webhook / push ingestionDeferred; see spec-deferred
Source lifecycle actionsDeferred (e.g., deleting source data after export); see spec-deferred
Event-driven collection triggersDeferred; architecturally distinct from the pull-based Collection Profile
Grant signing and token formatDeferred; current design is compatible
Trust registry and connector certificationDeferred
Consent screen visual designSurface-specific; semantic rendering obligations remain in scope
Local audit-log schema and user-facing access historyDeployment-specific; core defines auditable protocol primitives only
Interoperable audit/transparency event formatSeparate companion profile if standardized
Point-in-time reconstructionDeferred (reconstructing full state at a past timestamp)
Canonical view naming vocabularyDeferred; will be informed by implementation experience
Predicate-based grant scopingDeferred; see spec-deferred for subset template design direction
Derivative dataDeferred and unresolved; v0.1 authorizes reads of declared streams and states no default for the output of compute over them. See spec-deferred
Cross-source category grantsDeferred; grants bind to a single source.id in v0.1. See spec-deferred
Real-time streamingDifferent spec needed

Predicate-based grant scoping

v0.1 grants narrow access only by stream selection, named view or field projection, time range, and explicit resource identifiers. Generic predicate expressions (e.g., filter[sender_domain]=amazon.com as a grant parameter) are not supported.

Request-time filters are not grant scope. Owner-token current-capability reads MAY use filter[...] to narrow a result set, but those filters do not narrow any client grant. Client-token requests do not support request-time filters in v0.1; a client that needs a semantically bounded subset requests a named stream declared by the SourceDeclaration.

Derived subset streams (non-normative). A stream MAY represent either a source-native collection or a derived subset, provided its semantics are stable, versioned through the SourceDeclaration, and human-reviewable in consent UI. Implementations that need semantically bounded consent in v0.1 SHOULD prefer named streams with human-readable semantics (e.g., a source that exposes amazon_messages as a distinct stream) over ad hoc technical predicates. Stream names MUST NOT encode predicate logic or synthesize per-request subsets; derived streams MUST be statically declared in the SourceDeclaration.

The recommended future direction for this capability is declaration-defined parameterized subset templates with typed bound parameters and publisher-authored consent display strings. See spec-deferred for the design constraints and open questions that must be resolved before specifying this.

Extensions

PDPP capabilities beyond this specification (for example, search or aggregation interfaces) are defined in companion profiles, not by extending Core semantics. Implementations MUST NOT change the meaning of Core-granted access via extensions: a grant issued under this specification authorizes exactly what Sections 7 and 8 define, regardless of what additional capabilities a deployment offers. Optional capabilities MUST be discoverable via declared metadata rather than assumed to be present. Unrecognized declared capabilities MUST be ignorable by clients. A full capability-advertisement grammar is deliberately deferred to a future version.

Specification governance

PDPP protocol changes are proposed through public repository pull requests. In this repository, non-trivial protocol, reference contract, or architecture changes are tracked with OpenSpec before implementation so reviewers can audit the rationale, tasks, and requirement deltas.

Current active editors and maintainers are listed in MAINTAINERS.md. This repository uses a three-license split: PDPP protocol specification text (all root spec-*.md files and their mirrored site pages) is made available under the Community Specification License 1.0 (SPDX: Community-Spec-1.0; see LICENSE-specs); software packages, examples, and generated artifacts use Apache-2.0 (see LICENSE) unless a narrower file-local notice says otherwise; and user-facing documentation prose outside the specification uses CC BY 4.0 (see LICENSE-docs).


13. TypeScript Types

// --- Record model ---

interface BlobRef {
  blob_id: string;
  mime_type: string;       // IANA media type
  size_bytes: number;
  sha256: string;
  fetch_url?: string;      // Injected by resource server at read time; absent in producer output
}

interface ResourceRef {
  source_id: string;       // SourceDeclaration source.id absolute URI
  stream: string;
  record_id: string | string[];
}

// --- Selection (request-time) ---

interface TimeRange {
  since?: string;          // ISO 8601, inclusive >=
  until?: string;          // ISO 8601, exclusive <
}

interface StreamRequest {
  name: string;
  necessity?: 'required' | 'optional';
  instance_ids?: string[];
  time_range?: TimeRange;
  view?: string;           // Mutually exclusive with fields
  fields?: string[];       // Top-level field names only in v0.1; mutually exclusive with view
  resources?: string[];    // Canonical key strings per compound key encoding
}

interface PresetStreamSelection {
  name: string;
  view?: string;           // Mutually exclusive with fields
  fields?: string[];       // Top-level field names only in v0.1; mutually exclusive with view
}

// --- Source binding ---

interface SourceObject {
  kind: 'connector' | 'provider_native';
  id: string;              // Stable absolute URI for the authorization and data surface
}

interface SourceRequestObject {
  id: string;              // Stable absolute URI for the authorization and data surface
  // No `kind`: the AS derives provenance from the accepted declaration
}

type SelectionRequest = {
  type: 'https://pdpp.dev/data-access';
  source: SourceRequestObject;
  purpose_code: string;
  purpose_description?: string;
  access_mode: 'single_use' | 'continuous';
  retention?: {
    max_duration: string;
    on_expiry: 'delete' | 'anonymize';
  };
  client_claims?: { commitments?: string[] };
} & (
  | { streams: StreamRequest[]; selection_preset?: never }
  | { selection_preset: string; streams?: never }
);

// --- Grant (post-consent, immutable) ---

interface TimeConstraint extends TimeRange {
  field: string;
}

interface StreamGrant {
  name: string;
  instance_ids: string[];  // Non-empty, unique, and stream-scoped
  fields: string[];        // Non-empty and authoritative for RS enforcement
  time_constraint?: TimeConstraint;
  resources?: string[];    // Canonical key strings per compound key encoding
}

interface DataGrant {
  version: '0.1.0';
  grant_id: string;
  issued_at: string;
  subject: { id: string };
  client: { client_id: string; client_display?: Record<string, unknown> };
  source: SourceObject;
  source_declaration: { version: string };
  purpose_code: string;    // URI
  purpose_description?: string;
  access_mode: 'single_use' | 'continuous';
  streams: StreamGrant[];
  selection_preset?: string;
  retention?: {
    max_duration: string;  // ISO 8601 duration
    on_expiry: 'delete' | 'anonymize';
  };
  expires_at?: string;  // ISO 8601; absent means no expiry
}

// --- Source Declaration ---

interface StreamView {
  id: string;
  label: string;
  fields: string[];
}

interface StreamRelationship {
  name: string;
  stream: string;
  foreign_key: string;
  cardinality: 'has_many' | 'has_one';
}

interface StreamExpandCapability {
  name: string;
  default_limit?: number;
  max_limit?: number;
}

interface StreamQueryCapabilities {
  range_filters?: Record<string, Array<'gte' | 'gt' | 'lte' | 'lt'>>;
  expand?: StreamExpandCapability[];
  search?: {
    lexical_fields?: string[];
    semantic_fields?: string[];
  };
  aggregations?: {
    count?: true;
    count_distinct?: string[];
    group_by?: string[];
    group_by_time?: string[];
    max?: string[];
    min?: string[];
    sum?: string[];
  };
}

interface SourceDeclarationStream {
  name: string;
  description?: string;
  display?: { label?: string; detail?: string };
  semantics: 'append_only' | 'mutable_state';
  schema: Record<string, unknown>;
  primary_key: string[];
  cursor_field?: string;           // Logical ordering field for cursor-based reads and incremental sync
  consent_time_field?: string;     // Absent means time_range not supported for this stream
  selection: {
    // time_range capability derived from consent_time_field presence
    fields: boolean;
    resources: boolean;
  };
  views?: StreamView[];
  relationships?: StreamRelationship[];
  query?: StreamQueryCapabilities;
}

interface SourceDeclaration {
  protocol_version: '0.1.0';
  source: SourceObject;
  declaration_version: string;
  publisher: { id: string };
  display: { name: string };
  selection_presets?: Array<{
    id: string;
    label: string;
    streams: PresetStreamSelection[];
  }>;
  streams: SourceDeclarationStream[];
  extensions?: Record<string, unknown>;
}

// --- Token introspection response (PDPP extensions to RFC 7662) ---

interface PDPPIntrospectionResponse {
  active: boolean;
  pdpp_token_kind?: string;  // Core defines 'owner' and 'client'; companion profiles may add kinds (unknown kinds are unauthorized for Core operations)
  subject_id?: string;
  grant_id?: string;       // Present for client tokens
  client_id?: string;      // Present for client tokens
  exp?: number;            // Unix timestamp
  authorization_details?: Array<Record<string, unknown>>; // Approved RFC 9396 detail with Section 7 enforcement constraints
}

// --- Tombstone (response object) ---

interface TombstoneRecord {
  object: 'record';
  id: string;              // Canonical key string
  stream: string;
  deleted: true;
  deleted_at: string;      // ISO 8601, required
  emitted_at: string;      // ISO 8601, required
  // No data field
}

Appendix A: Purpose Code Registry

Registry governance: Purpose code registries under pdpp.dev are controlled by PDPP maintainers via a public change process. Implementations MUST treat unrecognized purpose URIs as opaque identifiers and MUST NOT reject requests solely because a purpose code is unrecognized.

Purpose codes are URIs. The following codes are defined by PDPP. Implementers may define additional codes using their own URI namespaces.

CodeDescription
https://pdpp.dev/purpose/personalizationTailoring the application experience to the user.
https://pdpp.dev/purpose/analyticsAnalyzing user data to produce insights for the user.
https://pdpp.dev/purpose/exportExporting data for the user's own use.
https://pdpp.dev/purpose/agent_contextProviding context to a personal AI agent.
https://pdpp.dev/purpose/ai_trainingUsing data to train AI models. The AS MUST obtain explicit affirmative user consent before issuing any grant with this purpose code. This is a protocol-level requirement, not merely advisory.
https://pdpp.dev/purpose/researchAcademic or market research.

Appendix B: Relationship to the Data Transfer Project (DTI)

PDPP and DTI are complementary protocols addressing different concerns. PDPP defines parameterized consent and disclosure semantics (the grant is the consent artifact; the query API is the disclosure mechanism). DTI defines canonical data models and transfer adapters (the mechanics of moving data between systems).

The two protocols can chain: a PDPP grant can authorize access to data that a DTI transfer then moves, using PDPP stream schemas to carry DTI canonical data model payloads. Formal integration between PDPP grants and DTI transfer manifests is a separate effort; no integration document is currently specified.

Note: “Data Transfer Project” is referred to as DTI (Data Transfer Initiative) in current usage, reflecting its evolution from the original DTP initiative.

On this page

1. IntroductionInteroperable core sectionsRelationship to existing standardsWhy a profile rather than a new authorization frameworkRelationship to sector-specific consent regimes2. Terminology and ActorsActorsProtocol rolesTrust registry queriesData conceptsRequirements Language3. System ArchitectureProtocol layering4. Record ModelStreamsStream semanticsIncremental sync for mutable streamsSplit ruleThe RECORD envelopeTimestampsForeign keysBinary data (blob_ref)Cross-stream references (resource_ref)5. Source DeclarationSourceDeclaration structureSourceDeclaration fieldsStream display metadataconsent_time_fieldRelationshipsViewsVersioning and snapshotsDeclaration acceptance6. Selection RequestClient display metadataPre-registered public client discoveryClient claimsSemantic classes and consent-surface renderingRequest-level parametersSource kindsAI training consentStream selection parametersSelection presets7. GrantGrant fieldsStreamGrant fieldsTime conceptsVersion layeringAccess modesTime constraint semanticsStanding authorizationGrant narrowingRecords from revoked grantsRetentionExamples8. Resource Server InterfaceGrant enforcementToken introspectionAuthenticationProtected resource metadataEndpointsList streamsGet stream metadataFreshness metadataList recordsGet a single recordDelete a record (owner-authenticated)Get a blobErrorsAPI versioning9. ConformanceAuthorization Server conformanceResource Server conformanceClient conformanceConformance test suite10. Security ConsiderationsDeclaration retrieval hygieneToken securityGrant integrityCredential handlingConnector trustTrust boundary responsibilitiesRevocation11. Privacy ConsiderationsData minimizationPurpose limitationAuditability and transparency boundaryRetention12. Scope and BoundariesIn scope (v0.1)Out of scope (v0.1)Predicate-based grant scopingExtensionsSpecification governance13. TypeScript TypesAppendix A: Purpose Code RegistryAppendix B: Relationship to the Data Transfer Project (DTI)