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
objectfield 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_…orfm_test_…) or a session token fromPOST /v1/sessions. - CORS is open on every route, so browsers and webviews can call
/v1directly with a session token. PreflightOPTIONSreturns 204. - Every response carries an
X-Request-Idheader. Quote it when you report a problem. - Caching: responses are
Cache-Control: no-store, exceptGET /v1/processing-messages, which is public and cacheable for an hour. - Timeouts:
POST /v1/narrationsis 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||CANCELLEDregardless 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
/v1without notice; read responses tolerantly. Anything that removes or changes the meaning of a field ships as/v2with the old version kept alive. firemic_reportis explicitly extended and additive.model.extractionandmodel.transcriptionchange 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.