Webhooks
Hint uses webhooks to let consumers of our API (such as yourself) know about important events in Hints system. This section includes general information about webhooks, as well as specific details on how Hint sends out events to your webhooks.
What are Webhooks?
Webhooks solve a very specific problem. Consumers of any API often need to know about events that either A.) are not the direct result of a request, or B.) happen asynchronously. In these cases, the consumer could periodically poll the API for changes, but this is wasteful, and also slow, as information is only ever as up-to-date as the frequency of polling.
Webhooks are the modern solution. You give us a url to hit on your servers, and we'll post a request to that url as soon as some important event happens (eg. a charge failing, or patient being created). Our POST request to your route will include the event name, and the "data" for that event. (eg. "charge.failed", and the charge itself, respectively).
Giving Us Your Webhook URL
Your webhook URL (like other attributes of your Partner) can be set via our API, using your partner API key. You should have received this key in the email when we created your sandbox account.
Hint supports 2 levels of webhook endpoints: global and integration specific. Global endpoints will receive all events across all integrations. Integration specific endpoints will only receive events for the specified integration. If you use a single endpoint for all your customers you will likely want to use a global endpoint. If you send events for each customer to a separate server you will likely want to use an integration specific webhook endpoint.
Receiving Events On Your End
This is just like creating any old route on your server. It might look like this:
require "json"
# Using Sinatra
post "/my/webhook/url" do
# Retrieve the request's body and parse it as JSON
event_json = JSON.parse(request.body.read)
# Do something with event_json
status 200
end
// Using Express
app.post("/my/webhook/url", function(request, response) {
// Retrieve the request's body and parse it as JSON
var event_json = JSON.parse(request.body);
// Do something with event_json
response.send(200);
});
import json
from django.http import HttpResponse
# Using Django
def my_webhook_view(request):
# Retrieve the request's body and parse it as JSON
event_json = json.load(request.body)
# Do something with event_json
return HttpResponse(status=200)
; using compojure
(defn handle-webhook [{event :body}]
(do-things-with-event event)
{:status 200})
(defroutes app-routes
(POST "/my/webhooks" [] handle-webhook)
// Retrieve the request's body and parse it as JSON
$input = @file_get_contents("php://input");
$event_json = json_decode($input);
// Do something with $event_json
http_response_code(200); // PHP 5.4 or greater
Once that's in place, and you've given us your Webhook URL, then you'll immediately start being notified of Hint events.
Responding to Events
Assuming you are receiving the events as planned, please respond with a 2XX status so that we know you received it, and don't wastefully retry them.
Retrying Events
If we do not receive a 2XX status response, we will continue retrying the event every hour for 72 hours.
Receiving events behind a CSRF-protected server
If you're using Rails, Django, or another web framework, your server may automatically check that POST request bodies it receives contain a CSRF token. This is an important security feature that helps protect you and your users from cross-site request forgery. However, it may also prevent your server from receiving legitimate webhooks. You may need to exempt from CSRF protection the Rails route or Django view you use to receive webhooks, like so...
class WebhookController < ApplicationController
# If your controller accepts requests other than Hint webhooks,
# you'll probably want to use `protect_from_forgery` to add CSRF
# protection for your application. But don't forget to exempt
# your webhook route!
protect_from_forgery :except => :webhook
def webhook
# Process webhook data in `params`
end
end
import json
# Webhooks are always sent as HTTP POST requests, so we want to ensure
# that only POST requests will reach your webhook view. We can do that by
# decorating `webhook()` with `require_POST`.
#
# Then to ensure that the webhook view can receive webhooks, we need
# also need to decorate `webhook()` with `csrf_exempt`.
@require_POST
@csrf_exempt
def webhook(request):
# Process webhook data in `request.body`
What Do Events Look Like?
Hint's events that get POST'ed to your webhook URL will all have the following attributes:
{
id: "evt-jKi2jlalOJk3",
created_at: "2015-02-19T10:49:21.419-08:00",
type: "patient.created",
practice_id: "pra-6BIBTelN3rM5",
object: {
"id": "pat-eN9DgCQ9iQxL",
"created_at": "2015-02-19T10:49:21.419-08:00",
"updated_at": "2015-02-20T08:37:21.419-08:00",
"first_name": "Stefani",
"last_name": "Germanotta",
"name": "Stefani Germanotta"
// ... rest of Patient JSON
}
}
What Events Get Sent Out?
Hint will POST created
, updated
, and destroyed
and other object specific types like inactive
events to your webhook URL for the following objects...
patient
, membership
, practitioner
, company
, employee_division
, customer_invoice
, and signup_attempt
.
For example, whenever a patient is created, updated, has a membership end, or is destroyed, you will receive a patient.created
, patient.updated
, patient.inactive
or patient.archived
event, respectively. The same is true of the other objects listed above. If you want to process all events only for specific objects then you should process all types that begin with that object name such as patient.
.
Also, if the Practice deactivates the integration, Hint sends a special webhook called integration.deactivated
. It's context is the integration object, which is identical to the Integration object listed in the API.
If you would like HintOS to only send webhooks for specific objects or events, email us at [email protected] and we configure those event types for you.
View a list of Events using the api
You can view a list of events, or find information about a specific event if you have the event id. See the WebhookRequest reference docs to try it out.
Validating Webhook Authenticity
Hint signs each webhook, so that you can (optionally) validate that it is in fact Hint that sent you the webhook, and that the payload has not been tampered with. Here's how it works:
Hint takes your Partner API key and the webhook payload and sends it through a sha256 hash. We put this hash in the Webhook in the headers as X-Hint-Signature
. Here are rough examples of how you could verify these signatures:
def verify_signature(payload_body)
actual_signature = request.headers['X-Hint-Signature']
algorithm_used = actual_signature.split("=").first
expected_signature = "#{algorithm_used}=" + OpenSSL::HMAC.hexdigest(
OpenSSL::Digest.new(algorithm_used),
ENV['HINT_PARTNER_API_KEY'],
payload_body,
)
unless Rack::Utils.secure_compare(expected_signature, actual_signature)
fail 500, "Signatures didn't match!"
end
end
import hashlib, hmac
# Note: expected_signature is always the value of the 'X-Hint-Signature' header.
def verify(api_key, request_body_string, expected_signature):
hmac_digest = "sha256=" + hmac.new(key=api_key,
request_body_string,
digestmod=hashlib.sha256).hexdigest()
return hmac.compare_digest(unicode(expected_signature), unicode(hmac_digest))
Notes for validating webhooks
- Be sure to use the stringified JSON the request body in your hash params.
- For now, the algorithm will always be "sha256". But the verification method above is written to be compatible with algorithm changes should sha256 become insecure in coming years.
- Using a plain
==
operator is not advised. A method likesecure_compare
performs a "constant time" string comparison, which renders it safe from certain timing attacks against regular equality operators.
Updated 5 months ago