1. Overview — one API, every EveryGuard product
FirmGuard, AgentGuard, NurseryGuard, HotelGuard, CareGuard and the rest of the EveryGuard family share one backend, one database, and one outbound webhooks engine. A customer registers a webhook endpoint in their own product dashboard — Settings → Webhooks (also reachable as Integrations on some products) — and we POST a signed JSON event to it whenever something they care about happens: a customer due-diligence check completes, a statutory diary entry is logged, a readiness self-check finishes.
The contract — envelope shape, signature scheme, retry behaviour, error handling — is identical whichever product you’re on. Only the event catalogue differs per vertical (§2), because different products have different things worth telling you about. If you’ve integrated one EveryGuard webhook, you already know how to integrate all of them.
Getting started
- Log in to your dashboard and open Settings → Webhooks.
- Add an HTTPS endpoint URL and tick the event types you want to receive.
- Copy the signing secret shown — it is only ever displayed once.
- Use “Send test” to fire a
webhook.pingevent and confirm your endpoint verifies it (§4).
2. Event catalogue
Each product only exposes the events that make sense for it. The catalogue is
additive-only: an event type is never renamed or repurposed —
a genuinely new kind of event is always a new type. You can fetch the live,
subscribable list for your own account at
GET /api/integrations/webhooks/events.
| Product | Event type | Fires when… |
|---|---|---|
| NurseryGuard | diary.entry_created | A statutory diary entry is logged |
| NurseryGuard | diary.entry_updated | A diary entry is updated or its status changes |
| NurseryGuard | self_check.completed | An EYFS readiness self-check completes |
| AgentGuard | cdd.check_completed | A customer CDD (due-diligence) check completes |
| FirmGuard | cdd.check_completed | A customer CDD (due-diligence) check completes |
| Every product | webhook.ping | You press “Send test” on an endpoint |
More products are progressively wired onto this same engine; a vertical with no rows in the table above simply has no events published yet — the contract below is what it will use the day it does.
Example — cdd.check_completed
Payloads are curated per event type to carry only what the endpoint needs to react — ids, verdicts, categories, counts. A CDD-check payload never contains the subject’s name, date of birth, address, or any document image; those stay inside your dashboard, behind your own authentication.
{
"id": "3fa8a13e-9e2b-4b8b-9e02-9e6b6ee2f2b1",
"type": "cdd.check_completed",
"api_version": "2026-07-10",
"created_at": "2026-07-14T09:15:32.104Z",
"vertical": "agent",
"data": {
"check_id": 48213,
"verdict": "clear",
"pep_verdict": "clear",
"sanctions_result": "no_match",
"internal_reference": "CASE-2026-0417"
}
}
No customer PII, ever. verdict and
pep_verdict are one of clear / review /
match; sanctions_result is one of
no_match / review_required / match.
internal_reference echoes back whatever reference you
supplied when you ran the check — it is your own identifier, not ours.
3. Envelope format
Every event, on every product, is wrapped in the same envelope:
| Field | Type | Meaning |
|---|---|---|
id | string (uuid) | Unique event id. Also sent as the X-Webhook-Id header — use it to de-duplicate (§5). |
type | string | The event type, e.g. cdd.check_completed (also sent as X-Webhook-Event). |
api_version | string | The envelope/contract version in force when the event was built. Currently 2026-07-10. |
created_at | string (ISO 8601) | UTC timestamp the event was generated. |
vertical | string | Which product the event belongs to — nursery, agent, firm, etc. |
data | object | The curated, event-specific payload (§2). Additive-only per type. |
data may gain new fields for a given type over time
without warning — treat unknown fields as safe to ignore. A genuinely
breaking change to the envelope itself would ship under a new
api_version, never by silently changing the current one.
4. Verifying the signature
Every delivery is a signed HTTPS POST, Stripe-style. Alongside
Content-Type: application/json, three headers are sent:
| Header | Value |
|---|---|
X-Webhook-Signature | t=<unix-seconds>,v1=<hex-hmac> |
X-Webhook-Id | The event’s id (uuid) — your idempotency key |
X-Webhook-Event | The event’s type |
v1 is computed as
HMAC_SHA256(signing_secret, "<t>.<raw_request_body>"), hex-encoded,
where signing_secret is the whsec_… value shown
once when you created the endpoint. Recompute it over the exact raw
request body bytes — not a re-serialised copy of the parsed JSON,
which can differ in whitespace or key order.
// Node.js — verify an incoming EveryGuard webhook
import crypto from 'crypto'
function verifyEveryGuardWebhook(rawBody, signatureHeader, signingSecret, {
toleranceSeconds = 300, // reject anything older than 5 minutes
} = {}) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((kv) => kv.split('=', 2))
)
const t = Number(parts.t)
const v1 = parts.v1
if (!t || !v1) throw new Error('malformed signature header')
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) {
throw new Error('timestamp outside tolerance — possible replay')
}
const expected = crypto
.createHmac('sha256', signingSecret)
.update(`${t}.${rawBody}`)
.digest('hex')
const a = Buffer.from(v1)
const b = Buffer.from(expected)
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error('signature mismatch')
}
return true // verified — safe to process
}
Reject stale timestamps. Checking t against the
current time (a 5-minute tolerance is a reasonable default) is what stops a
captured request from being replayed later. Use a constant-time comparison
(crypto.timingSafeEqual or equivalent) for the digest itself —
never a plain string ===.
5. Delivery, retries & idempotency
- Every delivery is a single signed
POSTwith a 10-second send timeout. - A failed or timed-out attempt is retried with exponential backoff, up to 6 attempts total (roughly 30s, 1m, 2m, 4m, 8m between attempts).
- If an endpoint racks up 5 consecutive dead deliveries (every attempt of an event exhausted), it is automatically switched off (
active=false) and the account owner gets an in-app notice to re-enable it from Settings → Webhooks. Any successful delivery resets the streak to zero. - You can register up to 5 endpoints per account.
- Endpoint URLs must be HTTPS and cannot resolve to a private, loopback, link-local, or cloud-metadata address — checked both at registration and again on every send.
- Return any 2xx status quickly to acknowledge receipt — the response body is not inspected. Do your processing after responding if it’s non-trivial.
- Delivery is at-least-once, not exactly-once: retries (and, rarely, network conditions) can cause the same event to arrive more than once. De-duplicate on
id(equivalently, theX-Webhook-Idheader) before acting on an event a second time.
6. Managing endpoints
Endpoints can be managed from your dashboard (Settings →
Webhooks) or directly against the REST surface below, under
/api/integrations. All routes use your normal account
authentication; creating or deleting an endpoint requires an account
admin/principal role — read-only staff logins can list and view, not write.
| Endpoint | What it does |
|---|---|
GET/webhooks/events | The subscribable event catalogue for your product |
GET/webhooks | List your registered endpoints (secret masked) |
POST/webhooks | { url, events[] } — create an endpoint; returns the signing secret once |
DELETE/webhooks/:id | Remove an endpoint |
POST/webhooks/:id/test | Send a webhook.ping test event |
GET/webhooks/:id/deliveries | Recent delivery log for that endpoint (status, HTTP code, attempts) |
The signing secret (whsec_…) is generated server-side and
returned in the POST /webhooks response body exactly
once. We store it encrypted, never in plain text, and cannot show it to
you again — if you lose it, delete the endpoint and create a new one.
7. Zapier
EveryGuard products connect to Zapier through the same REST-hook contract used above — turning a Zap on or off subscribes or unsubscribes a webhook endpoint automatically, and a polling fallback lets Zapier show sample data before the first real event arrives. Find it under Settings → Integrations → Zapier in your dashboard.
8. Case-management connectors
Clio is our first native case-management connector — connect it from your dashboard’s Settings → Integrations page, pick a contact (and optionally a matter) when you start a check, and the completed verdict is filed back into the matter as a note automatically. It rides the REST API in §9 below rather than a bespoke integration, so anything you build against that API today keeps working unchanged as we add more connectors. LEAP and Actionstep are planned next.
9. REST API — starting checks programmatically
Webhooks (§§2–6 above) tell your systems when something
happens. This REST surface is the other half: it lets your own systems
start a check and read back curated results directly, without
a 7-day user login token. It lives alongside the webhooks contract under
/api/v1, and it is the exact platform our native Clio connector
(§8) is built on.
Authentication
Mint an API key from your dashboard — Settings →
Integrations → API keys. The key is shown once
at creation (eg_live_…); we store only a hash and cannot
show it to you again — if you lose it, revoke it and create a new one.
Send it on every request as either header:
Authorization: Bearer eg_live_51b2b6b7c4a94f2c8e6a1d0f9b3c2a11
# — or —
X-Api-Key: eg_live_51b2b6b7c4a94f2c8e6a1d0f9b3c2a11
| Field | Meaning |
|---|---|
label | Your own name for the key (e.g. “Practice-management sync”) — shown in your key list, never the secret itself |
scopes | checks:read and/or checks:write. A key without checks:write gets 403 missing_scope on POST /checks; without checks:read on the GET endpoints. |
| Limit | Up to 5 active keys per account. Revoke a key you no longer use rather than losing track of it. |
Endpoints
| Endpoint | Scope | What it does |
|---|---|---|
POST/api/v1/checks | checks:write | Start a check — sends the subject a verify link, exactly like the in-app “Send a verify link” flow |
GET/api/v1/checks/:id | checks:read | Fetch one check’s curated status/verdict |
GET/api/v1/checks | checks:read | List your checks, newest first (limit, status query params) |
Start a check
curl https://api.readyvetstaff.com/api/v1/checks \
-H "Authorization: Bearer eg_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+447700900123",
"channel": "sms",
"internal_reference": "CASE-2026-0417",
"crm": { "provider": "clio", "contact_id": "1234567", "matter_id": "7654321" }
}'
// 201 Created
{
"check": {
"id": "48213",
"status": "pending",
"verify_link_sent_to": "jane@example.com"
}
}
email, phone, channel,
internal_reference and crm are all optional —
supply at least one of email/phone so there’s
somewhere to send the link. crm is a free-form
{ provider, contact_id, matter_id } object: it’s echoed back
on the check, and for a connected provider (Clio today) it’s used to file
the completed result back into your case-management system.
Read a check
curl https://api.readyvetstaff.com/api/v1/checks/48213 \
-H "Authorization: Bearer eg_live_…"
{
"id": "48213",
"status": "completed",
"verdict": "clear",
"pep_verdict": "clear",
"sanctions_result": "no_match",
"internal_reference": "CASE-2026-0417",
"crm": { "provider": "clio", "contact_id": "1234567", "matter_id": "7654321" },
"created_at": "2026-07-14T09:02:11.000Z",
"completed_at": "2026-07-14T09:15:32.104Z"
}
Curated, same as webhooks. These responses carry only what you submitted plus the verdict fields — no document images, no evidence binaries, no PII beyond what your own request supplied.
Errors & quota
| Status | Meaning |
|---|---|
402 quota_exceeded | Your plan’s monthly check allowance is used up. In-app checks continue on a soft cap — an AML obligation is never blocked on a quota number — but the API enforces the hard equivalent so a runaway integration can’t rack up an unbounded bill. Upgrade your plan or wait for the next billing cycle. |
403 missing_scope | Your key doesn’t carry the scope the endpoint needs — mint a new key with checks:write and/or checks:read. |
Requests are rate-limited to roughly 120 per minute per key.
Back off on a 429 and retry with jitter.
Works with your CMS. Our native Clio
connector (Settings → Integrations) is built on this exact API —
connecting it just wires a Clio contact/matter picker onto
POST /checks and files the completed verdict back into the
matter as a note, automatically. If you use a different case-management
system, this REST surface is how you build the same thing yourself.
10. Support
Questions about integrating, a specific delivery that looks stuck, or a product
whose events aren’t listed above yet — contact us at
legal@everyguard.uk or through your
dashboard. Please include the endpoint id and, where relevant, the delivery id
from GET /webhooks/:id/deliveries.