Back to Legal & Trust Centre
Technical reference Public

Developer & API Documentation

Document OwnerFriam Limited
Document ReferenceFRM-DEV-001
Document Version1.1
Current API Version2026-07-10
Effective Date14 July 2026
ClassificationPublic
AudienceEngineers integrating with any EveryGuard product

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

  1. Log in to your dashboard and open Settings → Webhooks.
  2. Add an HTTPS endpoint URL and tick the event types you want to receive.
  3. Copy the signing secret shown — it is only ever displayed once.
  4. Use “Send test” to fire a webhook.ping event 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.

ProductEvent typeFires when…
NurseryGuarddiary.entry_createdA statutory diary entry is logged
NurseryGuarddiary.entry_updatedA diary entry is updated or its status changes
NurseryGuardself_check.completedAn EYFS readiness self-check completes
AgentGuardcdd.check_completedA customer CDD (due-diligence) check completes
FirmGuardcdd.check_completedA customer CDD (due-diligence) check completes
Every productwebhook.pingYou 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:

FieldTypeMeaning
idstring (uuid)Unique event id. Also sent as the X-Webhook-Id header — use it to de-duplicate (§5).
typestringThe event type, e.g. cdd.check_completed (also sent as X-Webhook-Event).
api_versionstringThe envelope/contract version in force when the event was built. Currently 2026-07-10.
created_atstring (ISO 8601)UTC timestamp the event was generated.
verticalstringWhich product the event belongs to — nursery, agent, firm, etc.
dataobjectThe 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:

HeaderValue
X-Webhook-Signaturet=<unix-seconds>,v1=<hex-hmac>
X-Webhook-IdThe event’s id (uuid) — your idempotency key
X-Webhook-EventThe 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 POST with 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, the X-Webhook-Id header) 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.

EndpointWhat it does
GET/webhooks/eventsThe subscribable event catalogue for your product
GET/webhooksList your registered endpoints (secret masked)
POST/webhooks{ url, events[] } — create an endpoint; returns the signing secret once
DELETE/webhooks/:idRemove an endpoint
POST/webhooks/:id/testSend a webhook.ping test event
GET/webhooks/:id/deliveriesRecent 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
FieldMeaning
labelYour own name for the key (e.g. “Practice-management sync”) — shown in your key list, never the secret itself
scopeschecks:read and/or checks:write. A key without checks:write gets 403 missing_scope on POST /checks; without checks:read on the GET endpoints.
LimitUp to 5 active keys per account. Revoke a key you no longer use rather than losing track of it.

Endpoints

EndpointScopeWhat it does
POST/api/v1/checkschecks:writeStart a check — sends the subject a verify link, exactly like the in-app “Send a verify link” flow
GET/api/v1/checks/:idchecks:readFetch one check’s curated status/verdict
GET/api/v1/checkschecks:readList 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

StatusMeaning
402 quota_exceededYour 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_scopeYour 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.