openapi: 3.1.0
info:
  title: FireMic partner API
  version: "1.0"
  summary: Voice-to-NERIS incident drafting inside a partner's own product.
  description: |
    One call turns a firefighter's spoken narration (or a transcript) into a NERIS v1-shaped
    incident draft the partner's software fills in. The partner remains the NERIS submitter of
    record; this API has no submit route. Human docs: https://fire-mic.com/developers
  contact:
    name: FireMic
    url: https://fire-mic.com/about
servers:
  - url: https://app.fire-mic.com/v1
security:
  - bearer: []
tags:
  - name: Credentials
  - name: Narrations
  - name: Reference
paths:
  /ping:
    get:
      tags: [Credentials]
      summary: Confirm a credential and report its limits
      operationId: ping
      responses:
        "200":
          description: The credential is valid.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Ping" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Suspended" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /sessions:
    post:
      tags: [Credentials]
      summary: Mint a department-scoped session token (API key only)
      operationId: createSession
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SessionRequest" }
      responses:
        "201":
          description: Session minted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Session" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/ScopeMismatch" }
        "404": { $ref: "#/components/responses/DepartmentNotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /narrations:
    post:
      tags: [Narrations]
      summary: Turn audio or a transcript into a NERIS-shaped draft (synchronous)
      operationId: createNarration
      description: |
        Typically returns in 4–8 seconds for a one-minute narration. Set a client timeout of at
        least 120 seconds. Not idempotent: a retry creates a second narration.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NarrationRequest" }
      responses:
        "200":
          description: The finished draft.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Narration" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/ScopeMismatch" }
        "404": { $ref: "#/components/responses/DepartmentNotFound" }
        "422":
          description: EMPTY_TRANSCRIPT — the audio produced no usable speech. details.id and details.transcript are set.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502":
          description: TRANSCRIPTION_FAILED or EXTRACTION_FAILED. details.id is set; EXTRACTION_FAILED also carries details.transcript.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /narrations/{id}:
    get:
      tags: [Narrations]
      summary: Re-read a narration
      operationId: getNarration
      parameters:
        - $ref: "#/components/parameters/NarrationId"
      responses:
        "200":
          description: The narration.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Narration" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /narrations/{id}/outcome:
    post:
      tags: [Narrations]
      summary: Report what the firefighter did with the draft
      operationId: reportOutcome
      parameters:
        - $ref: "#/components/parameters/NarrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OutcomeRequest" }
      responses:
        "200":
          description: Recorded.
          content:
            application/json:
              schema:
                type: object
                required: [ok, id, outcome_at]
                properties:
                  ok: { type: boolean, const: true }
                  id: { type: string }
                  outcome_at: { type: string, format: date-time }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /departments/{neris_id}:
    get:
      tags: [Reference]
      summary: Directory check for a NERIS department id
      operationId: getDepartment
      parameters:
        - name: neris_id
          in: path
          required: true
          schema: { $ref: "#/components/schemas/NerisDepartmentId" }
      responses:
        "200":
          description: Known department.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Department" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/DepartmentNotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /processing-messages:
    get:
      tags: [Reference]
      summary: Optional "while you wait" lines, by phase (public)
      operationId: processingMessages
      security: []
      responses:
        "200":
          description: Cacheable for an hour.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProcessingMessages" }
components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: An API key (fm_live_… / fm_test_…) or a session token from POST /sessions.
  parameters:
    NarrationId:
      name: id
      in: path
      required: true
      schema: { type: string, pattern: "^nar_[A-Za-z0-9]{24}$" }
  responses:
    BadRequest:
      description: Validation failed; error.code says which field.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: MISSING_CREDENTIAL, INVALID_CREDENTIAL, KEY_REVOKED or SESSION_EXPIRED.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Suspended:
      description: PARTNER_SUSPENDED.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ScopeMismatch:
      description: DEPARTMENT_SCOPE_MISMATCH — the credential cannot act for that department.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    DepartmentNotFound:
      description: DEPARTMENT_NOT_FOUND.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: NOT_FOUND.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: RATE_LIMITED. Retry after the seconds in the Retry-After header.
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unavailable:
      description: PARTNER_API_UNAVAILABLE. Retry with backoff.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details: {}
    NerisDepartmentId:
      type: string
      pattern: "^FD\\d{8}$"
      description: NERIS department id from the national directory, e.g. FD34007744.
    Mode:
      type: string
      enum: [test, live]
    Department:
      type: object
      required: [neris_id, name, city, state]
      properties:
        object: { type: string, const: department }
        neris_id: { $ref: "#/components/schemas/NerisDepartmentId" }
        name: { type: [string, "null"] }
        city: { type: [string, "null"] }
        state: { type: [string, "null"] }
    Ping:
      type: object
      required: [ok, partner, mode, credential, limits, server_time]
      properties:
        ok: { type: boolean, const: true }
        partner:
          type: object
          properties:
            slug: { type: string }
            name: { type: string }
        mode: { $ref: "#/components/schemas/Mode" }
        credential:
          type: object
          properties:
            via: { type: string, enum: [api_key, session] }
            scope: { type: string, enum: [partner, department] }
            department: { type: [string, "null"] }
            department_name: { type: [string, "null"] }
        limits:
          type: object
          additionalProperties:
            type: object
            properties:
              per_window: { type: integer }
              window_seconds: { type: integer }
        server_time: { type: string, format: date-time }
    SessionRequest:
      type: object
      properties:
        department: { $ref: "#/components/schemas/NerisDepartmentId" }
        external_ref: { type: string, maxLength: 200 }
        ttl_seconds: { type: integer, minimum: 60, maximum: 3600, default: 900 }
    Session:
      type: object
      required: [object, token, token_type, expires_at, mode, department]
      properties:
        object: { type: string, const: session }
        token: { type: string }
        token_type: { type: string, const: Bearer }
        expires_at: { type: string, format: date-time }
        mode: { $ref: "#/components/schemas/Mode" }
        department: { $ref: "#/components/schemas/Department" }
        external_ref: { type: [string, "null"] }
    AudioInput:
      type: object
      required: [data, mime]
      properties:
        data:
          type: string
          description: "Base64 of the audio file (a data: URL prefix is tolerated). Decoded size up to 25 MB."
        mime:
          type: string
          description: "audio/webm, audio/ogg, audio/mp4, audio/m4a, audio/aac, audio/wav, audio/mpeg, audio/mp3 or audio/flac."
        duration_seconds:
          type: number
          minimum: 0
          maximum: 900
    NarrationRequest:
      type: object
      description: Exactly one of `audio` or `transcript`.
      properties:
        department:
          $ref: "#/components/schemas/NerisDepartmentId"
        audio: { $ref: "#/components/schemas/AudioInput" }
        transcript: { type: string, minLength: 1, maxLength: 50000 }
        external_ref: { type: string, maxLength: 200 }
        metadata:
          type: object
          description: Stored and echoed, not interpreted. Up to 4 KB of JSON.
    Gap:
      type: object
      required: [field, label, question]
      properties:
        field: { type: string, description: Stable field id. }
        label: { type: string }
        question: { type: string, description: The clarification question, written for a firefighter. }
    Narration:
      type: object
      required: [id, object, status, mode, department, input, transcript, neris, gaps, modules, timings, model, created_at]
      properties:
        id: { type: string, pattern: "^nar_[A-Za-z0-9]{24}$" }
        object: { type: string, const: narration }
        status: { type: string, enum: [complete, failed, processing] }
        mode: { $ref: "#/components/schemas/Mode" }
        department: { $ref: "#/components/schemas/Department" }
        external_ref: { type: [string, "null"] }
        input:
          type: object
          properties:
            kind: { type: string, enum: [audio, text] }
            mime: { type: [string, "null"] }
            audio_seconds: { type: [number, "null"] }
        transcript: { type: [string, "null"] }
        neris:
          type: [object, "null"]
          description: NERIS v1 incident payload — the module shape NERIS accepts on its own incident endpoint. Empty modules omitted. Incident types encoded as "L1||L2||L3".
          additionalProperties: true
        firemic_report:
          type: [object, "null"]
          description: The FireMic-native report the payload was built from. Extended and additive.
          additionalProperties: true
        gaps:
          type: array
          items: { $ref: "#/components/schemas/Gap" }
        modules:
          type: object
          properties:
            activated: { type: array, items: { type: string } }
            failed: { type: array, items: { type: string } }
        timings:
          type: [object, "null"]
          properties:
            transcribe_ms: { type: [integer, "null"] }
            stage1_ms: { type: integer }
            stage2_ms: { type: integer }
            total_ms: { type: integer }
        model:
          type: object
          properties:
            extraction: { type: string }
            transcription: { type: string }
        metadata: { type: [object, "null"] }
        error:
          type: [object, "null"]
          properties:
            code: { type: string }
        outcome: { type: [object, "null"] }
        outcome_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        completed_at: { type: [string, "null"], format: date-time }
    OutcomeRequest:
      type: object
      required: [accepted]
      properties:
        accepted: { type: boolean }
        edited_fields:
          type: array
          maxItems: 200
          items: { type: string, maxLength: 120 }
        neris:
          type: object
          description: The final NERIS payload as submitted, if you want us to learn from corrected values.
          additionalProperties: true
        submitted_to_neris: { type: boolean }
        neris_incident_id: { type: string, maxLength: 120 }
        review_seconds: { type: number, minimum: 0, maximum: 86400 }
        notes: { type: string, maxLength: 2000 }
    ProcessingMessages:
      type: object
      properties:
        object: { type: string, const: processing_messages }
        suggested_interval_ms: { type: integer }
        phases:
          type: object
          properties:
            transcribing: { type: array, items: { type: string } }
            extracting_core: { type: array, items: { type: string } }
            extracting_modules: { type: array, items: { type: string } }
        note: { type: string }
