Guide to Certn Webhooks: Features and Best Practices
Certn webhooks keep a partner's systems up to date with background checks as they progress. Certn runs criminal record, identity, employment and reference checks, and when an applicant's report changes, it posts the updated applicant to your webhook URL so your hiring or onboarding workflow can move on without polling the API.
This guide covers how Certn webhooks are enabled and signed, what a delivery contains, how retries behave, and the practices that keep a Certn receiver reliable.
What are Certn webhooks?
Certn has one webhook URL per partner. Every update for every applicant goes to it as a POST, and partner resellers share the same URL and secret across all their accounts and sub-accounts. You can't choose events, because Certn doesn't have any: the body is the applicant object itself, and the state of the background check lives in fields such as status and report_status.
Signing is opt-in. Once enabled, each request carries a Certn-Signature header that works like Stripe's: a t= timestamp and one or more v1= signatures, each a hex HMAC-SHA256 of <t>.<raw body>.
Certn webhook features
| Feature | Details |
|---|---|
| Configuration | The Partner page: enter the URL and click Enable Integration |
| Scope | One URL and one secret per partner, shared by resellers' accounts and sub-accounts |
| Event selection | None. Every applicant update is sent |
| Event type | None. The body is the applicant object |
| Signature header | Certn-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>], on one line |
| Scheme | HMAC-SHA256, hex digest. v1 is the only live scheme |
| Signed content | <t>.<raw body> |
| Signing | Opt-in |
| Secret | A secret string on the Partner page, used as UTF-8 |
| Retries | 408, 500, 502, 503 and 504: up to three more attempts, with growing waits |
| Not retried | Any other status above 399 |
| Source IPs | Not published |
Common events
There is no event catalogue. Each delivery is the full applicant object, with about 150 top-level keys in Certn's example. These are the ones that tell you what has changed:
| Field | Example value | Meaning |
|---|---|---|
id | 1ba124f1-d473-44c0-a066-d623b2cb6253 | The applicant ID, which correlates updates |
short_uid | pmndgAeH | A short reference for the applicant |
status / status_label | Returned | The applicant's state |
report_status | COMPLETE | Progress of the whole report |
order_status | COMPLETE | Progress of the order |
result / result_label | CLEARED | The overall result |
*_result | objects or null | One per check, such as us_, equifax_result and reference_result |
Treat the body as a snapshot of the applicant, not as a description of one change. Compare it with what you stored last time to work out what happened, and branch on the status fields rather than on anything that names an event.
Setting up Certn webhooks
- In Certn, open the Partner tab.
- Enter your webhook URL.
- Click Enable Integration.
- Copy the secret string and store it server-side, for example as
CERTN_WEBHOOK_SECRET.
The URL can be changed at any time. Certn's setup instructions say the secret can't be changed and to contact support if it is compromised. The same page later describes rolling a secret from the dashboard with a 48-hour overlap, so check with Certn which applies to your account.
Signed payloads are opt-in, and Certn's documentation doesn't say where the switch is. Ask your Certn contact to turn signing on before you rely on it. When you receive Certn webhooks through Hookdeck, signing has to be on: create a Certn source, paste the signing secret, and note that unsigned deliveries fail verification.
Securing Certn webhooks
Split Certn-Signature on commas, trim each element, take t and every v1, and ignore other schemes. Compute an HMAC-SHA256 of <t>.<raw body> with the secret as a UTF-8 key, and accept the request if any v1 matches in constant time.
const crypto = require('crypto');
const express = require('express');
function verifyCertnSignature(rawBody, header, secret) {
if (!header || !secret) return false;
let t;
const candidates = [];
for (const part of header.split(',')) {
const [key, ...rest] = part.trim().split('=');
if (key.trim() === 't') t = rest.join('=').trim();
else if (key.trim() === 'v1') candidates.push(rest.join('=').trim());
}
if (!/^\d+$/.test(t || '') || candidates.length === 0) return false;
const expected = Buffer.from(
crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex'),
'utf8'
);
return candidates.some((candidate) => {
const received = Buffer.from(candidate.toLowerCase(), 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
app.post('/webhooks/certn', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.CERTN_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifyCertnSignature(req.body, req.get('certn-signature'), secret)) {
return res.sendStatus(401);
}
const applicant = JSON.parse(req.body.toString('utf8'));
// Store the snapshot keyed by applicant.id, then act on report_status asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
def verify_certn_signature(raw_body: bytes, header, secret: str) -> bool:
if not header or not secret:
return False
t, candidates = None, []
for part in header.split(","):
key, sep, value = part.strip().partition("=")
if sep and key.strip() == "t":
t = value.strip()
elif sep and key.strip() == "v1":
candidates.append(value.strip())
if not t or not (t.isascii() and t.isdigit()) or not candidates:
return False
expected = hmac.new(
secret.encode("utf-8"), t.encode("utf-8") + b"." + raw_body, hashlib.sha256
).hexdigest().encode("utf-8")
return any(hmac.compare_digest(c.lower().encode("utf-8"), expected) for c in candidates)
Certn says you can reject a payload whose timestamp is too old, but names no tolerance. The Certn webhook skill leaves that window off by default, so it accepts Hookdeck retries and replays of older events. If you turn the window on with CERTN_, note that Certn signs each of its own retries afresh, but Hookdeck retries and replays resend the original t, so any that arrive after the window has passed fail the check.
Certn's docs warn that Express doesn't give you the original body string without extra middleware. express.raw() on the route solves that.
Make Certn webhooks production-ready. Hookdeck Event Gateway verifies the
Certn-Signatureheader, stores every applicant update, and delivers it to your services with retries and replay you control.
Certn webhook limitations and pain points
Unsigned until you opt in
The Problem: Deliveries arrive with no Certn-Signature header, so there is nothing to verify.
Why It Happens: Signing is optional and off unless it has been enabled for your partner account, and the documentation doesn't say where to turn it on.
Workarounds:
- Ask Certn to enable signed payloads before you go live.
- Reject requests without the header once signing is on, so a forged unsigned request isn't accepted.
How Hookdeck Can Help: Hookdeck's Certn source type expects the header. Unsigned deliveries are marked as failed verification and aren't delivered to your destinations, which tells you straight away that signing is off.
No event types
The Problem: You can't route or filter on what happened, only on the applicant's current state.
Why It Happens: Certn posts the applicant object whenever it changes. No field or header says "report completed" or "check failed".
Workarounds:
- Store the last snapshot per applicant
idand diff the status fields to decide what changed. - Act on terminal states, such as
report_statusofCOMPLETE, and treat earlier snapshots as progress.
How Hookdeck Can Help: Hookdeck filters can match on body fields such as report_status, so only completed reports reach the service that acts on them.
Few retries, and some errors are final
The Problem: An update is lost after a short outage, or after a single 4xx response.
Why It Happens: Certn retries only 408, 500, 502, 503 and 504, up to three more times. Any other status above 399, including 401, 404 or 429, ends delivery on the first attempt.
Workarounds:
- Return
5xx, not4xx, for transient problems on your side. - Read the applicant from the Certn API after an outage to catch missed updates.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, so your own errors and downtime don't end Certn's delivery. It then delivers to your service with automatic retries.
Best practices
Verify against the raw body
The signed string uses the request body exactly as sent. Use express.raw() in Express, await request.text() in a Next.js route handler and await request.body() in FastAPI, and parse only after the check passes.
Accept any matching v1
During a secret roll, Certn sends one v1 per active secret. Check each one and accept the request if any matches.
Make processing idempotent
There is no event ID. Key updates on the applicant id together with the status fields or modified, and make sure applying the same snapshot twice has no extra effect. See our guide to webhook idempotency.
Protect the payload
The applicant object contains personal data and check results. Keep the endpoint on HTTPS, restrict who can read stored payloads, and drop fields you don't need.
Acknowledge fast and process asynchronously
Verify, store the snapshot, return 200, and do the work afterwards. See why to process webhooks asynchronously.
Conclusion
Certn posts the whole applicant object to one partner-wide URL, with no event types, and signs it only once signing is enabled. Verify Certn-Signature against <t>.<raw body> and accept any matching v1, keep timestamp checks optional, route on report_status and the other status fields, and return 5xx rather than 4xx for temporary failures so Certn retries.
Hookdeck Event Gateway sits in front of your receiver, checks every Certn signature, and holds each applicant update until your service has processed it.
Get started with Hookdeck for free and handle Certn webhooks reliably in minutes.