Connector API

Push a data point. Get the right training.

Edifyd doesn't own your HR, credential or incident records — it consumes signals from the systems that do, and turns each into a training need: recommend a course from your library or author one, then offer it to the right people. One HMAC-signed webhook, idempotent, no new auth surface.

How it works

Every system in your stack already knows something that implies training: a licence about to lapse, an incident on a site, a role change, a policy update, an onboarding gap. Edifyd calls each of these a data point. You POST it to one webhook; Edifyd records it as a signal, resolves it to the teachable topic, and either recommends an existing course or offers to author one. It lands in the Training needs inbox for an admin to offer (assign), author, or dismiss — and external signals also surface inline on the Readiness board.

HRIS / SORCredential registry Incident / near-missLMS / WFMPolicy manager

The source record stays where it lives. Edifyd never becomes your system of record — it reacts to the signal.

Quick start

  • 1. Ask your Edifyd operator for your tenant slug and the shared signing secret.
  • 2. Sign the raw JSON body with HMAC-SHA256 and send it as the x-edifyd-signature header.
  • 3. POST a signal.* event to the webhook. That's it — the need appears in the console.
POST https://www.edifyd.com/engine/v1/integration/<tenant>/webhook

Authentication

There is no API key and no login. Each request is authenticated by an HMAC-SHA256 signature of the raw request body, keyed by a shared secret issued by your Edifyd operator. Send the lowercase hex digest in the x-edifyd-signature header. A missing or wrong signature is rejected with 401 bad_signature. Signatures are compared in constant time, so sign the exact bytes you send (don't re-serialise between signing and sending).

# bash — sign and send
SECRET="your-shared-secret"
BODY='{"event":"signal.credential_expiring","data":{"email":"jordan@acme.com","topic":"Working at Heights"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -sS https://www.edifyd.com/engine/v1/integration/acme/webhook \
  -H "Content-Type: application/json" \
  -H "x-edifyd-signature: $SIG" \
  -d "$BODY"
// node — the same, no dependencies
const crypto = require('crypto');
const body = JSON.stringify({ event: 'signal.incident', data: { site: 'Site A', topic: 'Manual Handling' } });
const sig  = crypto.createHmac('sha256', process.env.SECRET).update(body).digest('hex');
await fetch('https://www.edifyd.com/engine/v1/integration/acme/webhook', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'x-edifyd-signature': sig },
  body,
});
# python
import hmac, hashlib, json, requests
body = json.dumps({"event": "signal.role_assigned", "data": {"external_worker_id": "W-123", "topic": "Forklift Operation"}})
sig  = hmac.new(SECRET.encode(), body.encode(), hashlib.sha256).hexdigest()
requests.post("https://www.edifyd.com/engine/v1/integration/acme/webhook",
              data=body, headers={"Content-Type": "application/json", "x-edifyd-signature": sig})

The webhook

POST /engine/v1/integration/<tenant>/webhook

Send a JSON object with an event name and a data payload:

FieldRequiredNotes
eventyessignal.<type> (e.g. signal.credential_expiring), or signal with data.type.
data.typeif not in eventThe signal type; drives the mapping defaults.
data.external_worker_idone of theseResolve the affected worker. Falls back to data.email, then data.learner_id.
data.emailAlternate worker key.
data.siteCohort target when there is no specific worker.
data.topicrecommendedThe teachable topic — this is what matches a course (or seeds authoring).
data.categorye.g. Refresher. Optional.
data.severity4 blocking · 3 soon · 2 info (default) · 1 low.
data.dedupe_keyIdempotency key. Defaults to type:worker|site:topic.
data.payloadThe raw data point, kept for the inbox & audit (e.g. { expires_at, credential }).
Explicit fields on the request always win over the per-tenant defaults in your signal map. Send only what you have — Edifyd fills the rest from the map.

Event types

The type is free-form, but these are the recognised patterns. Any type you send can be mapped to a topic.

EventTypical dataBecomes
signal.credential_expiringworker + topic + expires_ata refresher offered to the worker
signal.credential_expiredworker + topica blocking need (severity 4)
signal.incidentsite + topic (often no worker)a cohort need for that site
signal.role_assignedworker + new role → topicrole-readiness training
signal.policy_updatedsite/cohort + topica policy acknowledgement / briefing
signal.onboarding_gapworker + topican onboarding step to complete

Examples

A lapsing licence, for one worker

{
  "event": "signal.credential_expiring",
  "data": {
    "email": "jordan@acme.com",
    "topic": "Working at Heights",
    "category": "Refresher",
    "severity": 3,
    "payload": { "credential": "WAH licence", "expires_at": "2026-07-15" }
  }
}

An incident, for a whole site (no named worker)

{
  "event": "signal.incident",
  "data": {
    "site": "Site A",
    "topic": "Manual Handling",
    "severity": 4,
    "payload": { "ref": "INC-2043", "summary": "strain lifting stock" }
  }
}

No course matches the topic? The need comes back flagged author one — a click (or, if enabled, auto-author) drafts a course on that topic.

Signal mapping

So a source system's event types become teachable topics without code, each tenant has a signal map: per type, a default topic, category and severity. Edit it in the console (Training needs → Automation & signal mapping) or via the admin API. Explicit fields on a webhook always override the map.

GET /engine/v1/admin/<tenant>/signal-map
PUT /engine/v1/admin/<tenant>/signal-map
// PUT body — map event types to defaults, and toggle auto-author
{
  "map": {
    "license_expiring":  { "topic": "Working at Heights", "category": "Refresher", "severity": 3 },
    "near_miss":         { "topic": "Hazard Awareness",   "category": "Toolbox",   "severity": 4 }
  },
  "autoAuthor": false
}
The admin API (/v1/admin/…) is authenticated with your Edifyd console session/token — it's for your own back-office, not for external systems. External systems only ever call the signed webhook.

What happens next

  • Resolve the worker — by external_worker_idemaillearner_id. None? It's a cohort need, targeted by site.
  • Apply defaultstopic/category/severity from your signal map, unless the payload set them.
  • Resolve to a need — Edifyd matches your published library on the topic. A match becomes a recommendation; no match becomes author one.
  • Land in Training needs — an admin clicks Offer (assign to the worker, a group, or a whole site), Author, or Dismiss.
  • Surface in Readiness — external signals also appear as a reason-row on the readiness board, beside credentials and onboarding, so a live gap shows up in "who's ready, and why".

Idempotency

Re-send freely — retries, replays, and periodic full syncs are safe. The same open data point (same dedupe_key) never creates a duplicate need; the response returns "deduped": true. If you don't set a key, Edifyd derives one from type + worker/site + topic. Use a stable key (e.g. your source record id) when you want an update to coalesce onto the same need.

Responses & errors

StatusBodyMeaning
200{ "ok": true, "applied": true, "signalId": "sig_…", "deduped": false }Accepted; a need was created.
200{ "ok": true, "applied": true, "deduped": true }Accepted; matched an existing open need.
401{ "error": "bad_signature" }Missing/invalid x-edifyd-signature.

The webhook responds as soon as the signal is recorded; any downstream authoring runs in the background and never blocks your call.

Auto-author (optional)

When several people need the same topic and nothing in your library fits, Edifyd can draft a course automatically. It's opt-in per tenant (off by default), and produces drafts only — never published or assigned without a human review, and metered by your AI plan. Turn it on in Training needs → Automation & signal mapping, or with { "autoAuthor": true } on the signal-map API. You stay in control; the webhook just keeps feeding the inbox.

Full field-level reference lives with the engine in readiness-engine/docs/training-signals-webhook.md. Need a sandbox tenant and secret? Talk to us and we'll set you up.