Agent skill
Breezy HR Webhooks Skill
Receive and verify Breezy HR webhooks (recruiting / ATS). Use when setting up a Breezy HR webhook handler, debugging X-Hook-Signature verification failures, or handling events like candidateAdded, candidateStatusUpdated, candidateDeleted and companyPositionAdded. The signature is a bare hex HMAC-SHA256 of the raw body keyed with the whole whsec_ secret; it is not Standard Webhooks.
Install this skill
npx skills add hookdeck/webhook-skills --skill breezy-hr-webhooks
Breezy HR is a recruiting / applicant tracking system. It POSTs a JSON event to your HTTPS endpoint when candidates, positions and team notes change. Webhooks are available on Pro plans and are managed through the Breezy API.
When to Use This Skill
- How do I receive Breezy HR webhooks?
- How do I verify the Breezy HR
X-Hook-Signatureheader? - Why is my Breezy HR webhook signature verification failing?
- How do I handle
candidateAddedorcandidateStatusUpdatedevents? - Do I strip the
whsec_prefix from the Breezy HR secret? (No.)
Signature Verification (core)
| Item | Value |
|---|---|
| Header | X-Hook-Signature (also sent: X-Breezy-Webhook-Version) |
| Algorithm | HMAC-SHA256, hex digest, no prefix |
| Signed content | The raw request body |
| Key | The whole whsec_... secret as a UTF-8 string |
The secret only looks like a Standard Webhooks / Svix secret. Do not use the standardwebhooks or svix libraries, and do not strip or base64-decode the whsec_ prefix.
const crypto = require('crypto');
function verifyBreezySignature(rawBody, signature, secret) {
if (!signature) return false;
const expected = crypto
.createHmac('sha256', secret) // whole whsec_ string, UTF-8
.update(rawBody) // raw bytes, not JSON.stringify(req.body)
.digest('hex');
const a = Buffer.from(signature.trim().toLowerCase());
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Breezy's own code samples re-serialize the parsed JSON, but their troubleshooting section says the signature covers the raw body. Use the raw body: with Express that means express.raw({ type: '*/*' }), so verification works whatever Content-Type arrives.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
Every payload is an envelope: { "_.
Event (type) | Triggered When |
|---|---|
candidateAdded | A candidate enters the system (applied, sourced, recruited, referred) |
candidateStatusUpdated | A candidate moves to a different pipeline stage |
candidateDeleted | A candidate is deleted in the web app |
companyPositionAdded | A position is created |
companyPositionUpdated | Position details change |
companyPositionDeleted | A position is deleted |
companyPositionStateUpdated | Position state changes (draft, published, closed...) |
companyNotePosted | A team discussion note is posted |
companyNoteUpdated | A team discussion note is edited |
companyNoteDeleted | A team discussion note is deleted |
Deduplicate on the envelope _id; Breezy can occasionally deliver an event more than once. Respond with a 2xx within 30 seconds; failures are retried up to 10 times with exponential backoff.
Environment Variables
BREEZY_HR_WEBHOOK_SECRET=whsec_your_secret # returned once when you create the endpoint
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 breezy-hr --path /webhooks/breezy-hr
Breezy only accepts public HTTPS URLs (no localhost), so use the tunnel URL when creating the webhook endpoint.
Reference Materials
- references/overview.md - Breezy HR webhook concepts and events
- references/setup.md - Create an endpoint and get the secret
- references/verification.md - Signature details and gotchas