Agent skill
Outhire Webhooks Skill
Receive and verify Outhire (outhire.ai) AI recruiting webhooks, signed with Standard Webhooks. Use when setting up an Outhire webhook endpoint under Settings > Webhooks, getting the whsec_ signing secret, debugging webhook-id / webhook-timestamp / webhook-signature verification, or handling candidate.applied, candidate.stage_changed and webhook.ping events.
Install this skill
npx skills add hookdeck/webhook-skills --skill outhire-webhooks
Outhire is an AI recruiting platform. Its outbound webhooks push candidate lifecycle events (a candidate applies, a candidate changes pipeline stage) to your systems. Each delivery is a JSON POST signed with Standard Webhooks: webhook-id, webhook-timestamp and webhook-signature headers and a per-webhook whsec_ secret.
When to Use This Skill
- How do I receive Outhire webhooks?
- How do I verify an Outhire webhook signature?
- Where do I find my Outhire webhook signing secret?
- How do I handle
candidate.appliedorcandidate.stage_changedevents? - Why is my Outhire
webhook-signatureverification failing? - How do I dedupe Outhire webhooks when the payload has no event ID?
Verification (core)
| Header | Value |
|---|---|
webhook-id | Unique ID of this delivery |
webhook-timestamp | Unix seconds when the request was sent |
webhook-signature | One or more space-separated v1,<base64 HMAC-SHA256> entries |
Signed content is {webhook-id}.{webhook-timestamp}.{raw body}. The key is the base64-decoded part of the secret after whsec_; keep the full whsec_ value in OUTHIRE_WEBHOOK_SECRET. The examples use standardwebhooks (npm, PyPI), which compares in constant time against every v1, entry. Hookdeck also accepts the svix-* spelling of the same headers, so the examples read webhook-* and fall back to svix-id, svix-timestamp and svix-signature when webhook-id is absent.
const { Webhook } = require('standardwebhooks');
// rawBody: exact request body string, NOT re-serialised JSON
function verifyOuthireWebhook(rawBody, get, secret = process.env.OUTHIRE_WEBHOOK_SECRET) {
const prefix = get('webhook-id') ? 'webhook' : 'svix'; // svix-* fallback
return new Webhook(secret).verify(rawBody, {
'webhook-id': get(`${prefix}-id`),
'webhook-timestamp': get(`${prefix}-timestamp`),
'webhook-signature': get(`${prefix}-signature`),
}); // returns the parsed event; throws → respond 400
}
Python: from standardwebhooks.webhooks import Webhook, then Webhook(secret).verify(await request.body(), {...same mapping...}).
Retries and the 5-minute window. The library always enforces a fixed 5-minute timestamp tolerance. Hookdeck retries and replays resend the original timestamp, so the library rejects a late retry. If you receive Outhire webhooks through Hookdeck, use the manual verifier in references/verification.md (HMAC-SHA256 over
{id}.{timestamp}.{raw body}, everyv1,entry compared in constant time) where the window can be dropped or widened.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Envelope
{ "event": "candidate.stage_changed", "occurred_at": "2026-03-11T00:00:00.000Z",
"candidate": { "id": 123, "name": "Jane Doe" }, "job": { "id": 456, "title": "Account Executive" },
"company": { "id": 789 },
"stage_change": { "previous_stage": "applied", "new_stage": "interview", "changed_by": "user" } }
The top-level field is event. There is no event ID in the body: dedupe on the webhook-id header.
Common Event Types
| Event | Fired when |
|---|---|
candidate.applied | A candidate submits a self-serve application (not candidates created via API or other intake) |
candidate.stage_changed | A candidate moves between pipeline stages (changed_by: user, joel or api) |
webhook.ping | An admin clicks Send test ping. A signed delivery, not a challenge |
These three are the documented events; the list may not be exhaustive, so acknowledge unknown events with 2xx.
Delivery Semantics
- Return a
2xxwithin 10 seconds. Enqueue the event and process it asynchronously. - Outhire keys each delivery and doesn't resend one that already succeeded, but be idempotent anyway.
- The docs don't describe retries or ordering. Use
occurred_atif you need to order events. - Missing headers or a bad signature: respond
400. Handlers fail closed (500) whenOUTHIRE_WEBHOOK_SECRETis unset.
Environment Variables
OUTHIRE_WEBHOOK_SECRET=whsec_xxxxx # full value including the prefix; one per webhook
Get it from Settings > Webhooks after saving the webhook (admins only).
Local Development
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 outhire --path /webhooks/outhire
Add the printed URL with Add webhook in Settings > Webhooks, subscribe to the events, then use Send test ping.
Reference Materials
- references/overview.md - Events, payload fields, delivery behavior
- references/setup.md - Settings > Webhooks setup, custom headers, signing secret
- references/verification.md - Library and manual verification, gotchas, debugging