Agent skill
Vitable Health Webhooks Skill
Receive and verify Vitable Health webhooks. Use when setting up Vitable Health embedded-benefits webhook handlers, debugging X-Vitable-Signature (HMAC-SHA512) verification failures, or handling enrollment.accepted, enrollment.terminated, employee.eligibility_granted, and employee.deduction_created events.
Install this skill
npx skills add hookdeck/webhook-skills --skill vitable-health-webhooks
When to Use This Skill
- How do I receive Vitable Health webhooks?
- How do I verify the
X-Vitable-Signatureheader (HMAC-SHA512)? - Why does my Vitable webhook signature verification fail?
- How do I handle
enrollment.accepted,enrollment.terminated,employee.eligibility_, orgranted employee.deduction_events?created - How do I deduplicate Vitable webhook deliveries on
event_id?
Essential Code (USE THIS)
Vitable signs every delivery with HMAC-SHA512 using the subscription's secret key (Vitable's team provides it on request; each webhook subscription has its own).
| Header | Description |
|---|---|
X-Vitable-Signature | sha512=<128 hex characters> |
X-Vitable-Timestamp | <unix_timestamp> (Vitable's sample code compares it with the current time in seconds) |
Signed content is <X-Vitable-Timestamp value>.<raw body bytes>. The key is the secret's UTF-8 bytes, used as-is (not hex- or base64-decoded). Verify against the raw body bytes: never JSON.parse/JSON.stringify or decode and re-encode the body first.
Verification (Node.js)
const crypto = require('crypto');
// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyVitable(rawBody, headers, secret) {
const signature = headers['x-vitable-signature'];
const timestamp = headers['x-vitable-timestamp'];
if (!secret || !signature || !timestamp) return false;
// Accept the value with or without the sha512= prefix; hex is case-insensitive
const hex = (signature.startsWith('sha512=') ? signature.slice(7) : signature).toLowerCase();
if (!/^[0-9a-f]{128}$/.test(hex)) return false;
const expected = crypto.createHmac('sha512', secret)
.update(Buffer.from(timestamp, 'latin1')).update('.').update(rawBody).digest();
const given = Buffer.from(hex, 'hex'); // safe: format validated above
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
Verification (Python)
import hashlib, hmac, re
def verify_vitable(raw_body: bytes, headers, secret: str) -> bool:
signature = headers.get("x-vitable-signature")
timestamp = headers.get("x-vitable-timestamp")
if not (secret and signature and timestamp):
return False
digest = signature.removeprefix("sha512=").lower()
if not re.fullmatch(r"[0-9a-f]{128}", digest):
return False
message = timestamp.encode("latin-1") + b"." + raw_body
expected = hmac.new(secret.encode(), message, hashlib.sha512).digest()
return hmac.compare_digest(bytes.fromhex(digest), expected)
Respond 401 when the signature or timestamp header is missing or empty, malformed, or doesn't match.
Timestamp window (opt-in)
Vitable's own examples reject timestamps more than 300 seconds old. Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck applies no tolerance itself, so a fixed window rejects legitimate retries. The examples therefore leave the check off unless VITABLE_ is set to a positive number (unset or 0 = off). See references/verification.md.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
Events are thin: {event_. Dispatch on event_name, then fetch the resource from the Vitable API with resource_id. Deliveries are at-least-once, so deduplicate on event_id.
| Event | Description |
|---|---|
enrollment.started | An enrollment has started |
enrollment.elected | An employee has elected a benefit |
enrollment.accepted | An enrollment has been accepted |
enrollment.granted | An enrollment has been granted |
enrollment.waived | An employee has waived a benefit |
enrollment.terminated | An enrollment has been terminated |
employee.eligibility_ | An employee has been granted benefits eligibility |
employee.eligibility_ | An employee's benefits eligibility has been terminated |
employee.deactivated | An employee has been deactivated |
employee.deduction_ | A new payroll deduction has been generated for an employee |
See references/overview.md and the events reference.
Environment Variables
VITABLE_WEBHOOK_SECRET=your_subscription_secret_key # provided by Vitable's team, used as UTF-8
VITABLE_WEBHOOK_TOLERANCE_SECONDS=0 # optional; a positive value opts in to the replay window
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 vitable-health --path /webhooks/vitable-health
Reference Materials
- references/overview.md - Vitable webhook concepts, payload, retries, events
- references/setup.md - Getting a subscription and secret key
- references/verification.md - Signature verification details, gotchas, timestamp window