# Proposed contract, version 1: endpoints, errors, paging, idempotency, security and the capability of every
# operation. Resource schemas were initialized from the data model (docs/06-data) and are kept aligned with it; input
# schemas (*Input, *Request) and views are specific to this contract. Status: proposal for review, not confirmed design.
# Conventions: docs/05-interfaces/choosing-an-interface.md. Vendor extensions: x-moonitoring-audience (who calls),
# x-moonitoring-capability (the capability checked), x-moonitoring-entitlement (the entitlement feature checked),
# x-moonitoring-requirements (the requirements of 1.requisiti-raffinati.txt the operation serves).
openapi: 3.1.0
info:
  title: Access Webhooks API
  version: 1.0.0
  description: 'How the platform''s events reach systems outside it (decided 2026-10-30, D9). An Organization registers
    where it wants its events: an HTTPS endpoint, which Pub/Sub calls with a Google-signed token (a push subscription),
    or a Google Cloud service account that pulls them (a pull subscription). Each registration is a Pub/Sub subscription
    on the topic `platform.external.v1`, filtered to the Organization and the event types chosen; Google retries,
    backs off and dead-letters, so there is no delivery worker and no shared secret.'
servers:
- url: https://access.moonitoring.com/webhooks/v1
  description: Prod
- url: https://stage-access.moonitoring.com/webhooks/v1
  description: Stage, through IAP
security:
- bearer: []
tags:
- name: Event types
  description: What can be subscribed to
- name: Subscriptions
  description: Where events are delivered
- name: Failures
  description: Dead-lettered deliveries
paths:
  /event-types:
    get:
      summary: List subscribable event types
      operationId: listWebhookEventTypes
      tags:
      - Event types
      parameters:
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Cursor
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Limit
      responses:
        '200':
          description: One page of results.
          content:
            application/json:
              schema:
                type: object
                description: One page of items.
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEventType'
                  next_cursor:
                    type: string
                    description: Where the next page starts; absent on the last page.
                required:
                - items
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
      x-moonitoring-audience: any
      x-moonitoring-capability: null
      x-moonitoring-requirements:
      - '141.4'
      - '48'
      - '52'
      - '5'
      - '8'
      - '68'
      - '112'
  /subscriptions:
    get:
      summary: List subscriptions
      operationId: listWebhookSubscriptions
      tags:
      - Subscriptions
      parameters:
      - name: organization
        in: query
        required: false
        schema:
          type: string
          format: uuid
        description: The Organization whose subscriptions are listed; staff only, members use their active Organization.
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Cursor
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Limit
      responses:
        '200':
          description: One page of results.
          content:
            application/json:
              schema:
                type: object
                description: One page of items.
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookSubscription'
                  next_cursor:
                    type: string
                    description: Where the next page starts; absent on the last page.
                required:
                - items
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
      x-moonitoring-audience: organization
      x-moonitoring-capability: access.webhooks.manage
      x-moonitoring-requirements:
      - '144'
      - '2'
      - '1002.26'
    post:
      summary: Create a subscription
      operationId: createWebhookSubscription
      tags:
      - Subscriptions
      parameters:
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/IdempotencyKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionInput'
      responses:
        '201':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
        '422':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/UnprocessableEntity
      x-moonitoring-audience: organization
      x-moonitoring-capability: access.webhooks.manage
      x-moonitoring-requirements:
      - '144'
      - '2'
      - '1002.26'
  /subscriptions/{subscription_id}:
    get:
      summary: Read a subscription
      operationId: getWebhookSubscription
      tags:
      - Subscriptions
      parameters:
      - name: subscription_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
      x-moonitoring-audience: organization
      x-moonitoring-capability: access.webhooks.manage
      x-moonitoring-requirements:
      - '144'
      - '2'
      - '1002.26'
    patch:
      summary: Update a subscription
      operationId: updateWebhookSubscription
      description: 'Pub/Sub filters cannot change, so a change of event types creates a new Pub/Sub subscription with
        the new filter and deletes the old one once its backlog is delivered; an event may arrive twice in between, and the
        receiver deduplicates it by its id.'
      tags:
      - Subscriptions
      parameters:
      - name: subscription_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionInput'
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
        '422':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/UnprocessableEntity
      x-moonitoring-audience: organization
      x-moonitoring-capability: access.webhooks.manage
      x-moonitoring-requirements:
      - '144'
      - '2'
      - '1002.26'
    delete:
      summary: Delete a subscription
      operationId: deleteWebhookSubscription
      description: Deletes the Pub/Sub subscription; undelivered messages are lost.
      tags:
      - Subscriptions
      parameters:
      - name: subscription_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '204':
          description: Done.
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
      x-moonitoring-audience: organization
      x-moonitoring-capability: access.webhooks.manage
      x-moonitoring-requirements:
      - '144'
      - '2'
      - '1002.26'
  /subscriptions/{subscription_id}/test:
    post:
      summary: Send a test event
      operationId: testWebhookSubscription
      description: Publishes a `subscription.test.v1` event to platform.external.v1 with the attribute `moonitoring-subscription`
        set to this subscription; every subscription's filter accepts events carrying its own id, so only this one
        receives it.
      tags:
      - Subscriptions
      parameters:
      - name: subscription_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '202':
          description: The result.
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
      x-moonitoring-audience: organization
      x-moonitoring-capability: access.webhooks.manage
      x-moonitoring-requirements:
      - '144'
      - '2'
      - '1002.26'
  /subscriptions/{subscription_id}/resume:
    post:
      summary: Resume a paused subscription
      operationId: resumeWebhookSubscription
      description: After the receiver was fixed; dead-lettered events are not replayed unless redelivered one by
        one.
      tags:
      - Subscriptions
      parameters:
      - name: subscription_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
      x-moonitoring-audience: organization
      x-moonitoring-capability: access.webhooks.manage
      x-moonitoring-requirements:
      - '144.9'
      - '2'
      - '1002.26'
  /subscriptions/{subscription_id}/failures:
    get:
      summary: List failed deliveries
      operationId: listFailedDeliveries
      description: Events Pub/Sub gave up on, from the dead-letter topic; kept for 30 days.
      tags:
      - Failures
      parameters:
      - name: subscription_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Cursor
      - $ref: ../../../../common/v1/schemas.yaml#/components/parameters/Limit
      responses:
        '200':
          description: One page of results.
          content:
            application/json:
              schema:
                type: object
                description: One page of items.
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/FailedDelivery'
                  next_cursor:
                    type: string
                    description: Where the next page starts; absent on the last page.
                required:
                - items
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
      x-moonitoring-audience: organization
      x-moonitoring-capability: access.webhooks.manage
      x-moonitoring-requirements:
      - '145'
      - '2'
      - '1002.26'
  /failures/{failure_id}/redeliver:
    post:
      summary: Deliver a failed event again
      operationId: redeliverFailedDelivery
      description: Republishes the event to platform.external.v1 with the attribute `moonitoring-subscription` set to
        this subscription, which alone receives it; the receiver deduplicates it by its event id.
      tags:
      - Failures
      parameters:
      - name: failure_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '202':
          description: The result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FailedDelivery'
        '400':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/BadRequest
        '401':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Unauthorized
        '403':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Forbidden
        '404':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/NotFound
        '409':
          $ref: ../../../../common/v1/schemas.yaml#/components/responses/Conflict
      x-moonitoring-audience: organization
      x-moonitoring-capability: access.webhooks.manage
      x-moonitoring-requirements:
      - '145.9'
      - '2'
      - '1002.26'
components:
  schemas:
    WebhookSubscription:
      type: object
      description: 'A registered destination for an Organization''s events: one Pub/Sub subscription on `platform.external.v1`,
        filtered to the Organization and its event types.'
      properties:
        id:
          type: string
          format: uuid
        organization:
          type: string
          format: uuid
          description: ID of the Organization that owns the subscription.
        kind:
          type: string
          enum:
          - https_push
          - pubsub_pull
          description: 'https_push: Pub/Sub calls `url` with an OIDC token; pubsub_pull: `service_account` pulls
            from `subscription_name`.'
        url:
          type: string
          description: Push only. A public HTTPS address; private and internal addresses are refused.
          format: uri
        audience:
          type: string
          description: Push only. The `aud` claim of the OIDC token Google sends, to be checked by the receiver
            together with the issuer accounts.google.com and the e-mail of the delivery service account. It is the push
            URL, Pub/Sub's default.
        service_account:
          type: string
          description: Pull only. The customer's Google service account e-mail, granted `roles/pubsub.subscriber`
            on the subscription.
          format: email
        subscription_name:
          type: string
          description: Pull only. The full Pub/Sub subscription name to pull from, in our project.
        event_types:
          type: array
          items:
            type: string
          description: Event names with their version, e.g. data_issue.opened.v1; the subscription's filter.
        status:
          type: string
          enum:
          - pending_verification
          - active
          - paused
          - disabled
          description: 'pending_verification: a push address that has not answered its challenge yet, which
            receives nothing; paused: by the platform after 24 hours of failures, with the owner told; disabled:
            by the owner.'
        paused_reason:
          type: string
        created_by:
          type: string
          format: uuid
          description: ID of the User.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
      - id
      - organization
      - kind
      - event_types
      - status
      - created_at
      - updated_at
    FailedDelivery:
      type: object
      description: An event Pub/Sub gave up delivering to a subscription, read from the dead-letter topic.
      properties:
        id:
          type: string
          format: uuid
        subscription:
          type: string
          format: uuid
        event_id:
          type: string
          description: The event's unique ID; a receiver ignores an ID it has already processed.
        event_type:
          type: string
        attempts:
          type: integer
          description: Deliveries Pub/Sub attempted before dead-lettering.
        last_response_status:
          type: integer
          description: HTTP status answered by the receiver, push only.
        last_error:
          type: string
        dead_lettered_at:
          type: string
          format: date-time
        redelivered_at:
          type: string
          format: date-time
      required:
      - id
      - subscription
      - event_id
      - event_type
      - attempts
      - dead_lettered_at
    WebhookEventType:
      type: object
      description: 'An event an Organization may subscribe to: those marked as deliverable outside the platform
        in the systems'' event contracts.'
      properties:
        name:
          type: string
          description: e.g. sync_run.failed.v1.
        system:
          type: string
          description: The system that publishes it.
        description:
          type: string
        deprecated:
          type: boolean
      required:
      - name
      - system
    WebhookSubscriptionInput:
      type: object
      description: Registers or changes a destination. A push URL is verified with a signed challenge before the
        subscription becomes active; a pull subscription is created at once and the service account granted on it.
      properties:
        kind:
          type: string
          enum:
          - https_push
          - pubsub_pull
        url:
          type: string
          format: uri
        service_account:
          type: string
          format: email
        event_types:
          type: array
          items:
            type: string
          minItems: 1
        status:
          type: string
          enum:
          - active
          - disabled
      required:
      - kind
      - event_types
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'The short-lived token signed by `access`: an Organization user token, a staff token or a client
        token, as the operation''s audience says.'
