Developers
iComply API
Read-only access to an organisation's own iComply records (any organisation with the Integrations pack): the contractors on its register and their status on each site, sites, who's on site now, gate events, an incident summary, corrective actions and expiring documents. Keys are created by the organisation's administrator (Connections), shown once, and limited by scope and end date. 120 requests per minute per key. Every request is recorded in the organisation's access log. Webhooks (signed with HMAC-SHA256) are described under `x-webhooks`.
OpenAPI 3.1 description (JSON)
Keys and scopes
Your organisation's administrator creates a key in iComply (Connections). Send it with every request: Authorization: Bearer ick_…. A key can read only its scopes, stops working on its end date or when revoked, and is limited to 120 requests a minute (429 with Retry-After). Use https://app.icomplyaustralia.com.au/api/v1 or your organisation's own address.
read:subcontractors: Subcontractors and their status on each siteread:sites: Sitesread:gate: Who's on site now and gate events (sign-ins, refusals, overrides)read:safety: Incident summary and corrective actionsread:compliance: Documents and tickets expiringread:insights: Insights feed for Power BI: facts by site and month or day, injury rates (counts and hours; no names)
Lists come in pages: ?limit= (1 to 200, default 50) and ?cursor= from the previous page's nextCursor (null on the last page). Every answer has a strong ETag; send it back in If-None-Match to get 304 Not Modified. Errors are { "error": "…" }.
Endpoints
GET/api/v1/subcontractors
Subcontractors and their status
Company-wide status, and status on each site they're on (the site's own requirements too). Scope: `read:subcontractors`.
limit (query)cursor (query). From the previous page's nextCursorsite (query). Only subcontractors on this site
GET/api/v1/sites
Sites
Scope: `read:sites`.
limit (query)cursor (query). From the previous page's nextCursorstatus (query): active · closed
GET/api/v1/sites/{id}/on-site
Who's on site now
Names and companies only (no phone numbers). Scope: `read:gate`.
GET/api/v1/gate-events
Gate events
Oldest first. A stop-list refusal says only "Stop list". Scope: `read:gate`.
limit (query)cursor (query). From the previous page's nextCursorsite (query)from (query). Default: 6 days before `to`to (query). Default: today (Australian local dates). 93 days at most.
GET/api/v1/incidents/summary
Incident summary
Counts only: no descriptions and no people. Scope: `read:safety`.
site (query)from (query)to (query)
GET/api/v1/corrective-actions
Corrective actions
Owners aren't named. Scope: `read:safety`.
limit (query)cursor (query). From the previous page's nextCursorstatus (query): open · done · verified · cancelled · overduesite (query)
GET/api/v1/expiries
Documents and tickets expiring
Approved documents of accepted subcontractors expiring from today to today + days, soonest first. Scope: `read:compliance`.
limit (query)cursor (query). From the previous page's nextCursordays (query)
GET/api/v1/insights/{table}
Insights feed (Power BI)
Flat tables of facts for reporting tools: `sites` (Sites (id, name, state, status)); `site-months` (Facts by site and month: hours, injuries by class, days lost, gate refusals and overrides, inspections done and due, findings, corrective actions, diary days signed off); `site-days` (Facts by site and day: the same daily events, plus the snapshot figures kept that day (compliance, open and overdue items, coverage, SDS currency, assets due)); `injury-rates` (Injury rates by site and month, for the month and the rolling 12 months (LTIFR, MTIFR, TRIFR) with the counts and hours behind them); `findings-months` (Findings by site, month, type and category: raised, closed, overdue at the end of the month, average days to close (companies by register id, not name)). Counts, hours and rates only: no people's names, and companies aren't named. Works while the organisation has Insights on; a key with only this scope works without Integrations. Scope: `read:insights`.
table (path, required): sites · site-months · site-days · injury-rates · findings-monthsfrom (query). A month (2026-01) for month tables; a date (2026-10-01) for site-daysto (query)site (query)format (query): json · csv. csv: the same table as CSV (a header row, then the rows)
Webhooks
Each delivery is a POST with JSON and the header `X-IComply-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the endpoint's secret>`. Reject a timestamp more than 5 minutes old, and compare in constant time. Also `X-IComply-Event` and `X-IComply-Delivery`. Answer 2xx within 10 seconds; anything else is retried with backoff (1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 24 hours), and an endpoint is switched off after 15 failures in a row.
subcontractor.status_changed: A subcontractor's company-wide status changed (cleared, expiring or in review, not cleared)gate.refused: Someone was refused at a site's gate (not cleared, or on the stop list)gate.override: A refusal was overridden and the person let inincident.reported: An incident or near miss was reporteddocument.expiring: An approved document or ticket expires in 30, 14, 7 or 0 dayscorrective_action.overdue: A corrective action went overdue
Each body is { id, type, createdAt, data }. Payloads carry ids, sites and company names, not people's names. Checking a signature in Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (a string). header: X-IComply-Signature.
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes
const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return want.length === parts.v1?.length && timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1));
}