Skip to main content

Inspections API

This section of the DeskOS API covers inspection rounds, the checks inside them, the failures they found, the people who walked them and the meter readings taken along the way. From an outside system such as a facilities-management dashboard, a client compliance report, a BI stack, an energy platform, n8n or Zapier, or an AI agent you can pull what a site was supposed to inspect, what it actually inspected, what failed, who did it and what the electricity meter read.

The typical use is a client report. An operator running an FM contract pulls the compliance figure for a campus and a month, pulls the failed checks with their photographs and the tickets raised for them, pulls the electricity consumption for the same window, and publishes all of it without anyone exporting a spreadsheet by hand.

Read Authentication first

Getting a key, sending it, campus rules, the response shape and the errors every section shares are all on the Authentication page. This page only covers what is specific to inspections.

This section is read only, permanently

There is no create, update or delete endpoint here, and there will not be one. A round is evidence: it is walked in the DeskOS Inspect app by a named person standing in a named place, and corrected in the admin console with a mandatory reason written into the round's edit trail. An API key is neither a person nor a place, so it can supply neither. A POST or PATCH to any path on this page answers 405, and the attempt is recorded in your access log so you can see what was tried.


Base path​

All paths on this page are relative to:

https://oscar.nova.deskos.net/api/v1/inspect/external/

Your Developer Settings page shows the exact base URL for your workspace.


Permissions​

Inspection permissions all start with inspect:. An admin ticks them per key on the Developer Settings page.

PermissionWhat it allowsEndpoints
inspect:rounds:readRead the rounds a campus was expected to run, and the compliance figureGET rounds/, GET compliance/
inspect:inspections:readRead the rounds actually walked, and one round in fullGET inspections/, GET inspections/<id>/
inspect:failures:readRead failed checks and the checks that fail mostGET failures/, GET failing-items/
inspect:inspectors:readRead the staff who walk rounds, and what each of them walkedGET inspectors/, GET inspectors/activity/
inspect:setup:readRead checklists, schedules, checkpoints and campus settingsGET templates/, GET templates/<id>/, GET schedules/, GET checkpoints/, GET settings/
inspect:meters:readRead meters and every reading, with consumptionGET meters/, GET meters/<id>/, GET meter-readings/
inspect:meters:costsAdds the per-unit rate and the money each reading is worththe three meter endpoints above

GET health/ needs no permission and works with any key. Use it to confirm the key carries the inspect: permissions you expect before doing anything else.

Consumption is one right, what it cost is another

inspect:meters:read returns how many units a campus burned. inspect:meters:costs is what those units were worth, and it is a separate tick because it is money. An admin can only grant it if they hold Financials: view in DeskOS as well as Inspections: view. An admin who holds only Inspections: view still grants the rest of the section normally; the cost line alone stays greyed out on the Developer Settings page.

This means a key can legitimately read every reading and still have no idea what the bill is. That is the intended shape, not a gap.

A client compliance report normally wants inspect:rounds:read, inspect:inspections:read and inspect:failures:read. An energy dashboard wants inspect:meters:read alone, and inspect:meters:costs too if it is charging the consumption on.


Campuses​

Everything in this section belongs to a campus. The general campus rules are on the Authentication page. For this section:

  • Every list endpoint accepts ?campus_id= to narrow results. Without it they return everything the key can reach.
  • A key restricted to campuses sees rounds, inspections, failures, meters and settings for those campuses only, including on the detail routes. A round at a campus your key cannot reach answers 404, not an empty payload.
  • Asking for a campus_id the key cannot reach answers 404 rather than an empty list, because an empty list reads as "nothing happened there".
  • A checklist template can be org wide, with no campus of its own. Those are usable everywhere, so every key sees them.

The two things people get wrong​

Both are worth reading before you build anything.

A missed round has no inspection​

inspections/ lists rounds that were walked. A round nobody walked produced no record, so it cannot appear there. Only rounds/ can show it, because a round is materialised as an expected slot the moment its schedule says it is due, whether or not anybody turns up.

A consumer that builds a compliance figure by counting rows in inspections/ will always report one hundred percent, because the rounds that would have lowered it are the ones that are missing. Use compliance/ for the figure, or count rounds/ yourself.

One row per inspector, not per round​

The admin console collapses a round that three people walked between them into one line. This API does not. Each person's session is its own row with its own answers and its own score, carrying occurrence.id so you can group them back up.

That is deliberate: collapsing them would throw away which person answered which check, and "inspections per campus per user" is the single most common thing asked of this API. Group on occurrence.id when a slot is the unit you want. A round walked outside any schedule has no occurrence, and groups on template plus campus plus session_date.


Read the settings first​

GET settings/ tells you what each campus has switched on, and it changes how everything else reads.

  • Scoring off. Rounds at that campus report score: null. That means the round was never scored, not that it scored zero. Do not chart it as a zero.
  • Checkpoint mode off. An empty checkpoint_scans list means no scan was ever required, not that an inspector skipped the stickers.
  • Issue mode off. A failed check with no ticket is the configured behaviour, not a dropped ball.
  • Back entry allowed. Rounds can be filed for a past date, and is_back_entry on the round tells you which ones were.

A campus that has never opened the settings screen comes back with configured: false and every default filled in.


Endpoints​

Health​

GET health/

Needs no permission. Returns the key's name, organisation, permissions, campus restrictions and today's usage.


Rounds​

GET rounds/

The rounds a campus was expected to run, walked or not. One row per expected slot.

ParameterNotes
campus_idNarrow to one campus
date_from, date_toYYYY-MM-DD. Defaults to the last 30 days
statusComma separated: pending, in_progress, done, late, missed, skipped
template_id, schedule_idUUIDs
assigned_toStaff user UUID
page, page_sizeUp to 200 per page
{
"success": true,
"rounds": [
{
"id": "8a1f…",
"schedule_id": "3c22…",
"template": { "id": "6b90…", "name": "Washroom hourly" },
"campus": { "id": 12, "name": "Tower B" },
"assigned_to": { "id": "d1…", "name": "Asha Nair", "email": "asha@…" },
"scheduled_for": "2026-09-18T09:00:00+05:30",
"due_at": "2026-09-18T09:30:00+05:30",
"local_date": "2026-09-18",
"slot_label": "09:00",
"status": "late",
"completed_at": "2026-09-18T10:12:00+05:30",
"skip_reason": null,
"inspection_ids": ["f4c1…"]
}
],
"pagination": { "count": 96, "page": 1, "page_size": 50, "has_next": true }
}

What the statuses mean. Finishing before due_at is done. Finishing after it is late, and late still counts as done. A slot only becomes missed once the next slot of the same schedule has opened and nobody finished this one, so an hourly round walked at 10:50 for the 10:00 slot is late rather than missed. pending on a slot whose due_at has passed means nobody has walked it yet and its successor has not opened, not that it is lost. skipped is a slot an operator deliberately stood down.

Stale slots are settled as part of serving this endpoint, so it can never disagree with the admin console about what counts as missed.


Compliance​

GET compliance/

Completed over expected, for the key's campuses together and for each campus separately.

Takes campus_id, template_id, date_from and date_to.

{
"success": true,
"period": { "from": "2026-09-01", "to": "2026-09-30" },
"totals": {
"expected": 240, "completed": 226, "on_time": 201, "late": 25,
"missed": 14, "pending": 0, "skipped": 6, "compliance_rate": 94.2
},
"by_campus": [
{ "campus": { "id": 12, "name": "Tower B" }, "expected": 120, "completed": 118, "…": "…" }
]
}

Skipped slots leave the denominator. A round nobody was meant to run is not a round anybody missed. Everything else counts as expected, including slots still pending. compliance_rate is null when nothing was expected, which is not the same as zero: a site with no schedules set up has no record to be good or bad.

A campus-restricted key gets totals over its own campuses, not the organisation's.


Inspections​

GET inspections/

The rounds actually walked. One row per inspector.

ParameterNotes
campus_idNarrow to one campus
date_from, date_toOn session_date. Defaults to the last 30 days
statusComma separated: in_progress, completed, flagged
template_id, occurrence_idUUIDs
conducted_byStaff user UUID
has_issues, is_back_entrytrue or false
updated_sinceISO 8601 datetime. Catches rounds amended since then
page, page_sizeUp to 200 per page
{
"id": "f4c1…",
"template": { "id": "6b90…", "name": "Washroom hourly" },
"campus": { "id": 12, "name": "Tower B" },
"conducted_by": { "id": "d1…", "name": "Asha Nair", "email": "asha@…" },
"session_date": "2026-09-18",
"status": "completed",
"started_at": "2026-09-18T09:58:00+05:30",
"completed_at": "2026-09-18T10:12:00+05:30",
"total_items": 18, "pass_count": 16, "fail_count": 1, "na_count": 1,
"has_issues": true,
"occurrence": { "id": "8a1f…", "slot_label": "09:00", "status": "late", "…": "…" },
"is_back_entry": false,
"back_entry_recorded_at": null,
"back_entry_note": null,
"reopen_count": 1,
"last_amended_at": "2026-09-19T11:04:00+05:30",
"score": {
"percent": 88.9, "earned": 16, "total": 18,
"critical_failed": false, "outcome": "pass",
"threshold": 80, "critical_fails_round": true
}
}

score is read off the record, never recomputed, so what this API answers is what the report the operator already sent says. It is null at a campus that does not score. Inside it, percent can itself be null for a round walked before the campus switched scoring on, with the counts at zero: that round was never scored and inventing a figure for it would be a guess wearing the clothes of a record.

updated_since catches corrections. An amendment made today to last month's round moves nothing about its session_date, so a date window alone will miss it. Poll with updated_since to keep a copy in step.

Use is_back_entry when compliance matters. A round filed retrospectively for a date it did not happen on is not the same evidence as one walked on the day.


One inspection​

GET inspections/<id>/

Everything above, plus:

  • sections: the answers grouped by the section of the checklist they belong to, in the order the round is walked. Each answer carries its status (pass, fail, na, pending), its value, its comment, its photographs and the ticket raised for it, and the check's own weight, is_critical and require_image.
  • checkpoint_scans: each scan of a QR sticker, with scanned_at (when the inspector stood in front of it, reported by the app), recorded_at (when the server heard about it, which can be the next morning for a round walked with no signal) and matched (false means a sticker was scanned that does not belong to the section being answered).
  • edit_log: every correction made after the round was closed, with what the answer was, what it became, who changed it and the mandatory reason.

Failures​

GET failures/

Every failed check, with the evidence behind it and the ticket in front of it.

ParameterNotes
campus_id, template_id, item_id, conducted_byNarrow the list
date_from, date_toOn the round's date. Defaults to the last 30 days
critical_onlytrue for the checks that are not negotiable
onlyComma separated: ticketed, unticketed, open, repeating
repeat_window_daysWindow for fail_streak. Default 30
page, page_sizeUp to 200 per page
{
"response_id": "aa01…",
"inspection_id": "f4c1…",
"session_date": "2026-09-18",
"template": { "id": "6b90…", "name": "Washroom hourly" },
"campus": { "id": 12, "name": "Tower B" },
"conducted_by": { "id": "d1…", "name": "Asha Nair", "email": "asha@…" },
"item": { "id": "77c2…", "name": "Tap not dripping", "is_critical": false },
"comment": "Still dripping after yesterday's fix",
"responded_at": "2026-09-18T10:06:00+05:30",
"fail_streak": 3,
"images": [{ "id": "…", "url": "https://…", "caption": null }],
"issue": {
"id": "…", "source": "auto", "raised_at": "2026-09-18T10:12:00+05:30",
"repeat_count": 3, "is_repeat": true, "error": null,
"service_request": {
"id": "…", "reference": "SR-202609-0042", "status": "IN_PROGRESS",
"priority": "HIGH", "due_date": "2026-09-19T14:12:00+05:30",
"team": "Housekeeping", "assignee": { "…": "…" }
}
}
}

fail_streak is the number to act on. It counts the distinct rounds inside the window that found this same check failing at this campus. One is bad luck. More than one means the fix is not holding.

unticketed means two different things, both of which matter. Either no ticket was ever raised, or one was attempted and failed, in which case issue.service_request is empty and issue.error says why. Both mean nobody is on the hook for the failure, which is why they are one filter.

GET failing-items/

The checks that fail most, ranked, with answered, failed, fail_rate, last_failed_on and open_tickets per check per campus. Takes campus_id, template_id, date_from, date_to and paging.

fail_rate is out of the times the check was answered pass or fail. N/A answers leave the fraction entirely, the same way scoring drops them: a urinal that does not exist in the ladies' washroom must not drag a rate down or prop one up. The ranking puts the count first and the rate second, because twelve failures out of twenty is a worse problem than one failure out of one.


Inspectors​

GET inspectors/

The staff who can be put on a round: the same people the admin console's inspector picker offers. Members never appear, because a round is walked by the operator's own staff. Each row carries org_role and inspections_conducted across the key's campuses.

GET inspectors/activity/

Inspections per campus per user. Takes group_by:

  • campus_user (the default) is the cross-tab: one row per person per campus.
  • user rolls each person up across the key's campuses.
  • campus rolls each campus up across its people.

Also takes campus_id, template_id, date_from and date_to.

{
"period": { "from": "2026-09-01", "to": "2026-09-30" },
"group_by": "campus_user",
"rows": [
{
"inspector": { "id": "d1…", "name": "Asha Nair", "email": "asha@…" },
"campus": { "id": 12, "name": "Tower B" },
"inspections": 42, "completed": 41, "flagged": 1,
"back_entries": 2, "inspections_with_issues": 9,
"checks_answered": 748, "checks_failed": 31, "fail_rate": 4.1,
"last_inspection_on": "2026-09-18"
}
]
}

These count rounds actually walked, so none of them is a compliance figure. Use compliance/ for what was expected.

fail_rate is what a person's rounds found, not how good that person is. An inspector who walks the worst floor fails more checks than one who walks the best, and the number says nothing about either of them on its own.

inspector can be null where the staff account has since been deleted. The round still happened, and dropping the row would quietly shrink the count.


Templates, schedules, checkpoints and settings​

GET templates/
GET templates/<id>/

The checklists. The list carries section_count and item_count; the detail nests sections, their checks and each check's sub-checks, in the order a round walks them. Pass include_archived=true to see the parts that were retired. A template with campus: null is org wide.

Each check carries weight and is_critical. Both only mean anything at a campus with scoring switched on, which settings/ tells you.

GET schedules/

The rules that produce rounds: cadence, the window they run in, the times or weekdays, and grace_minutes, which is how long after a slot opens a round still counts as on time. Without it you cannot tell why a round finished at 10:20 is done and one finished at 11:20 is late. rounds/ is what these rules produced.

GET checkpoints/

The QR stickers, with their name, location hint, floor and campus.

Checkpoint codes are never returned

The code inside a sticker is the only evidence that somebody stood in front of it. Handing it out over a read API would turn proof of presence into a string anybody holding a key could print onto their own label, so it stays in the admin console, where the stickers are generated.

GET settings/

One row per campus. See Read the settings first.


Meters and readings​

GET meters/

Every meter with its latest reading. Takes campus_id, meter_type (comma separated: electricity, water, gas, other), is_active and paging.

GET meters/<id>/

One meter with its readings for the window (default the last 90 days) and the totals across them.

GET meter-readings/

This is the electricity endpoint. Filter meter_type=electricity for the bill.

ParameterNotes
campus_id, meter_id, meter_typeNarrow the list
reading_labelThe named reading, for example Monthly or Opening
read_byStaff user UUID
is_back_entrytrue or false
date_from, date_toOn reading_date. Defaults to the last 30 days
page, page_sizeUp to 200 per page
{
"id": "…",
"meter": { "id": "…", "name": "Tower B mains", "meter_type": "electricity",
"meter_number": "TB-MAINS-01", "scope_label": "Whole building" },
"campus": { "id": 12, "name": "Tower B" },
"reading_date": "2026-09-01",
"reading_label": "Monthly",
"reading_value": "184320.00",
"previous_value": "176180.00",
"consumption": "8140.00",
"unit": "kWh",
"read_by": { "id": "d1…", "name": "Asha Nair", "email": "asha@…" },
"notes": null,
"image_url": "https://…",
"is_back_entry": false,
"back_entry_recorded_at": null,
"last_edited_at": "2026-09-02T09:40:00+05:30",
"edit_count": 1,
"created_at": "2026-09-01T08:02:00+05:30",
"rate": "9.5000",
"amount": "77330.00"
}

rate and amount appear only for a key that also holds inspect:meters:costs. Everything else is there with inspect:meters:read alone.

Read consumption, do not difference reading_value yourself. A meter that was replaced or rolled over reads lower than last time, and the difference there is not a refund of electricity. consumption is clamped at zero for exactly that case, the same way the admin console clamps it. It is null when there is no previous reading, which is the first reading on a new meter.

rate is the rate snapshotted when the reading was submitted, falling back to the meter's current rate only when nothing was snapshotted. Re-pricing a meter never rewrites what last quarter cost.

edit_count is worth watching. A figure corrected three times is not the same evidence as one entered once, and anything pricing a bill off it should be able to see the difference.


Dates and windows​

Every list that covers a period takes date_from and date_to as YYYY-MM-DD. Give neither and the window is the last 30 days (90 for a single meter), so an unfiltered call is bounded rather than being a full-table read dressed up as a page of fifty. Give one and the other end is open.

date_from after date_to answers 400.

Dates like session_date, local_date and reading_date are business dates and carry no timezone. Timestamps like completed_at, scanned_at and raised_at are full ISO 8601 with the workspace's offset.


Instructions for AI agents​

The general rules (how a key is obtained, how to handle it, what to do on 401 and 403) are on the Authentication page. Read those first. This section adds what is specific to inspections.

Permissions to ask for​

  • "Report our compliance to a client": inspect:rounds:read and inspect:inspections:read, plus inspect:failures:read if the report lists what went wrong.
  • "Tell me what keeps failing": inspect:failures:read only.
  • "How much is each site doing?": inspect:inspectors:read, and inspect:rounds:read if the answer is meant to be a compliance figure rather than a count.
  • "Track our electricity": inspect:meters:read, and inspect:meters:costs as well only if the human actually needs the money.

There is no write permission to ask for. If a human asks you to file a round, correct an answer, close an issue or enter a reading, say that this API cannot and that it is done in the DeskOS Inspect app or the admin console, where the action is attributed to a person.

Behaviour rules​

  • Never build a compliance figure by counting inspections/. The rounds that would lower it are the rounds that produced no record. Use compliance/, or count rounds/.
  • Read settings/ before you interpret anything. A null score, an empty scan list and a failure with no ticket each mean something different at a campus that has that feature switched off.
  • Never report a null score as zero, and never recompute a score from the pass and fail counts. The stored figure is what the operator's own report shows, and a second opinion that disagrees with it is worse than no opinion.
  • Do not difference meter readings yourself. Read consumption, which is already clamped for a meter that was replaced or rolled over.
  • Do not present fail_rate on an inspector as a measure of that inspector. It is what their rounds found, and the floor they walk decides most of it.
  • Say when a round was filed late. is_back_entry and edit_count change how much a record is worth as evidence, and a human reporting to a client needs to know.
  • Treat a 404 as "not visible to this key", not as proof the thing does not exist. It may belong to a campus the key cannot reach.
  • Never assume a currency symbol on a meter amount. Currency is per organisation.
  • Respect the daily limit. health/ shows requests_today against daily_limit, shared across every section the key can reach. Use updated_since on inspections/ for incremental syncs and page_size=200 rather than fetching rounds one at a time.

Reporting back to the human​

When you finish, tell the human the key name you used (never the key itself), the campuses and date window you covered, how many rounds were expected against how many were walked, whether any of them were back entries, whether scoring is on at those campuses, and any 403 or 429 you hit. If you were asked about money and the key had no inspect:meters:costs, say that rather than estimating a figure.


Frequently asked questions​

How do I report compliance for a client? GET compliance/ with the campus and the month. Not by counting inspections/, which cannot see the rounds nobody walked.

Why does this round have no score? Either the campus does not score, in which case score is null, or the round predates scoring being switched on, in which case score.percent is null with the counts at zero. Neither is a zero.

How many inspections did each person do at each site last month? GET inspectors/activity/?group_by=campus_user&date_from=…&date_to=….

A round shows three inspectors in the console but I get three rows. Which is right? Both. The console collapses them for reading; the API keeps them apart so you can see who answered what. Group on occurrence.id to get the console's view.

Why can I read the meter but not what it cost? Your key holds inspect:meters:read and not inspect:meters:costs. The second one is money and needs an admin with Financials: view to grant it.

How do I get the electricity consumption for a campus and a month? GET meter-readings/?meter_type=electricity&campus_id=…&date_from=…&date_to=… and sum consumption. For one meter, GET meters/<id>/ gives you the totals already added up.

A failed check has no ticket. Is that a bug? Check settings/ for that campus. With issue mode off, no ticket is the configured behaviour. With it on, look at issue.error: a raise that failed leaves the reason there.

Why is a round from two days ago still pending? Its successor slot has not opened yet, so nothing has declared it missed. Rounds are settled when the next slot of the same schedule opens, not at midnight.

Can I see the code inside a checkpoint sticker? No, by design. See the warning under Checkpoints.

Can DeskOS push inspection events to me instead of me polling? Not yet. Webhooks cover members, companies and memberships today. See Webhooks. For inspections, poll inspections/ with updated_since.

Can I create a round or correct an answer through the API? No, and this will not change. See the warning at the top of this page.


Quick reference​

Base:    https://oscar.nova.deskos.net/api/v1/inspect/external/
Auth: Authorization: Api-Key dk_live_...
Method: GET only. Everything else answers 405.

GET health/
GET rounds/?campus_id=&date_from=&date_to=&status=&template_id=&schedule_id=
&assigned_to=&page=&page_size=
GET compliance/?campus_id=&template_id=&date_from=&date_to=
GET inspections/?campus_id=&date_from=&date_to=&status=&template_id=&occurrence_id=
&conducted_by=&has_issues=&is_back_entry=&updated_since=&page=&page_size=
GET inspections/<id>/
GET failures/?campus_id=&template_id=&item_id=&conducted_by=&date_from=&date_to=
&critical_only=&only=&repeat_window_days=&page=&page_size=
GET failing-items/?campus_id=&template_id=&date_from=&date_to=&page=&page_size=
GET inspectors/?campus_id=&page=&page_size=
GET inspectors/activity/?group_by=&campus_id=&template_id=&date_from=&date_to=
GET templates/?campus_id=&is_active=&page=&page_size=
GET templates/<id>/?include_archived=
GET schedules/?campus_id=&template_id=&is_active=&page=&page_size=
GET checkpoints/?campus_id=&is_active=&page=&page_size=
GET settings/?campus_id=
GET meters/?campus_id=&meter_type=&is_active=&page=&page_size=
GET meters/<id>/?date_from=&date_to=
GET meter-readings/?campus_id=&meter_id=&meter_type=&reading_label=&read_by=
&is_back_entry=&date_from=&date_to=&page=&page_size=

Permissions: inspect:rounds:read, inspect:inspections:read, inspect:failures:read,
inspect:inspectors:read, inspect:setup:read, inspect:meters:read,
inspect:meters:costs (needs Inspections: view AND Financials: view to grant)
Round status: pending, in_progress, done, late, missed, skipped (late still counts as done)
Compliance: completed / expected; skipped leaves the denominator
Rows: one inspection per inspector; group on occurrence.id for the round
Scores: read off the record; null where the campus does not score
Consumption: clamped at zero; null with no previous reading
Money: rate and amount only under inspect:meters:costs
Windows: default last 30 days (90 on one meter); dates are YYYY-MM-DD
Never returned: checkpoint codes

Getting help​

API keys, permissions, campus restrictions, revocation and the access log are all self-service from Developer Settings in your admin console (avatar menu, top right). For a higher daily limit or anything else, reach the DeskOS team through your usual support channel.