openapi: 3.1.0
info:
  title: WarnFire API
  version: 0.1.0
  description: Public contract for WarnFire tenant, responder, event-ingestion, and integration APIs.
servers:
  - url: https://api.warnfire.com
    description: WarnFire production API
paths:
  /v1/auth/login:
    post:
      summary: Exchange a customer email and password for a short-lived console session
      operationId: loginCustomer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
                password:
                  type: string
                  minLength: 1
                  maxLength: 255
                  format: password
      responses:
        "200":
          description: Authenticated 24-hour console session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerAuthResult"
        "401":
          description: Invalid email or password
        "403":
          description: Email is not verified (body {"error":"email_not_verified"}); verification is enforced when email is enabled
        "429":
          description: Login rate limit exceeded
  /v1/auth/logout:
    post:
      summary: Revoke the current web-console session
      operationId: logoutCustomer
      security:
        - consoleToken: []
      responses:
        "204":
          description: Session revoked or already revoked
        "401":
          description: Bearer token is missing
        "500":
          description: Session revocation failed
  /v1/auth/responder/login:
    post:
      summary: Exchange a tenant email and password for a mobile responder session
      operationId: loginResponder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
                password:
                  type: string
                  minLength: 1
                  maxLength: 255
                  format: password
      responses:
        "200":
          description: Authenticated 30-day responder session for secure device storage
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResponderAuthResult"
        "401":
          description: Invalid email or password
        "403":
          description: Email unverified or no enabled responder profile with the same tenant email
        "429":
          description: Login rate limit exceeded
  /v1/auth/responder/logout:
    post:
      summary: Revoke the current mobile responder session
      operationId: logoutResponder
      security:
        - responderToken: []
      responses:
        "204":
          description: Session revoked (idempotent)
  /v1/auth/verify/confirm:
    post:
      summary: Confirm an email address with a verification token
      operationId: confirmEmailVerification
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
              properties:
                token:
                  type: string
      responses:
        "200":
          description: Email verified
        "400":
          description: Invalid or expired token
  /v1/auth/verify/resend:
    post:
      summary: Resend a verification email (uniform response; no account enumeration)
      operationId: resendEmailVerification
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
      responses:
        "202":
          description: If an unverified account exists, a verification email was sent
        "429":
          description: Rate limit exceeded
  /v1/auth/password/forgot:
    post:
      summary: Request a password reset (uniform response; no account enumeration)
      operationId: requestPasswordReset
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
      responses:
        "202":
          description: If the account exists, a reset email was sent
        "429":
          description: Rate limit exceeded
  /v1/auth/password/reset:
    post:
      summary: Set a new password with a reset token (revokes all sessions)
      operationId: resetPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
                - password
              properties:
                token:
                  type: string
                password:
                  type: string
                  minLength: 12
                  maxLength: 255
                  format: password
      responses:
        "200":
          description: Password reset; all existing sessions revoked
        "400":
          description: Invalid/expired token or password outside policy
  /v1/me/account-portal-link:
    post:
      summary: Mint a one-time link from the mobile app to the web account page
      description: |
        The signed-in mobile app exchanges its responder session for a 60-second single-use hand-off URL. The app opens that URL in the browser, where the person changes their password or deletes their account without signing in again. The token is carried in the URL fragment so it never reaches a server log. Any earlier unopened link for the same person is invalidated.
      operationId: createAccountPortalLink
      security:
        - responderToken: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                intent:
                  type: string
                  enum:
                    - account
                    - password
                    - delete
                  description: |
                    Which section the page opens on. Presentation only — it grants nothing, so a tampered value cannot widen the session.
      responses:
        "201":
          description: Hand-off link created
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always no-store
          content:
            application/json:
              schema:
                type: object
                required:
                  - url
                  - expires_at
                properties:
                  url:
                    type: string
                    format: uri
                  expires_at:
                    type: string
                    format: date-time
        "401":
          description: Responder session is missing or invalid
        "404":
          description: Responder has no console identity to manage
  /v1/auth/account-portal/exchange:
    post:
      summary: Exchange a mobile hand-off for an account-portal session
      description: |
        Consumes the single-use hand-off and issues a 15-minute session accepted only by /v1/account/*. Consumption happens whether or not a session issues, so a replayed link is always dead. Invalid, expired, already-used, and since-disabled are all reported identically.
      operationId: exchangeAccountPortalHandoff
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
              properties:
                token:
                  type: string
      responses:
        "200":
          description: Portal session issued
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always no-store
          content:
            application/json:
              schema:
                type: object
                required:
                  - token
                  - expires_at
                  - intent
                properties:
                  token:
                    type: string
                  expires_at:
                    type: string
                    format: date-time
                  intent:
                    type: string
                    enum:
                      - account
                      - password
                      - delete
        "400":
          description: Invalid, expired, or already-used link
        "429":
          description: Too many exchange attempts from this address
  /v1/account/overview:
    get:
      summary: What the account page shows before anything is changed
      description: |
        Includes what deleting this account would actually do: `member` removes the person, `workspace` erases the entire organization because they are its last owner. The workspace counts exist so the confirmation screen can state the size of what is about to be destroyed.
      operationId: getAccountOverview
      security:
        - accountPortalToken: []
      responses:
        "200":
          description: Account summary
          content:
            application/json:
              schema:
                type: object
                required:
                  - email
                  - name
                  - role
                  - org_id
                  - org_name
                  - deletion_mode
                  - workspace
                  - subscription_active
                properties:
                  email:
                    type: string
                    format: email
                  name:
                    type: string
                  role:
                    type: string
                  org_id:
                    type: string
                    format: uuid
                  org_name:
                    type: string
                  deletion_mode:
                    type: string
                    enum:
                      - member
                      - workspace
                  subscription_active:
                    type: boolean
                  successor_email:
                    type: string
                    format: email
                    description: Present only when this person is the last owner and others remain — who becomes owner if they delete their account. Only a member who is enabled and has a password can be the successor.
                  successor_name:
                    type: string
                  handover_blocked:
                    type: boolean
                    description: Present (true) when this person is the last owner, others remain, but none of them can take ownership (all disabled or without a password). Deleting would be refused with 409 no_eligible_successor until one can.
                  workspace:
                    type: object
                    properties:
                      services:
                        type: integer
                      responders:
                        type: integer
                      schedules:
                        type: integer
                      policies:
                        type: integer
                      open_incidents:
                        type: integer
        "401":
          description: Portal session missing, expired, or revoked
  /v1/account/password:
    post:
      summary: Change your own password
      description: |
        Requires the current password. On success every other session ends — console, mobile, and any other portal tab — leaving only the tab that made the change.
      operationId: changeOwnPassword
      security:
        - accountPortalToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - current_password
                - new_password
              properties:
                current_password:
                  type: string
                  maxLength: 255
                  format: password
                new_password:
                  type: string
                  minLength: 12
                  maxLength: 255
                  format: password
      responses:
        "200":
          description: Password changed; all other sessions revoked
        "400":
          description: New password outside the 12-255 character policy
        "401":
          description: Portal session invalid, or current password is wrong
        "429":
          description: Too many attempts for this account
  /v1/account/delete:
    post:
      summary: Permanently delete your own account
      description: Immediately and irreversibly deletes the authenticated account. Requires the account password, the literal string DELETE, and the deletion mode shown by the overview and confirmed by the person. For the last owner of an organization this erases the whole workspace, resolves open incidents, stops their escalations, and cancels the subscription. For everyone else the identity is erased and the organization survives with its incident history anonymized. If the mode no longer matches what deleting would do, nothing is deleted and 409 is returned.
      operationId: deleteOwnAccount
      security:
        - accountPortalToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - password
                - confirm
                - mode
              properties:
                password:
                  type: string
                  maxLength: 255
                  format: password
                confirm:
                  type: string
                  enum:
                    - DELETE
                mode:
                  type: string
                  enum:
                    - member
                    - workspace
                  description: The deletion_mode from the overview the person confirmed against.
      responses:
        "200":
          description: Account deleted
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - mode
                properties:
                  status:
                    type: string
                    enum:
                      - deleted
                  mode:
                    type: string
                    enum:
                      - member
                      - workspace
                  org_name:
                    type: string
                  emptied_policies:
                    type: array
                    items:
                      type: string
                    description: Escalation policies left with no responder; the remaining administrators are emailed.
                  emptied_schedules:
                    type: array
                    items:
                      type: string
                  promoted_owner:
                    type: string
                    description: Email of the member made owner because the deleted account was the workspace's last owner; empty otherwise.
        "400":
          description: Confirmation string missing or wrong, or mode missing
        "401":
          description: Portal session invalid, or password is wrong
        "409":
          description: |
            Refused, nothing deleted: deletion_scope_changed — the confirmed mode no longer matches what deleting would do (re-read the overview and confirm again); no_eligible_successor — this is the last owner and no remaining member can take ownership (all disabled or without a password).
          content:
            application/json:
              schema:
                type: object
                required:
                  - error
                properties:
                  error:
                    type: string
                    enum:
                      - deletion_scope_changed
                      - no_eligible_successor
        "429":
          description: Too many attempts for this account
  /v1/account/logout:
    post:
      summary: End the account-portal session
      operationId: logoutAccountPortal
      security:
        - accountPortalToken: []
      responses:
        "204":
          description: Session revoked or already revoked
        "401":
          description: Portal session missing or expired
  /v1/events:
    post:
      summary: Ingest an event (Events API)
      description: |
        The universal ingestion primitive (PagerDuty-Events-v2 shaped). The `integration_key` authenticates the source and routes it to exactly one service. The deprecated `routing_key` alias remains accepted for PagerDuty-compatible senders during migration. Accepted events are committed durably before the response (fail loud, never silent). Idempotency is OPERATION-BOUND: a `client_event_id` is a replay only if it is reused with the same action, dedup key, and canonical request payload. Reusing a `client_event_id` for a different action (for example, a trigger's ID as a resolve), a different dedup key, or changed payload returns 409 and never changes state — checked independently of whether the alarm is currently active. A `trigger` opens or deduplicates into an incident and starts escalation; `resolve` clears the active alert and stops escalation; source `acknowledge` is recorded on the timeline but does not stop escalation. An unresolvable schedule never rolls back an accepted trigger: the incident is committed, the unusable step is recorded on its timeline, and escalation continues to later reachable responders.
      operationId: ingestEvent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventIn"
      responses:
        "202":
          description: Accepted and durably recorded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventAccepted"
        "400":
          description: Malformed or invalid event
        "401":
          description: Unknown, expired, revoked, or disabled integration key
        "403":
          description: |
            The integration key is valid but its owning tenant is suspended. The response body is {"error":"tenant_suspended"}. Distinct from 401 so a tenant recovering from a billing lapse gets an actionable signal.
        "409":
          description: |
            client_event_id already used for a different action or dedup key (operation-bound idempotency conflict).
        "429":
          description: Per-integration event limit or maximum active incidents exceeded.
  /v1/integrations/webhooks/{adapter}:
    post:
      summary: Ingest a provider webhook through a registered adapter
      description: |
        Translates one provider delivery into one canonical WarnFire event and then uses the same validation, idempotency, tenant routing, rate limits, and incident lifecycle as /v1/events. The integration key must be sent as an Authorization Bearer credential or X-WarnFire-Integration-Key; credentials in the JSON body are rejected. The built-in `generic` adapter accepts GenericWebhookEvent and rejects unknown fields and trailing JSON values. The built-in `alertmanager` adapter accepts a Prometheus Alertmanager webhook notification (AlertmanagerWebhook, payload version 4) and maps the whole alert group to one event whose dedup key derives from the stable groupKey. Request bodies are limited to 256 KiB.
      operationId: ingestAdaptedWebhook
      security:
        - integrationKey: []
      parameters:
        - name: adapter
          in: path
          required: true
          schema:
            type: string
            pattern: ^[a-z][a-z0-9-]{0,62}$
          example: generic
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/GenericWebhookEvent"
                - $ref: "#/components/schemas/AlertmanagerWebhook"
      responses:
        "202":
          description: Adapted, accepted, and durably recorded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventAccepted"
        "400":
          description: Malformed or invalid provider payload
        "401":
          description: Missing, conflicting, unknown, expired, revoked, or disabled integration key
        "403":
          description: Integration belongs to a suspended tenant
        "404":
          description: Adapter is not registered
        "409":
          description: Operation-bound idempotency conflict
        "413":
          description: Request exceeds 256 KiB
        "415":
          description: Adapter does not accept the supplied media type
        "429":
          description: Per-integration event limit or maximum active incidents exceeded
  /v1/incidents:
    get:
      summary: List the caller's organization's incidents
      description: |
        Authenticated and org-scoped: returns only incidents whose immutable escalation snapshot includes the responder represented by the token.
      operationId: listIncidents
      security:
        - responderToken: []
      responses:
        "200":
          description: The organization's incidents, newest first
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/IncidentSummary"
        "401":
          description: Missing or invalid responder token
  /v1/me/devices:
    post:
      summary: Register or rotate this app installation's native push token
      operationId: registerDevice
      security:
        - responderToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeviceRegistration"
      responses:
        "200":
          description: Device endpoint registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegisteredDevice"
        "400":
          description: Invalid registration
        "401":
          description: Missing or invalid responder token
        "413":
          description: Request body exceeds the endpoint limit
        "503":
          description: Native push is not configured
  /v1/me/status:
    get:
      summary: Get the signed-in responder's live operational status
      operationId: getResponderStatus
      security:
        - responderToken: []
      responses:
        "200":
          description: Identity, live on-call assignments, upcoming handoffs, and incident counts
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResponderStatus"
        "401":
          description: Missing or invalid responder token
  /v1/me/assistance-requests:
    get:
      summary: List the responder's pending or active incident assistance requests
      operationId: listAssistanceRequests
      security:
        - responderToken: []
      responses:
        "200":
          description: Requests addressed to the signed-in responder
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AssistanceRequest"
        "401":
          description: Missing or invalid responder token
  /v1/me/assistance-targets:
    get:
      summary: List responders and on-call teams available to invite into an incident
      operationId: listAssistanceTargets
      security:
        - responderToken: []
      responses:
        "200":
          description: Organization-scoped invitation targets
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistanceTargets"
        "401":
          description: Missing or invalid responder token
  /v1/incidents/{id}/assistance-requests:
    post:
      summary: Invite a responder or on-call team into an incident response
      description: Available only to an assigned responder or an active incident participant.
      operationId: createResponderAssistanceRequest
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAssistanceRequest"
      responses:
        "201":
          description: Invitation persisted and notification queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistanceRequest"
        "400":
          description: Invalid target, role, message, or incident state
        "401":
          description: Missing or invalid responder token
        "404":
          description: Incident not found or unavailable to this responder
        "409":
          description: A pending invitation already exists for the target
  /v1/assistance-requests/{id}:
    get:
      summary: Get an assistance invitation and the incident context needed to answer it
      operationId: getAssistanceRequest
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Invitation and incident
          content:
            application/json:
              schema:
                type: object
                required:
                  - request
                  - incident
                properties:
                  request:
                    $ref: "#/components/schemas/AssistanceRequest"
                  incident:
                    $ref: "#/components/schemas/Incident"
        "401":
          description: Missing or invalid responder token
        "404":
          description: Request not addressed to this responder
  /v1/assistance-requests/{id}/respond:
    post:
      summary: Accept now, commit to a future time, or decline an assistance invitation
      operationId: respondToAssistanceRequest
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - decision
              properties:
                decision:
                  type: string
                  enum:
                    - accept
                    - accept_later
                    - decline
                reason:
                  type: string
                  maxLength: 500
                available_at:
                  type: string
                  format: date-time
                  description: Required only for accept_later; between one minute and 24 hours from now.
      responses:
        "200":
          description: Recorded response
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistanceRequest"
        "400":
          description: Invalid response or availability time
        "401":
          description: Missing or invalid responder token
        "409":
          description: Invitation is no longer pending
  /v1/incidents/{id}/participants/me/leave:
    post:
      summary: Leave an incident response team while preserving the audit trail
      description: A responder who leaves may receive and accept a new invitation later.
      operationId: leaveIncidentResponse
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 500
      responses:
        "204":
          description: Participant left; timeline activity recorded
        "400":
          description: Invalid reason
        "401":
          description: Missing or invalid responder token
        "409":
          description: Responder is not currently participating
  /v1/me/devices/{installationID}:
    delete:
      summary: Revoke this app installation and its push endpoints
      operationId: revokeDevice
      security:
        - responderToken: []
      parameters:
        - name: installationID
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Device revoked
        "401":
          description: Missing or invalid responder token
        "404":
          description: Installation not found
  /v1/schedules/{id}/on-call:
    get:
      summary: Resolve who is on call for a schedule
      operationId: getOnCall
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: at
          in: query
          description: RFC 3339 instant; defaults to now.
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: The resolved responder
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OnCallResolution"
        "400":
          description: Invalid at timestamp
        "401":
          description: Missing or invalid responder token
        "404":
          description: Schedule not found in this organization
        "409":
          description: Schedule is disabled, empty, or has not begun
  /v1/incidents/{id}:
    get:
      summary: Get an incident with its timeline
      description: |
        Authenticated, org-scoped, and authorized: the caller must present a responder token, the incident must belong to the caller's organization, and the caller must be on the incident's escalation-plan snapshot. Returns 404 for a missing incident, one in another org, OR an unauthorized one — existence never leaks.
      operationId: getIncident
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The incident
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Incident"
        "401":
          description: Missing or invalid responder token
        "404":
          description: No such incident, in another org, or not authorized
  /v1/incidents/{id}/ack:
    post:
      summary: Acknowledge an incident (responder)
      description: |
        Authenticated + authorized (the responder must be on the incident's escalation-plan snapshot). The acknowledging identity is taken from the bearer token, never the request body. A successful acknowledgement is recorded before the API responds and stops further escalation for the incident.
      operationId: acknowledgeIncident
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "202":
          description: Acknowledged
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AckResult"
        "401":
          description: Missing or invalid responder token
        "403":
          description: Authenticated but not a responder for this incident
        "404":
          description: No such incident, or it belongs to another org
        "409":
          description: No active escalation for this incident
  /v1/tenant/me:
    get:
      summary: Return the authenticated tenant identity and organization
      description: |
        A console session receives its tenant principal directly. A tenant API key receives `{principal,scopes}` so an automation operator can verify tenant binding and effective authority without requiring any resource scope. A suspended tenant owner may still reach this debugging route with a console session for billing recovery; suspended-tenant API keys are rejected with 401, and other non-billing tenant routes return 402.
      operationId: getTenantIdentity
      security:
        - tenantToken: []
        - tenantAPIKey: []
      responses:
        "200":
          description: Tenant identity, or API-key identity with its effective scopes
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/TenantPrincipal"
                  - $ref: "#/components/schemas/TenantAPIKeyPrincipal"
              examples:
                session:
                  value:
                    id: 11111111-1111-4111-8111-111111111111
                    name: Andy Operator
                    email: andy@example.com
                    role: tenant_owner
                    org_id: 22222222-2222-4222-8222-222222222222
                    org_name: Example Operations
                    org_slug: example-operations
                    plan: team
                    org_status: active
                    billing_restricted: false
                apiKey:
                  value:
                    principal:
                      id: 33333333-3333-4333-8333-333333333333
                      name: incident-poller
                      email: ""
                      role: api_key
                      org_id: 22222222-2222-4222-8222-222222222222
                      org_name: Example Operations
                      org_slug: example-operations
                      plan: team
                      org_status: active
                      billing_restricted: false
                    scopes:
                      - incidents:read
        "401":
          description: Invalid, expired, revoked, disabled credential, or API key belonging to a suspended tenant
  /v1/tenant/incidents:
    get:
      summary: List the authenticated tenant's incident status projection
      description: |
        A least-privilege automation feed for mapping an alert to its service, observed integrations, current incident lifecycle, and current escalation state. A tenant console session may call this route directly. A tenant API key must hold `incidents:read`; `incidents:manage` implies that scope. The response deliberately omits summaries, event payloads and labels, responders, policies, destinations, provider attempts, communication links, integration-key metadata, and delivery counts.
        Results use stable keyset order `(incident.created_at DESC, incident.id DESC)`. The opaque cursor binds the first page's observation boundary, last tuple, and canonical filters. Incidents created after that boundary appear on the next polling cycle. Mutable lifecycle and escalation state is observed on each request; a multi-page traversal is not a transactionally frozen snapshot. `generated_at` is when the database observation completed, not a state-change revision. Responses are served with `Cache-Control: no-store`.
      operationId: listTenantIncidentStatuses
      security:
        - tenantToken: []
        - tenantAPIKey: []
      x-warnfire-required-scope: incidents:read
      parameters:
        - name: status
          in: query
          description: "`open` includes triggered and acknowledged incidents."
          schema:
            type: string
            enum:
              - open
              - resolved
              - all
            default: open
        - name: service_id
          in: query
          description: Return incidents for exactly this tenant-owned service.
          schema:
            type: string
            format: uuid
        - name: integration_id
          in: query
          description: Return incidents observed through this tenant-owned integration.
          schema:
            type: string
            format: uuid
        - name: dedup_key
          in: query
          description: |
            Exact alert deduplication key. To avoid ambiguous service-wide matches, service_id or integration_id is required whenever dedup_key is present. The server accepts at most 255 UTF-8 bytes.
          schema:
            type: string
            maxLength: 255
        - name: limit
          in: query
          description: Maximum number of incidents in this page.
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 100
        - name: cursor
          in: query
          description: |
            Opaque next_cursor from the immediately preceding page with the same canonical filters. Altered, malformed, or filter-mismatched cursors are rejected rather than restarted silently.
          schema:
            type: string
            maxLength: 4096
      responses:
        "200":
          description: Tenant-scoped incident status page, including an empty page when no incidents match
          headers:
            Cache-Control:
              description: This live tenant projection must not be stored by intermediaries or clients.
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentStatusPage"
              examples:
                activeIncidents:
                  summary: One active incident with another page available
                  value:
                    generated_at: 2026-08-12T18:42:01.123456Z
                    items:
                      - alert:
                          id: 11111111-1111-4111-8111-111111111111
                          dedup_key: checkout/high-error-rate
                          status: triggered
                          created_at: 2026-08-12T18:40:00Z
                        service:
                          id: 22222222-2222-4222-8222-222222222222
                          name: Checkout
                        origin_integration:
                          id: 33333333-3333-4333-8333-333333333333
                          name: Prometheus production
                        observed_integrations:
                          - id: 33333333-3333-4333-8333-333333333333
                            name: Prometheus production
                        incident:
                          id: 44444444-4444-4444-8444-444444444444
                          status: triggered
                          created_at: 2026-08-12T18:40:00Z
                        escalation:
                          state: running
                          current_step: 2
                          exhausted: false
                    next_cursor: opaque-server-cursor
                noMatches:
                  summary: No incidents matched the filters
                  value:
                    generated_at: 2026-08-12T18:42:01.123456Z
                    items: []
        "400":
          description: Invalid UUID, filter combination, limit, or cursor
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: invalid incident query
        "401":
          description: Missing, unknown, expired, revoked, or otherwise invalid tenant credential
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: invalid tenant credential
        "402":
          description: The authenticated tenant is suspended and limited to billing recovery
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: tenant_suspended
        "403":
          description: A valid tenant API key lacks incidents:read
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: insufficient_scope
                required: incidents:read
        "500":
          description: Internal projection or invariant failure; use X-Request-ID for support correlation
          headers:
            X-Request-ID:
              description: Request correlation identifier
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: internal
  /v1/tenant/incidents/{id}:
    get:
      summary: Read one tenant incident's least-privilege status projection
      description: |
        Returns the same non-sensitive item used by the collection, observed at request time. Tenant API keys require `incidents:read` and `incidents:manage` implies read. Missing and cross-tenant identifiers both return 404. Existing clients that require the full operational detail (frozen responders, delivery attempts, timeline, event payloads, participants, and communication links) may continue using `GET /v1/tenant/config/incidents/{id}`; that compatibility route is not a substitute for this bounded automation projection.
      operationId: getTenantIncidentStatus
      security:
        - tenantToken: []
        - tenantAPIKey: []
      x-warnfire-required-scope: incidents:read
      parameters:
        - name: id
          in: path
          required: true
          description: Incident UUID.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Current tenant-scoped incident status projection
          headers:
            Cache-Control:
              description: This live tenant projection must not be stored by intermediaries or clients.
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentStatusDetail"
              example:
                generated_at: 2026-08-12T18:42:01.123456Z
                item:
                  alert:
                    id: 11111111-1111-4111-8111-111111111111
                    dedup_key: checkout/high-error-rate
                    status: triggered
                    created_at: 2026-08-12T18:40:00Z
                  service:
                    id: 22222222-2222-4222-8222-222222222222
                    name: Checkout
                  origin_integration:
                    id: 33333333-3333-4333-8333-333333333333
                    name: Prometheus production
                  observed_integrations:
                    - id: 33333333-3333-4333-8333-333333333333
                      name: Prometheus production
                  incident:
                    id: 44444444-4444-4444-8444-444444444444
                    status: acknowledged
                    created_at: 2026-08-12T18:40:00Z
                    acked_at: 2026-08-12T18:41:17Z
                  escalation:
                    state: acknowledged
                    current_step: 2
                    exhausted: false
        "400":
          description: Incident ID is not a UUID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: incident id must be a UUID
        "401":
          description: Missing, unknown, expired, revoked, or otherwise invalid tenant credential
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: invalid tenant credential
        "402":
          description: The authenticated tenant is suspended and limited to billing recovery
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: tenant_suspended
        "403":
          description: A valid tenant API key lacks incidents:read
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: insufficient_scope
                required: incidents:read
        "404":
          description: Incident is missing or belongs to another tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: incident not found
        "500":
          description: Internal projection or invariant failure; use X-Request-ID for support correlation
          headers:
            X-Request-ID:
              description: Request correlation identifier
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantIncidentAPIError"
              example:
                error: internal
  /v1/tenant/settings:
    get:
      summary: Read tenant-wide operational policy
      operationId: getTenantOperationalSettings
      security:
        - tenantToken: []
      responses:
        "200":
          description: Effective tenant settings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantOperationalSettings"
    put:
      summary: Replace tenant-wide operational policy
      description: Tenant owners and administrators may update settings; every update is audited with its before and after values.
      operationId: updateTenantOperationalSettings
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TenantOperationalSettings"
      responses:
        "200":
          description: Saved tenant settings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantOperationalSettings"
        "400":
          description: Invalid setting or no delivery channel remains enabled
        "403":
          description: Tenant viewer cannot modify settings
  /v1/tenant/sessions:
    get:
      summary: List the caller's active console sessions
      operationId: listConsoleSessions
      security:
        - tenantToken: []
      responses:
        "200":
          description: Active sessions (the caller's is flagged current)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ConsoleSession"
  /v1/tenant/sessions/{id}:
    delete:
      summary: Revoke one of the caller's own sessions
      operationId: revokeConsoleSession
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Session revoked
        "404":
          description: No such session for this user
  /v1/tenant/sessions/revoke-others:
    post:
      summary: Revoke all of the caller's sessions except the current one
      operationId: revokeOtherConsoleSessions
      security:
        - tenantToken: []
      responses:
        "200":
          description: Count revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked:
                    type: integer
  /v1/tenant/audit:
    get:
      summary: Recent tenant security audit events (tenant_owner or tenant_admin)
      operationId: listTenantAudit
      security:
        - tenantToken: []
      responses:
        "200":
          description: Audit events, newest first
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AuditEvent"
        "403":
          description: tenant_owner or tenant_admin required
  /v1/tenant/billing:
    get:
      summary: Current subscription and trial state for the tenant
      description: |
        Reachable by any tenant role, and by a suspended tenant_owner (billing recovery). Reports plan, subscription_status, trial/period end, and org_status so the console can render a billing wall for a suspended tenant. billing_enabled reflects whether checkout is configured.
      operationId: getTenantBilling
      security:
        - tenantToken: []
      responses:
        "200":
          description: Billing state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingState"
        "401":
          description: Invalid tenant credential
  /v1/tenant/members:
    get:
      summary: List console identities in the credential's organization
      operationId: listTenantMembers
      security:
        - tenantToken: []
      responses:
        "200":
          description: Tenant identities
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TenantMember"
    post:
      summary: Create a tenant console identity
      description: Requires tenant_owner.
      operationId: createTenantMember
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TenantMemberInput"
      responses:
        "201":
          description: Tenant identity created
        "403":
          description: tenant_owner role required
  /v1/tenant/members/{id}:
    put:
      summary: Update a tenant console identity or role
      description: Requires tenant_owner and always scopes the target to the credential's organization.
      operationId: updateTenantMember
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TenantMemberInput"
      responses:
        "200":
          description: Tenant identity updated
        "403":
          description: tenant_owner role required
        "409":
          description: Update would remove the final enabled tenant_owner
  /v1/tenant/members/{id}/tokens:
    post:
      summary: Issue a 90-day tenant console token, returned once
      description: Requires tenant_owner.
      operationId: issueTenantMemberToken
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "201":
          description: Raw tenant token shown once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedConsoleToken"
        "403":
          description: tenant_owner role required
    delete:
      summary: Revoke every active token for a tenant identity
      description: Requires tenant_owner.
      operationId: revokeTenantMemberTokens
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Tokens revoked
        "403":
          description: tenant_owner role required
  /v1/tenant/config/snapshot:
    get:
      summary: Read the authenticated tenant's configuration and readiness
      description: |
        tenant_viewer is read-only; tenant_admin and tenant_owner may mutate configuration. Tenant API keys require `config:read`. This broad response includes member contact data and operational configuration; incident-only automation should use `/v1/tenant/incidents` with `incidents:read` instead.
      operationId: getTenantSnapshot
      security:
        - tenantToken: []
        - tenantAPIKey: []
      x-warnfire-required-scope: config:read
      responses:
        "200":
          description: Tenant configuration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminSnapshot"
  /v1/tenant/config/people:
    post:
      summary: Atomically create a tenant person
      description: tenant_admin may submit paging only; console_access or password changes require tenant_owner.
      operationId: createTenantPerson
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdminPersonInput"
      responses:
        "201":
          description: Person created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminPerson"
        "403":
          description: tenant_viewer cannot write, or tenant_owner is required for console access
        "409":
          description: Seat limit, email, owner-lifecycle, or active-configuration conflict
    put:
      summary: Atomically update a tenant person
      description: Omitted paging or console_access sections are left unchanged.
      operationId: updateTenantPerson
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdminPersonInput"
      responses:
        "200":
          description: Person updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminPerson"
        "403":
          description: tenant_viewer cannot write, or tenant_owner is required for console access
        "409":
          description: Seat limit, email, owner-lifecycle, or active-configuration conflict
  /v1/tenant/config/users/{id}/responder-tokens:
    post:
      summary: Issue (or rotate) a responder access token for one responder
      description: |
        tenant_admin/tenant_owner only. Creates a high-entropy bearer credential for a responder in the caller's org; the plaintext is returned exactly once. With rotate=true, the responder's currently active tokens are revoked in the same transaction.
      operationId: issueResponderToken
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                expires_in_days:
                  type: integer
                  minimum: 1
                  maximum: 365
                  default: 90
                rotate:
                  type: boolean
                  default: false
      responses:
        "201":
          description: Token issued (plaintext shown once)
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - token
                  - prefix
                  - expires_at
                properties:
                  id:
                    type: string
                    format: uuid
                  token:
                    type: string
                    description: The plaintext bearer token — shown only here.
                  prefix:
                    type: string
                  expires_at:
                    type: string
                    format: date-time
        "400":
          description: Unknown responder, responder outside the org, or invalid TTL
        "403":
          description: tenant_viewer cannot issue tokens
  /v1/tenant/config/responder-tokens/{id}:
    delete:
      summary: Revoke a responder access token
      description: tenant_admin/tenant_owner only. Revokes a token belonging to a responder in the caller's org.
      operationId: revokeResponderToken
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Token revoked
        "403":
          description: tenant_viewer cannot revoke tokens
        "404":
          description: No such active token in this org
  /v1/tenant/integrations/slack:
    get:
      summary: Read the tenant's Slack installation and explicit mappings
      operationId: getTenantSlackConfiguration
      security:
        - tenantToken: []
      responses:
        "200":
          description: Slack configuration without credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SlackConfiguration"
        "404":
          description: Slack is unavailable
  /v1/tenant/integrations/slack/oauth/start:
    post:
      summary: Create a single-use, tenant-bound Slack OAuth authorization URL
      description: Requires tenant_admin or tenant_owner. State expires after ten minutes and is consumed exactly once.
      operationId: startTenantSlackOAuth
      security:
        - tenantToken: []
      responses:
        "201":
          description: Authorization URL created
          content:
            application/json:
              schema:
                type: object
                required:
                  - authorize_url
                properties:
                  authorize_url:
                    type: string
                    format: uri
        "403":
          description: tenant_viewer cannot install integrations
  /v1/tenant/integrations/slack/channels:
    get:
      summary: List Slack channels available to the connected app
      description: Returns public channels and private channels the WarnFire app can access. Requires tenant_admin or tenant_owner.
      operationId: listTenantSlackChannels
      security:
        - tenantToken: []
      responses:
        "200":
          description: Available channel metadata
        "409":
          description: Slack is not connected or must be reauthorized for channel-read scopes
        "502":
          description: Slack did not accept the channel-list request
  /v1/tenant/integrations/slack/war-room:
    put:
      summary: Configure incident war rooms for the Slack installation
      operationId: putTenantSlackWarRoom
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - enabled
                - private
                - prefix
                - archive_on_resolve
              properties:
                enabled:
                  type: boolean
                private:
                  type: boolean
                prefix:
                  type: string
                  pattern: ^[a-z0-9][a-z0-9-]{0,29}$
                archive_on_resolve:
                  type: boolean
      responses:
        "200":
          description: War-room settings saved
        "400":
          description: Invalid channel prefix or no Slack installation
  /v1/tenant/integrations/slack/retry-failed:
    post:
      summary: Retry terminal Slack deliveries after correcting their cause
      description: Resets only failed service posts, war-room creation, status, invitation, and archive work in the currently connected workspace. Work still retrying is not duplicated.
      operationId: retryTenantSlackFailures
      security:
        - tenantToken: []
      responses:
        "200":
          description: Terminal work requeued
          content:
            application/json:
              schema:
                type: object
                required:
                  - retried
                properties:
                  retried:
                    type: integer
                    minimum: 0
  /v1/tenant/integrations/slack/user-links/{target}:
    put:
      summary: Explicitly bind one Slack member to one enabled WarnFire responder
      operationId: putTenantSlackUserLink
      security:
        - tenantToken: []
      parameters:
        - name: target
          in: path
          required: true
          description: Slack member ID to create or update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - responder_id
              properties:
                responder_id:
                  type: string
                  format: uuid
                slack_name:
                  type: string
      responses:
        "200":
          description: Identity linked
        "400":
          description: Invalid or cross-tenant mapping
        "403":
          description: tenant_viewer cannot modify integrations
    delete:
      summary: Remove a Slack responder mapping
      description: Removing an automatically email-matched link creates a durable opt-out. WarnFire will not recreate it unless an administrator explicitly links the responder again.
      operationId: deleteTenantSlackUserLink
      security:
        - tenantToken: []
      parameters:
        - name: target
          in: path
          required: true
          description: Existing mapping UUID to delete.
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Mapping removed
  /v1/tenant/integrations/slack/service-channels/{target}:
    put:
      summary: Route new incidents for a service to one Slack channel
      operationId: putTenantSlackServiceChannel
      security:
        - tenantToken: []
      parameters:
        - name: target
          in: path
          required: true
          description: Service UUID to create or update.
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - channel_id
                - enabled
              properties:
                channel_id:
                  type: string
                channel_name:
                  type: string
                enabled:
                  type: boolean
      responses:
        "200":
          description: Channel routing saved
        "400":
          description: Invalid or cross-tenant mapping
    delete:
      summary: Remove Slack routing for a service
      operationId: deleteTenantSlackServiceChannel
      security:
        - tenantToken: []
      parameters:
        - name: target
          in: path
          required: true
          description: Existing mapping UUID to delete.
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Mapping removed
  /v1/tenant/integrations/opsgenie/:
    get:
      summary: Read the tenant's Opsgenie migration connection and latest review draft
      description: Returns credential metadata only; the encrypted API key is never exposed.
      operationId: getTenantOpsgenieMigration
      security:
        - tenantToken: []
      responses:
        "200":
          description: Migration state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OpsgenieConfiguration"
        "404":
          description: Opsgenie migration is unavailable
  /v1/tenant/integrations/opsgenie/connection:
    put:
      summary: Validate and encrypt a read-only Opsgenie configuration credential
      description: Requires tenant_admin or tenant_owner. Only the fixed US and EU Opsgenie API hosts are allowed.
      operationId: putTenantOpsgenieConnection
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - region
                - api_key
              properties:
                region:
                  type: string
                  enum:
                    - us
                    - eu
                api_key:
                  type: string
                  minLength: 1
                  maxLength: 8192
                  writeOnly: true
      responses:
        "200":
          description: Credential validated and encrypted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OpsgenieConnection"
        "403":
          description: tenant_viewer cannot modify integrations
        "422":
          description: Opsgenie rejected the credential or configuration-read access
        "502":
          description: Opsgenie could not be reached or returned an invalid response
  /v1/tenant/integrations/opsgenie/imports:
    post:
      summary: Queue an immutable read-only Opsgenie inventory and compatibility assessment
      description: Requires tenant_admin or tenant_owner. This never changes live WarnFire configuration or writes to Opsgenie.
      operationId: createTenantOpsgenieImport
      security:
        - tenantToken: []
      responses:
        "202":
          description: Durable import queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OpsgenieImport"
        "403":
          description: tenant_viewer cannot start imports
        "409":
          description: Another import is already queued or running
  /v1/tenant/integrations/opsgenie/imports/{importID}/apply:
    post:
      summary: Activate a reviewed import — create live WarnFire configuration from the drafts
      description: |
        Requires tenant_admin or tenant_owner. Creates users, teams, schedules, and escalation policies from the reviewed drafts in one transaction, reusing the same integrity checks as normal admin creation. Gated on the cutover report being ready (no dangling references) and runs at most once per import; services and non-translatable resources are reported as skipped for manual follow-up.
      operationId: applyTenantOpsgenieImport
      security:
        - tenantToken: []
      parameters:
        - name: importID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Import applied
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OpsgenieApplyResult"
        "403":
          description: tenant_viewer cannot apply imports
        "409":
          description: The import is not ready to apply or has already been applied
  /v1/tenant/integrations/opsgenie/imports/{importID}/rollback:
    post:
      summary: Undo an applied import — delete exactly the configuration it created
      description: |
        Requires tenant_admin or tenant_owner. Deletes the users, teams, schedules, escalation policies, and overrides the apply created, in one transaction, using the recorded ledger. Refused (deleting nothing) if any created resource is now in use by a service or an incident, or if newer configuration references it. On success the import returns to the reviewed-but-unapplied state so it can be applied again.
      operationId: rollbackTenantOpsgenieImport
      security:
        - tenantToken: []
      parameters:
        - name: importID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Import rolled back
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OpsgenieRollbackResult"
        "403":
          description: tenant_viewer cannot roll back imports
        "409":
          description: The import was not applied, or its resources are in use
  /v1/tenant/config/delivery-activity:
    get:
      summary: Inspect tenant-scoped notification delivery outcomes
      operationId: getTenantDeliveryActivity
      security:
        - tenantToken: []
      parameters:
        - name: days
          in: query
          schema:
            type: integer
            minimum: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Tenant delivery activity and outcome counts
        "401":
          description: Invalid tenant credential
  /v1/tenant/config/onboarding/complete:
    post:
      summary: Atomically create a tenant's first response path
      description: Creates one responder, schedule, escalation policy, service, and Events API integration. This operation is accepted only while the tenant has no existing response configuration. The integration key is returned once.
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - responder_name
                - responder_email
                - schedule_name
                - timezone
                - handoff_time
                - policy_name
                - service_name
              properties:
                responder_name:
                  type: string
                responder_email:
                  type: string
                  format: email
                schedule_name:
                  type: string
                timezone:
                  type: string
                  example: America/Los_Angeles
                handoff_time:
                  type: string
                  example: 09:00
                policy_name:
                  type: string
                service_name:
                  type: string
      responses:
        "201":
          description: Workspace configured; integration_key is returned once
        "400":
          description: Invalid setup input
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "404":
          description: Admin API is disabled
        "409":
          description: Workspace already contains response configuration
      operationId: postV1TenantOnboardingComplete
  /v1/tenant/config/client-errors:
    post:
      summary: Report a privacy-bounded web console failure
      operationId: reportTenantWebClientError
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClientErrorReport"
      responses:
        "204":
          description: Error envelope recorded
        "400":
          description: Invalid or unsafe envelope
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "413":
          description: Request body exceeds the endpoint limit
  /v1/tenant/config/users:
    post:
      summary: Create a responder
      operationId: createTenantUser
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - email
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
      responses:
        "201":
          description: Responder created
        "400":
          description: Invalid input
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Conflicting responder
  /v1/tenant/config/users/{id}:
    put:
      summary: Edit or enable/disable a responder
      operationId: updateTenantUser
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - name
                - email
                - enabled
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
                enabled:
                  type: boolean
      responses:
        "200":
          description: Responder updated
        "400":
          description: Invalid input or ID
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Responder is still used by a schedule or policy
  /v1/tenant/config/cover-requests:
    get:
      summary: List pending requests to cover an on-call shift
      operationId: listTenantCoverRequests
      security:
        - tenantToken: []
      responses:
        "200":
          description: Pending cover requests
        "401":
          description: Invalid tenant credential
  /v1/tenant/config/cover-requests/{id}:
    delete:
      summary: Cancel a pending cover request
      operationId: cancelTenantCoverRequest
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Cover request canceled
        "400":
          description: Invalid or unknown cover request
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
  /v1/tenant/config/teams:
    post:
      summary: Create an incident response team with a primary on-call schedule
      operationId: createTenantTeam
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TeamInput"
      responses:
        "201":
          description: Team created
        "400":
          description: Invalid members, schedule, name, or slug
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Team slug already exists
  /v1/tenant/config/teams/{id}:
    put:
      summary: Update team membership and primary on-call schedule
      operationId: updateTenantTeam
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TeamInput"
      responses:
        "200":
          description: Team updated
        "400":
          description: Invalid team configuration
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
    delete:
      summary: Delete a team that has never been used in an incident invitation
      operationId: deleteTenantTeam
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Team deleted
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Team has recorded incident invitation history; disable it instead
  /v1/tenant/config/users/{id}/reachability-test:
    post:
      summary: Send and observe a real reachability test for one responder
      operationId: testTenantUserReachability
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                channel:
                  type: string
                  enum:
                    - sms
                    - voice
                  default: sms
      responses:
        "202":
          description: Provider delivery outcome or bounded pending outcome
        "400":
          description: Invalid responder, channel, or missing contact method
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
  /v1/tenant/config/schedules:
    post:
      summary: Create an on-call rotation
      operationId: createTenantSchedule
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSchedule"
      responses:
        "201":
          description: Schedule created
        "400":
          description: Invalid input
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Conflicting schedule or cross-org reference
  /v1/tenant/config/schedules/{id}:
    put:
      summary: Replace an on-call rotation atomically
      operationId: updateTenantSchedule
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateSchedule"
      responses:
        "200":
          description: Schedule updated
        "400":
          description: Invalid input or ID
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Disabling would break an enabled service, or the update conflicts
    delete:
      summary: Delete an unused schedule
      operationId: deleteTenantSchedule
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Schedule deleted
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Schedule is referenced by configuration or incident history
  /v1/tenant/config/schedules/{id}/timeline:
    get:
      summary: Computed shifts for one schedule over a range
      description: "The visual scheduler's data source: maximal slots with per-layer and per-override responder attribution, plus merged gaps where nobody is on call. Range defaults to now→+14 days and is capped at 35 days."
      operationId: getTenantScheduleTimeline
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Computed timeline
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScheduleTimeline"
        "400":
          description: Invalid or oversized range
        "401":
          description: Invalid tenant credential
        "404":
          description: Schedule not found
  /v1/tenant/config/unavailability:
    post:
      summary: Record a responder's unavailable interval and reroute inherited coverage
      operationId: createTenantUnavailability
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UnavailabilityWrite"
      responses:
        "201":
          description: Unavailable interval recorded
        "400":
          description: Invalid responder, range, kind, or note
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Interval conflicts with existing coverage state
  /v1/tenant/config/unavailability/{id}:
    delete:
      summary: Remove a responder's unavailable interval
      operationId: deleteTenantUnavailability
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Unavailable interval removed
        "400":
          description: Invalid or unknown interval
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
  /v1/tenant/config/schedule-overrides:
    post:
      summary: Add a temporary schedule override
      operationId: createTenantScheduleOverride
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScheduleOverrideWrite"
      responses:
        "204":
          description: Override created
        "400":
          description: Invalid input
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Conflicting or cross-org reference, or override_conflict with the overlapping overrides
  /v1/tenant/config/schedule-overrides/{id}:
    put:
      summary: Replace a schedule override atomically
      operationId: replaceTenantScheduleOverride
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScheduleOverrideWrite"
      responses:
        "204":
          description: Override replaced
        "400":
          description: Invalid input or unknown override
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Replacement overlaps another override and confirmation was not supplied
    delete:
      summary: Remove a schedule override
      operationId: deleteTenantScheduleOverride
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Override removed
        "400":
          description: Invalid or unknown override
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
  /v1/tenant/config/policies:
    post:
      summary: Create an escalation policy and ordered steps
      operationId: createTenantPolicy
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - steps
              properties:
                name:
                  type: string
                steps:
                  type: array
                  minItems: 1
                  items:
                    $ref: "#/components/schemas/PolicyStep"
      responses:
        "201":
          description: Policy created
        "400":
          description: Invalid input
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Conflicting or cross-org target
  /v1/tenant/config/policies/{id}:
    put:
      summary: Atomically replace an ordered escalation policy
      operationId: updateTenantPolicy
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePolicy"
      responses:
        "200":
          description: Policy updated
        "400":
          description: Invalid steps, target, or ID
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Disabling would break an enabled service
    delete:
      summary: Delete an unused escalation policy
      operationId: deleteTenantPolicy
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Policy deleted
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Policy is still referenced by a service
  /v1/tenant/config/maintenance-windows:
    post:
      summary: Schedule a tenant-wide or service maintenance window
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MaintenanceWindowInput"
      responses:
        "201":
          description: Window scheduled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MaintenanceWindow"
        "400":
          description: Invalid window
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
      operationId: postV1TenantMaintenanceWindows
  /v1/tenant/config/maintenance-windows/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    put:
      summary: Edit a scheduled maintenance window
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MaintenanceWindowInput"
      responses:
        "200":
          description: Window updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MaintenanceWindow"
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
      operationId: putV1TenantMaintenanceWindowsId
    delete:
      summary: Cancel a maintenance window and release suppressed paging
      security:
        - tenantToken: []
      responses:
        "204":
          description: Window canceled
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
      operationId: deleteV1TenantMaintenanceWindowsId
  /v1/tenant/config/services:
    post:
      summary: Create a service and Events API integration
      description: Returns a service-scoped, cryptographically random integration key once.
      operationId: createTenantService
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - policy_id
              properties:
                name:
                  type: string
                policy_id:
                  type: string
                  format: uuid
      responses:
        "201":
          description: Service created; integration key is returned once
        "400":
          description: Invalid input
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Conflicting or cross-org policy
  /v1/tenant/config/services/{id}:
    put:
      summary: Edit or enable/disable a service and its integration
      operationId: updateTenantService
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - name
                - policy_id
                - enabled
              properties:
                name:
                  type: string
                policy_id:
                  type: string
                  format: uuid
                enabled:
                  type: boolean
      responses:
        "200":
          description: Service updated
        "400":
          description: Invalid or disabled policy, or invalid ID
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
    delete:
      summary: Delete a service with no event or incident history
      operationId: deleteTenantService
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Service deleted
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: Service has event or incident history; disable it instead
  /v1/tenant/config/services/{id}/test-event:
    post:
      summary: Trigger a real test incident for a service
      description: Uses the service's Events API integration and the production ingestion, outbox, and escalation paths.
      operationId: triggerTenantTestEvent
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - summary
              properties:
                summary:
                  type: string
                  minLength: 1
                  maxLength: 1024
      responses:
        "202":
          description: Test incident accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventAccepted"
        "400":
          description: Invalid service ID or event
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "404":
          description: Service or integration not found in this organization
        "409":
          description: Integration unavailable
        "429":
          description: Trigger limit exceeded
  /v1/tenant/config/event-adapters/{adapter}/preview:
    post:
      summary: Preview how a provider webhook payload maps to a canonical event
      description: |
        Runs the named adapter over the embedded provider payload and returns the mapped canonical event without ingesting anything. Deterministic and side-effect free. A payload the adapter rejects still returns 200 with valid=false and the adapter's reason, so the console can show the diagnosis. Registered adapter names are listed in the admin snapshot's event_adapters field.
      operationId: previewTenantAdapterEvent
      security:
        - tenantToken: []
      parameters:
        - name: adapter
          in: path
          required: true
          schema:
            type: string
            pattern: ^[a-z][a-z0-9-]{0,62}$
          example: alertmanager
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdapterConsoleRequest"
      responses:
        "200":
          description: Mapping preview (valid or diagnosed rejection)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdapterPreviewResult"
        "400":
          description: Missing or malformed console envelope
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "404":
          description: Adapter is not registered
  /v1/tenant/config/services/{id}/event-adapters/{adapter}/test:
    post:
      summary: Send a provider webhook payload through an adapter as a real test event
      description: |
        Decodes the embedded provider payload with the named adapter and ingests the result for the service's Events API integration through the production validation, idempotency, dedup, rate-limit, and escalation paths. A trigger creates a real incident and pages the on-call responder. The mapped dedup_key and client_event_id are rewritten into a deterministic console-test namespace, so a pasted production payload can never touch a real provider-correlated incident, while re-sending an identical payload still replays and a trigger/resolve sample pair still correlates. Suspended organizations are refused (service reads as not found).
      operationId: testTenantAdapterEvent
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: adapter
          in: path
          required: true
          schema:
            type: string
            pattern: ^[a-z][a-z0-9-]{0,62}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdapterConsoleRequest"
      responses:
        "202":
          description: Adapted test event accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventAccepted"
        "400":
          description: Invalid service ID, console envelope, or provider payload
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "404":
          description: Service, integration, or adapter not found
        "409":
          description: Integration unavailable or event id conflict
        "429":
          description: Trigger limit exceeded
  /v1/tenant/config/services/{id}/integration-keys:
    post:
      summary: Create an additional integration key for zero-downtime rotation
      operationId: createTenantIntegrationKey
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationKeyParams"
      responses:
        "201":
          description: Integration key created and returned once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedIntegrationKey"
        "400":
          description: Invalid service, expiry, warning, or rate-limit configuration
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
  /v1/tenant/config/services/{id}/integration-keys/{keyID}/rotate:
    post:
      summary: Create a replacement while leaving the previous key active
      description: Update the sender and verify use of the replacement before revoking the previous key.
      operationId: rotateTenantIntegrationKey
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: keyID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationKeyParams"
      responses:
        "201":
          description: Replacement key created and returned once; previous key remains active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedIntegrationKey"
        "400":
          description: Invalid service, key, or configuration
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
  /v1/tenant/config/services/{id}/integration-keys/{keyID}/enabled:
    post:
      summary: Suspend or restore an integration key
      description: |
        Sets whether a key may authenticate, without destroying it. A disabled key reports status `disabled` and is rejected like an unknown credential until it is enabled again. Revocation is separate and terminal: a revoked key cannot be restored, and an expired key must be rotated rather than enabled. Disabling the last key that can still authenticate for an enabled service is refused, exactly as revoking it would be.
      operationId: setTenantIntegrationKeyEnabled
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: keyID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
      responses:
        "204":
          description: Key enablement updated
        "400":
          description: Invalid identifier, missing enabled, or the key is revoked or expired
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: An enabled service must retain one key that can authenticate
  /v1/tenant/config/services/{id}/integration-keys/{keyID}:
    delete:
      summary: Revoke an integration key immediately
      operationId: revokeTenantIntegrationKey
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: keyID
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Key revoked
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: An enabled service must retain one active integration key
  /v1/tenant/config/incidents/{id}:
    get:
      summary: Get the live operational view of an incident
      description: |
        Compatibility/full-detail surface for the web console and existing clients. It includes frozen responders, delivery attempts, timeline, event payloads, participants, assistance, and communication links. Tenant API keys require `incidents:read`. New automation that only needs alert-to-incident mapping and escalation state should use `/v1/tenant/incidents/{id}` instead.
      operationId: getTenantIncident
      security:
        - tenantToken: []
        - tenantAPIKey: []
      x-warnfire-required-scope: incidents:read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Incident, frozen escalation plan, delivery attempts, and timeline
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminIncident"
        "400":
          description: Invalid incident ID
        "401":
          description: Invalid tenant credential
        "404":
          description: Incident not found in this organization
  /v1/tenant/config/incidents/{id}/assistance-requests:
    post:
      summary: Invite a responder to join an active incident response team
      operationId: createTenantAssistanceRequest
      security:
        - tenantToken: []
        - tenantAPIKey: []
      x-warnfire-required-scope: incidents:manage
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
                - message
              oneOf:
                - required:
                    - target_user_id
                - required:
                    - target_team_id
              properties:
                target_user_id:
                  type: string
                  format: uuid
                target_team_id:
                  type: string
                  format: uuid
                role:
                  type: string
                  enum:
                    - incident_commander
                    - primary_responder
                    - subject_matter_expert
                    - communications_lead
                    - observer
                message:
                  type: string
                  minLength: 1
                  maxLength: 1000
      responses:
        "201":
          description: Durable invitation and push handoff queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssistanceRequest"
        "400":
          description: Invalid target, role, message, or resolved incident
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "409":
          description: A pending invitation already exists for this responder and incident
  /v1/tenant/config/incidents/{id}/ack:
    post:
      summary: Acknowledge an incident as an administrator
      operationId: acknowledgeTenantIncident
      security:
        - tenantToken: []
        - tenantAPIKey: []
      x-warnfire-required-scope: incidents:manage
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "202":
          description: Durable acknowledgement recorded and workflow notified
        "400":
          description: Invalid incident ID
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "404":
          description: Incident not found in this organization
        "409":
          description: Incident is already resolved
  /v1/tenant/config/incidents/{id}/resolve:
    post:
      summary: Force-resolve an incident as an administrator
      description: Atomically resolves the incident and attached firing signal generations, detaches their active correlation, records the operator and reason, and cancels escalation. A later fresh firing delivery creates a successor incident.
      operationId: resolveTenantIncident
      security:
        - tenantToken: []
        - tenantAPIKey: []
      x-warnfire-required-scope: incidents:manage
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 1
                  maxLength: 1000
      responses:
        "200":
          description: Incident was already resolved
        "202":
          description: Force resolution committed and escalation cancellation requested
          content:
            application/json:
              schema:
                type: object
                required:
                  - incident_id
                  - state_changed
                  - firing_signals
                properties:
                  incident_id:
                    type: string
                    format: uuid
                  state_changed:
                    type: boolean
                    description: True when this request performed the force-resolution transition.
                  firing_signals:
                    type: integer
                    minimum: 0
                    description: Attached firing signal generations closed by this operation.
        "400":
          description: Invalid incident ID, malformed body, or missing/invalid resolution reason
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
        "404":
          description: Incident not found in this organization
  /v1/tenant/config/notification-usage:
    get:
      summary: Read current notification usage, allowances, and credit state
      operationId: getTenantNotificationUsage
      security:
        - tenantToken: []
      responses:
        "200":
          description: Current-month notification usage and allowances
        "401":
          description: Invalid tenant credential
  /v1/tenant/config/notification-usage/international:
    post:
      summary: Enable or disable international notification delivery
      operationId: setTenantInternationalNotifications
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - allow
              properties:
                allow:
                  type: boolean
      responses:
        "200":
          description: Updated notification usage and allowance state
        "400":
          description: Invalid request body
        "401":
          description: Invalid tenant credential
        "403":
          description: Tenant role or API-key scope does not allow this change
  /v1/me/preferences:
    get:
      summary: Return the responder's notification preferences
      operationId: getResponderPreferences
      security:
        - responderToken: []
      responses:
        "200":
          description: Current preferences and the closed set of available sounds
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResponderPreferences"
        "401":
          description: Missing or invalid responder token
    put:
      summary: Set the responder's notification sound
      operationId: setResponderPreferences
      security:
        - responderToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - notification_sound
              additionalProperties: false
              properties:
                notification_sound:
                  type: string
                  enum:
                    - sos
                    - sos-urgent
                    - sos-low
                    - warnfire
                    - default
      responses:
        "200":
          description: Updated preferences
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResponderPreferences"
        "400":
          description: Unknown notification sound, or malformed body
        "401":
          description: Missing or invalid responder token
  /v1/me/shifts:
    get:
      summary: List the responder's upcoming shifts with cover candidates
      description: |
        Upcoming rotation occurrences for the caller across every schedule they belong to, with the peers who could cover each shift and any open cover requests already attached to it.
      operationId: listMyShifts
      security:
        - responderToken: []
      parameters:
        - name: days
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 35
          description: Days of lookahead (default 14).
      responses:
        "200":
          description: Upcoming shifts, always an array
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/MyShift"
        "400":
          description: days must be between 1 and 35
        "401":
          description: Missing or invalid responder token
  /v1/me/schedules/{id}/timeline:
    get:
      summary: Render one schedule's next 14 days for the responder
      description: |
        The schedule must be one the caller belongs to; membership failures return 404 rather than revealing schedule existence.
      operationId: getMyScheduleTimeline
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Timeline slots and gaps for the next 14 days
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScheduleTimeline"
        "400":
          description: Invalid schedule id
        "401":
          description: Missing or invalid responder token
        "404":
          description: Schedule not found, or the caller is not a member
  /v1/me/unavailability:
    get:
      summary: List the responder's own time away
      operationId: listMyUnavailability
      security:
        - responderToken: []
      responses:
        "200":
          description: Current and future time away, always an array
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ResponderUnavailability"
        "401":
          description: Missing or invalid responder token
    post:
      summary: Record time away for the responder
      operationId: createMyUnavailability
      security:
        - responderToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - starts_at
                - ends_at
              additionalProperties: false
              properties:
                starts_at:
                  type: string
                  format: date-time
                ends_at:
                  type: string
                  format: date-time
                  description: Must be after starts_at.
                kind:
                  type: string
                  enum:
                    - time_off
                    - sick
                    - personal
                  default: time_off
                note:
                  type: string
                  maxLength: 500
      responses:
        "201":
          description: Recorded time away
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResponderUnavailability"
        "400":
          description: Invalid time off (bad kind, window, or note)
        "401":
          description: Missing or invalid responder token
  /v1/me/unavailability/{id}:
    delete:
      summary: Remove the responder's own time away
      operationId: deleteMyUnavailability
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Removed
        "401":
          description: Missing or invalid responder token
        "404":
          description: Not found, including another responder's entry
  /v1/me/cover-requests:
    get:
      summary: List cover and swap requests involving the responder
      description: |
        Requests where the caller is the requester, the target, or an open-swap recipient; pending, or answered within the last day. Each entry carries a computed direction of incoming or outgoing.
      operationId: listMyCoverRequests
      security:
        - responderToken: []
      responses:
        "200":
          description: Cover requests, always an array
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CoverRequest"
        "401":
          description: Missing or invalid responder token
    post:
      summary: Ask a peer to cover or swap a shift
      operationId: createCoverRequest
      security:
        - responderToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - schedule_id
                - window_start
                - window_end
              additionalProperties: false
              properties:
                kind:
                  type: string
                  enum:
                    - cover
                    - swap
                  default: cover
                target_user_id:
                  type: string
                  description: Required for cover; optional for swap (empty broadcasts an open swap).
                schedule_id:
                  type: string
                layer_id:
                  type: string
                window_start:
                  type: string
                  format: date-time
                window_end:
                  type: string
                  format: date-time
                counter_start:
                  type: string
                  format: date-time
                  description: Required for swap.
                counter_end:
                  type: string
                  format: date-time
                  description: Required for swap.
                note:
                  type: string
                  maxLength: 500
      responses:
        "201":
          description: Created request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CoverRequest"
        "400":
          description: Invalid cover request
        "401":
          description: Missing or invalid responder token
        "409":
          description: The shift is no longer the caller's — the rotation changed
  /v1/me/cover-requests/{id}:
    delete:
      summary: Withdraw the responder's own pending request
      operationId: withdrawCoverRequest
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Withdrawn
        "401":
          description: Missing or invalid responder token
        "404":
          description: Not found, or not the caller's request
        "409":
          description: The request has already been answered
  /v1/me/cover-requests/{id}/respond:
    post:
      summary: Accept or decline a cover or swap request
      operationId: respondToCoverRequest
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - accept
              additionalProperties: false
              properties:
                accept:
                  type: boolean
                reason:
                  type: string
                  maxLength: 500
      responses:
        "200":
          description: The answered request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CoverRequest"
        "400":
          description: Malformed body
        "401":
          description: Missing or invalid responder token
        "404":
          description: Not found, or not addressed to the caller
        "409":
          description: Already answered, or the shift changed since it was offered
  /v1/me/diagnostics:
    post:
      summary: Send an in-app diagnostics report
      description: |
        The report is logged for support; nothing is stored against the account and no field is echoed back. Body limit 64 KiB; unknown fields are ignored.
      operationId: sendResponderDiagnostics
      security:
        - responderToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                app:
                  type: string
                note:
                  type: string
                  maxLength: 2000
                release:
                  type: string
                  maxLength: 64
                entries:
                  type: array
                  items:
                    type: object
                    properties:
                      at:
                        type: string
                      event:
                        type: string
                      fields:
                        type: object
                        additionalProperties: true
      responses:
        "204":
          description: Report accepted
        "400":
          description: Unreadable or invalid diagnostics report
        "401":
          description: Missing or invalid responder token
        "413":
          description: Request body too large
  /v1/me/test-incident:
    post:
      summary: Trigger a test incident against the caller's own workspace
      description: |
        Idempotent within a 30-second window per responder. Uses the workspace's designated test service; no request body is read.
      operationId: sendTestIncident
      security:
        - responderToken: []
      responses:
        "202":
          description: Test event accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventAccepted"
        "401":
          description: Missing or invalid responder token
        "403":
          description: Tenant suspended
        "409":
          description: No enabled service is available for notification tests
        "429":
          description: Trigger limit exceeded; retry after the Retry-After interval
  /v1/incidents/{id}/resolve:
    post:
      summary: Resolve an incident as a responder
      description: |
        The caller must be a responder on the incident's escalation plan; an optional note is recorded first. Resolution is idempotent — resolving an already-resolved incident still returns 202.
      operationId: resolveIncident
      security:
        - responderToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                note:
                  type: string
      responses:
        "202":
          description: Resolution accepted
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - by
                properties:
                  status:
                    type: string
                    enum:
                      - resolved
                  by:
                    type: string
                    description: Resolving responder's name.
        "400":
          description: Malformed body
        "401":
          description: Missing or invalid responder token
        "404":
          description: Not found, including incidents outside the caller's escalation plan
  /v1/tenant/api-keys:
    get:
      summary: List the workspace's API keys
      description: |
        Signed-in console session only (owner or admin seat); API keys cannot manage API keys. Responses carry Cache-Control no-store.
      operationId: listTenantAPIKeys
      security:
        - tenantToken: []
      responses:
        "200":
          description: Keys and the allowed scope taxonomy
          content:
            application/json:
              schema:
                type: object
                required:
                  - keys
                  - scopes
                properties:
                  keys:
                    type: array
                    items:
                      $ref: "#/components/schemas/TenantAPIKey"
                  scopes:
                    type: array
                    items:
                      type: string
        "401":
          description: Invalid tenant credential
        "402":
          description: Tenant suspended
        "403":
          description: Session-only endpoint, or the seat is not owner/admin
    post:
      summary: Create a scoped API key
      description: The plaintext token is returned exactly once.
      operationId: createTenantAPIKey
      security:
        - tenantToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - scopes
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 120
                scopes:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum:
                      - config:read
                      - config:manage
                      - incidents:read
                      - incidents:manage
                expires_in_days:
                  type: integer
                  minimum: 0
                  maximum: 3650
                  default: 0
                  description: 0 means the key never expires.
      responses:
        "201":
          description: Created key with its one-time plaintext token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantAPIKeyCreated"
        "400":
          description: Invalid name, scopes, or expiry
        "401":
          description: Invalid tenant credential
        "402":
          description: Tenant suspended or billing-restricted
        "403":
          description: Session-only endpoint, or the seat is not owner/admin
        "409":
          description: Active key limit reached (25); revoke a key first
  /v1/tenant/api-keys/{id}:
    delete:
      summary: Revoke an API key
      operationId: revokeTenantAPIKey
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Revoked
        "401":
          description: Invalid tenant credential
        "402":
          description: Tenant suspended or billing-restricted
        "403":
          description: Session-only endpoint, or the seat is not owner/admin
        "404":
          description: No live key with that id
  /v1/tenant/api-keys/{id}/rotate:
    post:
      summary: Rotate an API key's secret
      description: |
        The old secret stops working immediately; there is no overlap window. The new plaintext token is returned exactly once.
      operationId: rotateTenantAPIKey
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "201":
          description: The same key with a fresh one-time token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantAPIKeyCreated"
        "401":
          description: Invalid tenant credential
        "402":
          description: Tenant suspended or billing-restricted
        "403":
          description: Session-only endpoint, or the seat is not owner/admin
        "404":
          description: No active key with that id
  /v1/tenant/members/{id}/password:
    post:
      summary: Set a member's password as the workspace owner
      operationId: setTenantMemberPassword
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - password
              additionalProperties: false
              properties:
                password:
                  type: string
                  minLength: 12
                  maxLength: 255
                require_change:
                  type: boolean
                  default: false
      responses:
        "200":
          description: Password set
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - set
        "400":
          description: Member id must be a UUID, or the password is outside 12-255 characters
        "401":
          description: Invalid tenant credential
        "402":
          description: Tenant suspended or billing-restricted
        "403":
          description: Session-only endpoint, or tenant_owner role required
        "404":
          description: User not found in this workspace
  /v1/tenant/members/{id}/password-setup:
    post:
      summary: Email a member a password setup link
      operationId: sendTenantMemberPasswordSetup
      security:
        - tenantToken: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "202":
          description: Setup email queued; delivery outcome is recorded in the audit trail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - sent
        "400":
          description: Member id must be a UUID
        "401":
          description: Invalid tenant credential
        "402":
          description: Tenant suspended or billing-restricted
        "403":
          description: Session-only endpoint, or tenant_owner role required
        "404":
          description: User not found in this workspace
  /v1/notification-sources:
    get:
      summary: The sender identities WarnFire pages from
      description: |
        Static, public. Lets responders allowlist WarnFire's phone numbers, email sender, and links before their first page.
      operationId: getNotificationSources
      security: []
      responses:
        "200":
          description: Sender identities
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationSources"
  /v1/public/sms-rates:
    get:
      summary: The published per-destination SMS credit rates
      operationId: getPublicSMSRates
      security: []
      responses:
        "200":
          description: Active rate catalog's public view
          content:
            application/json:
              schema:
                type: object
                required:
                  - version
                  - published_at
                  - destinations
                properties:
                  version:
                    type: string
                  published_at:
                    type: string
                    format: date-time
                  destinations:
                    type: array
                    items:
                      type: object
                      properties:
                        calling_prefix:
                          type: string
                        country:
                          type: string
                        band:
                          type: string
                        credits_per_segment:
                          type: integer
                        prohibited:
                          type: boolean
                        enterprise_only:
                          type: boolean
        "404":
          description: No active rate catalog
        "503":
          description: Rates unavailable
  /v2/enqueue:
    post:
      summary: Ingest a PagerDuty Events API v2 event
      description: |
        Compatibility endpoint for senders that already speak the PagerDuty Events API v2. The integration key travels in the body as routing_key; there is no Authorization header. Unknown fields are tolerated, severities are clamped rather than rejected, and a deterministic dedup key is synthesized for keyless triggers. Content-Type must be JSON; body limit 256 KiB.
      operationId: enqueuePagerDutyEvent
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PagerDutyEvent"
      responses:
        "202":
          description: Event processed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PagerDutyResult"
        "400":
          description: Invalid event, or routing_key is invalid or not recognized
        "403":
          description: Account is suspended
        "409":
          description: dedup_key already used for a different action; the response echoes the dedup_key
        "413":
          description: Request body too large
        "415":
          description: Content-Type must be application/json
        "429":
          description: Rate limit exceeded; Retry-After is 60 seconds
  /v1/integrations/webhooks/azure/{key}:
    post:
      summary: Ingest an Azure Monitor common alert
      description: |
        Azure Monitor cannot set authorization headers, so the integration key is the final path segment (an auth_token query parameter is also accepted; supplying both with different values is rejected). Fired maps to trigger and Resolved to resolve; Sev0-4 map onto WarnFire severities. Body limit 256 KiB.
      operationId: ingestAzureWebhook
      security: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
          description: Integration key for the receiving service.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Azure Monitor common alert schema; unknown fields are tolerated. data.essentials.alertRule and a Fired/Resolved monitorCondition are required.
      responses:
        "202":
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventAccepted"
        "400":
          description: Invalid adapter payload or event
        "401":
          description: Integration key required, conflicting, or unknown
        "403":
          description: Tenant suspended
        "409":
          description: Replayed event id with a different action
        "413":
          description: Request body too large
        "415":
          description: application/json required
        "429":
          description: Trigger limit exceeded or ingest overloaded; honor Retry-After
  /v1/integrations/webhooks/gcp/{key}:
    post:
      summary: Ingest a Google Cloud Monitoring alert (key in path)
      operationId: ingestGCPWebhookKeyed
      security: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Cloud Monitoring webhook payload; incident.incident_id and an open/closed incident.state are required. Unknown fields are tolerated.
      responses:
        "202":
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventAccepted"
        "400":
          description: Invalid adapter payload or event
        "401":
          description: Integration key required, conflicting, or unknown
        "403":
          description: Tenant suspended
        "409":
          description: Replayed event id with a different action
        "413":
          description: Request body too large
        "415":
          description: application/json required
        "429":
          description: Trigger limit exceeded or ingest overloaded; honor Retry-After
  /v1/integrations/webhooks/gcp:
    post:
      summary: Ingest a Google Cloud Monitoring alert (native token auth)
      description: |
        Accepts GCP's native webhook token form, where the integration key is the auth_token query parameter instead of a path segment.
      operationId: ingestGCPWebhook
      security: []
      parameters:
        - name: auth_token
          in: query
          required: true
          schema:
            type: string
          description: Integration key for the receiving service.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Cloud Monitoring webhook payload; identical to the keyed GCP route.
      responses:
        "202":
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventAccepted"
        "400":
          description: Invalid adapter payload or event
        "401":
          description: Integration key required or unknown
        "403":
          description: Tenant suspended
        "409":
          description: Replayed event id with a different action
        "413":
          description: Request body too large
        "415":
          description: application/json required
        "429":
          description: Trigger limit exceeded or ingest overloaded; honor Retry-After
  /v1/integrations/webhooks/cloudwatch/{key}:
    post:
      summary: Receive a CloudWatch alarm via Amazon SNS
      description: |
        SNS transport, not a plain webhook. Every message's SNS signature and certificate chain are verified; the first SubscriptionConfirmation binds the topic ARN to the integration key (trust-on-first-use) and is auto-confirmed. Response codes are shaped for SNS's retry behavior; permanent failures return 200 with an empty body so SNS stops retrying.
      operationId: ingestCloudWatchWebhook
      security: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Amazon SNS envelope; the CloudWatch alarm JSON travels in the Message field.
      responses:
        "200":
          description: Notification ingested (body carries the ingest result), or a permanent failure SNS must not retry (empty body)
        "204":
          description: Subscription confirmed, or unsubscribe acknowledged
        "400":
          description: Unreadable request
        "401":
          description: Missing key, invalid SNS signature, or unbound topic
        "409":
          description: Integration key is already bound to another SNS topic
        "413":
          description: Request body too large
        "429":
          description: Rate limit exceeded
        "502":
          description: Subscription confirmation failed
        "503":
          description: CloudWatch SNS is unavailable on this deployment
components:
  schemas:
    CustomerAuthResult:
      type: object
      required:
        - principal
      properties:
        token:
          type: string
          description: Shown once; stored only as a digest. Empty when email_verification_required is true (verify before logging in).
        expires_at:
          type: string
          format: date-time
        principal:
          $ref: "#/components/schemas/TenantPrincipal"
        email_verification_required:
          type: boolean
          description: True when signup withheld the session pending email verification.
    ResponderAuthResult:
      type: object
      required:
        - token
        - expires_at
        - responder
      properties:
        token:
          type: string
          description: Opaque mobile session stored in the device keychain; never entered by a person.
        expires_at:
          type: string
          format: date-time
        responder:
          type: object
          required:
            - id
            - name
            - email
            - org_id
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            email:
              type: string
              format: email
            org_id:
              type: string
              format: uuid
    EventIn:
      type: object
      required:
        - event_action
      anyOf:
        - required:
            - integration_key
        - required:
            - routing_key
      properties:
        integration_key:
          type: string
          description: Service-scoped integration key that authenticates the event source.
        routing_key:
          type: string
          deprecated: true
          description: Deprecated compatibility alias for integration_key.
        event_action:
          type: string
          enum:
            - trigger
            - acknowledge
            - resolve
        dedup_key:
          type: string
          description: |
            Correlates events for one active alert. Required for acknowledge / resolve; on trigger it is generated server-side if omitted.
        client_event_id:
          type: string
          description: Caller-supplied unique id for request idempotency. Distinct from dedup_key.
        payload:
          description: Required when event_action is trigger.
          $ref: "#/components/schemas/EventPayload"
    EventAccepted:
      type: object
      required:
        - status
        - dedup_key
      properties:
        status:
          type: string
          example: accepted
        dedup_key:
          type: string
          description: The correlation key for the active alert (echoed or generated).
        incident_id:
          type: string
          format: uuid
          description: Incident acted on, when one exists.
    GenericWebhookEvent:
      type: object
      additionalProperties: false
      required:
        - event_action
      description: Canonical event envelope without a body credential; the integration key is supplied in an HTTP header.
      properties:
        event_action:
          type: string
          enum:
            - trigger
            - acknowledge
            - resolve
        dedup_key:
          type: string
          maxLength: 255
          description: Required for acknowledge and resolve; optional for trigger.
        client_event_id:
          type: string
          maxLength: 255
          description: Stable provider delivery identifier used for operation-bound idempotency.
        payload:
          description: Required when event_action is trigger.
          $ref: "#/components/schemas/EventPayload"
    AlertmanagerWebhook:
      type: object
      required:
        - version
        - groupKey
        - status
        - alerts
      description: |
        Prometheus Alertmanager webhook notification, payload version 4. Unknown fields are tolerated because Alertmanager adds fields without bumping the version. The group's status drives the event action (firing → trigger, resolved → resolve); the dedup key derives from groupKey; the idempotency key is a hash of the canonical mapped event (action, dedup key, and payload with sorted collections), so provider retries replay safely while any visible update becomes a new event. A resolved group containing a firing member alert is rejected. The conventional commonLabels severity values critical/error/warning/ warn/info are honored; anything else maps to error and the original value is preserved in the event labels.
      properties:
        version:
          type: string
          enum:
            - "4"
        groupKey:
          type: string
          minLength: 1
        truncatedAlerts:
          type: integer
          minimum: 0
        status:
          type: string
          enum:
            - firing
            - resolved
        receiver:
          type: string
        groupLabels:
          type: object
          additionalProperties:
            type: string
        commonLabels:
          type: object
          additionalProperties:
            type: string
        commonAnnotations:
          type: object
          additionalProperties:
            type: string
        externalURL:
          type: string
        alerts:
          type: array
          description: May be empty only when truncatedAlerts is positive.
          items:
            type: object
            required:
              - status
              - fingerprint
            properties:
              status:
                type: string
                enum:
                  - firing
                  - resolved
              labels:
                type: object
                additionalProperties:
                  type: string
              annotations:
                type: object
                additionalProperties:
                  type: string
              startsAt:
                type: string
                format: date-time
              endsAt:
                type: string
                format: date-time
              generatorURL:
                type: string
              fingerprint:
                type: string
                minLength: 1
    IncidentSummary:
      type: object
      required:
        - id
        - status
        - summary
        - created_at
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - triggered
            - acknowledged
            - resolved
        summary:
          type: string
        created_at:
          type: string
        resolved_at:
          type: string
          format: date-time
        service_id:
          type: string
          format: uuid
        service_name:
          type: string
        severity:
          type: string
          enum:
            - critical
            - error
            - warning
            - info
        environment:
          type: string
        region:
          type: string
        escalation_exhausted:
          type: boolean
        paging_suppressed_until:
          type: string
          format: date-time
    DeviceRegistration:
      type: object
      required:
        - installation_id
        - platform
        - provider
        - environment
        - token
      properties:
        installation_id:
          type: string
          maxLength: 200
        app_identifier:
          type: string
          description: Native application identifier. Older compatible clients may omit it.
        platform:
          type: string
          enum:
            - ios
            - android
        provider:
          type: string
          enum:
            - apns
            - fcm
        environment:
          type: string
          enum:
            - development
            - production
        app_version:
          type: string
        locale:
          type: string
        device_name:
          type: string
          maxLength: 120
        device_model:
          type: string
          maxLength: 120
        token:
          type: string
          description: Native APNs/FCM token; encrypted before persistence.
    RegisteredDevice:
      type: object
      required:
        - installation_id
        - endpoint_id
      properties:
        installation_id:
          type: string
        endpoint_id:
          type: string
          format: uuid
    ResponderStatus:
      type: object
      required:
        - user_id
        - name
        - email
        - org_id
        - org_name
        - generated_at
        - is_on_call
        - responders_total
        - responders_active
        - schedules
        - stats
      properties:
        user_id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        org_id:
          type: string
          format: uuid
        org_name:
          type: string
        generated_at:
          type: string
          format: date-time
        is_on_call:
          type: boolean
        responders_total:
          type: integer
          minimum: 0
        responders_active:
          type: integer
          minimum: 0
          description: Distinct enabled responders whose valid credential was used in the last five minutes.
        schedules:
          type: array
          items:
            type: object
            required:
              - id
              - name
              - timezone
              - on_call_names
              - user_is_on_call
              - override_active
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              timezone:
                type: string
              on_call_names:
                type: array
                items:
                  type: string
              user_is_on_call:
                type: boolean
              next_handoff_at:
                type: string
                format: date-time
              override_active:
                type: boolean
        stats:
          type: object
          required:
            - triggered
            - acknowledged
            - resolved_24h
            - critical_open
            - escalation_exhausted_open
            - received_24h
            - average_ack_seconds_24h
            - average_resolve_seconds_24h
          properties:
            triggered:
              type: integer
              minimum: 0
            acknowledged:
              type: integer
              minimum: 0
            resolved_24h:
              type: integer
              minimum: 0
            critical_open:
              type: integer
              minimum: 0
            escalation_exhausted_open:
              type: integer
              minimum: 0
            received_24h:
              type: integer
              minimum: 0
            oldest_open_at:
              type: string
              format: date-time
            average_ack_seconds_24h:
              type: number
              minimum: 0
            average_resolve_seconds_24h:
              type: number
              minimum: 0
    AssistanceRequest:
      type: object
      required:
        - id
        - incident_id
        - incident_summary
        - target_user_id
        - target_name
        - requested_by_name
        - requested_role
        - message
        - status
        - expires_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        incident_id:
          type: string
          format: uuid
        incident_summary:
          type: string
        target_user_id:
          type: string
          format: uuid
        target_name:
          type: string
        target_team_id:
          type: string
          format: uuid
        target_team_name:
          type: string
        requested_by_name:
          type: string
        requested_role:
          type: string
          enum:
            - incident_commander
            - primary_responder
            - subject_matter_expert
            - communications_lead
            - observer
        message:
          type: string
        status:
          type: string
          enum:
            - pending
            - accepted
            - accepted_delayed
            - declined
            - expired
            - canceled
        available_at:
          type: string
          format: date-time
        response_reason:
          type: string
        responded_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    AssistanceTargets:
      type: object
      required:
        - users
        - teams
      properties:
        users:
          type: array
          items:
            $ref: "#/components/schemas/AssistanceTarget"
        teams:
          type: array
          items:
            $ref: "#/components/schemas/AssistanceTarget"
    CreateAssistanceRequest:
      type: object
      required:
        - role
        - message
      oneOf:
        - required:
            - target_user_id
        - required:
            - target_team_id
      properties:
        target_user_id:
          type: string
          format: uuid
        target_team_id:
          type: string
          format: uuid
        role:
          type: string
          enum:
            - incident_commander
            - primary_responder
            - subject_matter_expert
            - communications_lead
            - observer
        message:
          type: string
          minLength: 1
          maxLength: 1000
    Incident:
      type: object
      required:
        - id
        - status
        - timeline
        - events
        - update_count
        - maintenance_override
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - triggered
            - acknowledged
            - resolved
        summary:
          type: string
        source:
          type: string
        severity:
          type: string
          enum:
            - critical
            - error
            - warning
            - info
        component:
          type: string
        observed_at:
          type: string
          format: date-time
        description:
          type: string
        environment:
          type: string
        region:
          type: string
        details:
          type: object
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
              - type: "null"
        labels:
          type: object
          additionalProperties:
            type: string
        tags:
          type: array
          items:
            type: string
        links:
          type: array
          items:
            $ref: "#/components/schemas/EventLink"
        acked_by:
          type: string
        update_count:
          type: integer
          minimum: 0
        paging_suppressed_until:
          type: string
          format: date-time
        maintenance_override:
          type: boolean
        timeline:
          type: array
          items:
            $ref: "#/components/schemas/TimelineEntry"
        events:
          type: array
          items:
            $ref: "#/components/schemas/IncidentEvent"
        comms:
          type: array
          items:
            $ref: "#/components/schemas/IncidentCommsLink"
    OnCallResolution:
      type: object
      required:
        - schedule_id
        - user_id
        - at
        - override
      properties:
        schedule_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        at:
          type: string
          format: date-time
        override:
          type: boolean
        responders:
          type: array
          description: Every responder contributed by an active rotation layer, in layer order.
          items:
            type: object
            required:
              - user_id
              - override
            properties:
              user_id:
                type: string
                format: uuid
              layer_id:
                type: string
                format: uuid
              layer_name:
                type: string
              override:
                type: boolean
    AckResult:
      type: object
      required:
        - status
        - by
      properties:
        status:
          type: string
          example: acknowledged
        by:
          type: string
          description: Identity resolved from the bearer token.
    TenantPrincipal:
      type: object
      required:
        - id
        - name
        - email
        - role
        - org_id
        - org_name
        - org_slug
        - plan
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        role:
          type: string
          enum:
            - tenant_owner
            - tenant_admin
            - tenant_viewer
        org_id:
          type: string
          format: uuid
        org_name:
          type: string
        org_slug:
          type: string
        plan:
          type: string
          enum:
            - trial
            - homelab
            - team
            - business
            - enterprise
        org_status:
          type: string
          enum:
            - active
            - suspended
    TenantAPIKeyPrincipal:
      type: object
      additionalProperties: false
      required:
        - principal
        - scopes
      properties:
        principal:
          type: object
          additionalProperties: false
          required:
            - id
            - name
            - email
            - role
            - org_id
            - org_name
            - org_slug
            - plan
            - org_status
            - billing_restricted
          properties:
            id:
              type: string
              format: uuid
              description: Tenant API-key ID.
            name:
              type: string
              description: Human-assigned API-key name.
            email:
              type: string
              const: ""
              description: API keys have no mailbox.
            role:
              type: string
              const: api_key
            org_id:
              type: string
              format: uuid
            org_name:
              type: string
            org_slug:
              type: string
            plan:
              type: string
              enum:
                - trial
                - homelab
                - team
                - business
                - enterprise
            org_status:
              type: string
              enum:
                - active
                - suspended
            billing_restricted:
              type: boolean
        scopes:
          type: array
          uniqueItems: true
          items:
            type: string
            enum:
              - config:read
              - config:manage
              - incidents:read
              - incidents:manage
    TenantIncidentStatusPage:
      type: object
      additionalProperties: false
      required:
        - generated_at
        - items
      properties:
        generated_at:
          type: string
          format: date-time
          description: Database observation completion time, not a state-change revision.
        items:
          type: array
          items:
            $ref: "#/components/schemas/TenantIncidentStatusItem"
        next_cursor:
          type: string
          description: Opaque keyset cursor for the next page with identical filters; omitted at the end.
    TenantIncidentAPIError:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: string
        required:
          type: string
          description: Required API-key scope, present for insufficient_scope.
    TenantIncidentStatusDetail:
      type: object
      additionalProperties: false
      required:
        - generated_at
        - item
      properties:
        generated_at:
          type: string
          format: date-time
          description: Database observation completion time, not a state-change revision.
        item:
          $ref: "#/components/schemas/TenantIncidentStatusItem"
    TenantOperationalSettings:
      type: object
      additionalProperties: false
      required:
        - war_rooms_enabled
        - war_room_min_severity
        - war_room_auto_close
        - war_room_archive_delay_seconds
        - allow_push
        - allow_email
        - allow_sms
        - allow_voice
        - default_ack_timeout_seconds
        - usage_warning_percent
      properties:
        war_rooms_enabled:
          type: boolean
        war_room_min_severity:
          type: string
          enum:
            - warning
            - error
            - critical
        war_room_auto_close:
          type: boolean
        war_room_archive_delay_seconds:
          type: integer
          minimum: 0
          maximum: 604800
          default: 3600
        allow_push:
          type: boolean
        allow_email:
          type: boolean
        allow_sms:
          type: boolean
        allow_voice:
          type: boolean
        default_ack_timeout_seconds:
          type: integer
          minimum: 30
          maximum: 3600
        sms_monthly_cap:
          type:
            - integer
            - "null"
          minimum: 0
        voice_monthly_cap:
          type:
            - integer
            - "null"
          minimum: 0
        usage_warning_percent:
          type: integer
          minimum: 50
          maximum: 100
    ConsoleSession:
      type: object
      required:
        - id
        - created_at
        - current
      properties:
        id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        user_agent:
          type: string
        ip:
          type: string
        current:
          type: boolean
          description: True for the session making this request.
    AuditEvent:
      type: object
      required:
        - id
        - created_at
        - action
      properties:
        id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        actor_email:
          type: string
        actor_role:
          type: string
        action:
          type: string
          example: login
        target_type:
          type: string
        target_id:
          type: string
        detail:
          type: object
          additionalProperties: true
        ip:
          type: string
    BillingState:
      type: object
      required:
        - plan
        - subscription_status
        - cancel_at_period_end
        - has_subscription
        - seats
        - org_status
        - billing_enabled
      properties:
        plan:
          type: string
          enum:
            - trial
            - homelab
            - team
            - business
            - enterprise
        subscription_status:
          type: string
          enum:
            - trialing
            - active
            - past_due
            - canceled
            - unpaid
            - incomplete
            - incomplete_expired
            - paused
            - comped
            - free
        trial_ends_at:
          type: string
          format: date-time
        current_period_end:
          type: string
          format: date-time
        cancel_at_period_end:
          type: boolean
        has_subscription:
          type: boolean
        seats:
          type: integer
          minimum: 1
          description: Distinct enabled people across paging and console access, deduplicated by normalized email.
        founding:
          type: boolean
        org_status:
          type: string
          enum:
            - active
            - suspended
        billing_enabled:
          type: boolean
        billing_restricted:
          type: boolean
        dunning_started_at:
          type: string
          format: date-time
    TenantMember:
      allOf:
        - $ref: "#/components/schemas/TenantMemberInput"
        - type: object
          required:
            - id
            - org_id
          properties:
            id:
              type: string
              format: uuid
            org_id:
              type: string
              format: uuid
    TenantMemberInput:
      type: object
      additionalProperties: false
      required:
        - name
        - email
        - role
        - enabled
      properties:
        name:
          type: string
        email:
          type: string
          format: email
        role:
          type: string
          enum:
            - tenant_owner
            - tenant_admin
            - tenant_viewer
        enabled:
          type: boolean
    CreatedConsoleToken:
      type: object
      required:
        - token
        - expires_at
      properties:
        token:
          type: string
          description: 256-bit bearer credential returned only in this response.
        expires_at:
          type: string
          format: date-time
    AdminSnapshot:
      type: object
      required:
        - users
        - teams
        - schedules
        - overrides
        - policies
        - services
        - incidents
        - warnings
        - ready
        - ingest_protections
      properties:
        users:
          type: array
          items:
            type: object
            required:
              - id
              - name
              - email
              - enabled
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              email:
                type: string
                format: email
              enabled:
                type: boolean
        teams:
          type: array
          items:
            $ref: "#/components/schemas/Team"
        schedules:
          type: array
          items:
            type: object
            description: Includes current on-call identity and a bounded 14-day coverage preview.
        event_adapters:
          type: array
          description: Registered provider webhook adapter names, for the console adapter tester.
          items:
            type: string
        responder_tokens:
          type: array
          items:
            type: object
            description: Responder bearer-credential lifecycle (secret never included; prefix + status only).
            properties:
              id:
                type: string
                format: uuid
              user_id:
                type: string
                format: uuid
              user_name:
                type: string
              prefix:
                type: string
              status:
                type: string
                enum:
                  - active
                  - expired
                  - revoked
              created_at:
                type: string
                format: date-time
              expires_at:
                type: string
                format: date-time
              last_used_at:
                type: string
                format: date-time
              rotated:
                type: boolean
        overrides:
          type: array
          items:
            type: object
        policies:
          type: array
          items:
            type: object
        services:
          type: array
          items:
            type: object
            description: Includes enabled state, integration_health, last_event_at, and redacted integration-key lifecycle metadata.
        ingest_protections:
          type: object
          required:
            - default_triggers_per_minute
            - max_active_incidents_per_service
          properties:
            default_triggers_per_minute:
              type: integer
            max_active_incidents_per_service:
              type: integer
              description: Maximum number of incidents that may be active for one service. Existing incidents remain actionable after the limit is reached.
        incidents:
          type: array
          items:
            $ref: "#/components/schemas/IncidentSummary"
        maintenance_windows:
          type: array
          items:
            $ref: "#/components/schemas/MaintenanceWindow"
        warnings:
          type: array
          items:
            type: object
            required:
              - code
              - severity
              - resource_type
              - message
            properties:
              code:
                type: string
              severity:
                type: string
                enum:
                  - warning
                  - error
              resource_type:
                type: string
              resource_id:
                type: string
                format: uuid
              message:
                type: string
        ready:
          type: boolean
          description: False when an enabled paging path is unusable.
    AdminPersonInput:
      type: object
      additionalProperties: false
      required:
        - name
        - email
      properties:
        responder_id:
          type: string
          format: uuid
          description: Existing paging identity; omit when creating that half.
        console_id:
          type: string
          format: uuid
          description: Existing console identity; omit when creating that half.
        name:
          type: string
        email:
          type: string
          format: email
        paging:
          type: object
          additionalProperties: false
          required:
            - enabled
          description: Omit to leave the responder identity unchanged.
          properties:
            enabled:
              type: boolean
            phone:
              type: string
              pattern: ^$|^\+[1-9][0-9]{7,14}$
            sms_consent:
              type: boolean
              description: Required when phone is non-empty.
        console_access:
          type: object
          additionalProperties: false
          required:
            - role
            - enabled
          description: Omit to leave console role and sign-in access unchanged.
          properties:
            role:
              type: string
              enum:
                - tenant_owner
                - tenant_admin
                - tenant_viewer
            enabled:
              type: boolean
        password:
          type: object
          additionalProperties: false
          required:
            - action
          properties:
            action:
              type: string
              enum:
                - keep
                - setup_link
                - set
            value:
              type: string
              minLength: 12
              maxLength: 255
              writeOnly: true
            require_change:
              type: boolean
    AdminPerson:
      type: object
      properties:
        responder:
          type: object
          required:
            - id
            - name
            - email
            - enabled
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            email:
              type: string
              format: email
            phone:
              type: string
            enabled:
              type: boolean
        console_access:
          $ref: "#/components/schemas/TenantMember"
        password_setup_status:
          type: string
          enum:
            - sent
            - send_error
    SlackConfiguration:
      type: object
      required:
        - user_links
        - channels
        - delivery_issues
      properties:
        installation:
          type: object
          required:
            - id
            - team_id
            - team_name
            - bot_user_id
            - enabled
          properties:
            id:
              type: string
              format: uuid
            team_id:
              type: string
            team_name:
              type: string
            bot_user_id:
              type: string
            enabled:
              type: boolean
            war_room:
              type: object
              required:
                - enabled
                - private
                - prefix
                - archive_on_resolve
              properties:
                enabled:
                  type: boolean
                private:
                  type: boolean
                prefix:
                  type: string
                archive_on_resolve:
                  type: boolean
            missing_scopes:
              type: array
              items:
                type: string
        user_links:
          type: array
          items:
            type: object
            required:
              - id
              - slack_user_id
              - slack_name
              - responder_id
              - responder_name
            properties:
              id:
                type: string
                format: uuid
              slack_user_id:
                type: string
              slack_name:
                type: string
              responder_id:
                type: string
                format: uuid
              responder_name:
                type: string
              origin:
                type: string
                enum:
                  - admin
                  - email_match
        channels:
          type: array
          items:
            type: object
            required:
              - id
              - service_id
              - service_name
              - channel_id
              - channel_name
              - enabled
            properties:
              id:
                type: string
                format: uuid
              service_id:
                type: string
                format: uuid
              service_name:
                type: string
              channel_id:
                type: string
              channel_name:
                type: string
              enabled:
                type: boolean
              last_error_code:
                type: string
              delivery_state:
                type: string
                enum:
                  - pending
                  - sending
                  - failed
              attempts:
                type: integer
                minimum: 0
        delivery_issues:
          type: array
          items:
            type: object
            required:
              - incident_id
              - kind
              - state
              - attempts
              - error_code
              - terminal
            properties:
              incident_id:
                type: string
                format: uuid
              kind:
                type: string
                enum:
                  - service_message
                  - war_room_open
                  - war_room_status
                  - war_room_invite
                  - war_room_archive
              state:
                type: string
                enum:
                  - retrying
                  - failed
              attempts:
                type: integer
                minimum: 1
              error_code:
                type: string
              destination:
                type: string
              next_retry_at:
                type: string
                format: date-time
              terminal:
                type: boolean
    OpsgenieConfiguration:
      type: object
      required:
        - imports
        - items
      properties:
        connection:
          $ref: "#/components/schemas/OpsgenieConnection"
        imports:
          type: array
          items:
            $ref: "#/components/schemas/OpsgenieImport"
        items:
          type: array
          items:
            $ref: "#/components/schemas/OpsgenieImportItem"
        report:
          type: object
          description: Cutover-readiness assessment of the latest import (go/no-go, totals, blockers, warnings).
        shadow:
          type: object
          description: Schedule shadow run comparing the translated drafts against Opsgenie on-call over the coming week.
        alert_shadow:
          type: object
          description: "Alert-stream shadow: a sample of recent real Opsgenie alerts replayed against the drafts, scoring whether WarnFire would page the responder Opsgenie actually engaged."
    OpsgenieConnection:
      type: object
      required:
        - id
        - region
        - api_key_hint
        - enabled
        - last_validated_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        region:
          type: string
          enum:
            - us
            - eu
        api_key_hint:
          type: string
          description: Redacted display suffix only.
        enabled:
          type: boolean
        last_validated_at:
          type: string
          format: date-time
        last_import_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    OpsgenieImport:
      type: object
      required:
        - id
        - status
        - exact_count
        - approximate_count
        - ambiguous_count
        - unsupported_count
        - created_at
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - queued
            - running
            - succeeded
            - failed
        exact_count:
          type: integer
          minimum: 0
        approximate_count:
          type: integer
          minimum: 0
        ambiguous_count:
          type: integer
          minimum: 0
        unsupported_count:
          type: integer
          minimum: 0
        error_code:
          type: string
        created_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
        applied_at:
          type: string
          format: date-time
          description: Set once the reviewed import has been applied to live configuration.
    OpsgenieApplyResult:
      type: object
      required:
        - import_id
        - applied_at
        - created
        - skipped
      properties:
        import_id:
          type: string
          format: uuid
        applied_at:
          type: string
          format: date-time
        created:
          type: object
          required:
            - users
            - teams
            - schedules
            - escalations
            - overrides
          properties:
            users:
              type: integer
              minimum: 0
            teams:
              type: integer
              minimum: 0
            schedules:
              type: integer
              minimum: 0
            escalations:
              type: integer
              minimum: 0
            overrides:
              type: integer
              minimum: 0
        skipped:
          type: array
          items:
            type: object
            required:
              - resource_type
              - name
              - external_id
              - reason
            properties:
              resource_type:
                type: string
              name:
                type: string
              external_id:
                type: string
              reason:
                type: string
    OpsgenieRollbackResult:
      type: object
      required:
        - import_id
        - removed
      properties:
        import_id:
          type: string
          format: uuid
        removed:
          type: object
          required:
            - users
            - teams
            - schedules
            - escalations
            - overrides
          properties:
            users:
              type: integer
              minimum: 0
            teams:
              type: integer
              minimum: 0
            schedules:
              type: integer
              minimum: 0
            escalations:
              type: integer
              minimum: 0
            overrides:
              type: integer
              minimum: 0
    ClientErrorReport:
      type: object
      additionalProperties: false
      required:
        - client
        - operation
        - code
      properties:
        client:
          type: string
          enum:
            - web
            - mobile
        operation:
          type: string
          pattern: ^[A-Za-z0-9_.:-]{1,64}$
        code:
          type: string
          pattern: ^[A-Za-z0-9_.:-]{1,64}$
        status:
          type: integer
          minimum: 0
          maximum: 599
        release:
          type: string
          pattern: ^[A-Za-z0-9_.:-]{1,64}$
        correlation_id:
          type: string
          format: uuid
    TeamInput:
      type: object
      additionalProperties: false
      required:
        - name
        - slug
        - primary_schedule_id
        - member_user_ids
        - enabled
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        slug:
          type: string
          pattern: ^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$
        description:
          type: string
          maxLength: 1000
        primary_schedule_id:
          type: string
          format: uuid
        member_user_ids:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
            format: uuid
        enabled:
          type: boolean
    CreateSchedule:
      type: object
      additionalProperties: false
      required:
        - name
        - timezone
        - layers
      properties:
        name:
          type: string
        timezone:
          type: string
          example: America/Los_Angeles
        team_id:
          type: string
          format: uuid
          description: Owning team ("Cloud on-call" belongs to the Cloud team); omit or empty for an unowned schedule.
        layers:
          type: array
          minItems: 1
          maxItems: 20
          items:
            $ref: "#/components/schemas/ScheduleLayer"
    UpdateSchedule:
      allOf:
        - $ref: "#/components/schemas/CreateSchedule"
        - type: object
          required:
            - enabled
          properties:
            enabled:
              type: boolean
    ScheduleTimeline:
      type: object
      properties:
        schedule_id:
          type: string
          format: uuid
        name:
          type: string
        timezone:
          type: string
        enabled:
          type: boolean
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        slots:
          type: array
          items:
            type: object
            properties:
              starts_at:
                type: string
                format: date-time
              ends_at:
                type: string
                format: date-time
              override:
                type: boolean
              responders:
                type: array
                items:
                  type: object
                  properties:
                    user_id:
                      type: string
                      format: uuid
                    user_name:
                      type: string
                    layer_id:
                      type: string
                      format: uuid
                      description: Empty for a schedule-wide override.
                    layer_name:
                      type: string
                    override:
                      type: boolean
                    override_id:
                      type: string
                      format: uuid
        gaps:
          description: Merged spans where nobody is on call.
          type: array
          items:
            type: object
            properties:
              starts_at:
                type: string
                format: date-time
              ends_at:
                type: string
                format: date-time
    UnavailabilityWrite:
      type: object
      required:
        - user_id
        - starts_at
        - ends_at
      properties:
        user_id:
          type: string
          format: uuid
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        kind:
          type: string
          enum:
            - time_off
            - sick
            - personal
          default: time_off
        note:
          type: string
          maxLength: 500
    ScheduleOverrideWrite:
      type: object
      additionalProperties: false
      required:
        - schedule_id
        - user_id
        - starts_at
        - ends_at
      properties:
        schedule_id:
          type: string
          format: uuid
        layer_id:
          type: string
          format: uuid
          description: Optional layer scope; omit to replace all active layers.
        user_id:
          type: string
          format: uuid
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        note:
          type: string
          description: Free-text reason shown in tooltips and audit history.
        kind:
          type: string
          enum:
            - override
            - swap
            - vacation
          default: override
        confirm_conflicts:
          type: boolean
          default: false
          description: When false, a write that overlaps existing overrides fails with 409 override_conflict listing them; retry with true to commit anyway (last-created wins for the overlap).
    PolicyStep:
      type: object
      additionalProperties: false
      required:
        - delay_seconds
        - condition
        - targets
      properties:
        position:
          type: integer
          minimum: 1
        delay_seconds:
          type: integer
          minimum: 0
          maximum: 604800
        condition:
          type: string
          enum:
            - while_unacknowledged
            - while_unresolved
        targets:
          type: array
          minItems: 1
          maxItems: 20
          items:
            type: object
            additionalProperties: false
            oneOf:
              - required:
                  - user_id
              - required:
                  - schedule_id
            properties:
              user_id:
                type: string
                format: uuid
              schedule_id:
                type: string
                format: uuid
    UpdatePolicy:
      type: object
      additionalProperties: false
      required:
        - name
        - enabled
        - steps
      properties:
        name:
          type: string
        enabled:
          type: boolean
        steps:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/PolicyStep"
    MaintenanceWindowInput:
      type: object
      required:
        - name
        - starts_at
        - ends_at
        - timezone
        - allow_critical_override
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        reason:
          type: string
          maxLength: 1000
        service_id:
          type: string
          format: uuid
          description: Omit for all services in the tenant.
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        timezone:
          type: string
          example: America/Los_Angeles
        allow_critical_override:
          type: boolean
          default: true
    MaintenanceWindow:
      type: object
      required:
        - id
        - name
        - reason
        - starts_at
        - ends_at
        - timezone
        - allow_critical_override
        - status
        - created_by
        - created_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        reason:
          type: string
        service_id:
          type: string
          format: uuid
        service_name:
          type: string
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        timezone:
          type: string
          example: America/Los_Angeles
        allow_critical_override:
          type: boolean
        status:
          type: string
          enum:
            - scheduled
            - active
            - completed
            - canceled
        created_by:
          type: string
        created_at:
          type: string
          format: date-time
    AdapterConsoleRequest:
      type: object
      additionalProperties: false
      required:
        - payload
      description: Console envelope wrapping a raw provider webhook payload for preview or send-test.
      properties:
        payload:
          description: |
            The provider payload as a JSON value. The console parses and re-serializes pasted text, so adapters receive an equivalent JSON value rather than the provider's original raw bytes.
        content_type:
          type: string
          description: Media type the adapter should assume. Defaults to application/json.
    AdapterPreviewResult:
      type: object
      required:
        - adapter
        - valid
      properties:
        adapter:
          type: string
        valid:
          type: boolean
        error:
          type: string
          description: Present when valid is false; the adapter's rejection reason.
        event:
          description: Present when valid is true; the mapped canonical event (credentials always empty).
          $ref: "#/components/schemas/GenericWebhookEvent"
    IntegrationKeyParams:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          description: Human-readable sender or environment name.
        expires_at:
          type:
            - string
            - "null"
          format: date-time
        trigger_limit_per_minute:
          type: integer
          minimum: 0
          maximum: 10000
          description: Zero uses the account plan limit.
        expiry_warning_days:
          type: integer
          minimum: 0
          maximum: 90
          default: 7
          description: Zero disables the warning.
        unused_warning_days:
          type: integer
          minimum: 0
          maximum: 365
          default: 7
          description: Zero disables the warning.
    CreatedIntegrationKey:
      type: object
      required:
        - id
        - service_id
        - integration_key
        - display
        - name
        - created_at
      properties:
        id:
          type: string
          format: uuid
        service_id:
          type: string
          format: uuid
        integration_key:
          type: string
          description: Secret returned by this response only.
        display:
          type: string
          example: wf_live_••••8K2P
        name:
          type: string
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
    AdminIncident:
      type: object
      required:
        - id
        - status
        - summary
        - service_id
        - service_name
        - dedup_key
        - created_at
        - current_step
        - escalation_exhausted
        - responders
        - attempts
        - assistance_requests
        - participants
        - timeline
        - events
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - triggered
            - acknowledged
            - resolved
        summary:
          type: string
        service_id:
          type: string
          format: uuid
        service_name:
          type: string
        dedup_key:
          type: string
        created_at:
          type: string
          format: date-time
        acked_at:
          type: string
          format: date-time
        acked_by:
          type: string
        resolved_at:
          type: string
          format: date-time
        current_step:
          type: integer
          minimum: 0
        escalation_exhausted:
          type: boolean
        paging_suppressed_until:
          type: string
          format: date-time
        maintenance_override:
          type: boolean
        responders:
          type: array
          items:
            type: object
            required:
              - position
              - user_id
              - name
              - delay_seconds
            properties:
              position:
                type: integer
                minimum: 1
              user_id:
                type: string
                format: uuid
              name:
                type: string
              delay_seconds:
                type: integer
                minimum: 0
                maximum: 604800
        attempts:
          type: array
          items:
            type: object
            required:
              - id
              - step_position
              - responder_name
              - channel
              - status
              - queued_at
            properties:
              id:
                type: string
                format: uuid
              step_position:
                type: integer
                minimum: 1
              responder_name:
                type: string
              channel:
                type: string
              status:
                type: string
              provider_status:
                type: string
              queued_at:
                type: string
                format: date-time
              provider_accepted_at:
                type: string
                format: date-time
              delivered_at:
                type: string
                format: date-time
              failed_at:
                type: string
                format: date-time
              failure_code:
                type: string
              failure_detail:
                type: string
                maxLength: 256
        assistance_requests:
          type: array
          items:
            $ref: "#/components/schemas/AssistanceRequest"
        participants:
          type: array
          items:
            type: object
            required:
              - user_id
              - name
              - role
              - status
            properties:
              user_id:
                type: string
                format: uuid
              name:
                type: string
              role:
                type: string
              status:
                type: string
                enum:
                  - committed
                  - active
                  - left
              available_at:
                type: string
                format: date-time
              joined_at:
                type: string
                format: date-time
              left_at:
                type: string
                format: date-time
        timeline:
          type: array
          items:
            type: object
            required:
              - seq
              - kind
              - data
              - at
            properties:
              seq:
                type: integer
                format: int64
              kind:
                type: string
              data:
                type: object
              at:
                type: string
                format: date-time
        events:
          type: array
          items:
            $ref: "#/components/schemas/IncidentEvent"
        comms:
          type: array
          items:
            $ref: "#/components/schemas/IncidentCommsLink"
    ResponderPreferences:
      type: object
      required:
        - notification_sound
        - available_sounds
      properties:
        notification_sound:
          type: string
          enum:
            - sos
            - sos-urgent
            - sos-low
            - warnfire
            - default
        available_sounds:
          type: array
          items:
            type: string
    MyShift:
      type: object
      required:
        - schedule_id
        - schedule_name
        - starts_at
        - ends_at
        - override
        - candidates
      properties:
        schedule_id:
          type: string
        schedule_name:
          type: string
        layer_id:
          type: string
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        override:
          type: boolean
        candidates:
          type: array
          items:
            type: object
            required:
              - user_id
              - name
              - away
            properties:
              user_id:
                type: string
              name:
                type: string
              away:
                type: boolean
              away_note:
                type: string
        requests:
          type: array
          items:
            type: object
            required:
              - id
              - status
            properties:
              id:
                type: string
              status:
                type: string
              target_name:
                type: string
    ResponderUnavailability:
      type: object
      required:
        - id
        - user_id
        - user_name
        - starts_at
        - ends_at
        - kind
      properties:
        id:
          type: string
        user_id:
          type: string
        user_name:
          type: string
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        kind:
          type: string
          enum:
            - time_off
            - sick
            - personal
        note:
          type: string
    CoverRequest:
      type: object
      required:
        - id
        - kind
        - status
        - requester_user_id
        - requester_name
        - schedule_id
        - schedule_name
        - window_start
        - window_end
        - created_at
        - direction
      properties:
        id:
          type: string
        kind:
          type: string
          enum:
            - cover
            - swap
        status:
          type: string
        requester_user_id:
          type: string
        requester_name:
          type: string
        target_user_id:
          type: string
        target_name:
          type: string
        schedule_id:
          type: string
        schedule_name:
          type: string
        layer_id:
          type: string
        window_start:
          type: string
          format: date-time
        window_end:
          type: string
          format: date-time
        counter_start:
          type: string
          format: date-time
        counter_end:
          type: string
          format: date-time
        note:
          type: string
        decline_reason:
          type: string
        created_at:
          type: string
          format: date-time
        responded_at:
          type: string
          format: date-time
        direction:
          type: string
          enum:
            - incoming
            - outgoing
          description: Computed per reader.
    TenantAPIKey:
      type: object
      required:
        - id
        - name
        - display
        - scopes
        - created_by
        - created_at
        - status
      properties:
        id:
          type: string
        name:
          type: string
        display:
          type: string
          description: Masked form of the token.
        scopes:
          type: array
          items:
            type: string
        created_by:
          type: string
        created_at:
          type: string
        expires_at:
          type: string
        revoked_at:
          type: string
        last_used_at:
          type: string
        status:
          type: string
          enum:
            - active
            - expired
            - revoked
    TenantAPIKeyCreated:
      type: object
      required:
        - key
        - token
      properties:
        key:
          $ref: "#/components/schemas/TenantAPIKey"
        token:
          type: string
          description: Plaintext wfk_ secret, shown exactly once.
    NotificationSources:
      type: object
      required:
        - brand
        - phones
        - urls
        - socials
      properties:
        brand:
          type: string
        email:
          type: string
        phones:
          type: array
          items:
            type: object
            required:
              - number
              - label
              - sms
              - voice
            properties:
              number:
                type: string
              label:
                type: string
              sms:
                type: boolean
              voice:
                type: boolean
        urls:
          type: array
          items:
            type: object
            required:
              - label
              - url
            properties:
              label:
                type: string
              url:
                type: string
        socials:
          type: array
          items:
            type: object
            required:
              - service
              - url
            properties:
              service:
                type: string
              label:
                type: string
              url:
                type: string
    PagerDutyEvent:
      type: object
      description: PagerDuty Events API v2 event; unknown fields are tolerated.
      required:
        - routing_key
        - event_action
      properties:
        routing_key:
          type: string
          description: The WarnFire integration key.
        event_action:
          type: string
          enum:
            - trigger
            - acknowledge
            - resolve
        dedup_key:
          type: string
          maxLength: 255
          description: Required for acknowledge and resolve; synthesized deterministically for keyless triggers.
        client:
          type: string
        client_url:
          type: string
        payload:
          type: object
          properties:
            summary:
              type: string
              maxLength: 1024
              description: Required for trigger.
            source:
              type: string
              maxLength: 255
            severity:
              type: string
              description: Clamped to critical/error/warning/info; unrecognized values become error.
            timestamp:
              type: string
              format: date-time
            component:
              type: string
              maxLength: 255
            group:
              type: string
            class:
              type: string
            custom_details:
              type: object
              additionalProperties: true
        images:
          type: array
          items:
            type: object
            properties:
              src:
                type: string
              href:
                type: string
              alt:
                type: string
        links:
          type: array
          items:
            type: object
            properties:
              href:
                type: string
              text:
                type: string
    PagerDutyResult:
      type: object
      required:
        - status
        - message
      properties:
        status:
          type: string
        message:
          type: string
        dedup_key:
          type: string
    EventPayload:
      type: object
      required:
        - summary
        - source
        - severity
      properties:
        summary:
          type: string
          minLength: 1
          maxLength: 1024
        source:
          type: string
          minLength: 1
          maxLength: 255
          description: The affected host/service/component.
        severity:
          type: string
          enum:
            - critical
            - error
            - warning
            - info
        component:
          type: string
          maxLength: 255
        description:
          type: string
          maxLength: 8192
        environment:
          type: string
          maxLength: 255
        region:
          type: string
          maxLength: 255
        maintenance_override:
          type: boolean
          default: false
          description: Requests that a critical event bypass an active maintenance window. Honored only when severity is critical and the active window allows overrides.
        details:
          type: object
          maxProperties: 64
          propertyNames:
            minLength: 1
            maxLength: 128
          description: Flat typed measurements and facts. Null removes an existing key on a repeated trigger.
          additionalProperties:
            oneOf:
              - type: string
                maxLength: 1024
              - type: number
              - type: boolean
              - type: "null"
        labels:
          type: object
          maxProperties: 64
          propertyNames:
            minLength: 1
            maxLength: 128
          additionalProperties:
            type: string
            maxLength: 1024
        tags:
          type: array
          maxItems: 64
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 128
        links:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/EventLink"
        timestamp:
          type: string
          format: date-time
          description: When the source observed the event (RFC 3339).
    AssistanceTarget:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    EventLink:
      type: object
      required:
        - href
      properties:
        href:
          type: string
          format: uri
          maxLength: 1024
        text:
          type: string
          maxLength: 255
    TimelineEntry:
      type: object
      required:
        - seq
        - kind
        - at
      properties:
        seq:
          type: integer
          format: int64
          description: Monotonic change cursor.
        kind:
          type: string
          example: notification_dispatched
        at:
          type: string
        data:
          type: object
    IncidentEvent:
      type: object
      required:
        - action
        - received_at
      properties:
        action:
          type: string
        received_at:
          type: string
          format: date-time
        actor:
          type: string
        summary:
          type: string
        source:
          type: string
        severity:
          type: string
        component:
          type: string
        description:
          type: string
        environment:
          type: string
        region:
          type: string
        labels:
          type: object
          additionalProperties:
            type: string
        tags:
          type: array
          items:
            type: string
        links:
          type: array
          items:
            $ref: "#/components/schemas/EventLink"
        details:
          type: object
          additionalProperties:
            type: string
        raw:
          description: Exact accepted event payload retained for incident investigation.
    IncidentCommsLink:
      type: object
      required:
        - kind
        - external_id
        - name
        - url
        - archived
      properties:
        kind:
          type: string
        external_id:
          type: string
        name:
          type: string
        url:
          type: string
          format: uri
        archived:
          type: boolean
        archived_at:
          type: string
          format: date-time
    TenantIncidentStatusItem:
      type: object
      additionalProperties: false
      description: |
        Non-sensitive automation projection. It intentionally excludes incident content, people, configuration, provider evidence, and credentials.
      required:
        - alert
        - service
        - origin_integration
        - observed_integrations
        - incident
        - escalation
      properties:
        alert:
          $ref: "#/components/schemas/TenantIncidentAlert"
        service:
          $ref: "#/components/schemas/TenantIncidentService"
        origin_integration:
          description: Earliest linked trigger event, ordered by event received_at and then event ID.
          allOf:
            - $ref: "#/components/schemas/TenantIncidentIntegration"
        observed_integrations:
          type: array
          minItems: 1
          description: |
            Distinct integrations from all events linked to the incident, ordered with origin_integration first and then by name and ID.
          items:
            $ref: "#/components/schemas/TenantIncidentIntegration"
        incident:
          $ref: "#/components/schemas/TenantIncidentLifecycle"
        escalation:
          $ref: "#/components/schemas/TenantIncidentEscalation"
    Team:
      allOf:
        - $ref: "#/components/schemas/TeamInput"
        - type: object
          required:
            - id
          properties:
            id:
              type: string
              format: uuid
    OpsgenieImportItem:
      type: object
      required:
        - id
        - resource_type
        - external_id
        - name
        - mapping_status
        - source
        - findings
      properties:
        id:
          type: string
          format: uuid
        resource_type:
          type: string
          enum:
            - user
            - team
            - schedule
            - override
            - escalation
            - service
            - integration
        external_id:
          type: string
        name:
          type: string
        mapping_status:
          type: string
          enum:
            - exact
            - approximate
            - ambiguous
            - unsupported
        source:
          type: object
          description: Sanitized immutable provider source; credential-shaped fields are redacted.
        draft:
          type: object
          description: Review-only proposed WarnFire representation.
        findings:
          type: array
          items:
            type: string
    ScheduleLayer:
      type: object
      additionalProperties: false
      required:
        - name
        - starts_at
        - shift_unit
        - shift_length
        - active_weekdays
        - member_user_ids
      properties:
        id:
          type: string
          format: uuid
          description: Preserve this value when updating an existing layer.
        name:
          type: string
        position:
          type: integer
          minimum: 1
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        shift_unit:
          type: string
          enum:
            - hours
            - days
            - weeks
        shift_length:
          type: integer
          minimum: 1
        active_weekdays:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: integer
            minimum: 0
            maximum: 6
            description: Sunday is 0.
        active_start_time:
          type: string
          example: 09:00:00
        active_end_time:
          type: string
          example: 17:00:00
        member_user_ids:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
            format: uuid
    TenantIncidentAlert:
      type: object
      additionalProperties: false
      required:
        - id
        - dedup_key
        - status
        - created_at
      properties:
        id:
          type: string
          format: uuid
        dedup_key:
          type: string
        status:
          type: string
          enum:
            - triggered
            - resolved
        created_at:
          type: string
          format: date-time
        resolved_at:
          type: string
          format: date-time
    TenantIncidentService:
      type: object
      additionalProperties: false
      required:
        - id
        - name
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    TenantIncidentIntegration:
      type: object
      additionalProperties: false
      required:
        - id
        - name
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    TenantIncidentLifecycle:
      type: object
      additionalProperties: false
      required:
        - id
        - status
        - created_at
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - triggered
            - acknowledged
            - resolved
        created_at:
          type: string
          format: date-time
        acked_at:
          type: string
          format: date-time
        resolved_at:
          type: string
          format: date-time
    TenantIncidentEscalation:
      type: object
      additionalProperties: false
      required:
        - state
        - current_step
        - exhausted
      properties:
        state:
          type: string
          description: Normalized current state; terminal evidence statuses are preserved verbatim.
          enum:
            - resolved
            - acknowledged
            - suppressed
            - queued
            - running
            - handoff_failed
            - condition_satisfied
            - provider_accepted
            - activity_failed
            - delivery_unknown
            - known_failed
            - no_destination
            - skipped
            - no_evidence
            - legacy_unknown
        current_step:
          type: integer
          minimum: 0
        terminal_status:
          type: string
          enum:
            - condition_satisfied
            - provider_accepted
            - activity_failed
            - delivery_unknown
            - known_failed
            - no_destination
            - skipped
            - no_evidence
        exhausted:
          type: boolean
        stop_reason:
          type: string
        suppressed_until:
          type: string
          format: date-time
  securitySchemes:
    consoleToken:
      type: http
      scheme: bearer
      description: Current authenticated web-console session credential.
    responderToken:
      type: http
      scheme: bearer
      description: A responder bearer token identifying who is acknowledging.
    accountPortalToken:
      type: http
      scheme: bearer
      description: Short-lived credential for the self-service account page, obtained by exchanging a mobile hand-off. It authorizes only /v1/account/* and cannot open the console.
    integrationKey:
      type: http
      scheme: bearer
      description: Service-scoped event integration key. X-WarnFire-Integration-Key is also accepted for webhook senders that cannot set Authorization.
    tenantToken:
      type: http
      scheme: bearer
      description: Bearer credential scoped to one active tenant and a tenant_owner, tenant_admin, or tenant_viewer role.
    tenantAPIKey:
      type: http
      scheme: bearer
      bearerFormat: wfk_...
      description: |
        Tenant-scoped automation credential whose authority is its stored scope set. OpenAPI HTTP bearer security arrays are intentionally empty; the applicable `config:read`, `config:manage`, `incidents:read`, or `incidents:manage` requirement is named by each operation's x-warnfire-required-scope extension.
