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-Signature header (HMAC-SHA512)?
  • Why does my Vitable webhook signature verification fail?
  • How do I handle enrollment.accepted, enrollment.terminated, employee.eligibility_granted, or employee.deduction_created events?
  • 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).

HeaderDescription
X-Vitable-Signaturesha512=<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_WEBHOOK_TOLERANCE_SECONDS 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_id, organization_id, event_name, resource_type, resource_id, created_at}. 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.

EventDescription
enrollment.startedAn enrollment has started
enrollment.electedAn employee has elected a benefit
enrollment.acceptedAn enrollment has been accepted
enrollment.grantedAn enrollment has been granted
enrollment.waivedAn employee has waived a benefit
enrollment.terminatedAn enrollment has been terminated
employee.eligibility_grantedAn employee has been granted benefits eligibility
employee.eligibility_terminatedAn employee's benefits eligibility has been terminated
employee.deactivatedAn employee has been deactivated
employee.deduction_createdA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →