Webhooks
Elsin posts signed events to your own endpoint whenever something happens in hiring.
Adding an endpoint
Admins add endpoints in Settings, Webhooks: a public https address and the events to send, or none for all. Send test posts a webhook.test event. Zapier, Make and n8n can subscribe themselves through the API with an admin key that has settings:write.
Events
| Group | Events |
|---|---|
| Applications | application.created, application.stage_changed, application.rejected, application.hired |
| Verification | candidate.verified, candidate.flagged, review.decided, work_history.completed, day_one.completed |
| Interviews | interview.scheduled, interview.rescheduled, interview.cancelled, scorecard.submitted |
| Offers | offer.created, offer.approval_requested, offer.approved, offer.rejected, offer.sent, offer.accepted, offer.declined, offer.withdrawn, offer.expired |
| Jobs | job.approval_requested, job.approved, job.approval_rejected, job.published, job.closed |
What a payload holds
The event's ids, type and time, the ids of the candidate, application, job, interview or offer, and the candidate's name, email and verification level with the job's title, plus short facts such as the stage. Never a phone number, an address or a document.
Verifying the signature
Each request carries Elsin-Signature: t=<seconds>,v1=<hex>, an HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the endpoint's secret. Refuse a timestamp more than 300 seconds old. Elsin-Event-Id stays the same across retries, so use it to ignore repeats.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, header, rawBody) {
let t = null;
const signatures = [];
for (const part of (header ?? '').split(',')) {
const [key, value] = part.trim().split('=', 2);
if (key === 't') t = Number(value);
if (key === 'v1' && value) signatures.push(Buffer.from(value));
}
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = Buffer.from(createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'));
return signatures.some((s) => s.length === expected.length && timingSafeEqual(s, expected));
}Retries
Anything but a 2xx within 10 seconds is a failure, and redirects are not followed. Elsin tries 8 times in all: at once, then after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. After 20 failures in a row the endpoint is switched off; turn it back on to resume. The delivery log can resend any event.
Examples in Python and Go are on the developers page.