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.
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.
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.
| Permission | What it allows | Endpoints |
|---|---|---|
inspect:rounds:read | Read the rounds a campus was expected to run, and the compliance figure | GET rounds/, GET compliance/ |
inspect:inspections:read | Read the rounds actually walked, and one round in full | GET inspections/, GET inspections/<id>/ |
inspect:failures:read | Read failed checks and the checks that fail most | GET failures/, GET failing-items/ |
inspect:inspectors:read | Read the staff who walk rounds, and what each of them walked | GET inspectors/, GET inspectors/activity/ |
inspect:setup:read | Read checklists, schedules, checkpoints and campus settings | GET templates/, GET templates/<id>/, GET schedules/, GET checkpoints/, GET settings/ |
inspect:meters:read | Read meters and every reading, with consumption | GET meters/, GET meters/<id>/, GET meter-readings/ |
inspect:meters:costs | Adds the per-unit rate and the money each reading is worth | the 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.
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_idthe key cannot reach answers404rather 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_scanslist 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_entryon 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.
| Parameter | Notes |
|---|---|
campus_id | Narrow to one campus |
date_from, date_to | YYYY-MM-DD. Defaults to the last 30 days |
status | Comma separated: pending, in_progress, done, late, missed, skipped |
template_id, schedule_id | UUIDs |
assigned_to | Staff user UUID |
page, page_size | Up 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.
| Parameter | Notes |
|---|---|
campus_id | Narrow to one campus |
date_from, date_to | On session_date. Defaults to the last 30 days |
status | Comma separated: in_progress, completed, flagged |
template_id, occurrence_id | UUIDs |
conducted_by | Staff user UUID |
has_issues, is_back_entry | true or false |
updated_since | ISO 8601 datetime. Catches rounds amended since then |
page, page_size | Up 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 ownweight,is_criticalandrequire_image.checkpoint_scans: each scan of a QR sticker, withscanned_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) andmatched(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.
| Parameter | Notes |
|---|---|
campus_id, template_id, item_id, conducted_by | Narrow the list |
date_from, date_to | On the round's date. Defaults to the last 30 days |
critical_only | true for the checks that are not negotiable |
only | Comma separated: ticketed, unticketed, open, repeating |
repeat_window_days | Window for fail_streak. Default 30 |
page, page_size | Up 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.userrolls each person up across the key's campuses.campusrolls 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.
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.
| Parameter | Notes |
|---|---|
campus_id, meter_id, meter_type | Narrow the list |
reading_label | The named reading, for example Monthly or Opening |
read_by | Staff user UUID |
is_back_entry | true or false |
date_from, date_to | On reading_date. Defaults to the last 30 days |
page, page_size | Up 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:readandinspect:inspections:read, plusinspect:failures:readif the report lists what went wrong. - "Tell me what keeps failing":
inspect:failures:readonly. - "How much is each site doing?":
inspect:inspectors:read, andinspect:rounds:readif the answer is meant to be a compliance figure rather than a count. - "Track our electricity":
inspect:meters:read, andinspect:meters:costsas 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. Usecompliance/, or countrounds/. - Read
settings/before you interpret anything. Anullscore, 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
nullscore 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_rateon 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_entryandedit_countchange 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/showsrequests_todayagainstdaily_limit, shared across every section the key can reach. Useupdated_sinceoninspections/for incremental syncs andpage_size=200rather 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.
- Email: sales@deskos.net
- Phone: +91 742810 9069