openapi: "3.0.3"

info:
  title: SentryAgent.ai — Webhooks & Event Subscriptions
  version: 1.0.0
  description: |
    Webhook subscription management and delivery history endpoints for the
    SentryAgent.ai AgentIdP platform.

    Webhooks deliver real-time event notifications to registered HTTP endpoints
    when significant platform events occur (agent lifecycle changes, credential
    operations, token events).

    **All endpoints require a valid Bearer JWT** with appropriate scope:
    - `webhooks:read` — required for GET operations
    - `webhooks:write` — required for POST, PATCH, DELETE operations

    **Delivery mechanism:**
    - Events are delivered via `POST` to the registered `url`
    - Each delivery carries a signed JSON envelope (`X-SentryAgent-Signature` header)
    - Failed deliveries are retried with exponential backoff (up to 10 attempts)
    - After 10 failures the subscription is marked `dead_letter`

    **Supported event types:**
    `agent.created`, `agent.updated`, `agent.suspended`, `agent.reactivated`,
    `agent.decommissioned`, `credential.generated`, `credential.rotated`,
    `credential.revoked`, `token.issued`, `token.revoked`

servers:
  - url: http://localhost:3000/api/v1
    description: Local development server
  - url: https://api.sentryagent.ai/v1
    description: Production server

tags:
  - name: Webhook Subscriptions
    description: Create and manage webhook endpoint subscriptions
  - name: Webhook Deliveries
    description: Query delivery history and attempt records

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT access token obtained via `POST /token`.
        Include as `Authorization: Bearer <token>`.

  schemas:
    WebhookEventType:
      type: string
      enum:
        - agent.created
        - agent.updated
        - agent.suspended
        - agent.reactivated
        - agent.decommissioned
        - credential.generated
        - credential.rotated
        - credential.revoked
        - token.issued
        - token.revoked
      description: Platform event type that can be subscribed to.
      example: agent.created

    WebhookDeliveryStatus:
      type: string
      enum:
        - pending
        - delivered
        - failed
        - dead_letter
      description: |
        Current status of a delivery attempt.
        - `pending` — queued for delivery or awaiting retry
        - `delivered` — successfully delivered (HTTP 2xx from target)
        - `failed` — delivery failed; retry scheduled
        - `dead_letter` — all retries exhausted; no further attempts
      example: delivered

    WebhookSubscription:
      type: object
      description: A registered webhook subscription (signing secret never included in responses).
      required:
        - id
        - organization_id
        - name
        - url
        - events
        - active
        - failure_count
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Immutable system-assigned UUID for this subscription.
          readOnly: true
          example: "wh-abcd-1234-5678-ef01"
        organization_id:
          type: string
          format: uuid
          description: Organization that owns this subscription.
          readOnly: true
          example: "org-1234-5678-abcd-ef01"
        name:
          type: string
          description: Human-readable label for this subscription.
          example: "Agent lifecycle events"
        url:
          type: string
          format: uri
          description: HTTPS endpoint that receives webhook payloads.
          example: "https://my-app.example.com/webhooks/sentryagent"
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          description: List of event types this subscription receives.
          minItems: 1
          example:
            - agent.created
            - agent.decommissioned
        active:
          type: boolean
          description: Whether the subscription is currently active. Set to false to pause delivery.
          example: true
        failure_count:
          type: integer
          description: Number of consecutive delivery failures since last success.
          minimum: 0
          readOnly: true
          example: 0
        created_at:
          type: string
          format: date-time
          readOnly: true
          example: "2026-03-01T08:00:00.000Z"
        updated_at:
          type: string
          format: date-time
          readOnly: true
          example: "2026-03-28T11:30:00.000Z"

    CreateWebhookRequest:
      type: object
      description: Request body for creating a new webhook subscription.
      required:
        - name
        - url
        - events
      properties:
        name:
          type: string
          description: Human-readable label for this subscription.
          minLength: 1
          maxLength: 256
          example: "Agent lifecycle events"
        url:
          type: string
          format: uri
          description: HTTPS endpoint URL. Must be HTTPS in production.
          example: "https://my-app.example.com/webhooks/sentryagent"
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          minItems: 1
          description: Event types to subscribe to.
          example:
            - agent.created
            - agent.decommissioned

    UpdateWebhookRequest:
      type: object
      description: |
        Request body for partially updating a webhook subscription.
        All fields are optional; only provided fields are updated.
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 256
          example: "Agent events (updated)"
        url:
          type: string
          format: uri
          example: "https://my-app.example.com/webhooks/v2"
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          minItems: 1
          example:
            - agent.created
            - agent.updated
            - token.issued
        active:
          type: boolean
          description: Set to false to pause delivery without deleting the subscription.
          example: true

    WebhookDelivery:
      type: object
      description: A single webhook delivery attempt record.
      required:
        - id
        - subscription_id
        - event_type
        - payload
        - status
        - attempt_count
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
          example: "del-abcd-1234-5678-ef01"
        subscription_id:
          type: string
          format: uuid
          example: "wh-abcd-1234-5678-ef01"
        event_type:
          $ref: '#/components/schemas/WebhookEventType'
        payload:
          type: object
          description: The JSON payload that was sent (or attempted) to the target URL.
          additionalProperties: true
          example:
            id: "evt-1234-5678"
            event: "agent.created"
            timestamp: "2026-04-07T09:00:00.000Z"
            organization_id: "org-1234-5678-abcd-ef01"
            data:
              agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        status:
          $ref: '#/components/schemas/WebhookDeliveryStatus'
        http_status_code:
          type: integer
          nullable: true
          description: HTTP status code returned by the target endpoint (null if connection failed).
          example: 200
        attempt_count:
          type: integer
          description: Number of delivery attempts made so far.
          minimum: 1
          example: 1
        next_retry_at:
          type: string
          format: date-time
          nullable: true
          description: Scheduled time for the next retry attempt. Null if delivered or dead-lettered.
          example: null
        delivered_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when the delivery was successfully confirmed.
          example: "2026-04-07T09:00:05.000Z"
        created_at:
          type: string
          format: date-time
          readOnly: true
          example: "2026-04-07T09:00:00.000Z"
        updated_at:
          type: string
          format: date-time
          readOnly: true
          example: "2026-04-07T09:00:05.000Z"

    PaginatedDeliveriesResponse:
      type: object
      description: Paginated delivery history response.
      required:
        - deliveries
        - total
        - limit
        - offset
      properties:
        deliveries:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDelivery'
        total:
          type: integer
          example: 87
        limit:
          type: integer
          example: 20
        offset:
          type: integer
          example: 0

    ErrorResponse:
      type: object
      description: Standard error response envelope.
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: "WEBHOOK_NOT_FOUND"
        message:
          type: string
          example: "Webhook subscription with the specified ID was not found."
        details:
          type: object
          additionalProperties: true

  responses:
    Unauthorized:
      description: Missing or invalid Bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "UNAUTHORIZED"
            message: "A valid Bearer token is required to access this resource."

    Forbidden:
      description: Valid token but insufficient scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "FORBIDDEN"
            message: "You do not have permission to perform this action."

    NotFound:
      description: Webhook subscription not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "WEBHOOK_NOT_FOUND"
            message: "Webhook subscription with the specified ID was not found."

    InternalServerError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "INTERNAL_SERVER_ERROR"
            message: "An unexpected error occurred. Please try again later."

security:
  - BearerAuth: []

paths:
  /webhooks:
    post:
      operationId: createWebhookSubscription
      tags:
        - Webhook Subscriptions
      summary: Create a webhook subscription
      description: |
        Creates a new webhook subscription for the authenticated organization.
        A signing secret is generated automatically and returned once in the response
        under a `signingSecret` field. **Store this secret securely — it cannot be retrieved again.**

        Requires `webhooks:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
            example:
              name: "Agent lifecycle events"
              url: "https://my-app.example.com/webhooks/sentryagent"
              events:
                - agent.created
                - agent.decommissioned
      responses:
        '201':
          description: Webhook subscription created successfully.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/WebhookSubscription'
                  - type: object
                    properties:
                      signingSecret:
                        type: string
                        description: |
                          HMAC signing secret for verifying webhook payloads.
                          Returned only once at creation time.
                        example: "whsec_abcdef1234567890abcdef1234567890"
              example:
                id: "wh-abcd-1234-5678-ef01"
                organization_id: "org-1234-5678-abcd-ef01"
                name: "Agent lifecycle events"
                url: "https://my-app.example.com/webhooks/sentryagent"
                events:
                  - agent.created
                  - agent.decommissioned
                active: true
                failure_count: 0
                created_at: "2026-04-07T09:00:00.000Z"
                updated_at: "2026-04-07T09:00:00.000Z"
                signingSecret: "whsec_abcdef1234567890abcdef1234567890"
        '400':
          description: Validation error in request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "Request validation failed."
                details:
                  field: "url"
                  reason: "Must be a valid HTTPS URL."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      operationId: listWebhookSubscriptions
      tags:
        - Webhook Subscriptions
      summary: List webhook subscriptions
      description: |
        Returns all webhook subscriptions for the authenticated organization.
        Requires `webhooks:read` scope.
      responses:
        '200':
          description: List of webhook subscriptions returned successfully.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookSubscription'
              example:
                - id: "wh-abcd-1234-5678-ef01"
                  organization_id: "org-1234-5678-abcd-ef01"
                  name: "Agent lifecycle events"
                  url: "https://my-app.example.com/webhooks/sentryagent"
                  events:
                    - agent.created
                    - agent.decommissioned
                  active: true
                  failure_count: 0
                  created_at: "2026-03-01T08:00:00.000Z"
                  updated_at: "2026-03-01T08:00:00.000Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /webhooks/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: UUID of the webhook subscription.
        schema:
          type: string
          format: uuid
        example: "wh-abcd-1234-5678-ef01"

    get:
      operationId: getWebhookSubscription
      tags:
        - Webhook Subscriptions
      summary: Get a webhook subscription by ID
      description: |
        Returns a single webhook subscription record.
        Requires `webhooks:read` scope.
      responses:
        '200':
          description: Webhook subscription returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
              example:
                id: "wh-abcd-1234-5678-ef01"
                organization_id: "org-1234-5678-abcd-ef01"
                name: "Agent lifecycle events"
                url: "https://my-app.example.com/webhooks/sentryagent"
                events:
                  - agent.created
                  - agent.decommissioned
                active: true
                failure_count: 0
                created_at: "2026-03-01T08:00:00.000Z"
                updated_at: "2026-03-28T11:30:00.000Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    patch:
      operationId: updateWebhookSubscription
      tags:
        - Webhook Subscriptions
      summary: Update a webhook subscription
      description: |
        Partially updates a webhook subscription. Only provided fields are updated.
        Set `active: false` to pause delivery without deleting the subscription.
        Requires `webhooks:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookRequest'
            example:
              active: false
      responses:
        '200':
          description: Webhook subscription updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
              example:
                id: "wh-abcd-1234-5678-ef01"
                organization_id: "org-1234-5678-abcd-ef01"
                name: "Agent lifecycle events"
                url: "https://my-app.example.com/webhooks/sentryagent"
                events:
                  - agent.created
                  - agent.decommissioned
                active: false
                failure_count: 0
                created_at: "2026-03-01T08:00:00.000Z"
                updated_at: "2026-04-07T09:00:00.000Z"
        '400':
          description: Validation error in request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "Request validation failed."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    delete:
      operationId: deleteWebhookSubscription
      tags:
        - Webhook Subscriptions
      summary: Delete a webhook subscription
      description: |
        Permanently deletes a webhook subscription and stops all future deliveries.
        Any pending deliveries in the queue are cancelled.
        Requires `webhooks:write` scope.
      responses:
        '204':
          description: Webhook subscription deleted successfully. No response body.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /webhooks/{id}/deliveries:
    parameters:
      - name: id
        in: path
        required: true
        description: UUID of the webhook subscription.
        schema:
          type: string
          format: uuid
        example: "wh-abcd-1234-5678-ef01"

    get:
      operationId: listWebhookDeliveries
      tags:
        - Webhook Deliveries
      summary: List delivery history for a subscription
      description: |
        Returns the delivery history for a webhook subscription,
        ordered by `created_at` descending (most recent first).

        Use this endpoint to diagnose delivery failures and inspect
        payload content for historical events.

        Requires `webhooks:read` scope.
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/WebhookDeliveryStatus'
          description: Filter deliveries by status.
      responses:
        '200':
          description: Delivery history returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedDeliveriesResponse'
              example:
                deliveries:
                  - id: "del-abcd-1234-5678-ef01"
                    subscription_id: "wh-abcd-1234-5678-ef01"
                    event_type: "agent.created"
                    payload:
                      id: "evt-1234-5678"
                      event: "agent.created"
                      timestamp: "2026-04-07T09:00:00.000Z"
                      organization_id: "org-1234-5678-abcd-ef01"
                      data:
                        agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    status: "delivered"
                    http_status_code: 200
                    attempt_count: 1
                    next_retry_at: null
                    delivered_at: "2026-04-07T09:00:05.000Z"
                    created_at: "2026-04-07T09:00:00.000Z"
                    updated_at: "2026-04-07T09:00:05.000Z"
                total: 87
                limit: 20
                offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
