Skip to content

Developers · Partner API

API reference

Everything the partner API accepts and returns. Base URL https://app.fire-mic.com/v1, JSON in and out, bearer authentication, one error shape.

Last updated September 2026.

Conventions

  • Base URL https://app.fire-mic.com/v1. HTTPS only.
  • Requests are JSON (Content-Type: application/json). Audio travels base64-encoded inside the JSON body.
  • Responses are JSON. Success bodies carry an object field naming the shape. Every error, from every route, is:
{ "error": { "code": "DEPARTMENT_NOT_FOUND", "message": "No department with NERIS id FD00000000. …", "details": { "…": "optional" } } }
  • Authentication is Authorization: Bearer <credential>, where the credential is an API key (fm_live_… or fm_test_…) or a session token from POST /v1/sessions.
  • CORS is open on every route, so browsers and webviews can call /v1 directly with a session token. Preflight OPTIONS returns 204.
  • Every response carries an X-Request-Id header. Quote it when you report a problem.
  • Caching: responses are Cache-Control: no-store, except GET /v1/processing-messages, which is public and cacheable for an hour.
  • Timeouts: POST /v1/narrations is synchronous and typically returns in 4 to 8 seconds; set your client timeout to at least 120 seconds.

Authentication

API keys

Issued by FireMic, shown once, stored hashed. Two properties are fixed at issue time:

Property Values Meaning
mode test, live Same pipeline, same responses. Test keys have tighter rate limits and their narrations are flagged as test data.
scope partner, department A partner-scoped key names the department on each request. A department-scoped key is pinned to one department and rejects any other with 403 DEPARTMENT_SCOPE_MISMATCH. Use department-scoped keys anywhere a key must live on a device, such as a desktop installer with no server tier.

Keys are for servers. Do not embed a partner-scoped key in a browser, mobile app or desktop client; mint sessions instead. Revoking a key takes effect within one request and also invalidates every session minted from it.

Session tokens

POST /v1/sessions exchanges an API key for a signed token that can only act for one department, for at most an hour. This is the credential for anything running on a user’s device. A session token cannot create sessions, cannot read other departments, and is refused everywhere with 401 SESSION_EXPIRED once past its expiry.

Failures

Status Code When
401 MISSING_CREDENTIAL No Authorization: Bearer header.
401 INVALID_CREDENTIAL Unknown key, malformed token, or a session token used where a key is required.
401 KEY_REVOKED The key (or the key behind this session) was revoked.
401 SESSION_EXPIRED The session token is past expires_at. Mint a new one.
403 PARTNER_SUSPENDED The partner account is suspended.
503 PARTNER_API_UNAVAILABLE Our side cannot verify credentials right now. Retry with backoff.

Rate limits

Limits are per hour unless stated, counted on the expensive route (POST /v1/narrations) across five dimensions at once. A refusal is 429 RATE_LIMITED with a Retry-After header and details.retry_after_seconds.

Dimension Test key Live key
Per key 60 600
Per partner (all keys) 120 3,000
Per partner per day 500 20,000
Per department 200 400
Per source IP 300 600

The cheap routes (sessions, reads, outcome) share one ceiling of 1,200 per key per hour. Per-partner ceilings can be raised for a live partner; GET /v1/ping reports the ones your credential currently has. If our limiter itself is unavailable, test keys are refused (503) and live keys are allowed through: a limiter outage must never block a firefighter mid-incident.

Input limits

Input Limit
Audio, decoded 25 MB
Audio, duration 15 minutes (AUDIO_TOO_LONG when duration_seconds says more)
Audio, types audio/webm (opus), audio/ogg (opus), audio/mp4, audio/m4a, audio/aac, audio/wav, audio/mpeg, audio/mp3, audio/flac
Transcript 50,000 characters
external_ref 200 characters
metadata 4 KB of JSON
Outcome body 256 KB of JSON

Endpoints

GET /v1/ping

Confirms a credential and reports what it can do. Accepts a key or a session.

{
  "ok": true,
  "partner": { "slug": "alpine", "name": "Alpine Software" },
  "mode": "test",
  "credential": { "via": "api_key", "scope": "partner", "department": null, "department_name": null },
  "limits": {
    "partner_narration_key": { "per_window": 60, "window_seconds": 3600 },
    "partner_narration_partner_hour": { "per_window": 120, "window_seconds": 3600 },
    "partner_narration_partner_day": { "per_window": 500, "window_seconds": 86400 }
  },
  "server_time": "2026-09-02T10:51:30.003Z"
}

credential.department is the NERIS id a session token is scoped to (with its department_name), or null for a key.

POST /v1/sessions

API key only. Mints a department-scoped session token for a client device.

Request

Field Type Required Notes
department string for partner-scoped keys NERIS department id, FD + 8 digits. Omit for a department-scoped key.
external_ref string no Your incident or record reference, up to 200 characters. Carried on every narration made with the session.
ttl_seconds number no 60 to 3600. Default 900.

Response 201

{
  "object": "session",
  "token": "eyJhbGciOiJIUzI1NiJ9…",
  "token_type": "Bearer",
  "expires_at": "2026-09-02T11:04:55.000Z",
  "mode": "test",
  "department": { "neris_id": "FD34007744", "name": "Collingswood Fire Department", "city": "Collingswood", "state": "NJ" },
  "external_ref": "RA-SESSION-1"
}

Errors: 400 DEPARTMENT_REQUIRED, 400 DEPARTMENT_INVALID, 400 INVALID_TTL, 400 INVALID_EXTERNAL_REF, 403 DEPARTMENT_SCOPE_MISMATCH, 404 DEPARTMENT_NOT_FOUND, and 401 INVALID_CREDENTIAL when a session token is presented instead of a key.

POST /v1/narrations

The call. Accepts a key or a session. Synchronous: the response is the finished draft.

Request

Field Type Required Notes
department string see notes NERIS department id. Required with a partner-scoped key. Optional with a session or a department-scoped key, and if present it must match.
audio object one of audio / transcript { "data": "<base64>", "mime": "audio/mp4", "duration_seconds": 59.2 }. A data: URL prefix is tolerated. duration_seconds is optional.
transcript string one of audio / transcript Already-transcribed narration, or typed text. Skips transcription.
external_ref string no Up to 200 characters. Defaults to the session’s external_ref if any.
metadata object no Anything you want stored with the narration and echoed back, up to 4 KB. Not interpreted.

Response 200: a narration object with status: "complete".

Errors

Status Code Meaning
400 INVALID_BODY Body is not a JSON object.
400 INPUT_REQUIRED Neither or both of audio and transcript.
400 INVALID_AUDIO, UNSUPPORTED_AUDIO_TYPE, AUDIO_TOO_LARGE, AUDIO_TOO_LONG See input limits.
400 INVALID_TRANSCRIPT, TRANSCRIPT_TOO_LONG Empty, non-string, or over 50,000 characters.
400 INVALID_EXTERNAL_REF, INVALID_METADATA Over the limit or the wrong type.
400 DEPARTMENT_REQUIRED, DEPARTMENT_INVALID Missing or malformed NERIS id with a partner-scoped key.
403 DEPARTMENT_SCOPE_MISMATCH The body names a department the credential cannot act for.
404 DEPARTMENT_NOT_FOUND Not in the national directory.
422 EMPTY_TRANSCRIPT The audio produced no usable speech (a room recording, a muted mic). details.id and details.transcript are set; nothing was extracted.
429 RATE_LIMITED See rate limits.
502 TRANSCRIPTION_FAILED, EXTRACTION_FAILED An upstream stage failed. details.id is set; for EXTRACTION_FAILED, details.transcript carries the transcript so you can still show it. Retry once after a few seconds.
503 PARTNER_API_UNAVAILABLE Retry with backoff.

The call is not idempotent: a retry creates a second narration. Send external_ref so you can reconcile duplicates on your side, and do not retry a request that returned 200.

GET /v1/narrations/{id}

Re-reads a narration. Accepts a key or a session; only the partner that created the narration can read it. Returns the narration object, including outcome once reported. 404 NOT_FOUND for an unknown id or another partner’s narration.

POST /v1/narrations/{id}/outcome

Reports what the firefighter did with the draft. Optional, idempotent (the latest report wins), and the single most useful thing a partner can send.

Field Type Required Notes
accepted boolean yes Did the firefighter keep the draft as the basis of the report?
edited_fields string[] no Paths the firefighter changed before saving, up to 200. Use your own field names or the neris paths; both are useful.
neris object no The final NERIS payload as submitted, if you want us to learn from the corrected values. Up to 256 KB.
submitted_to_neris boolean no Whether the report was filed.
neris_incident_id string no The NERIS incident id after filing, up to 120 characters.
review_seconds number no Time from draft to save, 0 to 86,400.
notes string no Free text, up to 2,000 characters.

Response 200 { "ok": true, "id": "nar_…", "outcome_at": "2026-09-02T10:55:28.397Z" }. Errors: 400 INVALID_OUTCOME, 400 OUTCOME_TOO_LARGE, 404 NOT_FOUND.

GET /v1/departments/{neris_id}

Directory check. Accepts a key or a session.

{ "object": "department", "neris_id": "FD34007744", "name": "Collingswood Fire Department", "city": "Collingswood", "state": "NJ" }

Errors: 400 DEPARTMENT_INVALID, 404 DEPARTMENT_NOT_FOUND.

GET /v1/processing-messages

Public, no authentication, cacheable. The rotating lines FireMic’s own app shows while a narration is processing, grouped by the phase they describe. Show one at a time, in order, from a random starting point, changing every suggested_interval_ms. Entirely optional.

{
  "object": "processing_messages",
  "suggested_interval_ms": 2200,
  "phases": {
    "transcribing": ["Transcribing your narration…", "Turning speech into text…", "…"],
    "extracting_core": ["Reading the incident…", "Reading the callout…", "…"],
    "extracting_modules": ["…"]
  },
  "note": "Show one line at a time, in order, from a random start. Typical total wait: 6–8 s for a one-minute narration."
}

The narration object

Returned by POST /v1/narrations and GET /v1/narrations/{id}. This is a real production response for a 59-second recording, lightly trimmed:

{
  "id": "nar_UFaCShS95qMXWZuVIG5NmT1y",
  "object": "narration",
  "status": "complete",
  "mode": "test",
  "department": {
    "neris_id": "FD36103862",
    "name": "Sayville Fire Department",
    "city": "Sayville",
    "state": "NY"
  },
  "external_ref": "RA-2026-001848",
  "input": {
    "kind": "audio",
    "mime": "audio/mp4",
    "audio_seconds": 59.2
  },
  "transcript": "Engine 31 responding to 214 Maple Avenue for a reported structure fire. Dispatched at 1421. Enroute at 1423. On scene at 1427. On arrival we had a two-story wood frame single family dwelling with light smoke showing from the secon \u2026",
  "neris": {
    "base": {
      "department_neris_id": "FD36103862",
      "incident_number": "AUTO-1788346494711",
      "location": {
        "number": 214,
        "complete_number": "214",
        "street": "Maple",
        "street_postfix": "AVENUE",
        "incorporated_municipality": "Sayville",
        "state": "NY"
      },
      "location_use": {
        "use_type": "RESIDENTIAL||DETATCHED_SINGLE_FAMILY_DWELLING",
        "in_use": {
          "in_use": true
        }
      },
      "people_present": true,
      "animals_rescued": 0,
      "outcome_narrative": "Engine 31 responded to 214 Maple Avenue for a reported structure fire. Dispatched at 14:21, enroute at 14:23, and arrived on scene at 14:27. Upon arrival, crews \u2026",
      "displacement_count": 1,
      "displacement_causes": [
        "FIRE"
      ]
    },
    "incident_types": [
      {
        "type": "FIRE||STRUCTURE_FIRE||ROOM_AND_CONTENTS_FIRE",
        "primary": true
      }
    ],
    "dispatch": {
      "incident_number": "AUTO-1788346494711",
      "incident_clear": "2026-09-02T15:45:00Z",
      "call_arrival": "2026-09-02T14:21:00Z",
      "call_answered": "2026-09-02T14:21:00Z",
      "call_create": "2026-09-02T14:21:00Z",
      "location": {
        "number": 214,
        "complete_number": "214",
        "street": "Maple",
        "street_postfix": "AVENUE",
        "incorporated_municipality": "Sayville",
        "state": "NY"
      },
      "unit_responses": [
        {
          "reported_unit_id": "Engine 31",
          "staffing": 4,
          "dispatch": "2026-09-02T14:21:00Z",
          "enroute_to_scene": "2026-09-02T14:23:00Z",
          "on_scene": "2026-09-02T14:27:00Z",
          "unit_clear": "2026-09-02T15:45:00Z",
          "response_mode": "EMERGENT"
        },
        {
          "reported_unit_id": "Ladder 5",
          "staffing": 3,
          "on_scene": "2026-09-02T14:30:00Z",
          "unit_clear": "2026-09-02T15:45:00Z",
          "response_mode": "EMERGENT"
        }
      ]
    },
    "tactic_timestamps": {
      "command_established": "2026-09-02T14:27:00Z",
      "completed_sizeup": "2026-09-02T14:27:00Z",
      "primary_search_begin": "2026-09-02T14:30:00Z",
      "primary_search_complete": "2026-09-02T14:35:00Z",
      "water_on_fire": "2026-09-02T14:30:00Z",
      "fire_under_control": "2026-09-02T14:35:00Z",
      "fire_knocked_down": "2026-09-02T14:35:00Z"
    },
    "nonfd_aids": [
      "UTILITIES_PUBLIC_WORKS"
    ],
    "smoke_alarm": {
      "presence": {
        "type": "PRESENT",
        "working": true
      }
    },
    "fire_alarm": {
      "presence": {
        "type": "NOT_PRESENT"
      }
    },
    "other_alarm": {
      "presence": {
        "type": "NOT_PRESENT"
      }
    },
    "fire_suppression": {
      "presence": {
        "type": "NOT_PRESENT"
      }
    },
    "fire_detail": {
      "location_detail": {
        "type": "STRUCTURE",
        "floor_of_origin": 2,
        "arrival_condition": "SMOKE_SHOWING",
        "damage_type": "MINOR_DAMAGE",
        "room_of_origin_type": "BEDROOM",
        "cause": "HEAT_FROM_ANOTHER_OBJECT"
      },
      "water_supply": "HYDRANT_LESS_500",
      "investigation_needed": "YES",
      "investigation_types": [
        "NONE"
      ],
      "suppression_appliances": [
        "SMALL_DIAMETER_FIRE_HOSE"
      ]
    }
  },
  "firemic_report": {
    "\u2026": "the FireMic-native report the NERIS payload was built from; see \"firemic_report\" below"
  },
  "gaps": [
    {
      "field": "incident_number",
      "label": "Incident Number",
      "question": "What was the incident number assigned by dispatch?"
    }
  ],
  "modules": {
    "activated": [
      "fire_detail"
    ],
    "failed": []
  },
  "timings": {
    "transcribe_ms": 604,
    "stage1_ms": 3652,
    "stage2_ms": 474,
    "total_ms": 4730
  },
  "model": {
    "extraction": "firemic-extract-2026.09",
    "transcription": "firemic-asr-2026.09"
  },
  "metadata": null,
  "error": null,
  "outcome": null,
  "outcome_at": null,
  "created_at": "2026-09-02T10:54:41.120Z",
  "completed_at": "2026-09-02T10:54:45.850Z"
}
Field Type Meaning
id string nar_ + 24 characters. Opaque.
object "narration"
status complete, failed, processing A POST only ever returns complete; failed rows exist for reads after a 422/502.
mode test, live The key’s mode.
department object neris_id, name, city, state of the department the narration is for.
external_ref string or null Yours, echoed.
input object kind (audio or text), mime, audio_seconds as sent.
transcript string What was heard, after our fire-service vocabulary. Show it to the firefighter.
neris object The incident as a NERIS v1 incident payload: the same module keys and enumerated values NERIS accepts on its own incident endpoint. Empty modules are omitted. Incident types are `L1
firemic_report object The FireMic-native report the payload was built from. Superset of neris: it also carries fields NERIS has no slot for (smoke-detector notes, a few special-study answers) and keeps empty modules with their defaults, which is convenient for rendering a form. Treat it as extended and additive: keys may be added without notice.
gaps array Required fields the narration did not cover. Each has a stable field id, a human label, and the question we would ask the firefighter. A clarification endpoint that takes the answer and returns field updates is on the roadmap; today, the questions are yours to ask in your form.
modules object activated: which detail modules the core extraction decided applied (fire_detail, medical_details, hazsit_detail, exposures). failed: any of those whose detail call failed; the module is present but empty.
timings object Milliseconds: transcribe_ms (null for text input), stage1_ms, stage2_ms, total_ms.
model object Opaque version labels for the transcription and extraction stages. Quote them in a bug report. They change when the extraction changes materially.
metadata object or null Yours, echoed.
error object or null { "code": … } on a failed narration.
outcome, outcome_at object, string What you reported, if anything.
created_at, completed_at string ISO 8601 UTC.

What extraction does and does not do

  • Times spoken as clock times (“dispatched at 14:21”) become full ISO timestamps on the incident date. Times not spoken are estimated only for tactic milestones the firefighter described happening, in sequence after arrival; they are never invented for events that were not mentioned.
  • The department’s own city and state fill in when the narration omits them, and a mis-heard spelling of the department’s city is corrected to the directory spelling. A different town, such as mutual aid to the next department over, is left as spoken.
  • A call cancelled before arrival is coded NOEMERG||CANCELLED regardless of the nature it was dispatched as; the dispatched nature goes in the narrative.
  • Nothing carries a confidence score. A field is either filled from what was said or absent and listed in gaps. Show the transcript; the firefighter is the confidence score.

Data handling

What a narration stores: partner, key id, mode, department, your external_ref and metadata, the input kind, the transcript, the neris draft and firemic_report we returned, gaps, modules, timings, model versions, a hash of the caller’s IP, and the outcome you report.

  • Audio is stored in a private bucket only we can read and deleted after the partner’s retention window, 30 days by default, by a daily job. A partner can set the window shorter, including zero, in which case audio is never written at all.
  • Transcript, draft and outcome are retained. They are what lets us measure extraction quality per department and improve it, and they are the basis of any accuracy figures we report to you.
  • Isolation: a partner reads only its own narrations. Narration data is never shared with another partner and is not used to train anything outside FireMic.
  • Deletion on request at the partner, department or narration level.
  • NERIS credentials: never requested, never held. This API cannot submit to NERIS.
  • Test mode rows are flagged as test data and excluded from any quality reporting.

Versioning

  • The path carries the major version. Additive changes (new fields, new error codes, new endpoints) ship on /v1 without notice; read responses tolerantly. Anything that removes or changes the meaning of a field ships as /v2 with the old version kept alive.
  • firemic_report is explicitly extended and additive.
  • model.extraction and model.transcription change when the underlying extraction changes materially, so a partner can correlate a change in drafts with a change on our side.

Changelog

  • 2026-09 v1 published: sessions, narrations (audio and text), outcome reporting, department lookup, processing messages.