Webhooks

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 TypeScopeUse Case
Global endpointAll integrationsSingle endpoint receives events for every connected practice
Integration endpointSingle integrationSeparate 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}.

FieldMeaning
idUnique 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_atWhen the event happened.
typeThe event type. See Supported Events.
objectThe 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 object is identical, ignoring updated_at, to the last one Hint delivered to the same endpoint for the same record and event type, Hint skips it. For .updated events, the last .created delivery 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 resulting patient.updated event back to you. Other events the same request causes, such as status events like customer_invoice.paid, are still sent. To receive your own .updated events, set unmute_self_webhooks with Update Backend.

Supported Events

Hint can send the following webhook events:

ObjectEvents
patientpatient.created, patient.updated, patient.destroyed, patient.inactive
membershipmembership.created, membership.updated, membership.destroyed
practitionerpractitioner.created, practitioner.updated, practitioner.destroyed
companycompany.created, company.updated, company.destroyed
employee_divisionemployee_division.created, employee_division.updated, employee_division.destroyed
customer_invoicecustomer_invoice.created, customer_invoice.updated, customer_invoice.destroyed, customer_invoice.draft, customer_invoice.issued, customer_invoice.paid, customer_invoice.cancelled
invoiceinvoice.created, invoice.updated, invoice.destroyed, invoice.draft, invoice.issued, invoice.paid, invoice.cancelled
signup_attemptsignup_attempt.created, signup_attempt.updated
installationinstallation.activated, installation.deactivated
🚧

integration.activated and integration.deactivated were retired in this API version

They 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.activated and installation.deactivated instead - you get one per practice install, and the payload names the practice. Partners pinned to an earlier API version continue to receive the integration events.

📘

Configuring webhook events

To 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.

BehaviorDetails
Delivery guaranteeAt least once. The same event can arrive more than once.
OrderingNot guaranteed. Each event is delivered independently, and .updated events are held for a few seconds before sending.
Retry scheduleA failed delivery is retried once an hour, up to 72 attempts in total. After the last attempt the delivery is marked failed.
MutingEvery 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 mutedHint attempts at most one delivery per hour. Other events in that period are dropped, not queued.
RecoveryThe first successful delivery unmutes the integration.
Failure alertsAn 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

  1. Return 2XX quickly, then process asynchronously. Slow responses count as failures and cause retries of events you already received.
  2. De-duplicate on the event id.
  3. Do not rely on arrival order. Compare object.updated_at, or GET the resource, before overwriting newer data.
  4. 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 areWhere to find the webhooks signature key
A partnerPartner 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 AccessAdmin > 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.

  1. Make sure your handler returns a non-2XX status when a signature does not verify. Hint then retries those deliveries.
  2. Regenerate the key (practices), or select a new key (partners).
  3. 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
end
import 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