Integration API
Pull your safety data into a dashboard, push observations in from the field, or keep a roster in sync. One REST API, one key.
The API runs in one direction on purpose: other systems can send us records and read our data. They cannot edit or delete anything.
That means there is never a question about which system holds the truth. Corrections happen in ShieldSphere, where the audit trail lives. A compliance record that a third-party integration could silently rewrite is not much of a record.
An organization owner creates keys under Settings → Developers. You will see the key once, at creation.
Send it on every request:
curl https://app.shieldsphere.ai/api/v1/profiles \
-H "Authorization: Bearer ss_live_your_key_here"Each key carries specific permissions and can be limited to particular locations, which is useful if a system only covers one site. The Integration API is part of our enterprise plan; talk to us if you do not see the tab.
Almost everything is scoped to a location, so fetch those first and hold onto the ids.
GET /api/v1/profiles
{
"data": [
{
"id": "3f2c...",
"name": "North Plant",
"state": "OH",
"is_corporate": false
}
],
"has_more": false,
"next_cursor": null
}Skip anything with is_corporate: true. Those are reporting umbrellas, not real sites, and they will reject records.
Seven collections, all paginated the same way:
/api/v1/employees/api/v1/incidents: OSHA recordables/api/v1/assets/api/v1/chemicals/api/v1/inspections and /api/v1/inspections/{id}: inspections and their findings/api/v1/scans and /api/v1/scans/{id}: hazard scans and findings/api/v1/profilesFollow next_cursor until has_more is false. Cursors are opaque, so pass them back unchanged rather than building your own.
For an incremental sync, pass updated_since with the timestamp of your last run.
GET /api/v1/incidents?updated_since=2026-08-01T00:00:00Z&limit=100Records come back oldest first. That ordering is what makes updated_since reliable for incremental sync. A stable order means a cursor never skips or repeats a row while you page.
id as your identifier, not case_number. OSHA case numbers are sequential per location per year, so two of your sites will both have a 2026-001. Treat case_number as a display label.POST to the same collection paths. Always send an Idempotency-Key. If the request times out and you retry, the same key returns the original record instead of creating a second one.
curl -X POST https://app.shieldsphere.ai/api/v1/incidents \
-H "Authorization: Bearer ss_live_..." \
-H "Idempotency-Key: your-system-incident-4471" \
-H "Content-Type: application/json" \
-d '{
"location_id": "3f2c...",
"date_of_injury": "2026-08-04",
"description_of_injury": "Laceration to left hand from sheet metal edge",
"employee_name": "J. Rivera",
"classification": "injury",
"days_away_flag": true,
"num_days_away": 3
}'location_id, date_of_injury, description_of_injury, employee_name, and classification are required. Classification is the OSHA 300 column M value: injury, skin_disorder, respiratory_condition, poisoning, hearing_loss, or other_illness.
You do not need the full OSHA 301 up front. Send what you have and finish the record in ShieldSphere; the regulation allows seven days.
If your team already runs inspections somewhere else — a spreadsheet tool, a forms app, your own system — send them over and they land in the dashboard alongside inspections done in ShieldSphere, counting toward the same trends.
An inspection and its findings go in a single request. There is no endpoint for adding findings afterward, on purpose: a record that arrives in pieces shows up half-built in your trend charts while you are still sending it.
curl -X POST https://app.shieldsphere.ai/api/v1/inspections \
-H "Authorization: Bearer ss_live_..." \
-H "Content-Type: application/json" \
-d '{
"location_id": "3f2c...",
"inspection_date": "2026-08-21",
"inspector_name": "R. Delgado",
"source_app": "smartsheet",
"client_key": "sheet-4471-row-88",
"findings": [
{
"description": "Extension cord run through a doorway in the pack line",
"priority": "high",
"status": "open",
"corrective_action_recommendation": "Reroute overhead or install a floor channel"
},
{
"description": "Eyewash station blocked by pallets",
"priority": "critical",
"external_id": "row-88-b"
}
]
}'location_id and inspection_date are required, and every finding needs a description and a priority. Everything else is optional.
Priority does not have to match our wording. We accept the labels these systems actually use — critical, high, medium, low, plus P1–P4, 1–4, red/amber/green, and words like severe, major, and minor. Finding status is just as forgiving: closed, completed, and fixed all mean resolved.
Dates must be YYYY-MM-DD. We reject 03/04/2026 rather than guessing, since it means March 4 in one country and April 3 in another.
Imported inspections arrive finalized, because they record something that already happened. They count in your trends straight away. Pass "status": "draft" if you are staging something that is not finished yet — drafts stay out of the charts.
Send a client_key — your own row or record id — and a re-run of the same sync returns the original inspection instead of creating a duplicate. Unlike Idempotency-Key, it is stored on the record permanently, so it still protects you weeks later.
Read one back with GET /api/v1/inspections/{id} and you get the inspection with all of its findings. The list endpoint returns completed inspections by default; pass status=all to include drafts.
Send an observation with a photo and we analyze it against OSHA standards, using the same engine behind our scanner.
Analysis takes longer than an HTTP request should, so this returns a job id immediately.
POST /api/v1/observations
{
"image_url": "https://your-system.com/photos/abc.jpg",
"framework": "construction",
"state": "OH",
"location_id": "3f2c...",
"source": "procore",
"external_id": "obs-99812"
}
→ 202 Accepted
{
"data": {
"job_id": "8a1f...",
"status": "queued",
"_links": { "self": "/api/v1/jobs/8a1f..." }
}
}Poll the job until it finishes:
GET /api/v1/jobs/8a1f...
{
"data": {
"status": "succeeded",
"result": {
"scan_id": "c07d...",
"violation_count": 2,
"overall_risk_level": "high",
"violations": [
{
"hazard_category": "Fall Protection",
"description": "Worker on leading edge without personal fall arrest",
"priority": "critical",
"corrective_action": "Provide and require PFAS above 6 feet"
}
]
}
}
}Send source and external_id and we deduplicate for you. Replaying the same observation will not run a second analysis.
Rather than polling, register an endpoint under Settings → Developers and we will POST to it when something happens: a scan finishes, an incident is recorded, a record is created.
Verify every request before trusting it:
const expected =
'sha256=' +
crypto
.createHmac('sha256', process.env.SHIELDSPHERE_WEBHOOK_SECRET)
.update(`${req.headers['x-shieldsphere-timestamp']}.${rawBody}`)
.digest('hex')
// X-ShieldSphere-Signature carries the 'sha256=' prefix, so compare
// against the whole header value.
const presented = req.headers['x-shieldsphere-signature']
// Compare with crypto.timingSafeEqual, and reject anything
// whose timestamp is more than five minutes old.Use the raw request body, not a re-serialized object. Re-encoding changes the bytes and the signature will not match. You can send yourself a real test event from Settings → Developers to check your verification before going live. Return a 2xx quickly; we retry with backoff and pause an endpoint that keeps failing. Every delivery carries a stable X-ShieldSphere-Delivery id, so deduplicate on it: like every webhook system, delivery is at-least-once.
Every error has the same shape. Branch on code, not on the message.
{
"error": {
"code": "validation_failed",
"message": "location_id is required.",
"request_id": "f19c8a3e-..."
}
}Include the request_id if you contact support. It points at the exact request in our logs.
Rate limits are 120 reads and 60 writes per minute per key, in separate buckets, so a bulk sync will not throttle your dashboard. Watch X-RateLimit-Remaining and back off on a 429.
The complete OpenAPI 3.1 spec is at /openapi-v1.json. Point your client generator at it.
npx openapi-typescript https://app.shieldsphere.ai/openapi-v1.json \
-o src/shieldsphere.d.ts