Skip to content

Developers · Partner API

Quickstart

From a sandbox key to a NERIS-shaped draft. Every example below is complete and runs as written against production.

Last updated September 2026.

0. Check the key

curl -s https://app.fire-mic.com/v1/ping \
  -H "Authorization: Bearer $FIREMIC_KEY"
{
  "ok": true,
  "partner": { "slug": "your-company", "name": "Your Company" },
  "mode": "test",
  "credential": { "via": "api_key", "scope": "partner", "department": 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"
}

mode is test or live; both hit the same pipeline. limits are the ceilings this credential currently has.

1. Find the department’s NERIS id

Every request names a department by its NERIS id, the FD plus eight digits identifier from the national NERIS directory. Your product almost certainly stores it already, because it is the id NERIS itself files under. To check one:

curl -s https://app.fire-mic.com/v1/departments/FD34007744 \
  -H "Authorization: Bearer $FIREMIC_KEY"
{ "object": "department", "neris_id": "FD34007744", "name": "Collingswood Fire Department", "city": "Collingswood", "state": "NJ" }

An id we do not know returns 404 DEPARTMENT_NOT_FOUND. The free department lookup searches the same directory by name.

2. First narration, text mode

No audio needed. This is the fastest way to see the response shape.

curl -s https://app.fire-mic.com/v1/narrations \
  -H "Authorization: Bearer $FIREMIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "department": "FD34007744",
    "external_ref": "INC-2026-001847",
    "transcript": "Engine 31 responded to 214 Maple Avenue for a reported structure fire, dispatched 14:21, on scene 14:27. Two story wood frame, light smoke showing second floor rear. Pulled an inch and three quarter through the front door, fire in a rear bedroom, room and contents, knocked down in about five minutes. Ladder 5 did a primary search, all clear. One occupant self evacuated, evaluated on scene, refused transport. Smoke detectors present and operated. Cause appears to be a space heater. Cleared at 15:45."
  }'

You get back a narration object. The parts you will use first:

{
  "id": "nar_UFaCShS95qMXWZuVIG5NmT1y",
  "status": "complete",
  "transcript": "Engine 31 responded to 214 Maple Avenue …",
  "neris": {
    "incident_types": [{ "type": "FIRE||STRUCTURE_FIRE||ROOM_AND_CONTENTS_FIRE", "primary": true }],
    "base": { "department_neris_id": "FD34007744", "location": { "…": "…" }, "outcome_narrative": "…" },
    "dispatch": { "…": "…" },
    "fire_detail": { "…": "…" }
  },
  "gaps": [
    { "field": "incident_number", "label": "Incident Number", "question": "What is the incident number?" }
  ],
  "timings": { "transcribe_ms": null, "stage1_ms": 3652, "stage2_ms": 474, "total_ms": 4730 }
}

neris is the payload shape NERIS accepts on its own incident endpoint, so the field names are the ones your NERIS integration already knows. The API reference documents every key.

3. Audio mode

Post the recording base64-encoded. Any common container works: webm/opus, ogg/opus, mp4/m4a/aac, wav, mp3, flac. Mono at 32 to 64 kbps is plenty for speech and keeps a minute of audio under half a megabyte.

AUDIO_B64=$(base64 < narration.m4a | tr -d '\n')
curl -s https://app.fire-mic.com/v1/narrations \
  -H "Authorization: Bearer $FIREMIC_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"department\": \"FD34007744\",
    \"external_ref\": \"INC-2026-001847\",
    \"audio\": { \"data\": \"$AUDIO_B64\", \"mime\": \"audio/mp4\", \"duration_seconds\": 59.2 }
  }"

duration_seconds is optional but recommended; it is stored with the narration and lets us refuse a mistaken multi-hour upload before decoding it. Limits: 25 MB and 15 minutes per request.

4. Sessions: keys stay on the server, tokens go to the client

Never put an API key in a browser, a webview, a mobile app or a desktop installer. Mint a short-lived session for the incident instead, from wherever you can keep a secret:

curl -s https://app.fire-mic.com/v1/sessions \
  -H "Authorization: Bearer $FIREMIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "department": "FD34007744", "external_ref": "INC-2026-001847", "ttl_seconds": 900 }'
{
  "object": "session",
  "token": "eyJhbGciOiJIUzI1NiJ9…",
  "token_type": "Bearer",
  "expires_at": "2026-09-02T11:09:55.000Z",
  "mode": "test",
  "department": { "neris_id": "FD34007744", "name": "Collingswood Fire Department", "city": "Collingswood", "state": "NJ" },
  "external_ref": "INC-2026-001847"
}

The client then calls POST /v1/narrations with Authorization: Bearer <token> and no department field; the token already carries it. A session can only act for its department, cannot mint further sessions, and dies with the key it came from. CORS is open on /v1, so a browser or webview can call it directly.

If your desktop client has no server tier at all, use a department-scoped key in that install: it is pinned to one department at issue time, so a leaked key from one station cannot act for another.

5. Report the outcome

When the firefighter saves the report, tell us whether they kept the draft. Optional, one call, and the single most useful thing you can send us.

curl -s https://app.fire-mic.com/v1/narrations/nar_UFaCShS95qMXWZuVIG5NmT1y/outcome \
  -H "Authorization: Bearer $FIREMIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accepted": true,
    "edited_fields": ["base.location_use.level1", "dispatch.time_incident_clear"],
    "submitted_to_neris": true,
    "neris_incident_id": "…",
    "review_seconds": 48
  }'

The same first call in other languages

Each snippet posts a transcript with an API key and prints the primary incident type and the gaps. Swap the transcript for an audio object as in step 3 to send a recording, and swap the key for a session token on any client device.

C# (.NET 8)

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var http = new HttpClient { BaseAddress = new Uri("https://app.fire-mic.com/v1/"), Timeout = TimeSpan.FromSeconds(120) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("FIREMIC_KEY"));

var body = new
{
    department = "FD34007744",
    external_ref = "INC-2026-001847",
    transcript = "Engine 31 responded to 214 Maple Avenue for a reported structure fire …",
    // audio = new { data = Convert.ToBase64String(File.ReadAllBytes("narration.m4a")), mime = "audio/mp4" },
};

using var res = await http.PostAsJsonAsync("narrations", body);
using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!res.IsSuccessStatusCode)
{
    var err = doc.RootElement.GetProperty("error");
    Console.WriteLine($"{(int)res.StatusCode} {err.GetProperty("code")}: {err.GetProperty("message")}");
    return;
}
var root = doc.RootElement;
Console.WriteLine(root.GetProperty("neris").GetProperty("incident_types")[0].GetProperty("type").GetString());
foreach (var gap in root.GetProperty("gaps").EnumerateArray())
    Console.WriteLine($"still needed: {gap.GetProperty("label")}{gap.GetProperty("question")}");

WebView2 hosts: mint the session on the desktop side, open the hosted mic page with the token, and handle its one WebMessageReceived event; that page is documented with the embed guide.

Swift (iOS 15+)

import Foundation

struct Narration: Decodable {
    struct Gap: Decodable { let field: String; let label: String; let question: String }
    let id: String
    let transcript: String
    let neris: [String: AnyCodable]   // map to your model, or decode the modules you use
    let gaps: [Gap]
}

func narrate(token: String, audioURL: URL) async throws -> Narration {
    var req = URLRequest(url: URL(string: "https://app.fire-mic.com/v1/narrations")!)
    req.httpMethod = "POST"
    req.timeoutInterval = 120
    req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let audio = try Data(contentsOf: audioURL).base64EncodedString()
    // department omitted: a session token already carries it.
    req.httpBody = try JSONSerialization.data(withJSONObject: [
        "external_ref": "INC-2026-001847",
        "audio": ["data": audio, "mime": "audio/mp4"],
    ])
    let (data, response) = try await URLSession.shared.data(for: req)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw NSError(domain: "FireMic", code: 1, userInfo: [NSLocalizedDescriptionKey: String(decoding: data, as: UTF8.self)])
    }
    return try JSONDecoder().decode(Narration.self, from: data)
}

Record with AVAudioRecorder into .m4a (AAC, mono, 32 kbps) and send the file when the firefighter taps stop.

Kotlin (Android, OkHttp)

import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.File
import java.util.Base64
import java.util.concurrent.TimeUnit

val client = OkHttpClient.Builder().callTimeout(120, TimeUnit.SECONDS).build()

fun narrate(token: String, audio: File, externalRef: String): JSONObject {
    val body = JSONObject()
        .put("external_ref", externalRef)
        .put("audio", JSONObject()
            .put("data", Base64.getEncoder().encodeToString(audio.readBytes()))
            .put("mime", "audio/mp4"))
    val request = Request.Builder()
        .url("https://app.fire-mic.com/v1/narrations")
        .header("Authorization", "Bearer $token")
        .post(body.toString().toRequestBody("application/json".toMediaType()))
        .build()
    client.newCall(request).execute().use { res ->
        val json = JSONObject(res.body!!.string())
        if (!res.isSuccessful) {
            val err = json.getJSONObject("error")
            throw IllegalStateException("${res.code} ${err.getString("code")}: ${err.getString("message")}")
        }
        return json   // json.getJSONObject("neris"), json.getJSONArray("gaps"), json.getString("transcript")
    }
}

Record with MediaRecorder (AAC in an MPEG_4 container, mono, 32 kbps) and call narrate from a coroutine on Dispatchers.IO.

JavaScript (browser or webview, session token)

// The page got a session token from your backend; the key never reaches the browser.
async function narrate(sessionToken, audioBlob, externalRef) {
  const data = await new Promise((resolve, reject) => {
    const r = new FileReader();
    r.onload = () => resolve(r.result.split(',')[1]);   // strip the data: prefix
    r.onerror = reject;
    r.readAsDataURL(audioBlob);
  });
  const res = await fetch('https://app.fire-mic.com/v1/narrations', {
    method: 'POST',
    headers: { Authorization: `Bearer ${sessionToken}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ external_ref: externalRef, audio: { data, mime: audioBlob.type || 'audio/webm' } }),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${json.error.code}: ${json.error.message}`);
  return json;   // json.neris, json.gaps, json.transcript
}

// Recording: MediaRecorder with { mimeType: 'audio/webm;codecs=opus', audioBitsPerSecond: 48000 }

Node.js (server side, API key)

const res = await fetch('https://app.fire-mic.com/v1/narrations', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.FIREMIC_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    department: 'FD34007744',
    external_ref: 'INC-2026-001847',
    transcript: 'Engine 31 responded to 214 Maple Avenue for a reported structure fire …',
  }),
  signal: AbortSignal.timeout(120_000),
});
const narration = await res.json();
if (!res.ok) throw new Error(`${res.status} ${narration.error.code}: ${narration.error.message}`);
console.log(narration.neris.incident_types[0].type, narration.gaps.map(g => g.label));

What to build next

  1. Field map, once. Walk neris module by module against your incident model. It is the NERIS payload shape, so most keys map one to one. We will sit with your engineer for an hour to do it.
  2. Show the transcript. Next to the filled fields, always. The firefighter checks what was heard against what was said.
  3. Show the gaps. Each gap has a question written for a firefighter. Three empty fields with a question each beats a wall of red.
  4. Send outcomes. Accepted or not, and which fields were edited. That is the loop that makes the drafts better for your departments.