openapi: 3.1.0
info:
  title: IGNIS2AB Event Push API
  version: 1.0.0
  description: |
    Herstellerneutraler, mandantengetrennter HTTP-Push-Empfang für Leitstellen,
    Alarmierungsdienste und andere sendende Systeme.
    Ein Bearer-Token ist genau einer Organisation und einer Feuerwehr zugeordnet.
    Jede Änderung wird mit einer stabilen event_id erneut zustellbar und idempotent verarbeitet.
servers:
  - url: https://app.ignis2ab.com
    description: Production environment
  - url: https://demo.ignis2ab.com
    description: Demo environment
  - url: https://dev.ignis2ab.com
    description: Development and integration environment
security:
  - bearerAuth: []
paths:
  /api/v1/connect/health:
    get:
      operationId: eventPushHealth
      summary: Validate bearer token and assigned organization
      responses:
        '200':
          description: Connection is ready
          content:
            application/json:
              schema:
                type: object
                required: [status, integration, organization_id, site_id]
                properties:
                  status: {type: string, const: ok}
                  integration: {type: string, const: event-push}
                  organization_id: {type: string, format: uuid}
                  site_id: {type: string, format: uuid}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
  /api/v1/connect/events:
    post:
      operationId: pushEvent
      summary: Push one ordered mission, resource, or feedback change
      description: |
        Send exactly one event for one logical change. mission.created carries the initial
        mission snapshot. Later events contain only changed mission fields and only the
        affected resources or feedback. Resource and feedback changes use their dedicated
        event types and are not embedded in mission events.
        Repeating the same event_id with identical content succeeds as duplicate. Reusing it
        with changed content returns 409. The first event is mission.created with sequence 1;
        every later event must use exactly the preceding sequence plus one. A sender receiving
        sequence_gap must retry the missing predecessor before retrying the rejected event.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/Event'}
            examples:
              missionCreated:
                value:
                  event_id: adc-event-01892357
                  event_type: mission.created
                  occurred_at: '2026-08-12T14:31:22+02:00'
                  sequence: 1
                  mission:
                    id: adc-mission-4711
                    control_center_incident_number: LS-MSH-2026-004711
                    status: active
                    category: fire
                    keyword: B2 Gebäude
                    description: Rauchentwicklung aus Wohngebäude
                    alarmed_at: '2026-08-12T14:27:03+02:00'
                    started_at: null
                    location:
                      name: Wohnhaus
                      street: Beispielstraße
                      house_number: '12'
                      postal_code: '06542'
                      city: Allstedt
                      district: Beyernaumburg
                      latitude: 51.4734
                      longitude: 11.3572
                    transport_destination: null
              missionUpdate:
                value:
                  event_id: adc-event-01892358
                  event_type: mission.updated
                  occurred_at: '2026-08-12T14:35:00+02:00'
                  sequence: 2
                  mission:
                    id: adc-mission-4711
                    description: Rauchentwicklung bestätigt, Personen nicht mehr im Gebäude
      responses:
        '200':
          description: Event processed or already processed as an identical duplicate
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Accepted'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409':
          description: event_id conflict, sequence conflict, or a missing predecessor
        '415':
          description: Content-Type must be application/json
        '422':
          description: Required fields are missing or invalid
  /api/v1/connect/appointments:
    post:
      operationId: pushAppointment
      summary: Create or update an appointment and its participants
      description: |
        Transmits dates, exercises, service evenings or meetings together with the
        invited people and their current response. Each change has a stable change_id
        and a monotonically increasing revision per appointment. Repeating identical
        content with the same change_id is a successful duplicate; reusing the ID with
        different content returns 409. Older revisions are ignored. This is a complete
        appointment snapshot: participants replaces the prior list and [] clears it.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/Appointment'}
            examples:
              serviceEvening:
                value:
                  appointment_id: adc-appointment-2026-0815
                  change_id: adc-appointment-change-0042
                  revision: 3
                  title: Dienstabend Technische Hilfeleistung
                  description: Ausbildung am HLF
                  location: Gerätehaus Beyernaumburg
                  starts_at: '2026-08-15T18:00:00+02:00'
                  ends_at: '2026-08-15T20:00:00+02:00'
                  status: scheduled
                  participants:
                    - user_id: adc-user-88421
                      status: accepted
                      responded_at: '2026-08-13T10:12:00+02:00'
                    - user_id: adc-user-99117
                      status: maybe
                      responded_at: '2026-08-13T10:14:00+02:00'
      responses:
        '200':
          description: Appointment processed, ignored as stale, or already processed as an identical duplicate
          content:
            application/json:
              schema: {$ref: '#/components/schemas/AppointmentAccepted'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {description: change_id or revision was already used for different content}
        '422': {description: Required fields are missing or invalid}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: organization token
  responses:
    Unauthorized: {description: "Bearer token missing, invalid or expired"}
    Forbidden: {description: "Token has no enabled event-push connection"}
    BadRequest: {description: "Malformed JSON request"}
  schemas:
    Event:
      type: object
      additionalProperties: false
      required: [event_id, event_type, occurred_at, sequence, mission]
      properties:
        event_id: {type: string, minLength: 1, maxLength: 180, description: Stable unique delivery/event identifier}
        event_type:
          type: string
          enum: [mission.created, mission.updated, mission.ended, mission.cancelled, resource.updated, feedback.updated]
        occurred_at: {type: string, format: date-time}
        sequence:
          type: integer
          minimum: 1
          description: Contiguous order per mission. Starts at 1 and increments by exactly one; gaps and conflicting repetitions return 409
        mission: {$ref: '#/components/schemas/Mission'}
        resources:
          type: array
          minItems: 1
          description: Resource changes for this event; each id may occur only once
          items: {$ref: '#/components/schemas/Resource'}
        feedback:
          type: array
          minItems: 1
          description: Response or assignment changes for this event; each user_id may occur only once
          items: {$ref: '#/components/schemas/Feedback'}
      allOf:
        - if: {properties: {event_type: {const: resource.updated}}}
          then:
            required: [resources]
            not: {required: [feedback]}
            properties:
              mission: {$ref: '#/components/schemas/MissionReference'}
        - if: {properties: {event_type: {const: feedback.updated}}}
          then:
            required: [feedback]
            not: {required: [resources]}
            properties:
              mission: {$ref: '#/components/schemas/MissionReference'}
        - if: {properties: {event_type: {const: mission.created}}}
          then:
            not:
              anyOf: [{required: [resources]}, {required: [feedback]}]
        - if: {properties: {event_type: {const: mission.updated}}}
          then:
            not:
              anyOf: [{required: [resources]}, {required: [feedback]}]
            properties:
              mission:
                allOf:
                  - {$ref: '#/components/schemas/Mission'}
                  - not:
                      properties: {status: {enum: [ended, cancelled]}}
                      required: [status]
        - if: {properties: {event_type: {const: mission.ended}}}
          then:
            not:
              anyOf: [{required: [resources]}, {required: [feedback]}]
            properties:
              mission:
                $ref: '#/components/schemas/EndedMission'
        - if: {properties: {event_type: {const: mission.cancelled}}}
          then:
            not:
              anyOf: [{required: [resources]}, {required: [feedback]}]
            properties:
              mission:
                $ref: '#/components/schemas/CancelledMission'
    MissionReference:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id: {type: string, minLength: 1, maxLength: 180}
    EndedMission:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id: {type: string, minLength: 1, maxLength: 180}
        status: {type: string, const: ended, description: Optional; event_type is authoritative}
        ended_at: {type: [string, 'null'], format: date-time}
    CancelledMission:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id: {type: string, minLength: 1, maxLength: 180}
        status: {type: string, const: cancelled, description: Optional; event_type is authoritative}
        ended_at: {type: [string, 'null'], format: date-time}
    Mission:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id: {type: string, minLength: 1, maxLength: 180, description: Stable mission ID assigned by the sending system}
        control_center_incident_number: {type: [string, 'null'], maxLength: 120}
        status: {type: string, enum: [alarmed, active, ended, cancelled]}
        category: {type: [string, 'null']}
        keyword: {type: [string, 'null'], maxLength: 180}
        description: {type: [string, 'null']}
        alarmed_at: {type: [string, 'null'], format: date-time}
        started_at: {type: [string, 'null'], format: date-time}
        ended_at: {type: [string, 'null'], format: date-time}
        transport_destination: {type: [string, 'null']}
        location:
          type: object
          additionalProperties: false
          properties:
            name: {type: [string, 'null']}
            street: {type: [string, 'null']}
            house_number: {type: [string, 'null']}
            postal_code: {type: [string, 'null']}
            city: {type: [string, 'null']}
            district: {type: [string, 'null']}
            latitude: {type: [number, 'null'], minimum: -90, maximum: 90}
            longitude: {type: [number, 'null'], minimum: -180, maximum: 180}
    Resource:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id: {type: string, minLength: 1, maxLength: 180, description: Stable resource identity; journey_number does not change the identity}
        removed: {type: boolean, default: false, description: Remove an existing internal vehicle assignment for this mission}
        radio_call_sign: {type: [string, 'null'], maxLength: 180}
        journey_number: {type: [string, 'null'], maxLength: 120}
        status: {type: [string, 'null'], description: 'German BOS status, e.g. D, A, 1, 2, 3, 4, 7, 8'}
        status_at: {type: [string, 'null'], format: date-time}
        vehicle_id: {type: [string, 'null'], format: uuid, description: Preferred internal IGNIS2AB vehicle ID}
        registration_plate: {type: [string, 'null'], maxLength: 30, description: Fallback used to match a vehicle in the token-bound fire department}
        alarmed_at: {type: [string, 'null'], format: date-time}
        started_at: {type: [string, 'null'], format: date-time, description: Vehicle departure time}
        arrived_at: {type: [string, 'null'], format: date-time}
        ended_at: {type: [string, 'null'], format: date-time, description: Scene departure and return time when no separate times are supplied}
        crew_count: {type: [integer, 'null'], minimum: 0}
    Feedback:
      type: object
      additionalProperties: false
      required: [user_id, status]
      properties:
        user_id: {type: string, minLength: 1, maxLength: 180, description: Stable user ID assigned by the sending system and unique identity within the event}
        removed: {type: boolean, default: false, description: Remove an existing internal personnel assignment; a non-accepted status has the same assignment effect}
        status: {type: string, enum: [accepted, declined, maybe, no_response, unknown]}
        responded_at: {type: [string, 'null'], format: date-time}
        person_id: {type: [string, 'null'], format: uuid, description: Preferred internal IGNIS2AB person ID}
        personnel_number: {type: [string, 'null'], description: Fallback used to match personnel in the token-bound fire department}
        alarmed_at: {type: [string, 'null'], format: date-time}
        started_at: {type: [string, 'null'], format: date-time}
        ended_at: {type: [string, 'null'], format: date-time}
        incident_function:
          type: [string, 'null']
          enum: [incident_commander, platoon_leader, group_leader, squad_leader, machine_operator, driver, breathing_apparatus, safety_assistant, radio_operator, messenger, crew, other, null]
        task: {type: [string, 'null'], maxLength: 180}
    Appointment:
      type: object
      additionalProperties: false
      required: [appointment_id, change_id, revision, title, starts_at, participants]
      properties:
        appointment_id: {type: string, minLength: 1, maxLength: 180}
        change_id: {type: string, minLength: 1, maxLength: 180, description: Stable unique identity of this appointment change within the connection}
        revision: {type: integer, minimum: 1, description: Monotonically increasing per appointment}
        title: {type: string, minLength: 1, maxLength: 180}
        description: {type: [string, 'null']}
        location: {type: [string, 'null'], maxLength: 180}
        starts_at: {type: string, format: date-time}
        ends_at: {type: [string, 'null'], format: date-time}
        status: {type: string, enum: [scheduled, cancelled, completed]}
        participants:
          type: array
          uniqueItems: true
          description: Complete replacement list; user_id must occur at most once and an empty array clears the list
          items: {$ref: '#/components/schemas/Feedback'}
    Accepted:
      type: object
      additionalProperties: false
      required: [event_id, status, duplicate]
      properties:
        event_id: {type: string}
        status: {type: string, const: processed}
        duplicate: {type: boolean}
        incident_id: {type: string, format: uuid}
        resources: {type: integer, minimum: 0, description: Resource entries stored from this event}
        assigned_personnel: {type: integer, minimum: 0, description: Internal personnel assignments created or updated}
        unmatched_personnel: {type: integer, minimum: 0, description: Accepted feedback entries that could not be mapped to personnel}
        assigned_vehicles: {type: integer, minimum: 0, description: Internal vehicle assignments created or updated}
        unmatched_vehicles: {type: integer, minimum: 0, description: Vehicle resource entries that could not be mapped}
        feedback: {type: integer, minimum: 0, description: Feedback entries stored from this event}
        unmatched_feedback: {type: integer, minimum: 0, description: Feedback entries that could not be mapped to personnel}
    AppointmentAccepted:
      type: object
      additionalProperties: false
      required: [appointment_id, change_id, status, duplicate]
      properties:
        appointment_id: {type: string}
        change_id: {type: string}
        status: {type: string, enum: [processed, ignored]}
        duplicate: {type: boolean}
        stale: {type: boolean}
        matched_participants: {type: integer, minimum: 0}
        unmatched_participants: {type: integer, minimum: 0}
