Webhooks
Octrahire tells your systems when something happens in hiring, with signed messages that follow the Standard Webhooks specification. In beta.
Status
Standard Webhooks delivery
In betaSigned with HMAC-SHA256, with secrets made by Octrahire and shown once.
Delivery log and replay
In betaSee every delivery in settings and send one again.
Available: live in production today. In beta: deployed and usable today; it may still change. Coming soon: not usable yet.
Add a webhook
With the webhooks:manage scope, or from your workspace settings:
curl -X POST https://api.octrahire.ai/api/v1/public/webhooks \
-H "Authorization: Bearer ohk_your_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://hr.example.com/octrahire", "events": ["application.moved", "offer.accepted"]}'{"id": "whk_...", "url": "https://hr.example.com/octrahire",
"events": ["application.moved", "offer.accepted"], "secret": "whsec_..."}Use ["*"] for every event, and unsubscribe with DELETE /webhooks/{id}. The Zapier and Make apps, once published, will subscribe the same way.
Events
| Event | When |
|---|---|
| application.created | A candidate applied or was added to a role |
| application.moved | An application moved to another stage |
| application.rejected | An application was rejected, with the reason category |
| application.hired | An application was marked hired |
| interview.invited | A candidate was invited to an AI interview |
| interview.started | A candidate started an AI interview |
| interview.completed | A candidate finished an AI interview; grading is under way |
| interview.graded | An AI interview report is ready |
| interview.scheduled | A human interview was scheduled or rescheduled |
| scorecard.submitted | A reviewer submitted a scorecard |
| review.requested | A candidate asked for a human review of an AI decision |
| offer.sent | An offer was sent to a candidate |
| offer.accepted | A candidate accepted an offer |
| offer.declined | A candidate declined an offer |
| requisition.approved | A requisition was approved |
| role.opened | A role was opened for applications |
| role.closed | A role was closed |
| candidate.replied | A candidate replied by WhatsApp, email or SMS |
| candidate.erased | A candidate's personal data was erased |
GET /events returns the current catalogue. application.stage is kept for receivers that already use it.
What a delivery looks like
webhook-id: msg_2Vb7...
webhook-timestamp: 1791885600
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4={"type": "application.moved", "timestamp": "2026-10-13T10:00:00Z", "tenant": "acme",
"data": {"application_id": "...", "role_id": "...", "from_stage": "in_review", "to_stage": "offer"}}The signature is the base64 HMAC-SHA256 of "{webhook-id}.{webhook-timestamp}.{body}", keyed with the base64 part of your whsec_ secret. While a secret is being rotated, webhook-signature carries both signatures, separated by a space.
Reply with a 2xx status quickly, and do slow work after replying. Use webhook-id to ignore a message you have already handled.
Verify the signature
Use the raw request body, exactly as received, before parsing it. With the Standard Webhooks library:
from standardwebhooks.webhooks import Webhook
wh = Webhook("whsec_...") # your endpoint's secret
payload = wh.verify(raw_body, request.headers) # raises if the signature or timestamp is badimport crypto from "node:crypto";
export function verify(secret, headers, rawBody) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const sigs = headers["webhook-signature"] || "";
// Reject messages more than five minutes old or in the future.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
return sigs.split(" ").some((s) => {
const [version, sig] = s.split(",");
const got = Buffer.from(sig || "", "base64");
return version === "v1" && got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}Rotating a secret
POST /webhooks/{id}/rotate-secret returns a new secret. For a short while deliveries are signed with both the old and the new secret, so you can switch without missing a message.
Webhooks made before this format keep their old body and X-Octrahire-Signature header too, so existing receivers keep working.
Read next
See Octrahire with your own roles
A 30-minute walkthrough with your jobs or your programmes.
