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.
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-signatureheader. - 3. POST a
signal.*event to the webhook. That's it — the need appears in the console.
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
Send a JSON object with an event name and a data payload:
| Field | Required | Notes |
|---|---|---|
event | yes | signal.<type> (e.g. signal.credential_expiring), or signal with data.type. |
data.type | if not in event | The signal type; drives the mapping defaults. |
data.external_worker_id | one of these | Resolve the affected worker. Falls back to data.email, then data.learner_id. |
data.email | — | Alternate worker key. |
data.site | — | Cohort target when there is no specific worker. |
data.topic | recommended | The teachable topic — this is what matches a course (or seeds authoring). |
data.category | — | e.g. Refresher. Optional. |
data.severity | — | 4 blocking · 3 soon · 2 info (default) · 1 low. |
data.dedupe_key | — | Idempotency key. Defaults to type:worker|site:topic. |
data.payload | — | The raw data point, kept for the inbox & audit (e.g. { expires_at, credential }). |
Event types
The type is free-form, but these are the recognised patterns. Any type you send can be mapped to a topic.
| Event | Typical data | Becomes |
|---|---|---|
signal.credential_expiring | worker + topic + expires_at | a refresher offered to the worker |
signal.credential_expired | worker + topic | a blocking need (severity 4) |
signal.incident | site + topic (often no worker) | a cohort need for that site |
signal.role_assigned | worker + new role → topic | role-readiness training |
signal.policy_updated | site/cohort + topic | a policy acknowledgement / briefing |
signal.onboarding_gap | worker + topic | an 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.
// 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
}
/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_id→email→learner_id. None? It's a cohort need, targeted bysite. - Apply defaults —
topic/category/severityfrom 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
| Status | Body | Meaning |
|---|---|---|
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.
readiness-engine/docs/training-signals-webhook.md. Need a sandbox tenant and secret?
Talk to us and we'll set you up.