# =====================================================================
# Beacon Life Sciences — Unified CRO Platform Public API
# OpenAPI 3.1.0 contract (contract-first)
# Base: https://<host>/api/v1/ | Auth: OAuth2 client-credentials (org-scoped)
# Errors: RFC 9457 problem+json | PHI fields marked x-phi: true
# =====================================================================
openapi: 3.1.0
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
info:
  title: Beacon Life Sciences CRO Platform API
  version: 1.0.0
  summary: Unified EDC, ePRO, eConsent and trial-operations API for Beacon CRO.
  description: "Public API for Beacon Life Sciences' unified clinical-trial platform\n(EDC / ePRO / eConsent).\
    \ This is the contract-first OpenAPI 3.1\ndefinition; all integrations are built against this document.\n\
    \n## Base URL and versioning\nAll paths are relative to `https://<host>/api/v1/`. The major version\n\
    (`v1`) is part of the path. Additive changes (new optional fields, new\nendpoints) are non-breaking;\
    \ breaking changes ship only under a new\nmajor path segment.\n\n## Authentication and authorization\n\
    - Server-to-server integrations authenticate with **OAuth 2.0\n  client-credentials** (see `POST /oauth/token`).\
    \ Tokens are bearer\n  tokens (`Authorization: Bearer <token>`) with a 3600 s lifetime.\n- **Every\
    \ access token is org-scoped.** A token is bound to exactly one\n  organization (an `org` claim).\
    \ All reads and writes are implicitly\n  filtered to that organization; cross-organization access\
    \ is not\n  possible regardless of scopes held.\n- Scopes are granted per resource + action (for example\n\
    \  `studies:read`, `records:write`, `phi:read`). An operation's required\n  scopes are listed in its\
    \ `security` entry. Scopes the client did not\n  request are never issued.\n\n## PHI handling\n- Schema\
    \ properties that carry Protected Health Information are marked\n  `x-phi: true` (participant name,\
    \ date of birth, contact details,\n  participant identifiers, ePRO response content, correction history).\n\
    - Responses include PHI fields **only when the token holds `phi:read`**.\n  Without `phi:read`, PHI-bearing\
    \ properties are omitted (not masked),\n  and list/filter endpoints continue to work on non-PHI fields.\n\
    - **Every PHI read is audited**: each request that returns `x-phi`\n  content emits an immutable `phi.view`\
    \ audit event (see\n  `/audit-events`) containing actor, resource, timestamp and request id.\n- Treat\
    \ all `x-phi` data as confidential; do not log, cache, or\n  redistribute it outside your validated\
    \ environment.\n\n## Pagination (all list endpoints)\nCursor pagination. Request: `cursor` (opaque,\
    \ from `nextCursor`) and\n`limit` (1–100, default 25). Response envelope:\n`{ \"data\": [...], \"\
    nextCursor\": \"<opaque>|null\", \"hasMore\": bool }`.\nCursors are stable for 5 minutes; treat `nextCursor`\
    \ as opaque and do\nnot decode it. New results appended after a page was fetched may or may\nnot appear\
    \ on subsequent pages (forward-only guarantee).\n\n## Concurrency: ETag / If-Match\n`GET` and write\
    \ responses carry an `ETag` header (opaque version tag).\nMutations (`PATCH`, `PUT`) **require** `If-Match:\
    \ \"<etag>\"` from the\nmost recent read. On mismatch the API returns `412 Precondition Failed`\n\
    (problem+json); re-read and re-apply. Writes without `If-Match` are\nrejected with `428`.\n\n## Idempotency\
    \ (POST)\nEvery `POST` accepts an optional `Idempotency-Key` header (UUID\nrecommended). With the\
    \ same key within 24 h the server **replays the\noriginal response** without re-executing the operation.\
    \ If the same key\nis reused with a *different* request body, the API returns `409`\n(problem+json,\
    \ `type` ending in `/idempotency-key-reuse`). Keys expire\nafter 24 h; strongly recommended for participant\
    \ creation, data\ncapture, and export jobs.\n\n## Rate limiting\nEvery response carries `RateLimit-Limit`,\
    \ `RateLimit-Remaining` and\n`RateLimit-Reset` (delta-seconds until the window resets). Limits are\n\
    per token and per organization. On exhaustion the API returns `429`\nwith the same headers; back off\
    \ until `RateLimit-Reset`.\n\n## Errors\nAll errors use **RFC 9457 `application/problem+json`** with\
    \ fields\n`type`, `title`, `status`, `detail`, `instance`, `requestId`.\n`requestId` matches the `X-Request-Id`\
    \ response header — always quote\nit in support requests. Every response, success or failure, includes\n\
    `X-Request-Id`.\n\n## Deprecation policy\nWhen an operation or field is deprecated, responses carry\
    \ a\n`Deprecation` header (RFC 9745 style, e.g. `@1699999999`) plus a\n`Link` header pointing at the\
    \ replacement. All deprecations, sunset\ndates and migration notes are published at\n`/developers/changelog`\
    \ (see `externalDocs`). No breaking removal\nhappens without a 12-month Deprecation window.\n\n##\
    \ Webhooks\nSubscribable events: `participant.created`, `consent.signed`,\n`epro.response.submitted`,\
    \ `query.opened`, `export.completed`,\n`study.locked`. Deliveries are signed with **HMAC-SHA256**:\n\
    header `X-Beacon-Signature: t=<unix-seconds>,v1=<hex-digest>` where\n`v1 = HMAC_SHA256(secret, t +\
    \ '.' + rawBody)`. Verify `t` within a\n5-minute tolerance to reject replays. Failed deliveries retry\
    \ with\nexponential backoff (1 min, 2, 4, 8, 16, 32, 64 min, then hourly, up to\n24 h); endpoints\
    \ failing continuously are auto-disabled (status\n`failing` → `disabled`) and must be re-enabled via\
    \ `PATCH /webhooks/{id}`.\n"
  contact:
    name: Beacon Life Sciences API Support
    url: https://developers.beaconlifesciences.com
    email: api-support@beaconlifesciences.com
  license:
    name: Beacon API Terms of Service
    url: https://beaconlifesciences.com/legal/api-terms
externalDocs:
  description: Developer docs, guides and the API changelog (/developers/changelog).
  url: https://developers.beaconlifesciences.com/docs
servers:
- url: https://{host}/api/v1
  description: 'Base path https://<host>/api/v1/ — production by default; set `host` per environment (validate,
    sandbox, production).

    '
  variables:
    host:
      default: api.beaconlifesciences.com
      description: Environment-specific API hostname.
      enum:
      - api.beaconlifesciences.com
      - api.sandbox.beaconlifesciences.com
      - api.uat.beaconlifesciences.com
tags:
- name: OAuth
  description: Token issuance (client-credentials).
- name: Organizations
  description: The org bound to the token.
- name: Studies
  description: Study lifecycle and metadata.
- name: Sites
  description: Investigational sites per study.
- name: Forms
  description: eCRF form definitions and versions.
- name: Visits
  description: Visit schedule definitions.
- name: Participants
  description: Subject enrollment and status (PHI-capable).
- name: Data
  description: Study data points (EDC capture and corrections).
- name: Queries
  description: Data queries / discrepancy management.
- name: ePRO
  description: Patient-reported outcomes — instruments, invitations, responses.
- name: Consent
  description: eConsent status and signed artifacts (PHI-capable).
- name: Screenings
  description: Eligibility screening records.
- name: Exports
  description: Asynchronous data export jobs.
- name: Audit
  description: Immutable audit trail (org-wide).
- name: Webhooks
  description: Outbound event subscriptions.
components:
  securitySchemes:
    oauth2:
      type: oauth2
      description: 'OAuth 2.0 **client-credentials** flow for server-to-server

        integrations. Obtain tokens from `POST /api/v1/oauth/token`

        (also modeled as an operation below).


        **Org scoping (mandatory):** every access token is bound to a single

        organization — the token embeds an `org` claim fixed to the

        organization that owns the client credentials. All list, read and

        write operations are implicitly restricted to that organization''s

        data; passing another org''s identifiers resolves to `404` even when

        the requested scopes would otherwise permit the action. Scopes

        cannot widen access beyond the token''s org.


        **Scopes are per resource + action**, e.g. `studies:read`,

        `studies:write`, `sites:read`, `forms:write`, `records:read`,

        `records:write`, `epro:read`, `epro:write`, `consent:read`,

        `screenings:read`, `exports:create`, `phi:read`, `audit:read`,

        `webhooks:write`. Each operation lists its required scopes in its

        `security` field. Access to `x-phi` response content additionally

        requires `phi:read`, and every PHI read is audited (a `phi.view`

        audit event is recorded for each response containing x-phi data).

        '
      flows:
        clientCredentials:
          tokenUrl: /api/v1/oauth/token
          scopes:
            studies:read: Read study metadata and configuration.
            studies:write: Create and update studies; lock studies.
            sites:read: Read site configuration.
            sites:write: Create and update sites.
            forms:read: Read form definitions and versions.
            forms:write: Create and publish form versions.
            visits:read: Read visit schedule definitions.
            visits:write: Create and update visit definitions.
            participants:read: Read participants and their status.
            participants:write: Enroll participants; change status.
            records:read: Read captured study data points.
            records:write: Submit and correct data points (reasonForChange required).
            queries:read: Read data queries.
            queries:write: Open queries; change query status.
            epro:read: Read ePRO instruments, invitations and responses.
            epro:write: Create ePRO instruments and send invitations.
            consent:read: Read consent status and download signed consent PDFs.
            screenings:read: Read screening and eligibility records.
            screenings:write: Create screening records and eligibility checks.
            exports:create: Create and read export jobs; download export files.
            audit:read: Read the organization's audit event trail.
            webhooks:write: Create, update and delete webhook subscriptions.
            webhooks:read: Read webhook subscriptions and recent deliveries.
            phi:read: 'Receive PHI fields in responses (x-phi: true properties). Every PHI read is audited.'
  headers:
    XRequestId:
      description: Unique request identifier for tracing and support; echoed in every problem+json `requestId`.
      schema:
        type: string
        example: req_01J9F3ZK7M2QXWVUTSRQPONML
    ETag:
      description: 'Opaque entity-tag for the returned resource (strong validator). Pass back in `If-Match`
        on PATCH/PUT to enforce optimistic concurrency (412 on mismatch).

        '
      schema:
        type: string
        example: '"a1b2c3d4e5f6"'
    RateLimitLimit:
      description: Maximum number of requests permitted in the current window (per token, per org).
      schema:
        type: string
        example: '600'
    RateLimitRemaining:
      description: Requests remaining in the current window.
      schema:
        type: string
        example: '597'
    RateLimitReset:
      description: Delta-seconds until the current window resets.
      schema:
        type: string
        example: '42'
    Deprecation:
      description: 'Present when the operation (or a response field) is deprecated. RFC 9745-style value:
        the deprecation instant as `@<unix-seconds>`. Pair with the published changelog at /developers/changelog.

        '
      schema:
        type: string
        example: '@1735689600'
    Link:
      description: 'Standard link header. When `Deprecation` is present it carries `rel="sunset"`/successor
        information; on list responses it carries pagination links.

        '
      schema:
        type: string
        example: </api/v1/studies/{id}?version=2>; rel="successor-version"
    RetryAfter:
      description: Seconds to wait before retrying a rate-limited (429) request.
      schema:
        type: integer
        example: 30
    Location:
      description: URL of the newly created resource (201) or of the async job poll target (202).
      schema:
        type: string
        format: uri
  parameters:
    CursorParam:
      name: cursor
      in: query
      description: 'Opaque pagination cursor from a previous response''s `nextCursor`. Omit on the first
        page. Cursors are stable for 5 minutes.

        '
      required: false
      schema:
        type: string
    LimitParam:
      name: limit
      in: query
      description: Page size. 1-100, default 25.
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    StudyIdParam:
      name: id
      in: path
      required: true
      description: Study identifier (org-scoped).
      schema:
        $ref: '#/components/schemas/StudyId'
    SiteIdParam:
      name: siteId
      in: path
      required: true
      description: Site identifier within the study.
      schema:
        $ref: '#/components/schemas/SiteId'
    FormIdParam:
      name: formId
      in: path
      required: true
      description: Form identifier within the study.
      schema:
        $ref: '#/components/schemas/FormId'
    VisitIdParam:
      name: visitId
      in: path
      required: true
      description: Visit identifier within the study.
      schema:
        $ref: '#/components/schemas/VisitId'
    ParticipantIdParam:
      name: pid
      in: path
      required: true
      description: Participant identifier within the study.
      schema:
        $ref: '#/components/schemas/ParticipantId'
    DataPointIdParam:
      name: dataPointId
      in: path
      required: true
      description: Data-point identifier.
      schema:
        $ref: '#/components/schemas/DataPointId'
    QueryIdParam:
      name: queryId
      in: path
      required: true
      description: Data-query identifier.
      schema:
        $ref: '#/components/schemas/QueryId'
    ConsentIdParam:
      name: consentId
      in: path
      required: true
      description: Consent record identifier.
      schema:
        $ref: '#/components/schemas/ConsentId'
    ScreeningIdParam:
      name: screeningId
      in: path
      required: true
      description: Screening record identifier.
      schema:
        $ref: '#/components/schemas/ScreeningId'
    ExportIdParam:
      name: exportId
      in: path
      required: true
      description: Export job identifier.
      schema:
        $ref: '#/components/schemas/ExportJobId'
    WebhookIdParam:
      name: webhookId
      in: path
      required: true
      description: Webhook subscription identifier.
      schema:
        $ref: '#/components/schemas/WebhookId'
    IfMatchParam:
      name: If-Match
      in: header
      required: true
      description: '**Required on PATCH/PUT.** Opaque `ETag` from the most recent `GET` of this resource,
        e.g. `"a1b2c3d4e5f6"` (quotes included). A stale or missing value yields `412 Precondition Failed`;
        writes without the header yield `428 Precondition Required`.

        '
      schema:
        type: string
        example: '"a1b2c3d4e5f6"'
    IdempotencyKeyParam:
      name: Idempotency-Key
      in: header
      required: false
      description: 'Optional but strongly recommended on POST. Unique key (UUID recommended) identifying
        this logical operation. Same key within 24 h replays the original response without re-execution;
        same key with a different request body yields `409`. Keys expire after 24 h.

        '
      schema:
        type: string
        format: uuid
        example: 5f3a7c2e-9b41-4d0a-8f7e-2c6b9d1a4e83
  responses:
    ProblemDefault:
      description: 'Default/unexpected error (also covers 5XX). Always `application/problem+json` (RFC
        9457) with `X-Request-Id` for correlation.

        '
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    BadRequest:
      description: '400 — malformed request (invalid JSON, wrong types, malformed cursor, unknown query
        parameter combination).

        '
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: 'Resource not found within the token''s organization (org-scoped tokens never reveal
        cross-org existence).

        '
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unauthorized:
      description: Missing/expired bearer token or insufficient scope.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        WWW-Authenticate:
          description: Challenge per RFC 6750, e.g. `Bearer error="insufficient_scope"`.
          schema:
            type: string
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Forbidden:
      description: 'Authenticated but not permitted (scope missing, org boundary, or `phi:read` required
        but not held for x-phi content).

        '
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Conflict:
      description: 'Conflict — includes `Idempotency-Key` reuse with a different request body, and duplicate
        natural keys.

        '
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    PreconditionFailed:
      description: '412 — `If-Match` ETag does not match the current resource version. Re-read the resource
        and re-apply the change.

        '
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    PreconditionRequired:
      description: 428 — mutating request omitted the required `If-Match` header.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    ValidationFailed:
      description: '422 — request structurally valid but violates business rules (unknown field OID, invalid
        status transition, missing `reasonForChange` on data correction, ...).

        '
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    TooManyRequests:
      description: 429 — rate limit exhausted for this token/org window.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    CursorPage:
      type: object
      description: 'Cursor pagination envelope returned by every list endpoint. `nextCursor` is null on
        the last page; treat it as opaque.

        '
      required:
      - data
      - hasMore
      properties:
        data:
          type: array
          items: {}
        nextCursor:
          type:
          - string
          - 'null'
          description: Opaque cursor for the next page; null when exhausted.
        hasMore:
          type: boolean
          description: True when more pages exist (pass nextCursor to continue).
    StudyId:
      type: string
      pattern: ^std_[A-Za-z0-9]{5,32}$
      example: std_9f8e7d6c5b
    SiteId:
      type: string
      pattern: ^site_[A-Za-z0-9]{5,32}$
      example: site_4a3b2c1d0e
    FormId:
      type: string
      pattern: ^frm_[A-Za-z0-9]{5,32}$
      example: frm_1a2b3c4d5e
    FormVersionId:
      type: string
      pattern: ^fv_[A-Za-z0-9]{5,32}$
      example: fv_1a2b3c4d5e
    VisitId:
      type: string
      pattern: ^vis_[A-Za-z0-9]{5,32}$
      example: vis_7c8d9e0f1a
    ParticipantId:
      type: string
      pattern: ^pt_[A-Za-z0-9]{5,32}$
      description: 'Participant identifier. This is a participant-linking identifier and is treated as
        PHI (x-phi on the Participant.userId property).

        '
      example: pt_3e4f5a6b7c
    DataPointId:
      type: string
      pattern: ^dp_[A-Za-z0-9]{5,32}$
      example: dp_0a1b2c3d4e
    QueryId:
      type: string
      pattern: ^qry_[A-Za-z0-9]{5,32}$
      example: qry_2b3c4d5e6f
    EproInstrumentId:
      type: string
      pattern: ^ein_[A-Za-z0-9]{5,32}$
      example: ein_5d6e7f8a9b
    EproInvitationId:
      type: string
      pattern: ^inv_[A-Za-z0-9]{5,32}$
      example: inv_6e7f8a9b0c
    EproResponseId:
      type: string
      pattern: ^ers_[A-Za-z0-9]{5,32}$
      example: ers_7f8a9b0c1d
    ConsentId:
      type: string
      pattern: ^cns_[A-Za-z0-9]{5,32}$
      example: cns_8a9b0c1d2e
    ScreeningId:
      type: string
      pattern: ^scr_[A-Za-z0-9]{5,32}$
      example: scr_9b0c1d2e3f
    ExportJobId:
      type: string
      pattern: ^exp_[A-Za-z0-9]{5,32}$
      example: exp_0c1d2e3f4a
    AuditEventId:
      type: string
      pattern: ^aud_[A-Za-z0-9]{5,32}$
      example: aud_1d2e3f4a5b
    WebhookId:
      type: string
      pattern: ^wh_[A-Za-z0-9]{5,32}$
      example: wh_2e3f4a5b6c
    OrgId:
      type: string
      pattern: ^org_[A-Za-z0-9]{5,32}$
      example: org_5f6a7b8c9d
    UserId:
      type: string
      pattern: ^usr_[A-Za-z0-9]{5,32}$
      example: usr_6a7b8c9d0e
    AuditMixin:
      type: object
      description: Creation/update provenance stamped on every resource.
      properties:
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 UTC instant of creation.
        updatedAt:
          type: string
          format: date-time
          description: ISO 8601 UTC instant of last update.
        createdBy:
          allOf:
          - $ref: '#/components/schemas/UserId'
        updatedBy:
          allOf:
          - $ref: '#/components/schemas/UserId'
    Organization:
      type: object
      description: The organization bound to the access token (org-scoped).
      required:
      - id
      - name
      - status
      properties:
        id:
          $ref: '#/components/schemas/OrgId'
        name:
          type: string
          maxLength: 200
          description: Organization display name (sponsor/CRO legal entity).
        status:
          type: string
          enum:
          - active
          - suspended
          description: Org account status; suspended orgs reject writes.
        defaultTimezone:
          type: string
          description: IANA timezone applied when no study-level timezone is set.
          example: America/New_York
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    Study:
      type: object
      description: A clinical study (protocol) in the platform.
      required:
      - id
      - orgId
      - protocolCode
      - title
      - status
      properties:
        id:
          $ref: '#/components/schemas/StudyId'
        orgId:
          $ref: '#/components/schemas/OrgId'
        protocolCode:
          type: string
          description: Sponsor protocol number, unique within the org.
          example: BEACON-2026-014
        title:
          type: string
          maxLength: 300
          description: Full study title.
        shortTitle:
          type: string
          maxLength: 80
        phase:
          type: string
          description: Clinical phase.
          enum:
          - preclinical
          - phase1
          - phase2
          - phase3
          - phase4
          - observational
        therapeuticArea:
          type: string
          example: Oncology
        status:
          type: string
          description: 'Lifecycle status. `locked` is terminal for data capture (triggers the `study.locked`
            webhook); further data mutation is rejected.

            '
          enum:
          - draft
          - active
          - closed_to_enrollment
          - locked
        plannedStartDate:
          type: string
          format: date
        plannedEndDate:
          type: string
          format: date
        enrollmentTarget:
          type: integer
          minimum: 0
          description: Target number of randomized/enrolled participants.
        randomizationSchemeId:
          type:
          - string
          - 'null'
          description: Identifier of the randomization scheme (simplified — opaque reference).
        lockedAt:
          type:
          - string
          - 'null'
          format: date-time
          description: When the study was locked (null unless status is locked).
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
        links:
          type: object
          description: Convenience navigation links.
          additionalProperties:
            type: string
            format: uri
    StudyCreate:
      type: object
      description: Payload for `POST /studies` (Idempotency-Key recommended).
      required:
      - protocolCode
      - title
      - phase
      properties:
        protocolCode:
          type: string
          maxLength: 40
        title:
          type: string
          maxLength: 300
        shortTitle:
          type: string
          maxLength: 80
        phase:
          type: string
          enum:
          - preclinical
          - phase1
          - phase2
          - phase3
          - phase4
          - observational
        therapeuticArea:
          type: string
        plannedStartDate:
          type: string
          format: date
        plannedEndDate:
          type: string
          format: date
        enrollmentTarget:
          type: integer
          minimum: 0
    StudyUpdate:
      type: object
      description: 'Payload for `PATCH /studies/{id}` (If-Match required). Only the supplied properties
        change; `status` transitions are restricted (forward-only, `locked` is terminal).

        '
      properties:
        title:
          type: string
          maxLength: 300
        shortTitle:
          type: string
          maxLength: 80
        therapeuticArea:
          type: string
        plannedStartDate:
          type: string
          format: date
        plannedEndDate:
          type: string
          format: date
        enrollmentTarget:
          type: integer
          minimum: 0
        status:
          type: string
          description: Lifecycle transition. Allowed moves — draft to active, active to closed_to_enrollment,
            closed_to_enrollment/active to locked.
          enum:
          - draft
          - active
          - closed_to_enrollment
          - locked
    Site:
      type: object
      description: An investigational site participating in a study.
      required:
      - id
      - studyId
      - code
      - name
      - country
      - status
      properties:
        id:
          $ref: '#/components/schemas/SiteId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        code:
          type: string
          description: Site number/code within the study, unique per study.
          example: '101'
        name:
          type: string
          maxLength: 200
          description: Site (institution) name. Not PHI — organizational, not personal.
        principalInvestigator:
          type: string
          maxLength: 200
          description: 'Principal investigator name. Identified professional contact in a research-regulatory
            context; flagged x-phi out of caution — returned only with `phi:read`.

            '
          x-phi: true
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code.
          minLength: 2
          maxLength: 2
          example: US
        address:
          type: string
          maxLength: 300
          description: Site street address (organizational).
        status:
          type: string
          enum:
          - pending_activation
          - active
          - inactive
        enrollmentTarget:
          type: integer
          minimum: 0
        timezone:
          type: string
          example: America/Chicago
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    SiteCreate:
      type: object
      required:
      - code
      - name
      - country
      properties:
        code:
          type: string
          maxLength: 20
        name:
          type: string
          maxLength: 200
        principalInvestigator:
          type: string
          maxLength: 200
        country:
          type: string
          minLength: 2
          maxLength: 2
        address:
          type: string
          maxLength: 300
        timezone:
          type: string
        enrollmentTarget:
          type: integer
          minimum: 0
    SiteUpdate:
      type: object
      description: PATCH payload (If-Match required).
      properties:
        name:
          type: string
          maxLength: 200
        principalInvestigator:
          type: string
          maxLength: 200
        address:
          type: string
          maxLength: 300
        status:
          type: string
          enum:
          - pending_activation
          - active
          - inactive
        enrollmentTarget:
          type: integer
          minimum: 0
        timezone:
          type: string
    Form:
      type: object
      description: An eCRF form definition belonging to a study.
      required:
      - id
      - studyId
      - oid
      - title
      - status
      properties:
        id:
          $ref: '#/components/schemas/FormId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        oid:
          type: string
          description: 'Stable form OID (object identifier) used by integrations and exports; immutable
            after creation. Example "AE_LOG".

            '
        title:
          type: string
          maxLength: 200
        description:
          type: string
        status:
          type: string
          enum:
          - draft
          - published
          - retired
        currentVersionId:
          allOf:
          - $ref: '#/components/schemas/FormVersionId'
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    FormCreate:
      type: object
      required:
      - oid
      - title
      properties:
        oid:
          type: string
          maxLength: 64
          description: Stable OID; must be unique within the study.
        title:
          type: string
          maxLength: 200
        description:
          type: string
    FormVersion:
      type: object
      description: 'An immutable, publishable version of a form. Fields define the capture structure;
        publishing freezes the version for data capture.

        '
      required:
      - id
      - formId
      - version
      - status
      properties:
        id:
          $ref: '#/components/schemas/FormVersionId'
        formId:
          $ref: '#/components/schemas/FormId'
        version:
          type: string
          description: Semantic version label within the form, e.g. "1.0.0".
          example: 1.0.0
        status:
          type: string
          enum:
          - draft
          - published
          - retired
        fields:
          type: array
          description: Ordered field definitions for this version.
          items:
            $ref: '#/components/schemas/FormField'
        publishedAt:
          type:
          - string
          - 'null'
          format: date-time
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    FormField:
      type: object
      description: A single data-capture field within a form version.
      required:
      - oid
      - label
      - type
      properties:
        oid:
          type: string
          description: Field OID, unique within the form version (e.g. "AETERM").
        label:
          type: string
          maxLength: 200
        type:
          type: string
          description: Field data type.
          enum:
          - text
          - integer
          - decimal
          - boolean
          - date
          - datetime
          - single_choice
          - multi_choice
          - file
        required:
          type: boolean
          default: false
        phi:
          type: boolean
          description: 'True when values captured into this field are PHI; data-point content from phi
            fields is returned only with `phi:read`.

            '
        options:
          type: array
          description: Choices for single_choice/multi_choice fields.
          items:
            type: object
            required:
            - value
            - label
            properties:
              value:
                type: string
              label:
                type: string
        unit:
          type: string
          description: Measurement unit for decimal/integer fields (e.g. "kg").
        maxLength:
          type: integer
          minimum: 1
          description: Max length for text fields.
    FormVersionPublish:
      type: object
      description: Payload for `POST .../forms/{formId}/versions` (creates a draft version).
      required:
      - version
      - fields
      properties:
        version:
          type: string
          example: 1.1.0
        fields:
          type: array
          items:
            $ref: '#/components/schemas/FormField'
        publish:
          type: boolean
          default: false
          description: 'When true, the version is published atomically on creation (immutable thereafter);
            otherwise it remains draft.

            '
    Visit:
      type: object
      description: A protocol visit (schedule definition) within a study.
      required:
      - id
      - studyId
      - name
      - sequence
      - windowDays
      - status
      properties:
        id:
          $ref: '#/components/schemas/VisitId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        name:
          type: string
          maxLength: 120
          example: Baseline
        sequence:
          type: integer
          minimum: 1
          description: Ordinal position in the schedule.
        dayOffset:
          type:
          - integer
          - 'null'
          description: Target day relative to enrollment (null when unscheduled).
        windowDays:
          type: integer
          minimum: 0
          description: Allowed visit-window width in days.
        formIds:
          type: array
          description: Forms due at this visit (ordered).
          items:
            $ref: '#/components/schemas/FormId'
        proInstruments:
          type: array
          description: ePRO instruments due at this visit.
          items:
            $ref: '#/components/schemas/EproInstrumentId'
        status:
          type: string
          enum:
          - active
          - retired
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    VisitCreate:
      type: object
      required:
      - name
      - sequence
      properties:
        name:
          type: string
          maxLength: 120
        sequence:
          type: integer
          minimum: 1
        dayOffset:
          type:
          - integer
          - 'null'
        windowDays:
          type: integer
          minimum: 0
        formIds:
          type: array
          items:
            $ref: '#/components/schemas/FormId'
        proInstruments:
          type: array
          items:
            $ref: '#/components/schemas/EproInstrumentId'
    VisitUpdate:
      type: object
      description: PATCH payload (If-Match required).
      properties:
        name:
          type: string
          maxLength: 120
        dayOffset:
          type:
          - integer
          - 'null'
        windowDays:
          type: integer
          minimum: 0
        formIds:
          type: array
          items:
            $ref: '#/components/schemas/FormId'
        status:
          type: string
          enum:
          - active
          - retired
    Participant:
      type: object
      description: 'An enrolled participant. Properties flagged `x-phi: true` are returned ONLY when the
        token holds `phi:read`; otherwise they are omitted from responses. Every response that includes
        them is audited (phi.view event).

        '
      required:
      - id
      - studyId
      - siteId
      - subjectKey
      - status
      - enrollment
      properties:
        id:
          $ref: '#/components/schemas/ParticipantId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        siteId:
          $ref: '#/components/schemas/SiteId'
        subjectKey:
          type: string
          description: 'Non-PHI composite label (site code + sequential subject number, e.g. "101-004").
            Safe for display without phi:read.

            '
          example: 101-004
        screeningNumber:
          type: string
          description: Screening-assigned number (non-PHI label).
        status:
          type: string
          description: 'Lifecycle status. Allowed transitions — screening to enrolled, enrolled to withdrawn/completed;
            screen_fail terminal; withdrawn and completed terminal. PATCHing status emits a `participant.status_changed`
            audit event.

            '
          enum:
          - screening
          - screen_fail
          - enrolled
          - withdrawn
          - completed
        enrollment:
          type: object
          description: Enrollment details (dates are non-PHI study bookkeeping).
          required:
          - at
          properties:
            at:
              type: string
              format: date-time
              description: Enrollment (randomization) timestamp, null while screening.
            armId:
              type:
              - string
              - 'null'
              description: Opaque treatment-arm reference (simplified).
        person:
          type: object
          description: 'PHI block. Omitted entirely unless the token holds `phi:read`; presence in any
            response triggers an audit event. This object is a study-data placeholder for identity/contact
            details used by site staff — treat as confidential.

            '
          x-phi: true
          properties:
            firstName:
              type: string
              maxLength: 100
              x-phi: true
              description: Participant given name (PHI).
            lastName:
              type: string
              maxLength: 100
              x-phi: true
              description: Participant family name (PHI).
            dateOfBirth:
              type: string
              format: date
              x-phi: true
              description: Participant date of birth (PHI).
            email:
              type: string
              format: email
              maxLength: 320
              x-phi: true
              description: Contact email (PHI; used for ePRO/eConsent notifications).
            phone:
              type: string
              maxLength: 32
              x-phi: true
              description: Contact phone in E.164 (PHI).
            addressLine:
              type: string
              maxLength: 300
              x-phi: true
              description: Mailing address (PHI).
        enrolledAt:
          type:
          - string
          - 'null'
          format: date-time
        withdrawnReason:
          type:
          - string
          - 'null'
          description: 'Reason code when status is withdrawn. Free-text withdrawal narratives can reveal
            health status — treat content as sensitive.

            '
          enum:
          - adverse_event
          - consent_withdrawn
          - lost_to_followup
          - death
          - other
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    ParticipantCreate:
      type: object
      description: 'Payload for `POST /studies/{id}/participants` (Idempotency-Key strongly recommended
        — duplicates create PHI records). The PHI `person` block is accepted only from tokens holding
        `phi:read` (write path is not scope-gated separately from participants:write).

        '
      required:
      - siteId
      - subjectKey
      properties:
        siteId:
          $ref: '#/components/schemas/SiteId'
        subjectKey:
          type: string
          maxLength: 40
          description: Sponsor-assigned subject label; unique per study.
        screeningNumber:
          type: string
        person:
          type: object
          description: PHI identity/contact block (optional at creation).
          properties:
            firstName:
              type: string
              maxLength: 100
              x-phi: true
            lastName:
              type: string
              maxLength: 100
              x-phi: true
            dateOfBirth:
              type: string
              format: date
              x-phi: true
            email:
              type: string
              format: email
              x-phi: true
            phone:
              type: string
              x-phi: true
            addressLine:
              type: string
              x-phi: true
        metadata:
          type: object
          additionalProperties: true
          description: Non-PHI integration metadata key/values.
    ParticipantUpdate:
      type: object
      description: 'PATCH payload for `/studies/{id}/participants/{pid}` (If-Match required). The spec
        requires a `status` change to be explicit; PHI fields update via the `person` block.

        '
      properties:
        status:
          type: string
          description: Lifecycle transition (see Participant.status for allowed moves).
          enum:
          - screening
          - screen_fail
          - enrolled
          - withdrawn
          - completed
        withdrawalReason:
          type: string
          description: Required when status transitions to withdrawn.
          enum:
          - adverse_event
          - consent_withdrawn
          - lost_to_followup
          - death
          - other
        person:
          type: object
          description: PHI fields to update (fields omitted are left unchanged).
          properties:
            firstName:
              type: string
              x-phi: true
            lastName:
              type: string
              x-phi: true
            dateOfBirth:
              type: string
              format: date
              x-phi: true
            email:
              type: string
              format: email
              x-phi: true
            phone:
              type: string
              x-phi: true
            addressLine:
              type: string
              x-phi: true
        metadata:
          type: object
          additionalProperties: true
    DataPoint:
      type: object
      description: 'A single captured data point (EDC field value, ePRO item, or correction lineage).
        `value` is the current value; the `changeHistory` array captures corrections with mandatory `reasonForChange`.
        Content fields flagged `x-phi` are returned only with `phi:read` and every such read is audited.

        '
      required:
      - id
      - studyId
      - participantId
      - formOid
      - fieldOid
      - value
      - capturedAt
      - status
      properties:
        id:
          $ref: '#/components/schemas/DataPointId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        participantId:
          allOf:
          - $ref: '#/components/schemas/ParticipantId'
          x-phi: true
          description: Participant identifier (participant-linking = PHI).
        formOid:
          type: string
          description: Form OID the point belongs to (e.g. "AE_LOG").
        fieldOid:
          type: string
          description: Field OID within the form version (e.g. "AETERM").
        formVersionId:
          allOf:
          - $ref: '#/components/schemas/FormVersionId'
        visitId:
          allOf:
          - $ref: '#/components/schemas/VisitId'
        value:
          description: 'Current captured value. Type follows the field definition (string, number, boolean,
            string date/datetime, array for multi-choice). PHI when the field is flagged phi in the form
            version (x-phi below reflects that this can carry PHI).

            '
          x-phi: true
        units:
          type:
          - string
          - 'null'
        status:
          type: string
          description: Data-entry state.
          enum:
          - entered
          - source_verified
          - queried
          - corrected
          - frozen
          - locked
        isNormal:
          type:
          - boolean
          - 'null'
          description: Normal-range flag for lab-style fields.
        enteredBy:
          allOf:
          - $ref: '#/components/schemas/UserId'
        capturedAt:
          type: string
          format: date-time
          description: When the value was captured/submitted.
        changeHistory:
          type: array
          description: 'Correction lineage (most recent last). Each entry records the prior value, who
            changed it, why (reasonForChange is mandatory on corrections), and when. Prior values are
            PHI-bearing.

            '
          items:
            type: object
            required:
            - previousValue
            - newValue
            - reasonForChange
            - changedAt
            - changedBy
            properties:
              previousValue:
                x-phi: true
                description: Prior value (PHI when the field is phi-flagged).
              newValue:
                x-phi: true
                description: Corrected value (PHI when the field is phi-flagged).
              reasonForChange:
                type: string
                minLength: 3
                maxLength: 500
                description: 'Mandatory free-text audit rationale. Kept verbatim in the audit trail; may
                  describe the participant — treat as sensitive.

                  '
              changedAt:
                type: string
                format: date-time
              changedBy:
                allOf:
                - $ref: '#/components/schemas/UserId'
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    DataPointSubmit:
      type: object
      description: 'Payload for `PUT /studies/{id}/participants/{pid}/data` (upsert by formOid+fieldOid+visitId)
        and `POST` (append with explicit occurrence). `reasonForChange` is REQUIRED whenever an existing
        value is overwritten (audit rule) — omitting it on a change yields 422. Idempotency-Key recommended.

        '
      required:
      - formOid
      - fieldOid
      - value
      properties:
        formOid:
          type: string
        fieldOid:
          type: string
        formVersionId:
          $ref: '#/components/schemas/FormVersionId'
        visitId:
          $ref: '#/components/schemas/VisitId'
        occurrence:
          type: integer
          minimum: 1
          default: 1
          description: Repeating-fields occurrence index.
        value:
          description: New value; type per field definition.
          x-phi: true
        units:
          type:
          - string
          - 'null'
        reasonForChange:
          type: string
          minLength: 3
          maxLength: 500
          description: 'Mandatory when overwriting an existing non-null value. Recorded verbatim in the
            audit trail.

            '
        timestamp:
          type: string
          format: date-time
          description: Observation time (defaults to server receive time).
    Query:
      type: object
      description: 'A data query (discrepancy management item) raised against a data point or form. Opening
        emits the `query.opened` webhook event.

        '
      required:
      - id
      - studyId
      - status
      - type
      - openedAt
      - openedBy
      properties:
        id:
          $ref: '#/components/schemas/QueryId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        participantId:
          allOf:
          - $ref: '#/components/schemas/ParticipantId'
          x-phi: true
        dataPointId:
          allOf:
          - $ref: '#/components/schemas/DataPointId'
        formOid:
          type: string
        fieldOid:
          type: string
        visitId:
          allOf:
          - $ref: '#/components/schemas/VisitId'
        type:
          type: string
          enum:
          - verification
          - manual
          - auto_edit_check
          - range_check
          - missing_data
          description: Query origin/type.
        status:
          type: string
          description: 'Workflow status. Allowed transitions — open to answered/closed, answered to closed/reopened,
            reopened to answered/closed. PATCHing status requires `If-Match` and emits a `query.status_changed`
            audit event.

            '
          enum:
          - open
          - answered
          - closed
          - reopened
        message:
          type: string
          description: 'Query text. May quote participant data — treat content as potentially PHI; flagged
            x-phi.

            '
          x-phi: true
        openedBy:
          allOf:
          - $ref: '#/components/schemas/UserId'
        openedAt:
          type: string
          format: date-time
        resolvedAt:
          type:
          - string
          - 'null'
          format: date-time
        thread:
          type: array
          description: Message thread (system, site staff, monitors). Content may reference PHI.
          items:
            type: object
            required:
            - at
            - by
            - message
            - audience
            properties:
              at:
                type: string
                format: date-time
              by:
                allOf:
                - $ref: '#/components/schemas/UserId'
              message:
                type: string
                x-phi: true
              audience:
                type: string
                enum:
                - site
                - sponsor
                - both
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    QueryCreate:
      type: object
      description: Payload for `POST /studies/{id}/queries`.
      required:
      - type
      - message
      properties:
        participantId:
          $ref: '#/components/schemas/ParticipantId'
        dataPointId:
          $ref: '#/components/schemas/DataPointId'
        formOid:
          type: string
        fieldOid:
          type: string
        visitId:
          $ref: '#/components/schemas/VisitId'
        type:
          type: string
          enum:
          - verification
          - manual
          - auto_edit_check
          - range_check
          - missing_data
        message:
          type: string
          minLength: 1
          maxLength: 2000
    QueryStatusUpdate:
      type: object
      description: 'Payload for `PATCH /studies/{id}/queries/{queryId}` (status update; If-Match required).

        '
      required:
      - status
      properties:
        status:
          type: string
          enum:
          - open
          - answered
          - closed
          - reopened
        message:
          type: string
          maxLength: 2000
          description: Thread message recorded with the status change.
        audience:
          type: string
          enum:
          - site
          - sponsor
          - both
          default: both
    EproInstrument:
      type: object
      description: 'An ePRO instrument (questionnaire) configured in a study — library instrument instantiated
        for the study with schedule and language settings.

        '
      required:
      - id
      - studyId
      - code
      - title
      - status
      properties:
        id:
          $ref: '#/components/schemas/EproInstrumentId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        code:
          type: string
          description: Instrument code, e.g. "PRO-CTCAE" or library key.
          example: PRO-CTCAE
        title:
          type: string
          maxLength: 200
          description: Display title ("Patient Symptom Diary").
        libraryKey:
          type: string
          description: 'Reference to the platform instrument library item this was instantiated from (validated
            instruments, e.g. validated PROMs).

            '
        version:
          type: string
          description: Instrument version label.
          example: '2.1'
        languages:
          type: array
          description: Available languages (BCP-47).
          items:
            type: string
          example:
          - en-US
          - es-ES
        itemsCount:
          type: integer
          minimum: 0
          description: Number of questions.
        schedule:
          type: object
          description: Schedule summary (windows relative to visit or enrollment).
          properties:
            anchor:
              type: string
              enum:
              - visit
              - enrollment
              - daily
              description: What the schedule is anchored to.
            visitId:
              allOf:
              - $ref: '#/components/schemas/VisitId'
            dueOffsetDays:
              type:
              - integer
              - 'null'
            windowDays:
              type:
              - integer
              - 'null'
        status:
          type: string
          enum:
          - draft
          - active
          - retired
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    EproInstrumentCreate:
      type: object
      required:
      - code
      - title
      properties:
        code:
          type: string
        title:
          type: string
        libraryKey:
          type: string
        version:
          type: string
        languages:
          type: array
          items:
            type: string
        schedule:
          type: object
          properties:
            anchor:
              type: string
              enum:
              - visit
              - enrollment
              - daily
            visitId:
              $ref: '#/components/schemas/VisitId'
            dueOffsetDays:
              type:
              - integer
              - 'null'
            windowDays:
              type:
              - integer
              - 'null'
    EproInvitation:
      type: object
      description: 'A single invitation for a participant to complete an ePRO instrument. `delivery`/`completion`
        track the outbound notification and the participant''s progress.

        '
      required:
      - id
      - studyId
      - participantId
      - instrumentId
      - status
      - issuedAt
      properties:
        id:
          $ref: '#/components/schemas/EproInvitationId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        participantId:
          allOf:
          - $ref: '#/components/schemas/ParticipantId'
          x-phi: true
          description: Participant (participant-linking identifier = PHI).
        instrumentId:
          $ref: '#/components/schemas/EproInstrumentId'
        visitId:
          allOf:
          - $ref: '#/components/schemas/VisitId'
        status:
          type: string
          enum:
          - pending
          - delivered
          - viewed
          - completed
          - expired
          - canceled
        channel:
          type: string
          description: Delivery channel for the invitation link.
          enum:
          - email
          - sms
          - push
          - link_only
        dueAt:
          type:
          - string
          - 'null'
          format: date-time
        issuedAt:
          type: string
          format: date-time
        completedAt:
          type:
          - string
          - 'null'
          format: date-time
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    EproInvitationCreate:
      type: object
      description: Payload for `POST .../epro/invitations` (Idempotency-Key recommended).
      required:
      - participantId
      - instrumentId
      properties:
        participantId:
          $ref: '#/components/schemas/ParticipantId'
        instrumentId:
          $ref: '#/components/schemas/EproInstrumentId'
        visitId:
          $ref: '#/components/schemas/VisitId'
        channel:
          type: string
          enum:
          - email
          - sms
          - push
          - link_only
          default: email
        dueAt:
          type: string
          format: date-time
        language:
          type: string
          description: BCP-47 language for the invitation and instrument UI.
          default: en-US
    EproResponse:
      type: object
      description: 'A completed ePRO response set. `answers` carry the participant''s reported content
        — PHI (x-phi). Returned only with `phi:read` for the answer content; metadata is available with
        `epro:read`.

        '
      required:
      - id
      - studyId
      - participantId
      - instrumentId
      - invitationId
      - status
      - submittedAt
      properties:
        id:
          $ref: '#/components/schemas/EproResponseId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        participantId:
          allOf:
          - $ref: '#/components/schemas/ParticipantId'
          x-phi: true
        instrumentId:
          $ref: '#/components/schemas/EproInstrumentId'
        invitationId:
          $ref: '#/components/schemas/EproInvitationId'
        visitId:
          allOf:
          - $ref: '#/components/schemas/VisitId'
        status:
          type: string
          enum:
          - in_progress
          - submitted
          - partially_completed
          - expired
        language:
          type: string
        answers:
          type: array
          description: 'Item-level answers — PHI. Omitted unless token holds `phi:read` (then the read
            is audited).

            '
          x-phi: true
          items:
            type: object
            required:
            - itemOid
            - value
            properties:
              itemOid:
                type: string
                description: Instrument item identifier (e.g. "PAIN_3").
              value:
                x-phi: true
                description: Answer value; type per instrument item definition.
              unit:
                type:
                - string
                - 'null'
        submittedAt:
          type: string
          format: date-time
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    Consent:
      type: object
      description: 'eConsent record for a participant (per consent form/document). Identity + artifact
        metadata; the signed PDF is a separate binary download (GET .../consents/{consentId}/signed-pdf).

        '
      required:
      - id
      - studyId
      - participantId
      - documentCode
      - status
      - version
      properties:
        id:
          $ref: '#/components/schemas/ConsentId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        participantId:
          allOf:
          - $ref: '#/components/schemas/ParticipantId'
          x-phi: true
        documentCode:
          type: string
          description: Consent document code, e.g. "ICD-MAIN-v2".
        version:
          type: string
          description: Document version consented to.
          example: '2.0'
        status:
          type: string
          description: 'Signing status. `withdrawn` means consent was revoked (study participation consequences
            handled by the study team).

            '
          enum:
          - pending
          - signed
          - withdrawn
          - expired
          - superseded
        signedAt:
          type:
          - string
          - 'null'
          format: date-time
        signedBy:
          type:
          - string
          - 'null'
          maxLength: 200
          description: 'Name of the person who signed (participant or LAR). PHI.

            '
          x-phi: true
        witnessName:
          type:
          - string
          - 'null'
          maxLength: 200
          x-phi: true
          description: Witness name when applicable (PHI).
        method:
          type: string
          enum:
          - electronic
          - wet_ink
        locale:
          type: string
          description: Language of the consented document (BCP-47).
        pdfAvailable:
          type: boolean
          description: Whether a signed PDF artifact exists (GET via signed-pdf operation).
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    Screening:
      type: object
      description: 'Eligibility screening record linking inclusion/exclusion criteria to a participant
        (or pre-screening prospect).

        '
      required:
      - id
      - studyId
      - status
      - criteria
      properties:
        id:
          $ref: '#/components/schemas/ScreeningId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        participantId:
          allOf:
          - $ref: '#/components/schemas/ParticipantId'
          x-phi: true
          description: Present when screening is linked to a participant.
        visitId:
          allOf:
          - $ref: '#/components/schemas/VisitId'
        status:
          type: string
          enum:
          - pending
          - eligible
          - screen_fail
          - enrolled
        criteria:
          type: array
          description: Per-criterion evaluation (non-PHI criteria definitions).
          items:
            type: object
            required:
            - criterionOid
            - result
            properties:
              criterionOid:
                type: string
                description: Protocol criterion identifier (e.g. "INC-1").
              result:
                type: string
                enum:
                - met
                - not_met
                - pending
                - not_assessable
              assessedValue:
                description: 'Observed value the result is based on (may be a lab/clinical value — PHI
                  when participant-linked).

                  '
                x-phi: true
              assessedAt:
                type: string
                format: date-time
              assessedBy:
                allOf:
                - $ref: '#/components/schemas/UserId'
        failureReason:
          type:
          - string
          - 'null'
          description: 'Free-text screen-failure rationale. May reference participant condition — treat
            as sensitive (x-phi).

            '
          x-phi: true
        decidedAt:
          type:
          - string
          - 'null'
          format: date-time
        decidedBy:
          allOf:
          - $ref: '#/components/schemas/UserId'
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    ScreeningCreate:
      type: object
      description: Payload for `POST /studies/{id}/screenings`.
      required:
      - criteria
      properties:
        participantId:
          $ref: '#/components/schemas/ParticipantId'
        visitId:
          $ref: '#/components/schemas/VisitId'
        criteria:
          type: array
          items:
            type: object
            required:
            - criterionOid
            - result
            properties:
              criterionOid:
                type: string
              result:
                type: string
                enum:
                - met
                - not_met
                - pending
                - not_assessable
              assessedValue:
                x-phi: true
    ExportJob:
      type: object
      description: 'Asynchronous export job (created via `POST /studies/{id}/exports`, polled via `GET
        /studies/{id}/exports/{exportId}`). When `status` is `completed` the `downloadUrl` is populated
        — a pre-signed URL that expires (3600 s) and is refreshed by re-reading the job.

        '
      required:
      - id
      - studyId
      - type
      - format
      - status
      - requestedAt
      properties:
        id:
          $ref: '#/components/schemas/ExportJobId'
        studyId:
          $ref: '#/components/schemas/StudyId'
        type:
          type: string
          description: Export content type.
          enum:
          - clinical_data
          - audit_trail
          - dataset_sdtm
          - dataset_adam
        format:
          type: string
          enum:
          - csv
          - json
          - xlsx
          - sas7bdat
        scope:
          type: object
          description: Optional row filter (all optional; default is the whole study).
          properties:
            siteIds:
              type: array
              items:
                $ref: '#/components/schemas/SiteId'
            participantIds:
              type: array
              items:
                $ref: '#/components/schemas/ParticipantId'
            formOids:
              type: array
              items:
                type: string
            dateFrom:
              type: string
              format: date
            dateTo:
              type: string
              format: date
        includePhi:
          type: boolean
          default: false
          description: 'Request PHI inclusion. Accepted only when the token holds `phi:read` (otherwise
            403); the resulting file is PHI-bearing and its download is audited.

            '
        status:
          type: string
          enum:
          - queued
          - running
          - completed
          - failed
          - expired
        progress:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 100
          description: Percent complete (running).
        requestedBy:
          allOf:
          - $ref: '#/components/schemas/UserId'
        requestedAt:
          type: string
          format: date-time
        completedAt:
          type:
          - string
          - 'null'
          format: date-time
        sizeBytes:
          type:
          - integer
          - 'null'
          description: Result artifact size (completed).
        checksumSha256:
          type:
          - string
          - 'null'
          description: SHA-256 of the artifact for integrity verification.
        downloadUrl:
          type:
          - string
          - 'null'
          format: uri
          description: 'Pre-signed download URL, present when status is completed. Expires after 3600
            s — re-GET the job to obtain a fresh URL. Downloading emits an audit event.

            '
        error:
          type:
          - string
          - 'null'
          description: Failure detail when status is failed.
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    ExportJobCreate:
      type: object
      description: Payload for `POST /studies/{id}/exports` (async job creation).
      required:
      - type
      - format
      properties:
        type:
          type: string
          enum:
          - clinical_data
          - audit_trail
          - dataset_sdtm
          - dataset_adam
        format:
          type: string
          enum:
          - csv
          - json
          - xlsx
          - sas7bdat
        scope:
          type: object
          properties:
            siteIds:
              type: array
              items:
                $ref: '#/components/schemas/SiteId'
            participantIds:
              type: array
              items:
                $ref: '#/components/schemas/ParticipantId'
            formOids:
              type: array
              items:
                type: string
            dateFrom:
              type: string
              format: date
            dateTo:
              type: string
              format: date
        includePhi:
          type: boolean
          default: false
    AuditEvent:
      type: object
      description: 'Immutable audit-trail entry (21 CFR Part 11 style). Written by the platform; never
        mutated via API. Includes PHI-access records.

        '
      required:
      - id
      - orgId
      - type
      - category
      - occurredAt
      - actor
      - action
      - outcome
      properties:
        id:
          $ref: '#/components/schemas/AuditEventId'
        orgId:
          $ref: '#/components/schemas/OrgId'
        studyId:
          allOf:
          - $ref: '#/components/schemas/StudyId'
        type:
          type: string
          description: 'Event type. Examples — record.create, record.update, record.correction, phi.view,
            participant.status_changed, query.status_changed, consent.signed, consent.pdf.download, export.created,
            export.downloaded, study.locked, webhook.delivery_failed.

            '
          example: record.correction
        category:
          type: string
          enum:
          - data
          - participant
          - consent
          - epro
          - query
          - export
          - study
          - security
          - webhook
        occurredAt:
          type: string
          format: date-time
        actor:
          type: object
          description: Who performed the action (API client or user on whose behalf).
          required:
          - type
          - id
          properties:
            type:
              type: string
              enum:
              - client
              - user
              - system
            id:
              type: string
              description: Client id / user id / "system".
            orgId:
              allOf:
              - $ref: '#/components/schemas/OrgId'
        action:
          type: string
          enum:
          - create
          - read
          - update
          - delete
          - execute
          description: 'CRUD-style action. `read` events with category=participant or payloads containing
            x-phi content are the audit record of a PHI read (phi.view).

            '
        outcome:
          type: string
          enum:
          - success
          - failure
        resource:
          type: object
          description: Target of the action.
          properties:
            type:
              type: string
              description: Resource type, e.g. "DataPoint", "Participant", "Consent".
            id:
              type: string
            path:
              type: string
              description: API path of the resource.
        phiAccessed:
          type: boolean
          description: 'True when the event records a PHI read (`phi.view`); every response that included
            x-phi content produces one of these.

            '
        reasonForChange:
          type:
          - string
          - 'null'
          description: Verbatim audit rationale for data corrections.
        details:
          type: object
          additionalProperties: true
          description: Additional structured detail (field OID, old/new value presence flags, ...).
        requestId:
          type: string
          description: X-Request-Id of the triggering request.
        signature:
          type:
          - string
          - 'null'
          description: 'Hash-chained integrity reference for the entry (previous entry hash chain), as
            produced by the platform.

            '
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    WebhookEventEnum:
      type: string
      description: Subscribable event types.
      enum:
      - participant.created
      - consent.signed
      - epro.response.submitted
      - query.opened
      - export.completed
      - study.locked
    Webhook:
      type: object
      description: 'Outbound webhook subscription. Deliveries are signed HMAC-SHA256 with timestamp and
        retried with exponential backoff (see info.description "Webhooks").

        '
      required:
      - id
      - url
      - events
      - status
      - secretLastFour
      - createdAt
      properties:
        id:
          $ref: '#/components/schemas/WebhookId'
        url:
          type: string
          format: uri
          maxLength: 2048
          description: HTTPS endpoint receiving deliveries (https scheme enforced).
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventEnum'
        status:
          type: string
          description: 'Health status — failing means recent deliveries failed; auto-disables after sustained
            failure (24 h) until re-enabled.

            '
          enum:
          - active
          - failing
          - disabled
        secretLastFour:
          type: string
          maxLength: 4
          description: 'Last 4 characters of the signing secret, shown only to confirm which secret is
            in effect. The full secret is shown ONCE at creation in the `X-Beacon-Signing-Secret` response
            header.

            '
        lastDeliveryAt:
          type:
          - string
          - 'null'
          format: date-time
        lastDeliveryStatus:
          type:
          - string
          - 'null'
          description: Last delivery HTTP status (null when never delivered).
          example: '200'
        failureCount:
          type: integer
          minimum: 0
        auditMixin:
          $ref: '#/components/schemas/AuditMixin'
    WebhookCreate:
      type: object
      description: 'Payload for `POST /webhooks`. The signing secret is generated server-side and returned
        once via the `X-Beacon-Signing-Secret` response header (never readable again).

        '
      required:
      - url
      - events
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventEnum'
        description:
          type: string
          maxLength: 200
    WebhookUpdate:
      type: object
      description: 'PATCH payload (If-Match required). Set `rotateSecret: true` to rotate the signing
        secret (new secret returned once via X-Beacon-Signing-Secret on the successful PATCH).

        '
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventEnum'
        status:
          type: string
          enum:
          - active
          - disabled
        description:
          type: string
          maxLength: 200
        rotateSecret:
          type: boolean
          default: false
          description: 'When true, issues a new HMAC-SHA256 signing secret (returned once via the `X-Beacon-Signing-Secret`
            response header); the previous secret stops validating signatures immediately.

            '
    Problem:
      title: Problem
      type: object
      description: 'RFC 9457 problem detail (application/problem+json). `type` is a URI identifying the
        error class; docs at https://developers.beaconlifesciences.com/problems/{type}.

        '
      required:
      - type
      - title
      - status
      - detail
      - instance
      - requestId
      properties:
        type:
          type: string
          format: uri-reference
          description: 'URI reference identifying the problem type, e.g. https://developers.beaconlifesciences.com/problems/validation-error.

            '
          example: https://developers.beaconlifesciences.com/problems/validation-error
        title:
          type: string
          description: Short human-readable summary (stable per type).
        status:
          type: integer
          description: HTTP status code, duplicated from the response.
        detail:
          type: string
          description: Human-readable explanation specific to this occurrence.
        instance:
          type: string
          format: uri-reference
          description: URI reference of the occurrence (typically the request path).
        requestId:
          type: string
          description: 'Correlation id — mirrors the `X-Request-Id` response header; quote it in support
            requests.

            '
        errors:
          type: array
          description: Optional field-level details for validation errors.
          items:
            type: object
            required:
            - field
            - message
            properties:
              field:
                type: string
              message:
                type: string
    TokenResponse:
      title: OAuth Token Response
      type: object
      description: 'Successful client-credentials token response (RFC 6749 section 4.4.3 plus org binding).
        The token is org-scoped — see the `org` claim and the securitySchemes description.

        '
      required:
      - access_token
      - token_type
      - expires_in
      - scope
      - org
      properties:
        access_token:
          type: string
          description: Bearer token for the Authorization header.
          example: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.example-only-not-a-real-credential
        token_type:
          type: string
          enum:
          - Bearer
        expires_in:
          type: integer
          description: Token lifetime in seconds (3600).
          example: 3600
        scope:
          type: string
          description: Space-delimited scopes actually granted.
          example: studies:read records:write phi:read
        org:
          $ref: '#/components/schemas/OrgId'
        token_type_id:
          description: Reserved for future token-profile differentiation.
          type:
          - string
          - 'null'
    StudyList:
      description: Cursor page of studies.
      x-phi: false
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Study'
    SiteList:
      description: Cursor page of sites.
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Site'
    FormList:
      description: Cursor page of forms.
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Form'
    FormVersionList:
      description: Cursor page of form versions (newest first).
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/FormVersion'
    VisitList:
      description: Cursor page of visits (schedule order).
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Visit'
    ParticipantList:
      description: 'Cursor page of participants. When the token holds `phi:read`, `data[].person` and
        other x-phi fields are included and the read is audited; otherwise they are omitted.

        '
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Participant'
    DataPointList:
      description: 'Cursor page of data points. x-phi content (`value`, changeHistory values) requires
        `phi:read`.

        '
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/DataPoint'
    QueryList:
      description: Cursor page of queries (newest first).
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Query'
    EproInstrumentList:
      description: Cursor page of ePRO instruments.
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/EproInstrument'
    EproInvitationList:
      description: Cursor page of ePRO invitations (newest first).
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/EproInvitation'
    EproResponseList:
      description: 'Cursor page of ePRO responses; answer content is PHI (x-phi) and requires `phi:read`.

        '
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/EproResponse'
    ConsentList:
      description: Cursor page of consent records (PHI fields per token scope).
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Consent'
    ScreeningList:
      description: Cursor page of screening records.
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Screening'
    ExportJobList:
      description: Cursor page of export jobs (newest first).
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/ExportJob'
    AuditEventList:
      description: Cursor page of audit events (chronological, oldest first default).
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/AuditEvent'
    WebhookList:
      description: Cursor page of webhook subscriptions.
      allOf:
      - $ref: '#/components/schemas/CursorPage'
      - type: object
        required:
        - data
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/Webhook'
paths:
  /oauth/token:
    post:
      tags:
      - OAuth
      operationId: createToken
      summary: Issue an org-scoped access token (client credentials)
      description: 'OAuth 2.0 client-credentials token endpoint (RFC 6749 section 4.4). The issued token
        carries the organization binding of the client credentials (org claim) and only the scopes the
        client requests and is entitled to. Tokens expire after 3600 s; re-request rather than refresh.

        '
      security: []
      parameters: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
              - grant_type
              - client_id
              - client_secret
              - scope
              properties:
                grant_type:
                  type: string
                  enum:
                  - client_credentials
                client_id:
                  type: string
                  description: Client identifier issued to the integration.
                client_secret:
                  type: string
                  description: Client secret (transmitted only over TLS; never stored in the spec).
                scope:
                  type: string
                  description: 'Space-delimited list of requested scopes (see components.securitySchemes.oauth2.flows).
                    Scopes are granted per resource + action; the token''s org is fixed by the client
                    credentials.

                    '
                  example: studies:read records:write phi:read
      responses:
        '200':
          description: Token issued (org-scoped, with requested scopes).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Cache-Control:
              description: Token responses are never cached.
              schema:
                type: string
                example: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
        '400':
          description: Malformed request or unsupported grant_type.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '401':
          description: Invalid client credentials.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /organizations/me:
    get:
      tags:
      - Organizations
      operationId: getCurrentOrganization
      summary: Get the organization bound to the token
      description: 'Returns the organization record for the org embedded in the access token (every token
        is org-scoped — this is always the caller''s own organization).

        '
      security:
      - oauth2:
        - studies:read
      parameters: []
      responses:
        '200':
          description: The token's organization.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Organization'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies:
    get:
      tags:
      - Studies
      operationId: listStudies
      summary: List studies
      description: 'Cursor-paginated list of studies in the token''s organization (newest first). Filterable
        by status, phase, protocolCode and update watermark.

        '
      security:
      - oauth2:
        - studies:read
      parameters:
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        description: Filter by lifecycle status.
        schema:
          type: string
          enum:
          - draft
          - active
          - closed_to_enrollment
          - locked
      - name: phase
        in: query
        required: false
        description: Filter by clinical phase.
        schema:
          type: string
          enum:
          - preclinical
          - phase1
          - phase2
          - phase3
          - phase4
          - observational
      - name: protocolCode
        in: query
        required: false
        description: Exact-match filter on protocol code.
        schema:
          type: string
      - name: updatedSince
        in: query
        required: false
        description: Only studies changed after this ISO 8601 UTC instant (incremental sync watermark).
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of studies.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StudyList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Studies
      operationId: createStudy
      summary: Create a study
      description: 'Creates a study in the org. Idempotency-Key recommended — a repeated key replays the
        original 201 without creating a duplicate; key reuse with a different body yields 409.

        '
      security:
      - oauth2:
        - studies:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyCreate'
      responses:
        '201':
          description: Study created.
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Study'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}:
    get:
      tags:
      - Studies
      operationId: getStudy
      summary: Get a study
      description: 'Returns the study with an ETag for optimistic concurrency. May emit Deprecation/Link
        headers when part of the response is deprecated.

        '
      security:
      - oauth2:
        - studies:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      responses:
        '200':
          description: The study.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
            Deprecation:
              $ref: '#/components/headers/Deprecation'
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Study'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    patch:
      tags:
      - Studies
      operationId: updateStudy
      summary: Update a study
      description: 'Partial update (If-Match required). Status transitions are forward-only and `locked`
        is terminal; locking emits the `study.locked` webhook event and an audit event.

        '
      security:
      - oauth2:
        - studies:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IfMatchParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StudyUpdate'
      responses:
        '200':
          description: Updated study.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Study'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/sites:
    get:
      tags:
      - Sites
      operationId: listSites
      summary: List sites in the study
      security:
      - oauth2:
        - sites:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - pending_activation
          - active
          - inactive
      - name: country
        in: query
        required: false
        description: ISO 3166-1 alpha-2 country filter.
        schema:
          type: string
          minLength: 2
          maxLength: 2
      - name: updatedSince
        in: query
        required: false
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of sites.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Sites
      operationId: createSite
      summary: Add a site to the study
      description: 'Creates a site (code unique per study). Idempotency-Key supported — same key replays
        the original response.

        '
      security:
      - oauth2:
        - sites:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SiteCreate'
      responses:
        '201':
          description: Site created.
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Site'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/sites/{siteId}:
    get:
      tags:
      - Sites
      operationId: getSite
      security:
      - oauth2:
        - sites:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/SiteIdParam'
      responses:
        '200':
          description: The site (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Site'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    patch:
      tags:
      - Sites
      operationId: updateSite
      summary: Update a site (If-Match required)
      security:
      - oauth2:
        - sites:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/SiteIdParam'
      - $ref: '#/components/parameters/IfMatchParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SiteUpdate'
      responses:
        '200':
          description: Updated site.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Site'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/forms:
    get:
      tags:
      - Forms
      operationId: listForms
      summary: List eCRF form definitions
      security:
      - oauth2:
        - forms:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - draft
          - published
          - retired
      - name: updatedSince
        in: query
        required: false
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of forms.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Forms
      operationId: createForm
      summary: Create a form definition
      security:
      - oauth2:
        - forms:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormCreate'
      responses:
        '201':
          description: Form created (draft; create a version to capture data).
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Form'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/forms/{formId}:
    get:
      tags:
      - Forms
      operationId: getForm
      security:
      - oauth2:
        - forms:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/FormIdParam'
      responses:
        '200':
          description: The form (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Form'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    patch:
      tags:
      - Forms
      operationId: updateForm
      summary: Update a form definition (If-Match required)
      description: 'Only draft forms accept edits; published versions are immutable — create a new version
        instead.

        '
      security:
      - oauth2:
        - forms:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/FormIdParam'
      - $ref: '#/components/parameters/IfMatchParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormCreate'
      responses:
        '200':
          description: Updated form.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Form'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/forms/{formId}/versions:
    get:
      tags:
      - Forms
      operationId: listFormVersions
      summary: List versions of a form
      security:
      - oauth2:
        - forms:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/FormIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - draft
          - published
          - retired
      responses:
        '200':
          description: Cursor page of form versions (newest first).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormVersionList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Forms
      operationId: createFormVersion
      summary: Create (and optionally publish) a form version
      description: 'Creates a new version of the form. Published versions are immutable and become the
        capture target for new data points; `publish: true` publishes atomically on creation.

        '
      security:
      - oauth2:
        - forms:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/FormIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormVersionPublish'
      responses:
        '201':
          description: Version created (draft, or published when publish was true).
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormVersion'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/forms/{formId}/versions/{versionId}:
    get:
      tags:
      - Forms
      operationId: getFormVersion
      summary: Get a specific form version
      security:
      - oauth2:
        - forms:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/FormIdParam'
      - name: versionId
        in: path
        required: true
        schema:
          $ref: '#/components/schemas/FormVersionId'
      responses:
        '200':
          description: The form version (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormVersion'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/visits:
    get:
      tags:
      - Visits
      operationId: listVisits
      summary: List visit definitions
      security:
      - oauth2:
        - visits:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - active
          - retired
      responses:
        '200':
          description: Cursor page of visits (schedule order).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VisitList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Visits
      operationId: createVisit
      summary: Create a visit definition
      security:
      - oauth2:
        - visits:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VisitCreate'
      responses:
        '201':
          description: Visit created.
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Visit'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/visits/{visitId}:
    get:
      tags:
      - Visits
      operationId: getVisit
      security:
      - oauth2:
        - visits:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/VisitIdParam'
      responses:
        '200':
          description: The visit (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Visit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    patch:
      tags:
      - Visits
      operationId: updateVisit
      summary: Update a visit definition (If-Match required)
      security:
      - oauth2:
        - visits:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/VisitIdParam'
      - $ref: '#/components/parameters/IfMatchParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VisitUpdate'
      responses:
        '200':
          description: Updated visit.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Visit'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/participants:
    get:
      tags:
      - Participants
      operationId: listParticipants
      summary: List participants
      description: 'Cursor-paginated participants. x-phi content (the `person` block) requires `phi:read`;
        without it those properties are omitted. Every response that includes PHI is audited.

        '
      security:
      - oauth2:
        - participants:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: siteId
        in: query
        required: false
        description: Filter by site.
        schema:
          $ref: '#/components/schemas/SiteId'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - screening
          - screen_fail
          - enrolled
          - withdrawn
          - completed
      - name: subjectKey
        in: query
        required: false
        description: Exact match on the non-PHI subject label.
        schema:
          type: string
      - name: enrolledSince
        in: query
        required: false
        description: Only participants enrolled at/after this ISO 8601 UTC instant.
        schema:
          type: string
          format: date-time
      - name: updatedSince
        in: query
        required: false
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of participants (x-phi fields per phi:read scope).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParticipantList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Participants
      operationId: createParticipant
      summary: Enroll / register a participant
      description: 'Creates a participant (initially `screening` unless status given). Idempotency-Key
        strongly recommended — duplicate keys replay the original 201; different keys with the same subjectKey
        yield 409. PHI `person` content is accepted and echoed only for tokens with `phi:read`.

        '
      security:
      - oauth2:
        - participants:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ParticipantCreate'
      responses:
        '201':
          description: Participant created (x-phi content per phi:read).
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Participant'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/participants/{pid}:
    get:
      tags:
      - Participants
      operationId: getParticipant
      summary: Get a participant
      description: 'x-phi properties (`person` block) included only with `phi:read` (audited). The ETag
        is required for subsequent PATCH.

        '
      security:
      - oauth2:
        - participants:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ParticipantIdParam'
      responses:
        '200':
          description: The participant.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Participant'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    patch:
      tags:
      - Participants
      operationId: updateParticipant
      summary: Update participant status (and PHI contact fields)
      description: 'If-Match required. `status` transitions are enforced (see Participant.status). Status
        changes emit audit events and may emit `participant.created`-adjacent integration events. PHI
        `person` changes are accepted only with `phi:read` on the token.

        '
      security:
      - oauth2:
        - participants:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ParticipantIdParam'
      - $ref: '#/components/parameters/IfMatchParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ParticipantUpdate'
      responses:
        '200':
          description: Updated participant.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Participant'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/participants/{pid}/data:
    get:
      tags:
      - Data
      operationId: listParticipantData
      summary: List data points for a participant
      description: 'Cursor-paginated data points. Filter by form OID, visit, and update watermark. x-phi
        `value`/changeHistory content requires `phi:read`; every PHI read is audited.

        '
      security:
      - oauth2:
        - records:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ParticipantIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: formOid
        in: query
        required: false
        description: Filter by form OID (e.g. "AE_LOG").
        schema:
          type: string
      - name: fieldOid
        in: query
        required: false
        schema:
          type: string
      - name: visitId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/VisitId'
      - name: updatedSince
        in: query
        required: false
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of data points.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataPointList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Data
      operationId: submitParticipantData
      summary: Submit data points (batch, append-only capture)
      description: 'Submits one or more data points in a single atomic batch. New values are appended;
        overwriting an existing non-null value requires `reasonForChange` (missing reason yields 422).
        Idempotency-Key strongly recommended (batch-level replay).

        '
      security:
      - oauth2:
        - records:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ParticipantIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - items
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    $ref: '#/components/schemas/DataPointSubmit'
      responses:
        '200':
          description: Batch applied — returns the affected data points (with new ETags).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - hasMore
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DataPoint'
                  hasMore:
                    type: boolean
                    description: Always false for this non-paginated batch result.
                  results:
                    type: array
                    description: Per-item outcome (index-aligned with request items).
                    items:
                      type: object
                      required:
                      - index
                      - status
                      properties:
                        index:
                          type: integer
                          description: Zero-based index of the item in the request batch.
                        status:
                          type: string
                          enum:
                          - created
                          - updated
                          - failed
                        dataPointId:
                          $ref: '#/components/schemas/DataPointId'
                        error:
                          type:
                          - string
                          - 'null'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    put:
      tags:
      - Data
      operationId: upsertParticipantData
      summary: Upsert a single data point (targeted capture/correction)
      description: 'Upsert keyed by (formOid, fieldOid, visitId, occurrence). When the target already
        has a non-null value, `reasonForChange` is REQUIRED (422 without it). Idempotency-Key supported.
        `If-Match` is required when correcting an existing data point (428 when omitted on a correction,
        412 on ETag mismatch); omit it when creating a new data point.

        '
      security:
      - oauth2:
        - records:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ParticipantIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      - name: If-Match
        in: header
        required: false
        description: 'Opaque `ETag` of the existing data point — REQUIRED when this upsert is a correction
          (the target data point already exists), omitted when creating a new data point. Mismatch yields
          `412`; a correction without the header yields `428`.

          '
        schema:
          type: string
          example: '"a1b2c3d4e5f6"'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DataPointSubmit'
      responses:
        '200':
          description: Existing data point updated (correction lineage extended).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataPoint'
        '201':
          description: New data point created.
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataPoint'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/participants/{pid}/data/{dataPointId}:
    get:
      tags:
      - Data
      operationId: getDataPoint
      summary: Get a single data point
      security:
      - oauth2:
        - records:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ParticipantIdParam'
      - $ref: '#/components/parameters/DataPointIdParam'
      responses:
        '200':
          description: The data point (x-phi content per phi:read).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataPoint'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/queries:
    get:
      tags:
      - Queries
      operationId: listQueries
      summary: List data queries
      security:
      - oauth2:
        - queries:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - open
          - answered
          - closed
          - reopened
      - name: participantId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/ParticipantId'
      - name: type
        in: query
        required: false
        schema:
          type: string
          enum:
          - verification
          - manual
          - auto_edit_check
          - range_check
          - missing_data
      - name: openedSince
        in: query
        required: false
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of queries (newest first).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Queries
      operationId: createQuery
      summary: Open a data query
      description: 'Opens a query (emits `query.opened` webhook). Idempotency-Key supported.

        '
      security:
      - oauth2:
        - queries:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryCreate'
      responses:
        '201':
          description: Query created (status open).
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Query'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/queries/{queryId}:
    get:
      tags:
      - Queries
      operationId: getQuery
      security:
      - oauth2:
        - queries:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/QueryIdParam'
      responses:
        '200':
          description: The query with full thread.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Query'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    patch:
      tags:
      - Queries
      operationId: updateQueryStatus
      summary: Change query status
      description: 'If-Match required. Allowed transitions — open to answered/closed, answered to closed/reopened,
        reopened to answered/closed. Emits a `query.status_changed` audit event.

        '
      security:
      - oauth2:
        - queries:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/QueryIdParam'
      - $ref: '#/components/parameters/IfMatchParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryStatusUpdate'
      responses:
        '200':
          description: Updated query.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Query'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/screenings:
    get:
      tags:
      - Screenings
      operationId: listScreenings
      summary: List screening records
      security:
      - oauth2:
        - screenings:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - pending
          - eligible
          - screen_fail
          - enrolled
      - name: participantId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/ParticipantId'
      responses:
        '200':
          description: Cursor page of screenings.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScreeningList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Screenings
      operationId: createScreening
      summary: Create a screening record
      security:
      - oauth2:
        - screenings:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScreeningCreate'
      responses:
        '201':
          description: Screening created.
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Screening'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/screenings/{screeningId}:
    get:
      tags:
      - Screenings
      operationId: getScreening
      security:
      - oauth2:
        - screenings:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ScreeningIdParam'
      responses:
        '200':
          description: The screening record (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Screening'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/consents:
    get:
      tags:
      - Consent
      operationId: listConsents
      summary: List consent records
      description: 'Consent status per document/participant. PHI fields (signedBy, witnessName, participantId)
        require `phi:read`; PHI reads audited.

        '
      security:
      - oauth2:
        - consent:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: participantId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/ParticipantId'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - pending
          - signed
          - withdrawn
          - expired
          - superseded
      - name: documentCode
        in: query
        required: false
        schema:
          type: string
      - name: signedSince
        in: query
        required: false
        description: Only consents signed at/after this instant (incremental sync).
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of consent records.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsentList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/consents/{consentId}:
    get:
      tags:
      - Consent
      operationId: getConsent
      summary: Get consent status for one record
      security:
      - oauth2:
        - consent:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ConsentIdParam'
      responses:
        '200':
          description: The consent record (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Consent'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/consents/{consentId}/signed-pdf:
    get:
      tags:
      - Consent
      operationId: downloadSignedConsentPdf
      summary: Download the signed consent PDF (binary)
      description: 'Returns the signed consent artifact as `application/pdf`. Requires `consent:read`
        AND `phi:read` (the PDF contains identity and signature data); the download is audited (phi.view
        / consent.pdf.download). 403 when `phi:read` is not held.

        '
      security:
      - oauth2:
        - consent:read
        - phi:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ConsentIdParam'
      responses:
        '200':
          description: The signed consent PDF.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            Content-Disposition:
              description: Attachment filename, e.g. `attachment; filename="consent-cns_8a9b0c1d2e-v2.pdf"`.
              schema:
                type: string
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: 'Consent not found, or the signed artifact does not exist yet (status pending /
            pdfAvailable false).

            '
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/epro/instruments:
    get:
      tags:
      - ePRO
      operationId: listEproInstruments
      summary: List ePRO instruments in the study
      security:
      - oauth2:
        - epro:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - draft
          - active
          - retired
      responses:
        '200':
          description: Cursor page of ePRO instruments.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EproInstrumentList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - ePRO
      operationId: createEproInstrument
      summary: Add an ePRO instrument to the study
      security:
      - oauth2:
        - epro:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EproInstrumentCreate'
      responses:
        '201':
          description: Instrument created (draft until activated).
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EproInstrument'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/epro/instruments/{instrumentId}:
    get:
      tags:
      - ePRO
      operationId: getEproInstrument
      security:
      - oauth2:
        - epro:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - name: instrumentId
        in: path
        required: true
        description: ePRO instrument identifier.
        schema:
          $ref: '#/components/schemas/EproInstrumentId'
      responses:
        '200':
          description: The instrument (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EproInstrument'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/epro/invitations:
    get:
      tags:
      - ePRO
      operationId: listEproInvitations
      summary: List ePRO invitations
      security:
      - oauth2:
        - epro:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: participantId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/ParticipantId'
      - name: instrumentId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/EproInstrumentId'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - pending
          - delivered
          - viewed
          - completed
          - expired
          - canceled
      - name: issuedSince
        in: query
        required: false
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of invitations (newest first).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EproInvitationList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - ePRO
      operationId: createEproInvitation
      summary: Send an ePRO invitation to a participant
      description: 'Queues an invitation for delivery. Idempotency-Key supported — same key replays the
        original response and never duplicates the invite.

        '
      security:
      - oauth2:
        - epro:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EproInvitationCreate'
      responses:
        '201':
          description: Invitation queued (status pending).
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EproInvitation'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/epro/invitations/{invitationId}:
    get:
      tags:
      - ePRO
      operationId: getEproInvitation
      security:
      - oauth2:
        - epro:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - name: invitationId
        in: path
        required: true
        description: ePRO invitation identifier.
        schema:
          $ref: '#/components/schemas/EproInvitationId'
      responses:
        '200':
          description: The invitation (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EproInvitation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    patch:
      tags:
      - ePRO
      operationId: cancelEproInvitation
      summary: Cancel a pending invitation
      description: If-Match required. Only `pending` invitations can be canceled.
      security:
      - oauth2:
        - epro:write
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - name: invitationId
        in: path
        required: true
        description: ePRO invitation identifier.
        schema:
          $ref: '#/components/schemas/EproInvitationId'
      - $ref: '#/components/parameters/IfMatchParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - status
              properties:
                status:
                  type: string
                  enum:
                  - canceled
      responses:
        '200':
          description: Invitation canceled.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EproInvitation'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/epro/responses:
    get:
      tags:
      - ePRO
      operationId: listEproResponses
      summary: List ePRO responses
      description: 'Cursor-paginated ePRO responses (newest first). Answer content is PHI (x-phi) — returned
        only when the token holds `phi:read`; every PHI read is audited.

        '
      security:
      - oauth2:
        - epro:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: participantId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/ParticipantId'
      - name: instrumentId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/EproInstrumentId'
      - name: visitId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/VisitId'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - in_progress
          - submitted
          - partially_completed
          - expired
      - name: submittedSince
        in: query
        required: false
        description: Only responses submitted at/after this instant (incremental sync).
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of ePRO responses (answers per phi:read scope).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EproResponseList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/epro/responses/{responseId}:
    get:
      tags:
      - ePRO
      operationId: getEproResponse
      summary: Get one ePRO response set
      description: 'Answer content (x-phi) requires `phi:read`; the read is audited.

        '
      security:
      - oauth2:
        - epro:read
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - name: responseId
        in: path
        required: true
        description: ePRO response identifier.
        schema:
          $ref: '#/components/schemas/EproResponseId'
      responses:
        '200':
          description: The response set (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EproResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/exports:
    get:
      tags:
      - Exports
      operationId: listExports
      summary: List export jobs
      security:
      - oauth2:
        - exports:create
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - queued
          - running
          - completed
          - failed
          - expired
      responses:
        '200':
          description: Cursor page of export jobs (newest first).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportJobList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Exports
      operationId: createExport
      summary: Create an async export job
      description: 'Queues an asynchronous export (202 Accepted pattern; job status via `GET /studies/{id}/exports/{exportId}`).
        `includePhi: true` requires `phi:read` on the token (403 otherwise) and the resulting artifact
        + its download are audited. Idempotency-Key recommended — same key replays and does not enqueue
        a duplicate job.

        '
      security:
      - oauth2:
        - exports:create
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExportJobCreate'
      responses:
        '202':
          description: Export job accepted and queued.
          headers:
            Location:
              $ref: '#/components/headers/Location'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /studies/{id}/exports/{exportId}:
    get:
      tags:
      - Exports
      operationId: getExport
      summary: Get export job status (and download URL when complete)
      description: 'Poll this operation while `status` is queued/running. When `completed`, `downloadUrl`
        carries a pre-signed URL (3600 s expiry); re-GET to refresh. Downloading the artifact is audited.

        '
      security:
      - oauth2:
        - exports:create
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ExportIdParam'
      responses:
        '200':
          description: The export job (downloadUrl populated when completed).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportJob'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    delete:
      tags:
      - Exports
      operationId: cancelExport
      summary: Cancel a queued export job
      description: Only `queued` jobs can be canceled (running/completed cannot).
      security:
      - oauth2:
        - exports:create
      parameters:
      - $ref: '#/components/parameters/StudyIdParam'
      - $ref: '#/components/parameters/ExportIdParam'
      responses:
        '204':
          description: Job canceled (or already completed — then it is left untouched and a subsequent
            GET shows completed). No body.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /audit-events:
    get:
      tags:
      - Audit
      operationId: listAuditEvents
      summary: List audit events (org-wide, immutable trail)
      description: 'Cursor-paginated audit trail for the token''s organization. Filterable by study, category,
        event type, actor, PHI flag, and time window (chronological, oldest first by default). Reads of
        this trail are themselves audit-logged at the application layer.

        '
      security:
      - oauth2:
        - audit:read
      parameters:
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: studyId
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/StudyId'
      - name: category
        in: query
        required: false
        schema:
          type: string
          enum:
          - data
          - participant
          - consent
          - epro
          - query
          - export
          - study
          - security
          - webhook
      - name: type
        in: query
        required: false
        description: Event type filter (e.g. phi.view, record.correction, export.downloaded).
        schema:
          type: string
      - name: actorId
        in: query
        required: false
        schema:
          type: string
      - name: phiAccessed
        in: query
        required: false
        description: Filter to PHI-read events (phi.view) only.
        schema:
          type: boolean
      - name: since
        in: query
        required: false
        schema:
          type: string
          format: date-time
      - name: until
        in: query
        required: false
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Cursor page of audit events.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditEventList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /webhooks:
    get:
      tags:
      - Webhooks
      operationId: listWebhooks
      summary: List webhook subscriptions
      security:
      - oauth2:
        - webhooks:read
      parameters:
      - $ref: '#/components/parameters/CursorParam'
      - $ref: '#/components/parameters/LimitParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - active
          - failing
          - disabled
      - name: event
        in: query
        required: false
        description: Filter to webhooks subscribed to a given event.
        schema:
          $ref: '#/components/schemas/WebhookEventEnum'
      responses:
        '200':
          description: Cursor page of webhook subscriptions.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    post:
      tags:
      - Webhooks
      operationId: createWebhook
      summary: Create a webhook subscription
      description: 'The HMAC-SHA256 signing secret is generated server-side and returned exactly once
        via the `X-Beacon-Signing-Secret` response header (store it securely; it is not retrievable later
        — rotation issues a new secret). Idempotency-Key supported.

        '
      security:
      - oauth2:
        - webhooks:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKeyParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreate'
      responses:
        '201':
          description: Subscription created; signing secret in X-Beacon-Signing-Secret (shown once).
          headers:
            Location:
              $ref: '#/components/headers/Location'
            ETag:
              $ref: '#/components/headers/ETag'
            X-Beacon-Signing-Secret:
              description: 'One-time signing secret for HMAC-SHA256 delivery signatures (`X-Beacon-Signature:
                t=<unix-seconds>,v1=<hex>` where `v1 = HMAC_SHA256(secret, t + ''.'' + rawBody)`). Shown
                only on creation/rotation; never retrievable afterwards.

                '
              schema:
                type: string
                example: whsec_1a2b3c4d5e6f7g8h9i0j
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
  /webhooks/{webhookId}:
    get:
      tags:
      - Webhooks
      operationId: getWebhook
      security:
      - oauth2:
        - webhooks:read
      parameters:
      - $ref: '#/components/parameters/WebhookIdParam'
      responses:
        '200':
          description: The subscription (ETag returned).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    patch:
      tags:
      - Webhooks
      operationId: updateWebhook
      summary: Update / re-enable a webhook subscription
      description: 'If-Match required. Re-enable a disabled subscription by setting `status: active`.
        Omitting... rotating the secret: send `rotateSecret: true` to issue a new secret (returned once
        via X-Beacon-Signing-Secret).

        '
      security:
      - oauth2:
        - webhooks:write
      parameters:
      - $ref: '#/components/parameters/WebhookIdParam'
      - $ref: '#/components/parameters/IfMatchParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookUpdate'
      responses:
        '200':
          description: Updated subscription (new X-Beacon-Signing-Secret when rotation was requested).
          headers:
            X-Beacon-Signing-Secret:
              description: Present only when a secret rotation was requested in this PATCH.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
    delete:
      tags:
      - Webhooks
      operationId: deleteWebhook
      summary: Delete a webhook subscription
      security:
      - oauth2:
        - webhooks:write
      parameters:
      - $ref: '#/components/parameters/WebhookIdParam'
      responses:
        '204':
          description: Subscription deleted. No body.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/ProblemDefault'
