openapi: 3.1.0
info: {title: AckNexa API, version: 0.2.0}
servers: [{url: http://localhost:8088}]
security: [{bearerAuth: []}]
paths:
  /v1/voice/callback:
    post:
      security: []
      operationId: voiceGatewayCallback
      description: Organization-scoped gateway HMAC callback. Sign timestamp + '.' + raw body with HMAC-SHA256 keyed by SHA256(gateway secret). Only explicit digit 1 on answered acknowledges; plain answered never acknowledges. Terminal/stale callbacks cannot regress state. This is a vendor-neutral gateway contract, not a vendor-specific webhook.
      parameters:
        - {in: header, name: X-AckNexa-Timestamp, required: true, schema: {type: string, format: date-time}}
        - {in: header, name: X-AckNexa-Voice-Signature, required: true, schema: {type: string}, description: Hex signature without prefix; five-minute timestamp window}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event_id, notification_id, state, sequence]
              properties:
                event_id: {type: string, maxLength: 200}
                notification_id: {type: string, format: uuid}
                state: {type: string, enum: [queued, dialing, ringing, answered, no_answer, busy, failed, completed]}
                sequence: {type: integer, minimum: 1}
                digit: {type: string, enum: ['1']}
      responses:
        '200': {description: Recorded or idempotent duplicate; response identifies duplicate/stale callbacks}
        '400': {$ref: '#/components/responses/Problem'}
        '401': {$ref: '#/components/responses/Problem'}
        '403': {$ref: '#/components/responses/Problem'}
        '409': {$ref: '#/components/responses/Problem'}
  /v1/operations:
    get:
      operationId: getDeliveryHealth
      description: Organization admins only. Tenant-scoped queue counts, dead letters, failed escalation jobs, and failed notifications. Lists are limited to the latest 100 entries.
      responses:
        '200': {description: Delivery health overview}
        '403': {$ref: '#/components/responses/Problem'}
  /v1/operations/dead-letters/{deadLetterID}/retry:
    post:
      operationId: retryDeadLetter
      description: Organization admins only. Audits a retry and queues the original immutable raw event. Invalid payloads will be quarantined again until their parser or integration configuration is corrected.
      parameters:
        - {in: path, name: deadLetterID, required: true, schema: {type: string, format: uuid}}
      responses:
        '204': {description: Retry durably queued}
        '403': {$ref: '#/components/responses/Problem'}
        '404': {$ref: '#/components/responses/Problem'}
        '409': {$ref: '#/components/responses/Problem'}
  /v1/auth/login:
    post:
      security: []
      operationId: login
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/Login'}}}}
      responses: {'200': {description: Authenticated, content: {application/json: {schema: {type: object, properties: {token: {type: string}, expires_at: {type: string, format: date-time}}}}}}}
  /integrations/webhook/{integrationID}:
    post:
      security: []
      operationId: ingestWebhook
      parameters:
        - {in: path, name: integrationID, required: true, schema: {type: string, format: uuid}}
        - {in: header, name: X-AckNexa-Key, description: Required unless Bearer webhook secret or timestamped signature headers are supplied, schema: {type: string}}
        - {in: header, name: X-AckNexa-Timestamp, schema: {type: string, format: date-time}}
        - {in: header, name: X-AckNexa-Signature, schema: {type: string, pattern: '^sha256='}}
        - {in: header, name: Idempotency-Key, schema: {type: string}}
      requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: true}}}}
      responses: {'202': {description: Durably accepted; a duplicate returns the original raw_event_id}, '401': {$ref: '#/components/responses/Problem'}, '409': {description: Idempotency key already used for a different payload}}
  /v1/incidents:
    get:
      operationId: listIncidents
      parameters: [{in: query, name: status, schema: {$ref: '#/components/schemas/IncidentStatus'}}]
      responses: {'200': {description: Incidents, content: {application/json: {schema: {type: object, properties: {items: {type: array, items: {$ref: '#/components/schemas/Incident'}}}}}}}}
  /v1/incidents/{incidentID}:
    get:
      operationId: getIncident
      parameters: [{$ref: '#/components/parameters/IncidentID'}]
      responses: {'200': {description: Incident with timeline and context containing notification statuses and attempt history}}
  /v1/incidents/{incidentID}/acknowledge:
    post:
      operationId: acknowledgeIncident
      parameters: [{$ref: '#/components/parameters/IncidentID'}]
      requestBody: {$ref: '#/components/requestBodies/Version'}
      responses: {'200': {description: Acknowledged, content: {application/json: {schema: {$ref: '#/components/schemas/Incident'}}}}, '409': {$ref: '#/components/responses/Problem'}}
  /v1/incidents/{incidentID}/resolve:
    post:
      operationId: resolveIncident
      parameters: [{$ref: '#/components/parameters/IncidentID'}]
      requestBody: {required: true, content: {application/json: {schema: {type: object, required: [version, reason], properties: {version: {type: integer}, reason: {type: string}}}}}}
      responses: {'200': {description: Resolved}, '409': {$ref: '#/components/responses/Problem'}}
  /v1/incidents/{incidentID}/notes:
    post:
      operationId: addIncidentNote
      parameters: [{$ref: '#/components/parameters/IncidentID'}]
      requestBody: {required: true, content: {application/json: {schema: {type: object, required: [message], properties: {message: {type: string}}}}}}
      responses: {'204': {description: Added}}
  /v1/incidents/{incidentID}/assign:
    post:
      operationId: assignIncident
      parameters: [{$ref: '#/components/parameters/IncidentID'}]
      responses: {'204': {description: Assigned}}
  /v1/incidents/{incidentID}/escalate:
    post:
      operationId: escalateIncident
      parameters: [{$ref: '#/components/parameters/IncidentID'}]
      responses: {'204': {description: Immediate escalation queued}, '409': {$ref: '#/components/responses/Problem'}}
  /v1/on-call:
    get:
      operationId: getOnCall
      parameters: [{in: query, name: at, schema: {type: string, format: date-time}}]
      responses: {'200': {description: On-call responders}}
  /v1/events:
    get:
      operationId: streamEvents
      responses: {'200': {description: text/event-stream updates, content: {text/event-stream: {schema: {type: string}}}}}
  /v1/alerts:
    get: {operationId: listAlerts, responses: {'200': {description: Normalized alerts}}}
  /v1/teams:
    get: {operationId: listTeams, responses: {'200': {description: Teams}}}
    post: {operationId: createTeam, requestBody: {required: true, content: {application/json: {schema: {type: object, required: [name], properties: {name: {type: string}, slug: {type: string}, description: {type: string}}}}}}, responses: {'201': {description: Team created}}}
  /v1/teams/{teamID}/members:
    post:
      operationId: addTeamMember
      parameters: [{in: path, name: teamID, required: true, schema: {type: string, format: uuid}}]
      responses: {'204': {description: Membership saved}}
  /v1/services:
    get: {operationId: listServices, responses: {'200': {description: Services}}}
    post: {operationId: createService, responses: {'201': {description: Service created}}}
  /v1/integrations:
    get: {operationId: listIntegrations, responses: {'200': {description: Integrations without secrets}}}
    post: {operationId: createIntegration, responses: {'201': {description: Integration created; webhook secret returned once}}}
  /v1/schedules:
    get: {operationId: listSchedules, responses: {'200': {description: Schedules}}}
    post: {operationId: createSchedule, responses: {'201': {description: Schedule and initial rotation created}}}
  /v1/schedules/{scheduleID}/overrides:
    post:
      operationId: createScheduleOverride
      parameters: [{in: path, name: scheduleID, required: true, schema: {type: string, format: uuid}}]
      responses: {'201': {description: Override created}}
  /v1/escalation-policies:
    post: {operationId: createEscalationPolicy, responses: {'201': {description: Policy and ordered steps created}}}
  /v1/device-tokens:
    post: {operationId: registerDeviceToken, responses: {'204': {description: Device token registered}}}
components:
  securitySchemes: {bearerAuth: {type: http, scheme: bearer}}
  parameters:
    IncidentID: {in: path, name: incidentID, required: true, schema: {type: string, format: uuid}}
  requestBodies:
    Version: {required: true, content: {application/json: {schema: {type: object, required: [version], properties: {version: {type: integer}}}}}}
  responses:
    Problem: {description: RFC 9457-style problem, content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}}}
  schemas:
    Login: {type: object, required: [organization, email, password], properties: {organization: {type: string}, email: {type: string, format: email}, password: {type: string, format: password}}}
    IncidentStatus: {type: string, enum: [TRIGGERED, ACKNOWLEDGED, RESOLVED]}
    Incident:
      type: object
      required: [id, organization_id, service_id, title, severity, status, version, triggered_at]
      properties:
        id: {type: string, format: uuid}
        organization_id: {type: string, format: uuid}
        service_id: {type: string, format: uuid}
        title: {type: string}
        severity: {type: string, enum: [P1, P2, P3, P4, P5]}
        status: {$ref: '#/components/schemas/IncidentStatus'}
        version: {type: integer}
        triggered_at: {type: string, format: date-time}
    Problem: {type: object, required: [status, detail], properties: {type: {type: string}, status: {type: integer}, detail: {type: string}}}
