openapi: 3.1.0
info:
  title: OpenDPDP API
  version: 0.1.0
  description: >
    Implementation contract for the proposed OpenDPDP product. All examples are synthetic.
    This contract does not represent a government or Data Protection Board API.
  license:
    name: Apache-2.0
    identifier: Apache-2.0
servers:
  - url: https://api.example.invalid/v1
    description: Synthetic placeholder
security:
  - oauth2: []
tags:
  - name: Notices
  - name: Purposes
  - name: Consents
  - name: Principals
  - name: Cases
  - name: Retention
  - name: Incidents
  - name: Vendors
  - name: Inventory
  - name: Evidence
  - name: Connectors
paths:
  /notices:
    get:
      tags: [Notices]
      operationId: listNotices
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Notices]
      operationId: createNoticeVersion
      security:
        - oauth2: [notices:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NoticeVersionInput'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
        '409':
          $ref: '#/components/responses/Problem'
  /purposes:
    get:
      tags: [Purposes]
      operationId: listPurposes
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Purposes]
      operationId: createPurposeVersion
      security:
        - oauth2: [purposes:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PurposeVersionInput'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /consents:
    get:
      tags: [Consents]
      operationId: listConsentReceipts
      security:
        - oauth2: [consents:read]
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Consents]
      operationId: recordConsentDecision
      security:
        - oauth2: [consents:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConsentDecisionInput'
      responses:
        '201':
          description: Immutable receipt created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsentReceipt'
        '409':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
  /consents/{id}/withdraw:
    post:
      tags: [Consents]
      operationId: withdrawConsent
      security:
        - oauth2: [consents:write]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [occurred_at, channel, proof]
              properties:
                occurred_at:
                  type: string
                  format: date-time
                channel:
                  type: string
                proof:
                  $ref: '#/components/schemas/Proof'
      responses:
        '202':
          description: Withdrawal recorded and propagation queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsentReceipt'
  /data-principals/{ref}/preferences:
    get:
      tags: [Principals]
      operationId: getPreferences
      security:
        - oauth2: [preferences:read]
      parameters:
        - $ref: '#/components/parameters/PrincipalRef'
      responses:
        '200':
          $ref: '#/components/responses/Resource'
    patch:
      tags: [Principals]
      operationId: updatePreferences
      security:
        - oauth2: [preferences:write]
      parameters:
        - $ref: '#/components/parameters/PrincipalRef'
        - $ref: '#/components/parameters/IfMatch'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/merge-patch+json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          $ref: '#/components/responses/Resource'
        '409':
          $ref: '#/components/responses/Problem'
  /rights-requests:
    get:
      tags: [Cases]
      operationId: listRightsRequests
      security:
        - oauth2: [rights:read]
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Cases]
      operationId: createRightsRequest
      security:
        - oauth2: [rights:intake]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CaseInput'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /grievances:
    get:
      tags: [Cases]
      operationId: listGrievances
      security:
        - oauth2: [grievances:read]
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Cases]
      operationId: createGrievance
      security:
        - oauth2: [grievances:intake]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CaseInput'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /retention-policies:
    get:
      tags: [Retention]
      operationId: listRetentionPolicies
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Retention]
      operationId: createRetentionPolicy
      security:
        - oauth2: [retention:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/GenericObject'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /legal-holds:
    get:
      tags: [Retention]
      operationId: listLegalHolds
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Retention]
      operationId: proposeLegalHold
      security:
        - oauth2: [holds:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/GenericObject'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /deletion-jobs:
    get:
      tags: [Retention]
      operationId: listDeletionJobs
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Retention]
      operationId: planDeletionJob
      security:
        - oauth2: [deletion:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/GenericObject'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /incidents:
    get:
      tags: [Incidents]
      operationId: listIncidents
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Incidents]
      operationId: reportIncident
      security:
        - oauth2: [incidents:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entity_id, observed_at, summary]
              properties:
                entity_id: { type: string }
                observed_at: { type: string, format: date-time }
                summary: { type: string, maxLength: 2000 }
                source: { type: string }
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /vendors:
    get:
      tags: [Vendors]
      operationId: listVendors
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Vendors]
      operationId: createVendor
      security:
        - oauth2: [vendors:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/GenericObject'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /processing-activities:
    get:
      tags: [Inventory]
      operationId: listProcessingActivities
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Inventory]
      operationId: createProcessingActivity
      security:
        - oauth2: [inventory:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/GenericObject'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /evidence:
    get:
      tags: [Evidence]
      operationId: listEvidence
      security:
        - oauth2: [evidence:read]
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Evidence]
      operationId: registerEvidence
      security:
        - oauth2: [evidence:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/GenericObject'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
  /connectors:
    get:
      tags: [Connectors]
      operationId: listConnectors
      security:
        - oauth2: [connectors:read]
      responses:
        '200':
          $ref: '#/components/responses/PagedResources'
    post:
      tags: [Connectors]
      operationId: registerConnector
      security:
        - oauth2: [connectors:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectorInput'
      responses:
        '201':
          $ref: '#/components/responses/ResourceCreated'
components:
  securitySchemes:
    oauth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://identity.example.invalid/oauth2/token
          scopes:
            notices:write: Write notice versions
            purposes:write: Write purpose versions
            consents:read: Read consent receipt metadata
            consents:write: Record consent decisions
            preferences:read: Read preferences
            preferences:write: Change preferences
            rights:read: Read assigned rights cases
            rights:intake: Create rights requests
            grievances:read: Read assigned grievances
            grievances:intake: Create grievances
            retention:write: Manage retention
            holds:write: Propose and manage holds
            deletion:write: Plan deletion work
            incidents:write: Manage incidents
            vendors:write: Manage vendors
            inventory:write: Manage processing inventory
            evidence:read: Read scoped evidence
            evidence:write: Register evidence
            connectors:read: Read connector health
            connectors:write: Register connectors
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 16, maxLength: 128 }
    CorrelationId:
      name: X-Correlation-ID
      in: header
      required: false
      schema: { type: string, maxLength: 128 }
    IfMatch:
      name: If-Match
      in: header
      required: true
      schema: { type: string }
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
    ResourceId:
      name: id
      in: path
      required: true
      schema: { type: string }
    PrincipalRef:
      name: ref
      in: path
      required: true
      schema: { type: string, pattern: '^subj_[A-Za-z0-9_-]+$' }
  requestBodies:
    GenericObject:
      required: true
      content:
        application/json:
          schema:
            type: object
            additionalProperties: true
  responses:
    Resource:
      description: Resource
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Resource'
    ResourceCreated:
      description: Resource created
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Resource'
    PagedResources:
      description: Cursor-paged resources
      content:
        application/json:
          schema:
            type: object
            required: [items]
            properties:
              items:
                type: array
                items:
                  $ref: '#/components/schemas/Resource'
              next_cursor:
                type: [string, 'null']
    Problem:
      description: Problem Details error
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    Resource:
      type: object
      required: [id, resource_version, created_at]
      properties:
        id: { type: string }
        resource_version: { type: integer, minimum: 1 }
        created_at: { type: string, format: date-time }
      additionalProperties: true
    NoticeVersionInput:
      type: object
      required: [purpose_version_id, locale, itemised_data, specified_purpose, content]
      properties:
        purpose_version_id: { type: string }
        locale: { type: string, examples: [en-IN] }
        itemised_data:
          type: array
          minItems: 1
          items: { type: string }
        specified_purpose: { type: string, maxLength: 2000 }
        content: { type: string, maxLength: 50000 }
    PurposeVersionInput:
      type: object
      required: [name, processing_route, data_categories, principal_categories, owner_id]
      properties:
        name: { type: string, maxLength: 200 }
        processing_route:
          type: string
          enum:
            [
              consent,
              section_7_voluntary_provision,
              section_7_state,
              section_7_legal_disclosure,
              section_7_judicial,
              section_7_medical_emergency,
              section_7_health_threat,
              section_7_disaster,
              section_7_employment,
              open_legal_review,
            ]
        data_categories:
          type: array
          items: { type: string }
        principal_categories:
          type: array
          items: { type: string }
        owner_id: { type: string }
    Proof:
      type: object
      required: [method, reference]
      properties:
        method:
          type: string
          enum: [authenticated_session, signed_channel, assisted_record, offline_sync]
        reference: { type: string, maxLength: 256 }
      additionalProperties: false
    ConsentDecisionInput:
      type: object
      required:
        [principal_ref, purpose_version_id, notice_version_id, action, channel, occurred_at, proof]
      properties:
        principal_ref: { type: string }
        purpose_version_id: { type: string }
        notice_version_id: { type: string }
        action: { type: string, enum: [grant, deny] }
        channel: { type: string }
        occurred_at: { type: string, format: date-time }
        proof:
          $ref: '#/components/schemas/Proof'
    ConsentReceipt:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          required: [receipt_hash, action, recorded_at]
          properties:
            receipt_hash: { type: string }
            action: { type: string, enum: [grant, deny, withdraw] }
            recorded_at: { type: string, format: date-time }
            propagation_status:
              type: string
              enum: [not_required, queued, partial, complete]
    CaseInput:
      type: object
      required: [entity_id, request_type, preferred_locale, contact_channel]
      properties:
        entity_id: { type: string }
        request_type:
          type: string
          enum: [access_information, correction, completion, update, erasure, grievance, nomination]
        principal_ref: { type: string }
        description: { type: string, maxLength: 4000 }
        preferred_locale: { type: string }
        contact_channel: { type: string }
    ConnectorInput:
      type: object
      required: [name, connector_type, capabilities, credential_ref, network_policy_ref]
      properties:
        name: { type: string }
        connector_type: { type: string }
        capabilities:
          type: array
          items:
            type: string
            enum: [discover_metadata, find_subject, correct, suppress, delete, export]
        credential_ref: { type: string }
        network_policy_ref: { type: string }
    Problem:
      type: object
      required: [type, title, status, code, correlation_id, retryable]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        code: { type: string }
        detail: { type: string }
        correlation_id: { type: string }
        retryable: { type: boolean }
        field_errors:
          type: array
          items:
            type: object
            properties:
              field: { type: string }
              code: { type: string }
              message: { type: string }
