API keys
Each company runs its own Migna address, such as https://westland.mignasafetysolutions.com, and every request goes to that address. An admin makes a key under Admin Settings → API Keys: they name it after what will use it, tick the kinds of records it may read, and choose when it expires (30 days, 90 days, a year, or never). The key starts with mk_ and is shown once. We store only a SHA-256 fingerprint of it, so a lost key is revoked and replaced rather than recovered.
A key reads records and never creates, changes or deletes one. An admin can also tick Zapier Webhooks on a key, which lets it subscribe to webhook events, and unsubscribe, for the records it can read (below). A company can hold 20 active keys, and revoking one stops it on its next request.
Requests
Send the key in the Authorization header. GET /api/v1 lists what the key can read, which makes it a quick check that a key works.
curl -H "Authorization: Bearer mk_..." https://westland.mignasafetysolutions.com/api/v1
A list answers with a page of records, newest first, and a cursor for the next page:
GET /api/v1/corrective-actions?status=open&limit=2
{
"data": [
{
"id": "cm2k8x0ab0001",
"title": "Secure scaffold planks on level 2",
"description": "Wire or cleat every plank on the east landing.",
"source": "incident",
"status": "open",
"priority": "high",
"dueDate": "2026-10-03T00:00:00.000Z",
"completedAt": null,
"closeOutNotes": null,
"owner": "Site Lead",
"incidentId": "cm2k8wz7f0000",
"observationId": null,
"auditItemId": null,
"inspectionId": null,
"changeId": null,
"createdAt": "2026-10-01T14:30:00.000Z",
"updatedAt": "2026-10-01T14:30:00.000Z"
}
],
"nextCursor": "cm2k8x0ab0001"
}
GET /api/v1/<resource>/<id> returns one record under data.
Resources
A resource answers only if the key may read it and its module is turned on for the company.
| Path | Records | Filters |
|---|---|---|
/api/v1/incidents | Incidents, injuries, illnesses and near misses | — |
/api/v1/observations | Safety observations | — |
/api/v1/corrective-actions | Corrective actions | status |
/api/v1/audits | Site audits, with each item and a score | — |
/api/v1/inspections | Equipment inspections | — |
/api/v1/training-records | Training records | — |
/api/v1/workers | Workers on the roster | name |
/api/v1/job-sites | Job sites | name |
/api/v1/changes | Management of change requests | — |
/api/v1/permits | Permits to work | — |
/api/v1/policies | Policies, with acknowledgement counts | — |
/api/v1/sds | Safety data sheets | — |
/api/v1/risk-assessments | Risk assessments | — |
/api/v1/subcontractors | Subcontractors and their document status | — |
Paging & filters
limit: page size, 50 by default and at most 200.cursor: send backnextCursorfrom the previous page. It isnullon the last page.createdAfterandcreatedBefore: an ISO 8601 date such as2026-09-01or a date and time such as2026-09-01T12:00:00Z. A nightly sync can ask forcreatedAfterits last run.name(workers and job sites): matches part of the name, ignoring case.status(corrective actions):open(open or in progress),closed(completed or verified), or one ofopen,in_progress,completedandverified.
Errors & limits
Errors answer with JSON such as {"error": "This key cannot read permits."}, written to be read by a person.
| Status | Meaning |
|---|---|
| 400 | A bad cursor, date, filter or request body. |
| 401 | No key, or a key that is unknown, revoked or expired. |
| 403 | The key cannot read that resource, lacks the webhooks permission, or the module or the API is turned off. |
| 404 | No such resource, record or subscription. |
| 429 | More than 120 requests in a minute for this key. Wait for the number of seconds in Retry-After. |
Webhooks
An admin adds an endpoint under Admin Settings → Webhooks: an https:// address on the public internet and the events to send. We POST each event as JSON, signed with a secret that starts with whsec_ and is shown once (it can be rotated). Only events for modules the company has turned on are offered.
| Event | When | Record from |
|---|---|---|
incident.created | An incident is reported | incidents |
observation.created | A safety observation is reported | observations |
corrective_action.created | A corrective action is raised | corrective-actions |
corrective_action.closed | A corrective action is completed or verified | corrective-actions |
audit.created | A site audit is saved | audits |
inspection.failed | An equipment inspection fails | inspections |
training_record.created | A training record is added | training-records |
change.approved | A change is approved | changes |
change.implemented | A change is implemented | changes |
permit.approved | A permit is approved | permits |
permit.closed | A permit is closed out | permits |
A delivery
POST /your/endpoint
Content-Type: application/json
User-Agent: Migna-Webhooks/1
Migna-Event: inspection.failed
Migna-Delivery: cm2k9a1cd0004
Migna-Signature: t=1791297000,v1=5f2b6c...e9
{
"id": "cm2k9a1cd0004",
"event": "inspection.failed",
"createdAt": "2026-10-01T14:30:05.000Z",
"data": {
"id": "cm2k99zqe0003",
"equipment": { "id": "cm2k90b1x0002", "name": "Scissor lift 3", "identifier": "SL-03", "category": "Aerial lift", "jobSite": "Riverside Medical Office Building" },
"checklist": "Aerial lift pre-use",
"performedBy": "Operator",
"performedAt": "2026-10-01T14:28:00.000Z",
"outcome": "fail",
"notes": "Platform gate latch does not close.",
"items": [{ "item": "Gate and guardrails", "result": "fail", "note": "Latch does not close" }],
"createdAt": "2026-10-01T14:30:00.000Z"
}
}
data is the record exactly as GET /api/v1/<resource>/<id> returns it. id is the delivery’s own id, also in Migna-Delivery.
Answering, retries and repeats
- Answer with any 2xx status within 10 seconds, and do slow work afterwards. We do not follow redirects, so a 3xx counts as a failure.
- A failed delivery is tried again after 15 minutes, an hour, 4 hours, 12 hours and a day: six attempts in all.
- A delivery can arrive more than once. Use its
idto ignore one already handled. - An endpoint whose deliveries fail every attempt 15 times in a row is switched off, with the reason shown to the admin.
- We send only to addresses on the public internet, check where the address resolves before each delivery, and refuse private networks.
Checking the signature
Migna-Signature reads t=<unix seconds>,v1=<hex>. Compute an HMAC-SHA256 of t, a full stop, then the raw request body, with the signing secret as the key. Accept the request only if it equals v1, compared in constant time, and t is within five minutes of now.
Node.js
const crypto = require("crypto");
function fromMigna(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;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 || "", "hex");
return given.length === expected.length && crypto.timingSafeEqual(expected, given);
}
Python
import hashlib, hmac, time
def from_migna(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Check against the raw body as received. Parsing the JSON and serialising it again can change the bytes and fail the check.
Subscribing through the API
An integration platform that subscribes itself to events (the “REST hooks” pattern) uses a key that an admin has given the Zapier Webhooks permission. It can subscribe only to events about records the key can read, and sees and removes only its own subscriptions.
| Request | What it does |
|---|---|
POST/api/v1/hooks | Subscribe {"url": "https://...", "event": "incident.created"}. Answers 201 with the subscription’s id and its signing secret. |
GET/api/v1/hooks | This key’s subscriptions, and the events it may subscribe to. |
GET/api/v1/hooks/{id} | One subscription. |
DELETE/api/v1/hooks/{id} | Unsubscribe. |
curl -X POST https://westland.mignasafetysolutions.com/api/v1/hooks \
-H "Authorization: Bearer mk_..." -H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/migna", "event": "corrective_action.closed"}'
{
"id": "cm2kb3f9g0007",
"name": "corrective_action.closed",
"url": "https://example.com/hooks/migna",
"event": "corrective_action.closed",
"active": true,
"createdAt": "2026-10-02T09:15:00.000Z",
"secret": "whsec_..."
}
- Deliveries are signed exactly as above. The secret is returned when subscribing and not again; every subscription made with the same key shares it.
- A key holds at most 50 subscriptions. If the receiving address answers
410 Gone, we delete the subscription. - Each subscription is listed on the admin’s Webhooks page with the key that made it, where it can be turned off or deleted. Revoking the key deletes them all, and a key that expires or loses read access to a record type stops receiving those events.
What is never sent
Neither the API nor a webhook sends files, photos, signatures, voice recordings, workers’ phone numbers or email addresses, QR codes, or anything medical such as work restrictions. A privacy case comes back without the person’s name, location, description, body part or injury type, as on an exported OSHA log.