Agent skill

Factorial Webhooks Skill

Receive and authenticate Factorial HR webhooks. Use when setting up a Factorial webhook subscription and endpoint, verifying the x-factorial-wh-challenge header, debugging 401s, or handling subscription types like employees/employee/create_with_contract, timeoff/leave/create and ats/application/create.

Install this skill

npx skills add hookdeck/webhook-skills --skill factorial-webhooks


Factorial (HR software) sends a JSON webhook to your HTTPS endpoint when something happens: an employee is created, a leave request is approved, a shift is clocked in. Subscriptions are created through the API, one subscription_type per subscription. Webhooks are not signed.

Set a challenge on the webhook subscription. Without one, Factorial sends nothing to verify.

The optional challenge is a secret you define. Factorial returns it unchanged on every delivery in the x-factorial-wh-challenge header, and your endpoint authenticates the request by comparing the whole header value with the challenge you stored.

When to Use This Skill

  • How do I receive Factorial webhooks?
  • How do I verify the x-factorial-wh-challenge header?
  • Does Factorial sign webhook payloads? (No: use the subscription challenge)
  • How do I handle timeoff/leave/create or employees/employee/create_with_contract deliveries?
  • Why does my Factorial webhook endpoint return 401, or why do I receive no header at all?

Verification (core)

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyFactorialRequest(headers, challenge = process.env.FACTORIAL_WH_CHALLENGE) {
  if (!challenge) return false;                         // never accept when unconfigured
  const received = headers['x-factorial-wh-challenge']; // whole value, no prefix
  return Boolean(received) && safeEqual(received, challenge);
}
  • Header: x-factorial-wh-challenge: <challenge> (name is case-insensitive). Sent only if the subscription has a challenge.
  • Python: hmac.compare_digest(a.encode(), b.encode()) on bytes; never pass str (raises on non-ASCII).
  • Return 401 on a missing header or mismatch, 400 for a body that isn't a JSON object, 200 once handled.
  • Read the body raw (express.raw({ type: '*/*' })) and authenticate before parsing it.

A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS (Factorial requires it) and rotate the challenge if it leaks.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Subscription Types

The body is the affected resource's JSON with no event field: the subscription decides the type, in domain/resource/action form. Use one target_url per subscription (the examples add a ?type= hint) so your handler knows which resource it received.

Subscription typeSent When
employees/employee/create_with_contractAn employee is created together with a contract
employees/employee/updateAn employee record is modified
employees/employee/terminateAn employee is marked as terminated
contracts/contract_version/createA contract version is created
timeoff/leave/createA leave request is submitted
timeoff/leave/approveA leave request is approved
timeoff/leave/rejectA leave request is rejected
attendance/shift/clock_inAn employee clocks in a shift
ats/application/createA job application is created
ats/candidate/createA candidate is created

The full list is in the Webhooks section of Factorial's API reference.

Environment Variables

FACTORIAL_WH_CHALLENGE=your-subscription-challenge   # the `challenge` you set on the subscription

Local Development

Factorial can't reach localhost and only delivers over HTTPS. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 factorial --path /webhooks/factorial

Use the printed URL as the subscription's target_url.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →