Gareth Wilson Gareth Wilson

Guide to Vitable Health Webhooks: Features and Best Practices

Published


Vitable Health webhooks tell your platform what's happening with an employer's benefits: an employee becomes eligible, starts enrolment, elects or waives a benefit, has a payroll deduction generated, or is deactivated. Vitable provides embedded health benefits, and webhooks let partners such as payroll and HR platforms keep their own records in step without polling the API.

This guide covers how Vitable webhooks are registered and signed, the events and their order, how Vitable retries and trips its circuit breaker, and the practices that keep a Vitable receiver reliable.

What are Vitable Health webhooks?

You register a webhook endpoint URL with Vitable, and Vitable sends a POST to it when an event occurs. The payload is deliberately thin: it names the event and the affected resource but doesn't include the resource's data. You call the matching API endpoint with resource_id to get the current state.

Every request carries X-Vitable-Signature in the form sha512=<hex>. The hex is an HMAC-SHA512 of the X-Vitable-Timestamp header, a ., and the raw body, keyed with the subscription's secret key. Vitable issues the secret on request, and each subscription has its own.

Vitable Health webhook features

FeatureDetails
ConfigurationRegister the endpoint URL with Vitable
MethodPOST with a JSON body
Signature headerX-Vitable-Signature, sha512=<hex>
Timestamp headerX-Vitable-Timestamp, a Unix timestamp
SchemeHMAC-SHA512
Signed content<timestamp>.<raw body>
SecretOne per webhook subscription, from Vitable's team on request
PayloadEvent metadata and a resource_id, with no resource snapshot
Event IDevent_id, prefixed wevt_
DeliveryAt least once
RetriesUp to 14 attempts, exponential backoff with jitter, about 3.4 days in total
Circuit breakerOpens after 25 consecutive failed deliveries; Vitable re-enables it
Missed eventsQueryable through the Vitable API for a time range
TransportHTTPS required in production

Common events

Events are named {resource}.{action}. Vitable lists ten:

EventDescription
enrollment.startedAn enrollment has started
enrollment.electedAn employee has elected a benefit
enrollment.acceptedAn enrollment has been accepted
enrollment.waivedAn employee has waived a benefit
enrollment.grantedAn enrollment has been granted
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

Vitable's event flow shows how they connect. A census sync produces employee.eligibility_granted for each employee. An enrolment then sends enrollment.started, followed by enrollment.elected and enrollment.accepted, or by enrollment.waived if the employee declines. employee.deduction_created arrives for each enrolled employee on the 1st of each month. When an employee is terminated, expect employee.eligibility_terminated, enrollment.terminated for each enrolment, and employee.deactivated.

Every event uses the same payload:

{
  "event_id": "wevt_550e8400-e29b-41d4-a716-446655440000",
  "organization_id": "org_xyz789",
  "event_name": "enrollment.accepted",
  "resource_type": "enrollment",
  "resource_id": "enrl_7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "created_at": "2026-01-23T14:30:00+00:00"
}

Route on event_name, use organization_id to find the employer when you manage several, and deduplicate on event_id.

Setting up Vitable Health webhooks

  1. Email Vitable's team at dev@vitablehealth.com to register your endpoint URL and get the subscription's secret key.
  2. Store the secret server-side, for example as VITABLE_WEBHOOK_SECRET. Each subscription has its own secret, so keep one per endpoint.
  3. Make sure the endpoint uses HTTPS, which Vitable requires in production.

With Hookdeck. Vitable Health is a supported source type in Hookdeck. Create a Hookdeck source of the Vitable Health type, paste the subscription's secret key, and give Vitable the Hookdeck source URL as your endpoint. The source accepts POST only and checks the hex after sha512= in X-Vitable-Signature against <X-Vitable-Timestamp>.<raw body>. A request that fails the check still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

Securing Vitable Health webhooks

Join the X-Vitable-Timestamp header, a literal ., and the raw body bytes. Compute an HMAC-SHA512 with the secret as a UTF-8 string, then compare it in constant time with the hex that follows sha512= in X-Vitable-Signature. An SHA-512 digest is 128 hex characters.

const crypto = require('crypto');
const express = require('express');

function verifyVitableSignature(rawBody, header, timestamp, secret) {
  if (!header || !timestamp || !secret) return false;
  const match = /^sha512=([0-9a-f]{128})$/i.exec(header); // validate before decoding
  if (!match) return false;

  const signed = Buffer.concat([Buffer.from(`${timestamp}.`, 'latin1'), rawBody]);
  const expected = crypto.createHmac('sha512', secret).update(signed).digest();
  const received = Buffer.from(match[1], 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

// Optional replay check, off by default (0). Hookdeck retries and replays resend the original timestamp.
const TOLERANCE_SECONDS = Number(process.env.VITABLE_WEBHOOK_TOLERANCE_SECONDS || 0);

const app = express();

app.post('/webhooks/vitable', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.VITABLE_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  const timestamp = req.get('x-vitable-timestamp');
  if (!verifyVitableSignature(req.body, req.get('x-vitable-signature'), timestamp, secret)) {
    return res.sendStatus(401);
  }
  if (TOLERANCE_SECONDS > 0 && Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.event_id, route on event.event_name, then fetch event.resource_id asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import re

SIGNATURE = re.compile(r"sha512=([0-9a-fA-F]{128})")


def verify_vitable_signature(raw_body: bytes, header, timestamp, secret: str) -> bool:
    if not header or not timestamp or not secret:
        return False
    match = SIGNATURE.fullmatch(header)
    if not match:
        return False
    signed = timestamp.encode("latin-1") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha512).digest()
    return hmac.compare_digest(bytes.fromhex(match.group(1)), expected)

Vitable's own Python and Node samples reject any request whose timestamp is more than 300 seconds old. Behind Hookdeck, that window would reject retries and replays, which resend the original headers, while Hookdeck itself applies no timestamp tolerance. The samples above leave the check off by default. If you turn it on, choose a window longer than the retry and replay periods you rely on. Vitable's Node sample also calls crypto.timingSafeEqual on two strings without checking their lengths first, so a malformed header throws an error instead of returning false. The length check above avoids that.

Make Vitable Health webhooks production-ready. Hookdeck Event Gateway verifies the X-Vitable-Signature header, accepts every enrolment and deduction event while your service is down, and keeps your endpoint clear of Vitable's circuit breaker.

Vitable Health webhook limitations and pain points

A tripped circuit needs Vitable to reset it

The Problem: After a long outage, events stop arriving, and you can't turn them back on yourself.

Why It Happens: Vitable opens the circuit after 25 consecutive failed deliveries, counting deliveries rather than individual attempts. While it's open, new events aren't delivered, and you have to contact Vitable to re-enable the endpoint.

Workarounds:

  • Alert on failed deliveries in your own logs well before 25 in a row.
  • After Vitable re-enables the endpoint, query the API for the events you missed.

How Hookdeck Can Help: Hookdeck keeps answering Vitable while your service is down, so the circuit doesn't open, and you can replay the stored events once your service recovers.

Thin payloads mean an API call per event

The Problem: Every event needs a follow-up request before you can act on it, and monthly deductions arrive for every enrolled employee at once.

Why It Happens: Vitable leaves the resource data out of the payload on purpose, so you always fetch the current state with resource_id.

Workarounds:

  • Acknowledge first, then fetch the resource from a background job.
  • Throttle those fetches so a burst of employee.deduction_created events doesn't overwhelm the API or your workers.

How Hookdeck Can Help: Hookdeck can rate limit delivery to your service, so a monthly burst arrives at a pace your workers and API quota can handle.

Duplicates are expected

The Problem: The same event is processed twice, for example creating two deduction records.

Why It Happens: Vitable guarantees at-least-once delivery and says your endpoint may receive duplicate events.

Workarounds:

  • Record each event_id you've handled and skip repeats.
  • Make writes idempotent by keying them on the resource's ID.

How Hookdeck Can Help: Hookdeck can deduplicate requests on event_id before they reach your service.

Setup goes through Vitable's team

The Problem: You can't create, change or test a subscription on your own.

Why It Happens: Vitable's docs describe registering the endpoint with Vitable and getting each secret by email. They don't document a self-serve API or dashboard for subscriptions.

Workarounds:

  • Agree the endpoint URL before you contact Vitable, and keep it stable.
  • Use a single stable URL that you can route internally, so changing services doesn't need a new subscription.

How Hookdeck Can Help: Give Vitable a Hookdeck source URL once, then change where events go by editing the Hookdeck connection, without asking Vitable to update anything.

Best practices

Verify against the raw body

The signature covers the body exactly as sent. Use express.raw() in Express, await request.arrayBuffer() in a Next.js route handler, and await request.body() in FastAPI, and parse JSON only after the check.

Use HMAC-SHA512, not SHA-256

Vitable signs with SHA-512, and the header value starts with sha512=. Code adapted from a SHA-256 provider will compute the wrong digest and reject every request.

Deduplicate on event_id

Vitable recommends event_id for idempotency. See our guide to webhook idempotency.

Fetch fresh data

Call the API with resource_id after each event instead of relying on cached data. Events can arrive more than once and retries can be days late, so the API's current state is the one to trust.

Acknowledge fast and process asynchronously

Verify, store the event, return 200, then fetch and process. See why to process webhooks asynchronously.

Conclusion

Vitable Health signs every webhook with X-Vitable-Signature, sha512= followed by a hex HMAC-SHA512 of <timestamp>.<raw body>. Get the secret from Vitable's team, verify against the raw bytes, think twice before copying the five-minute window from Vitable's samples, deduplicate on event_id, and fetch each resource by resource_id.

Hookdeck Event Gateway sits in front of your receiver, checks every Vitable signature, and holds each event until your service has processed it.

Get started with Hookdeck for free and handle Vitable Health webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.