Event delivery, payload format, retries, signature validation
Hint uses webhooks to notify your application about important events in real time. Instead of polling for changes, register an endpoint and receive HTTP POST requests as events occur.
flowchart LR
HintEvent["Hint event<br/>(patient.created, etc.)"] --> Queue[Webhook queue]
Queue -->|Signed POST| PartnerEndpoint[Your webhook URL]
PartnerEndpoint -->|2XX ack| HintAck[Delivery success]
PartnerEndpoint -->|Non-2XX| Retry["Retry (backoff)"]
Retry --> Queue
classDef node fill:#ffffff,stroke:#16a34a,stroke-width:1px,color:#0f172a,font-size:12px
class HintEvent,Queue,PartnerEndpoint,HintAck,Retry node
Endpoint Configuration
Configure your webhook URL using your partner API key. Hint supports two endpoint types:
| Endpoint Type | Scope | Use Case |
|---|---|---|
| Global endpoint | All integrations | Single endpoint receives events for every connected practice |
| Integration endpoint | Single integration | Separate endpoint per practice |
Event Payload
All webhook events are sent as HTTP POST requests with JSON bodies:
{
"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"
}
}The object field contains the full resource representation - the same structure returned by the corresponding GET endpoint. For example, a patient.created event includes the same patient object you'd receive from GET /api/provider/patients/{id}.
| Field | Meaning |
|---|---|
id | Unique per event. Retries of the same delivery resend the same id, and every endpoint that receives the event gets the same id. Use it as your de-duplication key. Test deliveries, and events Hint support redelivers for you, use evt-test. |
created_at | When the event happened. |
type | The event type. See Supported Events. |
object | The resource as it was when Hint first attempted delivery, not when the event happened. Retries resend that same snapshot. |
Because the snapshot is taken at delivery time, the object can already reflect later changes. For example, a customer_invoice.issued event can arrive with "status": "paid" if the invoice was paid before delivery. Treat the event type as the trigger and the object (or a fresh GET) as the current state.
Events Hint does not send
- Unchanged objects. If an event's
objectis identical, ignoringupdated_at, to the last one Hint delivered to the same endpoint for the same record and event type, Hint skips it. For.updatedevents, the last.createddelivery counts too. - Your own updates. When your integration updates a resource through that resource's endpoint (for example,
PATCH /api/provider/patients/{id}), Hint does not send the resultingpatient.updatedevent back to you. Other events the same request causes, such as status events likecustomer_invoice.paid, are still sent. To receive your own.updatedevents, setunmute_self_webhookswith Update Backend.
Supported Events
Hint can send the following webhook events:
| Object | Events |
|---|---|
patient | patient.created, patient.updated, patient.destroyed, patient.inactive |
membership | membership.created, membership.updated, membership.destroyed |
practitioner | practitioner.created, practitioner.updated, practitioner.destroyed |
company | company.created, company.updated, company.destroyed |
employee_division | employee_division.created, employee_division.updated, employee_division.destroyed |
customer_invoice | customer_invoice.created, customer_invoice.updated, customer_invoice.destroyed, customer_invoice.draft, customer_invoice.issued, customer_invoice.paid, customer_invoice.cancelled |
invoice | invoice.created, invoice.updated, invoice.destroyed, invoice.draft, invoice.issued, invoice.paid, invoice.cancelled |
signup_attempt | signup_attempt.created, signup_attempt.updated |
installation | installation.activated, installation.deactivated |
integration.activatedandintegration.deactivatedwere retired in this API versionThey named the integration rather than the practice install, so a deactivation only reached you once the last install on the integration had come down. Handle
installation.activatedandinstallation.deactivatedinstead - you get one per practice install, and the payload names the practice. Partners pinned to an earlier API version continue to receive theintegrationevents.
Configuring webhook eventsTo add or remove specific events from being sent, log into the Partner Portal:
Delivery & Retries
Respond with a 2XX status code to acknowledge receipt. Any other status code, a timeout, or a connection error counts as a failure and triggers a retry.
| Behavior | Details |
|---|---|
| Delivery guarantee | At least once. The same event can arrive more than once. |
| Ordering | Not guaranteed. Each event is delivered independently, and .updated events are held for a few seconds before sending. |
| Retry schedule | A failed delivery is retried once an hour, up to 72 attempts in total. After the last attempt the delivery is marked failed. |
| Muting | Every failed attempt counts toward your integration's failure total, and any successful delivery resets it. When the total passes 100, the integration is muted. |
| While muted | Hint attempts at most one delivery per hour. Other events in that period are dropped, not queued. |
| Recovery | The first successful delivery unmutes the integration. |
| Failure alerts | An email to your technical contact when the integration is muted, and a daily email when any delivery reaches failed in the last 24 hours. |
Muting applies to the integration, meaning one practice's connection, not to the endpoint URL. With a global endpoint, a single failing practice does not mute the others.
flowchart LR
Active[Delivering] -->|Over 100 failed attempts| Muted[Muted]
Muted -->|At most 1 attempt per hour| Attempt{Attempt succeeds?}
Attempt -->|Yes| Active
Attempt -->|No| Muted
classDef node fill:#ffffff,stroke:#db2777,stroke-width:1px,color:#0f172a,font-size:12px
class Active,Muted,Attempt node
Design your handler for duplicates and gaps
- Return
2XXquickly, then process asynchronously. Slow responses count as failures and cause retries of events you already received. - De-duplicate on the event
id. - Do not rely on arrival order. Compare
object.updated_at, or GET the resource, before overwriting newer data. - Treat a webhook as a prompt to re-read, not as proof of final state. For financial records, read the resource from the API before acting.
Recover missed events
Hint does not replay dropped or failed events automatically after recovery, because a large backlog of out-of-date events could overwrite newer data.
- Review deliveries. List Webhook Requests returns each delivery with its status and stored payload. Practices with API Access can also see the request log under Admin > Developers.
- Replay. Contact partner support to have events redelivered. Redelivered events arrive with
id: "evt-test"and the resource's current state.
Security
Webhooks originate from Hint's servers, not browsers - there's no user session or cookies involved. CSRF protection doesn't apply and will reject every webhook if left enabled. Instead, authenticate webhooks by validating the X-Hint-Signature header.
Bypass CSRF, Validate Signatures
Every delivery carries X-Hint-Signature: sha256=<digest>. The digest is the lowercase hex HMAC-SHA256 of the raw request body, keyed with your webhooks signature key. The signature key is one of your partner API keys, by default the one created with your partner account. You can switch it to a different partner key, which lets you rotate it without changing the key you use for requests.
| You are | Where to find the webhooks signature key |
|---|---|
| A partner | Partner Portal > Webhooks > Settings > Webhooks Signature Key. The key's token is also returned as webhooks_signature_api_key.token by Show Backend. |
| A practice with API Access | Admin > Developers > Webhook Endpoints > Webhooks Signature Key. Live and sandbox have separate keys. |
Store it as a secret (for example HINT_WEBHOOK_SECRET), and keep separate values for sandbox and live.
Rotate the signature key
Rotation takes effect immediately, with no overlap period. Hint signs each delivery attempt when it sends it, so retries of earlier events are also signed with the new key.
- Make sure your handler returns a non-2XX status when a signature does not verify. Hint then retries those deliveries.
- Regenerate the key (practices), or select a new key (partners).
- Update your stored secret right away.
Deliveries rejected between steps 2 and 3 are retried an hour later and verify with the new key. Each rejection counts toward muting, so keep the gap short.
Exempt your webhook endpoint from CSRF checks and verify the HMAC signature:
class WebhookController < ApplicationController
protect_from_forgery except: :webhook
def webhook
verify_signature!(request.raw_post)
# Process webhook...
end
private
def verify_signature!(payload)
signature = request.headers['X-Hint-Signature']
algorithm, digest = signature.split('=')
expected = OpenSSL::HMAC.hexdigest(algorithm, ENV['HINT_WEBHOOK_SECRET'], payload)
head :forbidden unless Rack::Utils.secure_compare(expected, digest)
end
endimport hashlib, hmac
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.http import HttpResponse, HttpResponseForbidden
@require_POST
@csrf_exempt
def webhook(request):
if not verify_signature(request.body, request.headers.get('X-Hint-Signature')):
return HttpResponseForbidden()
# Process webhook...
return HttpResponse(status=200)
def verify_signature(body, signature):
expected = "sha256=" + hmac.new(
key=HINT_WEBHOOK_SECRET.encode(),
msg=body,
digestmod=hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Signature validation tips
- Use constant-time comparison (
secure_compare,hmac.compare_digest) to prevent timing attacks- Hash the raw request body, not a re-serialized object
Further Reading
- WebhookRequest - View webhook event history via API
- Global Webhook Endpoint - Configure partner-level endpoints
- Integration Webhook Endpoint - Configure per-integration endpoints
- Best Practices - Integration patterns and sync status tracking

