Guide to Hint Health Webhooks: Features and Best Practices
Hint Health runs the membership side of direct primary care: patient records, monthly memberships, employer plans and the invoices that go with them. Partner applications connect to many Hint practices at once, and practices with API Access build their own integrations. Hint's webhooks keep those systems in step by sending a JSON event whenever a patient, membership, practitioner, company or invoice changes.
This guide covers choosing between global and integration endpoints, Hint's event snapshot model, verifying X-Hint-Signature, and the failure count that can get an integration muted.
What are Hint Health webhooks?
A partner configures webhooks with its partner API key and picks one of two endpoint types. A global endpoint receives events from every connected practice, while an integration endpoint receives events for a single practice's connection. A practice with API Access registers its own endpoints in Hint's admin area instead.
Each event carries the resource in full, as a GET would return it, but the snapshot is taken when Hint first tries to deliver the event, not when the change happened. Hint also leaves some events out: it skips an event whose object hasn't changed since the last delivery to that endpoint, and it doesn't send .updated events triggered by your own API writes unless you set unmute_self_webhooks.
Hint Health webhook features
| Feature | Details |
|---|---|
| Signature | hex HMAC-SHA256 |
| Signature header | x-hint-signature |
| Body | JSON |
| Event routing | Body field type |
Hint's webhook reference also specifies:
- A 5-second connect timeout and a 20-second response timeout.
- Hourly retries, up to 72 attempts in total.
- Muting when an integration's failure count passes 100. Any success resets the count.
- Separate signature keys for live and sandbox.
- Order is not guaranteed, and
.updatedevents are held for a few seconds before sending.
Events and payloads
| Event routing | Body field type |
Hint documents 44 event types in total. Beyond those above, there are company.*, employee_division.*, signup_attempt.*, installation.activated and installation.deactivated, and invoice events, customer_invoice.* and invoice.*, that include issued, paid and cancelled. GET /partner/webhook_ returns the current list. The older integration.activated and integration.deactivated were retired in favour of the per-practice installation.* events.
This is the patient.created sample from the webhook samples library:
{
"id": "evt-jKi2jlalOJk3",
"created_at": "2015-02-19T10:49:21.419-08:00",
"type": "patient.created",
"practice_id": "pra-6BIBTelN3rM5",
"object": {
"id": "pat-eN9DgCQ9iQxL",
"first_name": "Jane",
"last_name": "Doe"
}
}
id is the event ID. It stays the same on every retry and for every endpoint that receives the event, which makes it the natural deduplication key. Test deliveries and events that Hint support redelivers use evt-test instead, so give those their own handling. practice_id tells a global endpoint which practice the event belongs to.
Because object is a snapshot from the first delivery attempt, it can be ahead of the event: a customer_invoice.issued event can arrive with "status": "paid". Use type as the trigger and the object, or a fresh GET, as the state.
Setting up Hint Health webhooks
- Decide between a global endpoint and integration endpoints, and register the URL with your partner API key. A practice with API Access uses Admin > Developers > Webhook Endpoints.
- Choose the events to send in the Partner Portal's webhooks page. Sandbox and production have separate portals.
- Find the Webhooks Signature Key: for partners under Partner Portal > Webhooks > Settings, and for practices under Admin > Developers > Webhook Endpoints. It's one of your partner API keys, by default the one created with your partner account. Store it as
HINT_WEBHOOK_SECRET, with separate values for sandbox and live.
With Hookdeck, create a source of the Hint Health type, give it the signature key as its verification secret, and register the source URL as the endpoint.
For local testing, forward the source to your machine:
npx hookdeck-cli listen 3000 hint-health --path /webhooks/hint-health
Securing Hint Health webhooks
X-Hint-Signature is sha256= followed by the lowercase hex HMAC-SHA256 of the raw body, keyed with the signature key as UTF-8 text. Strip the prefix, compute the HMAC over the received bytes, and compare in constant time.
Node.js verification
const crypto = require('node:crypto');
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['x-hint-signature'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const message = rawBody;
const candidates = [signature.startsWith('sha256=') ? signature.slice(7) : signature];
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
const received = Buffer.from(candidate, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The handler records the event against its id and returns within Hint's 20-second limit:
const express = require('express');
const app = express();
app.post('/webhooks/hint-health', express.raw({ type: '*/*' }), async (req, res) => {
const signatureKey = process.env.HINT_WEBHOOK_SECRET;
if (!signatureKey) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, signatureKey)) return res.sendStatus(401);
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!event || typeof event !== 'object' || Array.isArray(event)) return res.sendStatus(400);
try {
const isTest = event.id === 'evt-test';
await saveHintEvent({
eventId: isTest ? null : event.id,
type: event.type,
practiceId: event.practice_id,
objectId: event.object?.id,
snapshot: event.object,
isTest,
});
} catch {
return res.sendStatus(503);
}
res.sendStatus(200);
});
saveHintEvent stores real events with a unique constraint on eventId and keeps evt-test deliveries separate. A worker applies each snapshot only if its updated_at is newer than what you hold, since Hint doesn't guarantee order.
Hint asks receivers to return a non-2xx for a bad signature so that it retries. During a key rotation, deliveries rejected in the gap are retried an hour later with the new key. Hookdeck's Hint Health source answers 200 and records a mismatch as failed verification, so Hint will not retry those. Update the source's secret at the moment you rotate. Hookdeck applies no timestamp check, and Hint signs none.
Make Hint Health webhooks production-ready. Hookdeck Event Gateway acknowledges Hint within its 20-second window and keeps every patient, membership and invoice event while your service recovers, so failures never build towards muting.
Hint Health webhook limitations and pain points
Muting drops events
The Problem: After a long outage, Hint stops sending most events, and those events are not queued.
Why It Happens: Every failed attempt counts towards the integration's failure total. Past 100, the integration is muted: Hint makes at most one delivery attempt an hour and drops everything else in that period until one attempt succeeds.
Workarounds:
- Watch for Hint's muting email to your technical contact and its daily failed-delivery email.
- After unmuting, use List Webhook Requests to see what failed, and reconcile changed records through the API.
How Hookdeck Can Help: Hookdeck acknowledges every delivery, so the failure count stays at zero, and events wait in Hookdeck with retries to your service until it is back.
Snapshots can be ahead of the event
The Problem: An event's object already shows a later state than its type suggests.
Why It Happens: Hint captures the object at the first delivery attempt, and retries resend that same snapshot.
Workarounds:
- Use
typeto decide what happened, andobjector a freshGETto decide the current state. - Compare
object.updated_atwith your stored copy before overwriting.
How Hookdeck Can Help: Hookdeck stores each event exactly as Hint sent it, which makes it easy to see how snapshots and arrival order compared when debugging a sync issue.
Key rotation has no overlap
The Problem: Deliveries fail verification between rotating the key and deploying it.
Why It Happens: Rotation takes effect immediately. Hint signs each attempt at send time, so even retries of older events use the new key.
Workarounds:
- Deploy the new key as soon as you rotate, and return a non-
2xxon signature failure so Hint retries an hour later. - Keep the gap short, because each rejection counts towards muting.
How Hookdeck Can Help: With Hookdeck in front, the key is set on one source rather than on every service that consumes Hint events.
Your own writes are silent
The Problem: A change your integration makes through the API doesn't produce the .updated event other systems might expect.
Why It Happens: Hint doesn't echo .updated events caused by your own API calls to that resource's endpoint, though related status events such as customer_invoice.paid still arrive.
Workarounds:
- Update your own records directly when you write to Hint.
- Set
unmute_self_webhookswith Update Backend if you want those events.
How Hookdeck Can Help: Hookdeck can fan one Hint event out to several destinations, so services that need every update get the same stream.
Best practices
Answer inside 20 seconds
Hint gives up at 20 seconds even if your handler later succeeds, and counts that attempt as a failure. Store the event and reply, then process it, as explained in why to process webhooks asynchronously.
Deduplicate on the event id
The same id arrives on retries and at every endpoint. Store it uniquely, following the pattern in our guide to webhook idempotency, and treat evt-test separately.
Keep sandbox and live apart
Use separate secrets, endpoints and storage for sandbox and live, so a test practice never updates production records.
Protect patient data
Patient objects contain personal health information. Encrypt stored events, limit access, and keep payloads out of logs.
Conclusion
Hint Health webhooks send full snapshots of patients, memberships, invoices and other records, signed with an HMAC-SHA256 of the body in X-Hint-Signature. Verify the raw bytes, reply within 20 seconds, deduplicate on id, and apply snapshots by updated_at. Above all, keep failures low, because an integration that passes 100 failed attempts is muted and loses events.
Hookdeck Event Gateway keeps Hint deliveries succeeding and holds each event until your systems are ready.
Get started with Hookdeck for free and handle Hint Health webhooks reliably in minutes.