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:
- create clients, buildings and assessments assigned to someone in your team;
- change or cancel an assessment until the assessor starts it, and correct client and building details;
- find clients and buildings already in Fire Record, and see every report on a building;
- read where each report stands, and list what has changed;
- download the finished report PDF;
- follow the action plan once a report is signed off or validated: list its actions, update their status, and download the action tracker;
- receive webhooks when a report starts, changes status, is signed off, validated or cancelled, when a report is made in Fire Record, when its assessor, due date or visit date is changed in Fire Record, when an action is updated in Fire Record, and when its PDF is ready.
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:
https://api.firerecord.co.uk/api/v1Key. 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:
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 (401key_owner_inactive), or when the API is switched off for your organisation (403api_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.7or203.0.113.0/24, up to 20 (Settings, Integrations, Addresses). A call from anywhere else is refused with 403ip_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 examplecurl -dwithout 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). Sendnullto 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 example09: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;nullfor 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_foundorbuilding_not_foundin a body or query.
Safe to repeat.
- A create with an
external_idyou 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 innot_applied(a date sent asnullis not compared) and says so inmessage, for exampleAlready 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
messagesaying nothing was booked. To book that visit again, send a newexternal_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 theRetry-Afterheader says how many seconds to wait. Answers carryRateLimit-Policy,RateLimit-Limit,RateLimit-RemainingandRateLimit-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
| Field | Required | Notes |
|---|---|---|
| external_id | yes | your id for the client, up to 100 characters |
| name | yes | up to 255 characters |
| contact_person | no | up to 255 characters |
| no | a valid email address, up to 255 characters | |
| phone | no | up to 50 characters |
| address | no | one line, up to 500 characters |
Optional fields take null or an empty string for none.
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"}'{
"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
| Field | Required | Notes |
|---|---|---|
| external_id | yes | your id for the building, up to 100 characters |
| client_id or client_external_id | yes, exactly one | our client id, or your id used when creating the client |
| name | yes | up to 255 characters |
| address | no | the street, up to 500 characters with the town and postcode |
| town | no | up to 100 characters; only with address |
| postcode | no | up to 20 characters; only with address |
| premises_type | no | residential, 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_use | no | for 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_use | no | for 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 |
| storeys | no | floors above ground, including the ground floor: a whole number, 0 to 200 |
| people_on_site | no | the most people likely to be in the building: a whole number. The report prints it as Estimated Occupants |
| site_notes | no | access, 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_person | no | up to 255 characters |
| responsible_person_contact | no | up to 255 characters |
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.
| Field | Required | Notes |
|---|---|---|
| external_id | yes | your id for the job (your job number), up to 100 characters |
| building_id or building_external_id | yes, exactly one | our building id, or your id used when creating the building |
| assessor_email | yes | the email the person signs in to Fire Record with. They must be an active member of your team (any role, any case) |
| due_date | no | YYYY-MM-DD: when the report must be done |
| visit_date | no | YYYY-MM-DD: the site visit you have booked, shown to the assessor beside the due date and in their New task notification |
| visit_time | no | HH:MM, 24-hour, UK time: the time of that visit. Only with visit_date |
| assessment_type | no | the 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 |
| notes | no | about 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 |
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"}'{
"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"
}| Field | Meaning |
|---|---|
| id | our id |
| external_id | your id, or null for a report made in Fire Record |
| reference | the report reference, for example FRA-202610-79Z77L |
| status | where it stands (section 5) |
| editable | true while the job is still yours to change or cancel (status assigned) |
| bafe | true for a BAFE SP205 report |
| building_id, client_id | our ids |
| assessor_email | who it is assigned to |
| due_date, visit_date, visit_time | your dates, and the time of the visit |
| assessment_type | the survey Type, 1 to 4; null for a commercial building (PAS 79), and for a report made before the current Types (October 2026) |
| assessment_date | the 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_date | when the next assessment is due, once the assessor has set it |
| issue_date, overall_risk | the 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 |
| actions | once the action plan is frozen (section 5): { "total", "open", "in_progress", "completed" }, the number of actions in each state; null before |
| created_at | when the job was made |
| status_changed_at | when the report last showed up in the changes list (section 5) |
| report_available, report_url | whether the PDF can be fetched, and where (section 5) |
| app_url | the 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'sassessment_idandreference, and usually itsexternal_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 Start | After Start | |
|---|---|---|
| The job: assessor, due date, visit date and time, Type, notes | you change it (PATCH) | 409 already_started: change it in Fire Record |
| Cancelling the job | you cancel it (POST .../cancel) | 409 already_started |
| Client and building details, including names | you change them | 409 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 building | never through the API | never through the API |
| The action plan: each action's status, completed date, notes and who is responsible | not there yet | both 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 theRetry-Afterheader (about a minute). - 409
building_name_locked: some downloaded reports keep the building name and address they were issued with, so those cannot change (fieldnames 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, withchangedlisting 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_changedonce, withprevious_statusandexternal_idbothnull. - A report deleted in Fire Record:
assessment.status_changedwithcancelled. It stays readable ascancelleduntil 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
cancelledwith onlyassessment_id,external_id,statusandprevious_statusfilled 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_idandclient_idchange). 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=riversidesearches client names.GET /buildings?search=LS1 4ABsearches 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=...(orclient_external_id=..., not both) lists a client's buildings, and can be combined withsearch. An unknown client answers 422client_not_found.- Results are sorted by name.
searchis up to 100 characters;limitis 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_dateand 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.
{ "data": [ { "...": "assessment" } ], "has_more": true, "next_cursor": "eyJ1Ijoi..." }- For the next page, call again with
cursor=<next_cursor>. Keep going untilhas_moreisfalse: a page can hold fewer items thanlimit, even none, while there are more. limitis 1 to 100 (default 50). A cursor that cannot be read is refused with 422 (fieldcursor).changed_sinceis exclusive; encode a+in a time offset as%2B, or use aZtime. Withoutchanged_sincethe 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_atof the last item you saw, minus two minutes, and skip items whose id andstatus_changed_atyou have already handled. - The list covers every report in your organisation, including ones made in Fire Record (their
external_idisnull). Client and building edits are not in it.
Statuses
- A report that is not BAFE:
assigned, thenin_progress, thencompleted, thensigned_off. - A BAFE SP205 report:
assigned, thenin_progress, thenwith_validator, thenvalidated. A report the validator returns showsreturned, thenwith_validatoragain when it is resent; one taken back while it waits showsin_progress. A BAFE report never showscompletedorsigned_off. - Either can end
cancelled.
| status | Meaning |
|---|---|
| assigned | made, not started yet: the only status you can change or cancel |
| in_progress | the assessor pressed Start. For BAFE, also a report finished but not yet sent to the validator, or taken back to change |
| completed | not BAFE: finalised in Fire Record, which happens when it is downloaded there. Use report_available to know the PDF is there |
| signed_off | not BAFE: signed off by an admin. Can be reopened, which takes it back to completed |
| with_validator | BAFE: sent to the Lead for validation |
| returned | BAFE: sent back to the assessor to correct |
| validated | BAFE: validated by the Lead. Final |
| cancelled | cancelled by you, or deleted in Fire Record. Final |
How you learn that a report is validated
assessment.status_changedwith statusvalidated.report.ready, once the issued report with its certificate is available (report_availableturnstrue): fetch it fromreport_url.- As the safety net, the changes list shows it
validatedwithreport_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_availablesays so). Every download is the same file. - The answer is the file, with
Content-Type: application/pdfandContent-Disposition: attachment; filename="<reference>.pdf". Until it is available the answer is 404report_not_ready; a cancelled job answers 410cancelled; if the stored file cannot be read, 503report_unavailable(try again in a few minutes, or, if the message asks, tell Fire Record and quote the report reference). The answer also carriesCache-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:
{
"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
}
]
}| Field | Meaning |
|---|---|
| id | the action's id, for the update call. Use it, not ref, to match actions in your system |
| ref | its number in the report, for people (7.1 is the first action in section 7) |
| section_code, section_name | the report section it belongs to |
| location, issue, action | where, what was found, and what to do |
| priority | P1 (most urgent) to P4 |
| timescale | as the report prints it, for example Short-term (1-4 weeks) |
| status | open, in_progress or completed |
| completed_date | the day it was done, YYYY-MM-DD, or null |
| notes, responsible | progress 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 410cancelled; an id or number that is not on the list answers 404action_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
refmay repeat an earlier one, which is why matching is byid. - Every assessment carries
actionswith 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:
{
"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.
| type | When | data |
|---|---|---|
| assessment.status_changed | a report's status changes; also a report made in Fire Record, announced once with previous_status null | as above. For a report removed with its client, reference, bafe, building_id and client_id are null |
| assessment.updated | the assessor, due date or visit date was changed in Fire Record | assessment_id, external_id, reference, status, changed (assessor, due_date, visit_date), assessor_email, due_date, visit_date |
| report.ready | its PDF is available, or a newer copy is | assessment_id, external_id, reference, status, bafe, report_url |
| assessment.action_updated | an action of a frozen plan was updated in Fire Record | assessment_id, external_id, reference, action_id, ref, changed (status, completed_date, notes, responsible). Fetch the actions for the new values |
| test | Send a test event | message, 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/jsonanduser-agent: FireRecord-Webhooks/1webhook-id: a UUID, the same on every retrywebhook-timestamp: Unix seconds of this trywebhook-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:
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 matchPython, without a library:
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:
{ "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.
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_json | the body is not JSON |
| 401 | missing_key, invalid_key, key_owner_inactive | no key (or not a Fire Record key), a wrong or revoked key, or its maker is no longer an active admin |
| 403 | api_access_off | the API is switched off for your organisation |
| 403 | ip_not_allowed | the key is limited to other addresses |
| 403 | cors_refused | the call came from a web page: call from your server |
| 403 | hybrid_not_enabled | an assessment on a mixed building needs hybrid reports switched on |
| 404 | not_found | no such record in your organisation, or no such call (an unknown call without a valid key answers 401 first) |
| 404 | report_not_ready | the PDF is not available yet |
| 404 | actions_not_ready | the action plan is available once the report is signed off, or validated for BAFE |
| 404 | action_not_found | no action with that id or number on this report (see field) |
| 409 | action_ambiguous | two actions on this report have that number: use the action's id |
| 409 | assessment_already_open | the building already has an open report (named in the message) |
| 409 | already_started | the assessor has started this job: change it in Fire Record |
| 409 | report_in_progress | a report on this client or building is being worked on: its details are Fire Record's until it is finished |
| 409 | building_locked | a validated BAFE report on this building is still storing its issued copy: try again after Retry-After seconds |
| 409 | building_name_locked | a downloaded report keeps this building's name and address (see field) |
| 409 | building_read_only | a report on this building can no longer be changed, so neither can the building |
| 409 | cancelled | this job has been cancelled |
| 410 | cancelled | the PDF or action plan of a cancelled job: there is none |
| 413 | body_too_large | the body is over 2 MB |
| 422 | invalid_request | a field, query parameter or id is missing, wrong or unknown (see field), or a date that does not exist |
| 422 | client_not_found, building_not_found | the client or building is not one of yours |
| 422 | assessor_not_found | that email is not an active member of your team |
| 429 | rate_limited | more than 120 calls a minute on a key, or 600 from one address: wait the Retry-After seconds |
| 500 | server_error | our side, or a body in an encoding we cannot read; try again shortly |
| 503 | report_unavailable | the 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.