Fire RecordAPI v1
Contents

Fire Record API (v1)

For developers connecting their own system (a CRM, a job manager, a booking tool) to Fire Record. Your system can:

Fire Record switches the API on for your organisation. After that, the owner or an admin manages everything in Settings, Integrations.

1. The basics

Address. Every call starts with the API address shown in Settings, Integrations:

text
https://api.firerecord.co.uk/api/v1

Key. Make a key in Settings, Integrations, and name it after your system: assessors see that name in the notifications it sends them (in their lists, the job shows as assigned by the person who made the key). An organisation can have up to 10 live keys, with names up to 60 characters. The key is shown once, so copy it there and then. Send it on every call:

text
Authorization: Bearer fr_live_...

If your tool cannot set that header, send the key as the Basic auth username (the password is ignored).

  • Keep the key on your server, in an environment variable. Never put it in a web page or in code that runs in a browser: a call from a web page is refused with 403 cors_refused.
  • A key acts as the owner or admin who made it, for the whole organisation.
  • It stops working when it is revoked (401 invalid_key), when the person who made it is deactivated, leaves the team or stops being an admin (401 key_owner_inactive), or when the API is switched off for your organisation (403 api_access_off).
  • A key can be limited to your server's public IP addresses: single addresses or ranges, IPv4 or IPv6, for example 203.0.113.7 or 203.0.113.0/24, up to 20 (Settings, Integrations, Addresses). A call from anywhere else is refused with 403 ip_not_allowed, which names the address we saw; Activity shows the same address. An entry that is not an address or range is refused when you save it.

Format.

  • Send JSON with Content-Type: application/json. A body sent as a form (for example curl -d without that header) is read as form fields and refused; with any other type the body is ignored and the answer names a missing field.
  • Field names use snake_case. Text is trimmed.
  • Dates are days, written YYYY-MM-DD, and must be real days (2026-02-30 is refused). Send null to leave a date out or to clear it; an empty string is refused for a date. A date-time is accepted, but only its UTC day is kept, so send the day alone.
  • The one time of day is a visit's visit_time: HH:MM, 24-hour, UK time (for example 09:30).
  • Times in answers, such as created_at, are ISO 8601 in UTC.

Ids.

  • Every record has our id (id, a UUID we make) and your id (external_id, the id in your system, which you send: 1 to 100 characters, case-sensitive; null for records made in Fire Record). Clients, buildings and assessments each have their own set of your ids, so the same value can be a client and a building.
  • Where a call needs a client, building or assessment, you can send our id or your id. The one exception is the report PDF, which takes our id only. Save our id from each answer.
  • In a web address, encode your id (encodeURIComponent): ids with /, spaces, #, ? or accents all work once encoded.
  • An id of ours that is not a UUID is refused with 422. An id that is not one of your organisation's answers 404 in the web address, and 422 client_not_found or building_not_found in a body or query.

Safe to repeat.

  • A create with an external_id you have already used returns the record made the first time, unchanged, with status 200 instead of 201. If the repeat carried different values, the answer lists them in not_applied (a date sent as null is not compared) and says so in message, for example Already in Fire Record, so this was not changed: visit date. To change it, use PATCH /api/v1/assessments/external/JOB-1042. (for a job the assessor has started: change it in Fire Record). Use the change calls to change it. A retry or a double click never makes a copy, and two identical creates sent at the same moment make one record.
  • A cancelled job keeps your id. Sent again, it answers 200 with the cancelled job and a message saying nothing was booked. To book that visit again, send a new external_id, for example your job number with a suffix.
  • The client or building you name must still exist. If it was deleted in Fire Record, the create is refused (422) and you create it again.

Strict fields. An unknown field in a create or change body, or an unknown query parameter on the three list calls, is refused with a 422 naming the first one, so a typo is never silently ignored. Other calls ignore a query string, and cancel ignores any body.

Who owns what. Your system owns a job until the assessor presses Start; after that it is Fire Record's. Clients and buildings are never deleted through the API. Section 3 has the full rules.

Limits.

  • Up to 120 calls a minute for each key, and 600 a minute from one IPv4 address (or IPv6 /56 network). Over that you get 429 rate_limited, and the Retry-After header says how many seconds to wait. Answers carry RateLimit-Policy, RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.
  • Bodies up to 2 MB (413 body_too_large).

2. Create: client, then building, then assessment

The same three steps as in Fire Record: an assessment is assigned on a building, and a building belongs to a client. Each create answers 201 with the new record, or 200 with the existing one for an external_id you have used before.

POST /clients

FieldRequiredNotes
external_idyesyour id for the client, up to 100 characters
nameyesup to 255 characters
contact_personnoup to 255 characters
emailnoa valid email address, up to 255 characters
phonenoup to 50 characters
addressnoone line, up to 500 characters

Optional fields take null or an empty string for none.

bash
curl -X POST "$API/clients" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"external_id":"crm-c-104","name":"Riverside Housing Ltd","email":"ops@riverside.example"}'
json
{
  "id": "7f3a1c2e-...",
  "external_id": "crm-c-104",
  "name": "Riverside Housing Ltd",
  "contact_person": null,
  "email": "ops@riverside.example",
  "phone": null,
  "address": null,
  "created_at": "2026-10-15T09:12:03.120Z"
}

POST /buildings

FieldRequiredNotes
external_idyesyour id for the building, up to 100 characters
client_id or client_external_idyes, exactly oneour client id, or your id used when creating the client
nameyesup to 255 characters
addressnothe street, up to 500 characters with the town and postcode
townnoup to 100 characters; only with address
postcodenoup to 20 characters; only with address
premises_typenoresidential, commercial or mixed (any case). It decides the standard: BS 9792, PAS 79, or both. Leave it out, or send null or an empty string, if you do not know it: the assessor chooses it in Fire Record before starting, and premises_type reads null until then. Only the first value counts: a repeat with another value lists it in not_applied
residential_usenofor a residential or mixed building: Purpose-Built Flats, Converted Flats, HMO, Sheltered Housing, Maisonettes or Single Dwelling. Any case, spaces or hyphens; other text is kept as written, up to 100 characters
commercial_usenofor a commercial or mixed building: Office, Retail, Warehouse, School / Education, Healthcare, Hotel / Hospitality, Restaurant / Food Service, Place of Assembly, Industrial, Place of Worship or Care Home. Other text is kept as written, up to 255 characters
storeysnofloors above ground, including the ground floor: a whole number, 0 to 200
people_on_sitenothe most people likely to be in the building: a whole number. The report prints it as Estimated Occupants
site_notesnoaccess, parking and site notes, up to 2000 characters. For the assessor only: shown on the building and with their task, never printed in the report. They stay with the building for every visit
responsible_personnoup to 255 characters
responsible_person_contactnoup to 255 characters
bash
curl -X POST "$API/buildings" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"external_id":"crm-b-221","client_external_id":"crm-c-104","name":"Riverside Mansions","address":"12 River Street","town":"Leeds","postcode":"LS1 4AB","premises_type":"residential","residential_use":"Purpose-Built Flats","storeys":5,"people_on_site":60,"site_notes":"Key safe by the main door. Visitor parking at the rear."}'

The answer has the building with id, external_id, client_id, client_external_id, name, address, premises_type, residential_use, commercial_use, storeys, people_on_site, site_notes, responsible_person, responsible_person_contact, created_at.

One address line. Fire Record keeps a building's address as one line, so the address, town and postcode are joined with commas: the building above reads 12 River Street, Leeds, LS1 4AB, and that is what address returns. The postcode is written in capitals. A town or postcode the address already contains is not added again. town and postcode come with address: either without it is refused with 422 (field address), and changing the address means sending all three. Building search finds the postcode in the line.

Uses. A use that matches our list is kept in our words, which lets Fire Record pick the right checklist. residential_use is refused for a commercial building and commercial_use for a residential one (422); a mixed building, or one whose type is not chosen yet, takes both.

A client that is not one of yours is refused with 422 client_not_found. A mixed building is accepted; assigning an assessment on it needs hybrid reports switched on for your organisation.

POST /assessments

Assigns an assessment to someone in your team, as Assign does in Fire Record: they get a New task notification, named after your key, and the report waits in their Tasks. In an organisation with BAFE SP205 on, every new report is a BAFE report (bafe: true): there is nothing to send for it.

FieldRequiredNotes
external_idyesyour id for the job (your job number), up to 100 characters
building_id or building_external_idyes, exactly oneour building id, or your id used when creating the building
assessor_emailyesthe email the person signs in to Fire Record with. They must be an active member of your team (any role, any case)
due_datenoYYYY-MM-DD: when the report must be done
visit_datenoYYYY-MM-DD: the site visit you have booked, shown to the assessor beside the due date and in their New task notification
visit_timenoHH:MM, 24-hour, UK time: the time of that visit. Only with visit_date
assessment_typenothe survey Type, 1 to 4, for a residential or mixed building (BS 9792): 1 common parts, non-destructive; 2 common parts, destructive; 3 common parts and sample flats, non-destructive; 4 common parts and sample flats, destructive. Refused for a commercial building (PAS 79 has none). Left out, the job is Type 1, and the assessor can change it on Setup
notesnoabout this visit, shown to the assessor with the task (for example the site contact), up to 2000 characters. Notes that stay with the building go in its site_notes
bash
curl -X POST "$API/assessments" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"external_id":"crm-j-889","building_external_id":"crm-b-221","assessor_email":"michelle@yourfirm.example","due_date":"2026-11-03","visit_date":"2026-10-28","visit_time":"10:00","assessment_type":1,"notes":"Ask for Sam at reception"}'
json
{
  "id": "c52ff52e-...",
  "external_id": "crm-j-889",
  "reference": "FRA-202610-79Z77L",
  "status": "assigned",
  "editable": true,
  "bafe": true,
  "building_id": "024d5d03-...",
  "client_id": "7f3a1c2e-...",
  "assessor_email": "michelle@yourfirm.example",
  "due_date": "2026-11-03",
  "visit_date": "2026-10-28",
  "visit_time": "10:00",
  "assessment_type": 1,
  "assessment_date": null,
  "review_due_date": null,
  "issue_date": null,
  "overall_risk": null,
  "created_at": "2026-10-15T09:12:05.640Z",
  "status_changed_at": "2026-10-15T09:12:05.652Z",
  "report_available": false,
  "report_url": null,
  "actions": null,
  "app_url": "https://app.firerecord.co.uk/wizard/c52ff52e-.../setup"
}
FieldMeaning
idour id
external_idyour id, or null for a report made in Fire Record
referencethe report reference, for example FRA-202610-79Z77L
statuswhere it stands (section 5)
editabletrue while the job is still yours to change or cancel (status assigned)
bafetrue for a BAFE SP205 report
building_id, client_idour ids
assessor_emailwho it is assigned to
due_date, visit_date, visit_timeyour dates, and the time of the visit
assessment_typethe survey Type, 1 to 4; null for a commercial building (PAS 79), and for a report made before the current Types (October 2026)
assessment_datethe report's assessment date: null until the assessor presses Start; then the day of Start for a job you assigned (a report started in Fire Record keeps the day it was made), unless the assessor sets another
review_due_datewhen the next assessment is due, once the assessor has set it
issue_date, overall_riskthe report's issue date and its overall risk rating as the report prints it (for example Tolerable, or Very high on the five by five matrix), once the report is completed, signed off or validated; null before. For BAFE the issue date is the day of validation
actionsonce the action plan is frozen (section 5): { "total", "open", "in_progress", "completed" }, the number of actions in each state; null before
created_atwhen the job was made
status_changed_atwhen the report last showed up in the changes list (section 5)
report_available, report_urlwhether the PDF can be fetched, and where (section 5)
app_urlthe report in Fire Record, for a link in your system (it needs a Fire Record sign-in)

Refusals:

  • 422 building_not_found: the building is not one of yours.
  • 422 assessor_not_found: that email is not an active member of your team.
  • 409 assessment_already_open: see below. It gives the open report's assessment_id and reference, and usually its external_id.
  • 403 hybrid_not_enabled: an assessment on a mixed building needs hybrid reports switched on.

One open job per building. While a building has a report that is assigned, in progress, with the validator or returned, a new job on it is refused with 409 assessment_already_open, naming that report (it may be one started in Fire Record). This stops the same visit being booked twice. As soon as the last report is finished (completed, signed off, validated or cancelled), the next one is accepted: that is how you book the annual re-inspection. Two jobs sent for one building at the same moment cannot both get through.

A visit after the due date is accepted: that is your call.

3. Changing things: who owns what

Before the assessor presses StartAfter Start
The job: assessor, due date, visit date and time, Type, notesyou change it (PATCH)409 already_started: change it in Fire Record
Cancelling the jobyou cancel it (POST .../cancel)409 already_started
Client and building details, including namesyou change them409 report_in_progress while a report on them is in progress, with the validator or returned; yours again once it is finished
Deleting a client or buildingnever through the APInever through the API
The action plan: each action's status, completed date, notes and who is responsiblenot there yetboth sides change it, once the report is signed off or validated (section 5)

Every assessment carries editable: true while it is still yours to change. A Start pressed a moment before your change always wins, and your change is refused. These rules cover every report not yet started in your organisation, including ones assigned in Fire Record.

PATCH /assessments/{id}

Also PATCH /assessments/external/{external_id}. Send only the fields to change, at least one: assessor_email, due_date, visit_date, visit_time, assessment_type, notes. null clears a date, the time or the notes; clearing visit_date clears visit_time too, and a time needs a visit date, sent or already kept. The assessor and the Type cannot be cleared. The answer is the assessment.

A new assessor gets the New task notification and write access to the building; the previous one is told, and loses the building unless they have other work there or made the key. When the visit day or time changes, the assessor is told: "Visit moved: Hill Street. Now 19/11/2026 at 09:30 (was 18/11/2026 at 14:00).", or "Visit date removed" when you clear it. Sending the same visit again tells nobody. A change of the due date or the notes alone does not notify them. A cancelled job answers 409 cancelled.

POST /assessments/{id}/cancel

Also POST /assessments/external/{external_id}/cancel, with no body. Cancels a job that has not started, as Delete does in Fire Record: it leaves the assessor's Tasks and they are told. The answer is the assessment, now cancelled. Cancelling again is harmless. A cancelled job keeps your id, cannot be changed (409 cancelled) and has no PDF (410 cancelled).

PATCH /clients/{id} and PATCH /buildings/{id}

Also by /external/{external_id}. Clients: name, contact_person, email, phone, address. Buildings: name, address (with town and postcode), residential_use, commercial_use, storeys, people_on_site, site_notes, responsible_person, responsible_person_contact. Send at least one field. null or an empty string clears an optional field; a name cannot be cleared. Sending address replaces the whole address line, so send the town and postcode with it. The premises type, a building's client and your ids cannot be changed through the API.

A building change keeps the same rules as in Fire Record:

  • 409 building_locked: a validated BAFE report on the building is still storing its issued copy. Send it again after the seconds in the Retry-After header (about a minute).
  • 409 building_name_locked: some downloaded reports keep the building name and address they were issued with, so those cannot change (field names the first you sent; the message names both). Sending the name or address it already has is not a change, and the other details can still be changed.
  • 409 building_read_only: a report on the building was downloaded more than five months ago, so its details can no longer be changed.

A rename never changes a report already made. A stored PDF is written once. A BAFE report's issued copy is frozen at validation, and every download serves that same file. For other reports, the next download in Fire Record after a rename makes a new copy with the new name beside the old one; the API then serves the newest and announces it with report.ready.

What Fire Record changes, and what you hear

  • The assessor, due date or visit date changed in Fire Record (for example Reassign): webhook assessment.updated, with changed listing which.
  • An action's status, completed date, notes or who is responsible changed in Fire Record's Action Tracker: assessment.action_updated.
  • A report's status moves on: assessment.status_changed.
  • A report made in Fire Record rather than through the API: assessment.status_changed once, with previous_status and external_id both null.
  • A report deleted in Fire Record: assessment.status_changed with cancelled. It stays readable as cancelled until its building or client is deleted.
  • A building can only be deleted in Fire Record once its reports are gone; your id for it then answers 404.
  • A client deleted in Fire Record takes its buildings and their reports with it. Each of its reports not already cancelled (completed and signed-off ones too; not a validated report whose PDF you have had) is announced cancelled with only assessment_id, external_id, status and previous_status filled in. Your ids for all of them then answer 404, those reports drop out of the changes list, and sending the same ids again makes new records.
  • Not announced: client and building details edited in Fire Record (names, address, building details at Setup, site notes), notes, the visit time, the Type, the review date, the assessment date, the premises type chosen at Setup, and a report moved to another building at Setup (its building_id and client_id change). Read them with the GET calls when you need them. Our ids never change.

Your own changes are never sent back to you as webhooks, but they do appear in the changes list (section 5).

4. Find clients and buildings already in Fire Record

Before creating a building that may already exist (one assessed before you connected), look it up and use its id. Otherwise you would create a second copy.

  • GET /clients?search=riverside searches client names.
  • GET /buildings?search=LS1 4AB searches building names and addresses: a postcode as written in the address, or part of the street. Encode the search text in the address.
  • GET /buildings?client_id=... (or client_external_id=..., not both) lists a client's buildings, and can be combined with search. An unknown client answers 422 client_not_found.
  • Results are sorted by name. search is up to 100 characters; limit is 1 to 100 (default 25). There is no paging, so narrow the search if you reach the limit.

Both answer { "data": [ ... ] }, in the same shape as the create calls. A record made in Fire Record has external_id: null, and there is no call to attach your id to it: store our id in your system instead.

5. Read

  • GET /assessments/{id} reads one assessment by our id; GET /assessments/external/{external_id} by your id.
  • GET /clients/{id}, GET /buildings/{id}, and the same with /external/{external_id}.
  • GET /buildings/{id}/assessments (or /buildings/external/{external_id}/assessments) lists every report on the building, newest first, up to 100, cancelled ones included, each with its status, dates, review_due_date and whether its PDF is ready. Use it to see when the next assessment is due.

The changes list

GET /assessments?changed_since=2026-10-15T09:00:00Z&limit=50 lists the assessments that changed after that time, oldest change first: a new status, an assessor, due date or visit date changed in Fire Record, a PDF arriving, an action updated, a report first seen, and your own creates, changes, cancels and action updates. An item can also reappear with nothing new, for example after the API is switched off and on again.

json
{ "data": [ { "...": "assessment" } ], "has_more": true, "next_cursor": "eyJ1Ijoi..." }
  • For the next page, call again with cursor=<next_cursor>. Keep going until has_more is false: a page can hold fewer items than limit, even none, while there are more.
  • limit is 1 to 100 (default 50). A cursor that cannot be read is refused with 422 (field cursor). changed_since is exclusive; encode a + in a time offset as %2B, or use a Z time. Without changed_since the list starts from the beginning.
  • This is the safety net for a missed webhook, and the way to keep up if your system cannot receive webhooks. Poll every few minutes. For the next poll, start from the status_changed_at of the last item you saw, minus two minutes, and skip items whose id and status_changed_at you have already handled.
  • The list covers every report in your organisation, including ones made in Fire Record (their external_id is null). Client and building edits are not in it.

Statuses

  • A report that is not BAFE: assigned, then in_progress, then completed, then signed_off.
  • A BAFE SP205 report: assigned, then in_progress, then with_validator, then validated. A report the validator returns shows returned, then with_validator again when it is resent; one taken back while it waits shows in_progress. A BAFE report never shows completed or signed_off.
  • Either can end cancelled.
statusMeaning
assignedmade, not started yet: the only status you can change or cancel
in_progressthe assessor pressed Start. For BAFE, also a report finished but not yet sent to the validator, or taken back to change
completednot BAFE: finalised in Fire Record, which happens when it is downloaded there. Use report_available to know the PDF is there
signed_offnot BAFE: signed off by an admin. Can be reopened, which takes it back to completed
with_validatorBAFE: sent to the Lead for validation
returnedBAFE: sent back to the assessor to correct
validatedBAFE: validated by the Lead. Final
cancelledcancelled by you, or deleted in Fire Record. Final

How you learn that a report is validated

  1. assessment.status_changed with status validated.
  2. report.ready, once the issued report with its certificate is available (report_available turns true): fetch it from report_url.
  3. As the safety net, the changes list shows it validated with report_available: true.

The PDF

GET /assessments/{id}/report.pdf returns the PDF. It takes our id only (it is the report_url of the assessment) and needs your key like every call.

  • Not BAFE: the newest copy downloaded in Fire Record. Downloading again without changes does not make a new copy. Word files and the Trail of Records are never served.
  • BAFE: the issued report with its certificate, made at validation and available once it is stored (report_available says so). Every download is the same file.
  • The answer is the file, with Content-Type: application/pdf and Content-Disposition: attachment; filename="<reference>.pdf". Until it is available the answer is 404 report_not_ready; a cancelled job answers 410 cancelled; if the stored file cannot be read, 503 report_unavailable (try again in a few minutes, or, if the message asks, tell Fire Record and quote the report reference). The answer also carries Cache-Control: no-store. The API never makes a PDF itself.

report_available and report_url on the assessment tell you when it is there. The report.ready webhook says so too.

The action plan

A report's actions are what must be done after the assessment, each with its priority and timescale. While the report is being written they can still change, so they are available once the plan is frozen: when the report is signed off, or validated for BAFE. From then on the list keeps its actions and their ids, as Fire Record's Action Tracker does, and both sides record progress on it. Each call also works by /assessments/external/{external_id}/....

GET /assessments/{id}/actions lists them:

json
{
  "data": [
    {
      "id": "card-6c1e0a52",
      "ref": "7.1",
      "section_code": "7",
      "section_name": "Means of Escape",
      "location": "Ground floor corridor",
      "issue": "The fire door to the stairs is wedged open.",
      "action": "Remove the wedge and fit a hold-open device linked to the alarm.",
      "priority": "P1",
      "timescale": "Immediate (24 hours)",
      "status": "open",
      "completed_date": null,
      "notes": null,
      "responsible": null
    }
  ]
}
FieldMeaning
idthe action's id, for the update call. Use it, not ref, to match actions in your system
refits number in the report, for people (7.1 is the first action in section 7)
section_code, section_namethe report section it belongs to
location, issue, actionwhere, what was found, and what to do
priorityP1 (most urgent) to P4
timescaleas the report prints it, for example Short-term (1-4 weeks)
statusopen, in_progress or completed
completed_datethe day it was done, YYYY-MM-DD, or null
notes, responsibleprogress notes, and who is responsible for it

PATCH /assessments/{id}/actions/{action_id} updates one action. action_id is the action's id, or its number on the report (7.1) while only one action has that number: two with the same number answer 409 action_ambiguous, and you use the id. Send at least one of status (open, in_progress or completed), completed_date (a day, or null), notes (up to 2000 characters) and responsible (up to 255 characters). Marking an action completed without a completed_date dates it today (UK time), unless it already has a date; send completed_date to give another day. The answer is the action. Each change is recorded in the report's Trail of Records, as a change in Fire Record is. A validated BAFE report's actions can be updated too: only its report is locked.

GET /assessments/{id}/actions.pdf returns the action tracker as a PDF, as Fire Record makes it from the same list, with Content-Disposition: attachment; filename="<reference>-actions.pdf".

  • Before the plan is frozen all three answer 404 actions_not_ready, saying what it waits for; a cancelled job answers 410 cancelled; an id or number that is not on the list answers 404 action_not_found.
  • A report reopened after sign-off keeps its list; actions added to it then join the end of the list with new ids. Their ref may repeat an earlier one, which is why matching is by id.
  • Every assessment carries actions with the number of actions in each state, so the changes list is enough to keep counts up to date.
  • An update made in Fire Record comes as assessment.action_updated; yours are never sent back, but show the job again in the changes list.

6. Webhooks

Set your address in Settings, Integrations.

  • It must be https://, on a public host (not a private or local address), with no user name or password in it, up to 500 characters. It may carry its own key in the query string (for example ?key=...): we call it exactly as you give it.
  • Saving the first address makes your signing secret (see Checking the signature).
  • The Send a test event button sends one straight away and shows what your system answered.
  • Events that happen while no address is set are not sent later. Removing the address, or the API being switched off, stops deliveries still waiting; a changed address, or a new secret, applies to them.

Each webhook is a POST with a JSON body:

json
{
  "type": "assessment.status_changed",
  "timestamp": "2026-10-15T10:01:44.120Z",
  "data": {
    "assessment_id": "c52ff52e-...",
    "external_id": "crm-j-889",
    "reference": "FRA-202610-79Z77L",
    "status": "in_progress",
    "previous_status": "assigned",
    "bafe": true,
    "building_id": "024d5d03-...",
    "client_id": "7f3a1c2e-..."
  }
}

timestamp is when the event was recorded, and stays the same on every retry. Do not rely on the order of the fields.

typeWhendata
assessment.status_changeda report's status changes; also a report made in Fire Record, announced once with previous_status nullas above. For a report removed with its client, reference, bafe, building_id and client_id are null
assessment.updatedthe assessor, due date or visit date was changed in Fire Recordassessment_id, external_id, reference, status, changed (assessor, due_date, visit_date), assessor_email, due_date, visit_date
report.readyits PDF is available, or a newer copy isassessment_id, external_id, reference, status, bafe, report_url
assessment.action_updatedan action of a frozen plan was updated in Fire Recordassessment_id, external_id, reference, action_id, ref, changed (status, completed_date, notes, responsible). Fetch the actions for the new values
testSend a test eventmessage, organization

Ignore types you do not know. New event types may be added. Answer them with a 2xx and do nothing, so they are not retried.

Treat a webhook as a doorbell. When one arrives, fetch the assessment (GET /assessments/{assessment_id}) with your key, and save what that returns. Even a forged webhook can then never put wrong data into your system. If that answers 404 after a cancelled event, the report was removed with its client: treat it as cancelled.

Answer quickly. Reply with any 2xx within 15 seconds, then do the work. Redirects count as failures.

Retries. If we get no 2xx, we try again after 1, 5 and 30 minutes, then after 2, 6, 12, 24 and 24 hours: 9 tries over about three days, then the delivery is marked failed. You can resend a delivery from Settings, Integrations, Activity; a resend starts the schedule again. A test event that fails is retried the same way.

Duplicates and order. The same event keeps the same webhook-id header on every retry and resend. Ignore an id you have already handled. Events can arrive out of order, which is another reason to fetch the assessment rather than trust the event. Changes usually reach you within a minute or two.

What is watched. Every report of your organisation while the API is on. A report made in Fire Record and first seen more than 30 minutes later is recorded without an event. Reports more than 400 days old that never finished are no longer watched. Switching the API off and on again records every report as it stands, without events.

Checking the signature

Every webhook carries these headers (the Standard Webhooks scheme):

  • content-type: application/json and user-agent: FireRecord-Webhooks/1
  • webhook-id: a UUID, the same on every retry
  • webhook-timestamp: Unix seconds of this try
  • webhook-signature: v1, followed by a base64 HMAC-SHA256 (one signature)

The signed text is webhook-id + "." + webhook-timestamp + "." + body, using the raw body exactly as received. The key is your signing secret from Settings, Integrations, without its whsec_ prefix, base64-decoded. Refuse a timestamp more than 5 minutes away from your clock, old or ahead.

Node, with the standardwebhooks package:

js
import { Webhook } from 'standardwebhooks';
const wh = new Webhook(process.env.FIRE_RECORD_WEBHOOK_SECRET); // whsec_...
// rawBody: the request body as a string, before any JSON parsing
const event = wh.verify(rawBody, {
  'webhook-id': req.headers['webhook-id'],
  'webhook-timestamp': req.headers['webhook-timestamp'],
  'webhook-signature': req.headers['webhook-signature'],
}); // throws if it does not match

Python, without a library:

python
import base64, hashlib, hmac, time

def verify(secret, headers, raw_body: bytes):
    msg_id, ts, sig = headers['webhook-id'], headers['webhook-timestamp'], headers['webhook-signature']
    if abs(time.time() - int(ts)) > 300:
        raise ValueError('too old')
    key = base64.b64decode(secret.removeprefix('whsec_'))
    signed = f'{msg_id}.{ts}.'.encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    if not any(hmac.compare_digest(expected, s.split(',', 1)[1]) for s in sig.split(' ') if s.startswith('v1,')):
        raise ValueError('bad signature')

Make a new secret in Settings, Integrations replaces the secret at once, for deliveries still being retried too. There is no overlap, so update your system at the same time.

7. Errors

Every error has the same shape. The message says in words what to fix:

json
{ "error": { "code": "invalid_request", "message": "\"name\" is required.", "field": "name", "request_id": "eaa2f7fc-a4d0-4a4d-882f-90b256d47a99" } }

Every answer also carries the same id in the X-Request-ID header (send your own X-Request-ID and we use it). Quote it when you ask Fire Record about a call. Some refusals add details: report_in_progress, assessment_already_open and building_locked give the report's assessment_id and reference (assessment_already_open usually also its external_id); building_name_locked and building_read_only give its reference.

StatuscodeMeaning
400invalid_jsonthe body is not JSON
401missing_key, invalid_key, key_owner_inactiveno key (or not a Fire Record key), a wrong or revoked key, or its maker is no longer an active admin
403api_access_offthe API is switched off for your organisation
403ip_not_allowedthe key is limited to other addresses
403cors_refusedthe call came from a web page: call from your server
403hybrid_not_enabledan assessment on a mixed building needs hybrid reports switched on
404not_foundno such record in your organisation, or no such call (an unknown call without a valid key answers 401 first)
404report_not_readythe PDF is not available yet
404actions_not_readythe action plan is available once the report is signed off, or validated for BAFE
404action_not_foundno action with that id or number on this report (see field)
409action_ambiguoustwo actions on this report have that number: use the action's id
409assessment_already_openthe building already has an open report (named in the message)
409already_startedthe assessor has started this job: change it in Fire Record
409report_in_progressa report on this client or building is being worked on: its details are Fire Record's until it is finished
409building_lockeda validated BAFE report on this building is still storing its issued copy: try again after Retry-After seconds
409building_name_lockeda downloaded report keeps this building's name and address (see field)
409building_read_onlya report on this building can no longer be changed, so neither can the building
409cancelledthis job has been cancelled
410cancelledthe PDF or action plan of a cancelled job: there is none
413body_too_largethe body is over 2 MB
422invalid_requesta field, query parameter or id is missing, wrong or unknown (see field), or a date that does not exist
422client_not_found, building_not_foundthe client or building is not one of yours
422assessor_not_foundthat email is not an active member of your team
429rate_limitedmore than 120 calls a minute on a key, or 600 from one address: wait the Retry-After seconds
500server_errorour side, or a body in an encoding we cannot read; try again shortly
503report_unavailablethe stored PDF could not be read: try again shortly, or tell Fire Record if the message asks

8. Seeing what happened: Activity

Settings, Integrations, Activity shows every call your system made with a key we recognise, and every webhook Fire Record sent, newest first, for the last 90 days.

  • Tick Errors only to see just the calls that were refused and the webhooks that failed or are being retried.
  • Open a call to see its request id, the key, the address it came from, your id and our id where the call carried them, the error field and message, how long it took, and the JSON body of a POST (up to 4 KB).
  • Open a webhook to see what was sent and every try, with what your system answered and how long it took. Resend sends one that has not got through.

Calls with an unknown key, and calls refused as coming from a web page, are not listed.

9. Common questions

Does every webhook carry our job number? Yes. Every assessment event has external_id (your id; null for a report made in Fire Record) beside assessment_id (our id) and the report reference (null only for a report removed with its client). The test event carries none of these.

Can we look up an assessment by our job number? Yes: GET /assessments/external/{your job number}. A create with a job number you have already sent returns that job instead of making another, so a retry is always safe.

We deleted a stuck job in Fire Record. Can we send it again? Send it with a new external_id. A deleted job keeps your id and answers as cancelled until its building or client is deleted in Fire Record, which frees the ids of everything under it. Before Start you can also cancel a job yourself (POST .../cancel).

Can the visit date change after Start? Not through the API. Once the assessor presses Start the job is Fire Record's, and a change answers 409 already_started.

Why was a rename refused? A report on the client or building is being worked on (report_in_progress), a downloaded report keeps the building's name and address (building_name_locked), a validated BAFE report is still storing its issued copy (building_locked), or the building is read-only (building_read_only).

How do we hear about edits made in Fire Record? A new assessor, due date or visit date comes as assessment.updated, an updated action as assessment.action_updated. The assessment date, review date, notes and client and building details are not announced: read them when you need them.

Where do the visit time and site notes go? The time in the job's visit_time. Notes about the building that hold for every visit (access, parking, key safe) in the building's site_notes; notes about this visit only in the job's notes. Both are for the assessor and are never printed in the report.

When can we see the actions? Once the report is signed off, or validated for BAFE. Until then actions reads null; watch for the status change, then fetch them.

Which status ends a BAFE report? validated. A BAFE report has no signed_off.

10. Not in this version yet

These are planned and not in the API yet. Fields that are not listed in this guide are refused with a 422, so leave them out for now.

  • Documents per building, and the record of the pre-assessment request.