← Migna Safety Solutions Developers

Read your safety records from your own systems.

Migna has a REST API that reads records with an API key, and webhooks that tell your systems the moment something happens. Both come with the Company tier. This page is the reference; the user guide has the same material step by step.

Last reviewed 2026-10-02 · How we protect the data: Security & data handling

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.

PathRecordsFilters
/api/v1/incidentsIncidents, injuries, illnesses and near misses—
/api/v1/observationsSafety observations—
/api/v1/corrective-actionsCorrective actionsstatus
/api/v1/auditsSite audits, with each item and a score—
/api/v1/inspectionsEquipment inspections—
/api/v1/training-recordsTraining records—
/api/v1/workersWorkers on the rostername
/api/v1/job-sitesJob sitesname
/api/v1/changesManagement of change requests—
/api/v1/permitsPermits to work—
/api/v1/policiesPolicies, with acknowledgement counts—
/api/v1/sdsSafety data sheets—
/api/v1/risk-assessmentsRisk assessments—
/api/v1/subcontractorsSubcontractors and their document status—

Paging & filters

  • limit: page size, 50 by default and at most 200.
  • cursor: send back nextCursor from the previous page. It is null on the last page.
  • createdAfter and createdBefore: an ISO 8601 date such as 2026-09-01 or a date and time such as 2026-09-01T12:00:00Z. A nightly sync can ask for createdAfter its 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 of open, in_progress, completed and verified.

Errors & limits

Errors answer with JSON such as {"error": "This key cannot read permits."}, written to be read by a person.

StatusMeaning
400A bad cursor, date, filter or request body.
401No key, or a key that is unknown, revoked or expired.
403The key cannot read that resource, lacks the webhooks permission, or the module or the API is turned off.
404No such resource, record or subscription.
429More 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.

EventWhenRecord from
incident.createdAn incident is reportedincidents
observation.createdA safety observation is reportedobservations
corrective_action.createdA corrective action is raisedcorrective-actions
corrective_action.closedA corrective action is completed or verifiedcorrective-actions
audit.createdA site audit is savedaudits
inspection.failedAn equipment inspection failsinspections
training_record.createdA training record is addedtraining-records
change.approvedA change is approvedchanges
change.implementedA change is implementedchanges
permit.approvedA permit is approvedpermits
permit.closedA permit is closed outpermits

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 id to 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.

RequestWhat it does
POST/api/v1/hooksSubscribe {"url": "https://...", "event": "incident.created"}. Answers 201 with the subscription’s id and its signing secret.
GET/api/v1/hooksThis 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.