Skip to content

Commit 028e707

Browse files
authored
Add section on reproducing X-InboxHealth-Signature
1 parent a23c0f4 commit 028e707

File tree

1 file changed

+119
-1
lines changed

1 file changed

+119
-1
lines changed

README.md

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,127 @@ This section covers how to define new BillingCycleTemplates, control existing Bi
280280

281281
## Subscribing to Webhooks
282282
This section covers how to receive dynamic updates from Inbox Health via REST API webhooks that ensure API clients are immediately informed upon changes of relevant records in Inbox Health. Whether the change originates from a patient, provider, administrator or automated system these webhooks are triggered and immediately provide feedback
283+
284+
## X-InboxHealth-Signature Header
285+
This section will explain the steps required to reproduce the header X-InboxHealth-Signature which is used to verify that all webhook event requests coming from Inbox Health are authentic and haven’t been tampered with. The X-InboxHealth-Signature header is generated using the HMAC-SHA1 hashing algorithm, with the base signature being generated from the request URL and the POST body parameters, using your secret API key as the signing key.
286+
287+
The webhook event we will be demonstrating with will be a `patient_created` event with dummy JSON data to represent what a typical event would actually look like. The raw request looks like this
288+
289+
```
290+
POST https://coolcompany.com/api/v1/webhooks
291+
Content-Type: application/json
292+
Host: coolcompany.com
293+
Body: {"id":4805,"livemode":false,"created_at":"2019-08-29T11:00:26.000-04:00","event_type":"patient_created","event_data":{"object":{"id":1,"user_id":null,"enterprise_id":1,"first_name":"Ed","middle_name":"E","last_name":"Edison","created_at":"2019-08-24T23:08:14.691Z","updated_at":"2019-08-25T10:06:48.507Z","address_line_1":"123 Mansion Rd","address_line_2":null,"city":"Thimbleweed Park","state":"WA","cell_phone":"(555) 555-5555","secondary_phone":"(555) 555-5555","date_of_birth":"1987-10-05","primary":true,"ssn":"801-31-3292","sex":"Male","removed":false,"email":"[email protected]","zip":"06510","email_two":null,"health_record_id":"3425","notes":null,"balance_cents":8940,"soft_delete":0,"patient_plan_ids":[1],"last_notified_at":"2014-08-30T19:00:00.000Z","race":null,"language":"English","ethnicity":null,"marital_status":"Single","employment_status":null,"employer_name":null,"contact_preference":"both","occupation":null,"pharmacy_name":null,"pharmacy_address_line_1":null,"pharmacy_address_line_2":null,"pharmacy_city":null,"pharmacy_state":null,"pharmacy_phone_number":null,"maiden_name":null,"home_phone":null,"work_phone":"(555) 555-5555","practice_id":13,"sms_opt_out":false,"phone_opt_out":false,"emergency_contacts":[],"external_ids":[{"id":1,"issuer":"coolpm","external_id":"3425"}],"statement_cycle_started_at":null,"collection_warning_letter_date":null,"needs_card_update":false,"corrected_address_line_1":null,"corrected_address_line_2":null,"corrected_city":null,"corrected_state":null,"corrected_zip":null,"links":{"invoices":null,"payments":null,"notifications":null,"patient_payment_infos":null,"tickets":null},"object":"patient"}}}
294+
```
295+
296+
#### Collecting the Request URL
297+
298+
All webhook events sent by InboxHealth will be via a POST HTTP Method. The exact URL of the request is used as part of the base signature. So in our current example, the URL used would be https://coolcompany.com/api/v1/webhooks
299+
300+
#### Collecting Parameters
301+
302+
The second part of creating the base signature is collecting the request parameters, which are sorted and concatenated into a normalized string using the following rules:
303+
304+
URL encode every key and value
305+
Parameters are sorted by name, using lexicographical byte value ordering. If two or more parameters share the same name, they are sorted by their value
306+
Parameters are concatenated in their sorted order into a single string. For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61), even if the value is empty. Each name-value pair is separated by an '&' character (ASCII code 38)
307+
308+
Given our example POST body above, these parameters would be normalized into the following single string
309+
310+
```
311+
created_at=2019-08-29T12%3A06%3A53.000-04%3A00&event_data%5Bobject%5D%5Baddress_line_1%5D=123%20Mansion%20Rd&event_data%5Bobject%5D%5Baddress_line_2%5D=&event_data%5Bobject%5D%5Bbalance_cents%5D=0&event_data%5Bobject%5D%5Bcell_phone%5D=%28555%29%20555-5555&event_data%5Bobject%5D%5Bcity%5D=Thimbleweed%20Park&event_data%5Bobject%5D%5Bcollection_warning_letter_date%5D=&event_data%5Bobject%5D%5Bcontact_preference%5D=both&event_data%5Bobject%5D%5Bcorrected_address_line_1%5D=&event_data%5Bobject%5D%5Bcorrected_address_line_2%5D=&event_data%5Bobject%5D%5Bcorrected_city%5D=&event_data%5Bobject%5D%5Bcorrected_state%5D=&event_data%5Bobject%5D%5Bcorrected_zip%5D=&event_data%5Bobject%5D%5Bcreated_at%5D=2019-08-24T23%3A08%3A14.691Z&event_data%5Bobject%5D%5Bdate_of_birth%5D=1987-10-07&event_data%5Bobject%5D%5Bemail%5D=weirded%40edisons.com&event_data%5Bobject%5D%5Bemail_two%5D=&event_data%5Bobject%5D%5Bemployer_name%5D=&event_data%5Bobject%5D%5Bemployment_status%5D=&event_data%5Bobject%5D%5Benterprise_id%5D=10&event_data%5Bobject%5D%5Bethnicity%5D=&event_data%5Bobject%5D%5Bfirst_name%5D=Ed&event_data%5Bobject%5D%5Bhealth_record_id%5D=12345&event_data%5Bobject%5D%5Bhome_phone%5D=&event_data%5Bobject%5D%5Bid%5D=669&event_data%5Bobject%5D%5Blanguage%5D=English&event_data%5Bobject%5D%5Blast_name%5D=Edison&event_data%5Bobject%5D%5Blast_notified_at%5D=&event_data%5Bobject%5D%5Blinks%5D%5Binvoices%5D=&event_data%5Bobject%5D%5Blinks%5D%5Bnotifications%5D=&event_data%5Bobject%5D%5Blinks%5D%5Bpatient_payment_infos%5D=&event_data%5Bobject%5D%5Blinks%5D%5Bpayments%5D=&event_data%5Bobject%5D%5Blinks%5D%5Btickets%5D=&event_data%5Bobject%5D%5Bmaiden_name%5D=&event_data%5Bobject%5D%5Bmarital_status%5D=Single&event_data%5Bobject%5D%5Bmiddle_name%5D=E&event_data%5Bobject%5D%5Bneeds_card_update%5D=false&event_data%5Bobject%5D%5Bnotes%5D=&event_data%5Bobject%5D%5Bobject%5D=patient&event_data%5Bobject%5D%5Boccupation%5D=&event_data%5Bobject%5D%5Bpharmacy_address_line_1%5D=&event_data%5Bobject%5D%5Bpharmacy_address_line_2%5D=&event_data%5Bobject%5D%5Bpharmacy_city%5D=&event_data%5Bobject%5D%5Bpharmacy_name%5D=&event_data%5Bobject%5D%5Bpharmacy_phone_number%5D=&event_data%5Bobject%5D%5Bpharmacy_state%5D=&event_data%5Bobject%5D%5Bphone_opt_out%5D=false&event_data%5Bobject%5D%5Bpractice_id%5D=13&event_data%5Bobject%5D%5Bprimary%5D=true&event_data%5Bobject%5D%5Brace%5D=&event_data%5Bobject%5D%5Bremoved%5D=false&event_data%5Bobject%5D%5Bsecondary_phone%5D=%28555%29%20555-5555&event_data%5Bobject%5D%5Bsex%5D=Male&event_data%5Bobject%5D%5Bsms_opt_out%5D=false&event_data%5Bobject%5D%5Bsoft_delete%5D=0&event_data%5Bobject%5D%5Bssn%5D=801-31-3292&event_data%5Bobject%5D%5Bstate%5D=WA&event_data%5Bobject%5D%5Bstatement_cycle_started_at%5D=&event_data%5Bobject%5D%5Bupdated_at%5D=2019-08-25T10%3A06%3A48.507Z&event_data%5Bobject%5D%5Buser_id%5D=&event_data%5Bobject%5D%5Bwork_phone%5D=%28555%29%20555-5555&event_data%5Bobject%5D%5Bzip%5D=06510&event_type=patient_created&id=4806&livemode=false
312+
```
313+
314+
For readability purposes, the normalized parameters split into an array would be as follows
315+
316+
```
317+
[0] = "created_at=2019-08-29T12%3A06%3A53.000-04%3A00"
318+
[1] = "event_data%5Bobject%5D%5Baddress_line_1%5D=123%20Mansion%20Rd"
319+
[2] = "event_data%5Bobject%5D%5Baddress_line_2%5D="
320+
[3] = "event_data%5Bobject%5D%5Bbalance_cents%5D=0"
321+
[4] = "event_data%5Bobject%5D%5Bcell_phone%5D=%28555%29%20555-5555"
322+
[5] = "event_data%5Bobject%5D%5Bcity%5D=Thimbleweed%20Park"
323+
[6] = "event_data%5Bobject%5D%5Bcollection_warning_letter_date%5D="
324+
[7] = "event_data%5Bobject%5D%5Bcontact_preference%5D=both"
325+
[8] = "event_data%5Bobject%5D%5Bcorrected_address_line_1%5D="
326+
[9] = "event_data%5Bobject%5D%5Bcorrected_address_line_2%5D="
327+
[10] = "event_data%5Bobject%5D%5Bcorrected_city%5D="
328+
[11] = "event_data%5Bobject%5D%5Bcorrected_state%5D="
329+
[12] = "event_data%5Bobject%5D%5Bcorrected_zip%5D="
330+
[13] = "event_data%5Bobject%5D%5Bcreated_at%5D=2019-08-24T23%3A08%3A14.691Z"
331+
[14] = "event_data%5Bobject%5D%5Bdate_of_birth%5D=1987-10-07"
332+
[15] = "event_data%5Bobject%5D%5Bemail%5D=weirded%40edisons.com"
333+
[16] = "event_data%5Bobject%5D%5Bemail_two%5D="
334+
[17] = "event_data%5Bobject%5D%5Bemployer_name%5D="
335+
[18] = "event_data%5Bobject%5D%5Bemployment_status%5D="
336+
[19] = "event_data%5Bobject%5D%5Benterprise_id%5D=10"
337+
[20] = "event_data%5Bobject%5D%5Bethnicity%5D="
338+
[21] = "event_data%5Bobject%5D%5Bfirst_name%5D=Ed"
339+
[22] = "event_data%5Bobject%5D%5Bhealth_record_id%5D=12345"
340+
[23] = "event_data%5Bobject%5D%5Bhome_phone%5D="
341+
[24] = "event_data%5Bobject%5D%5Bid%5D=669"
342+
[25] = "event_data%5Bobject%5D%5Blanguage%5D=English"
343+
[26] = "event_data%5Bobject%5D%5Blast_name%5D=Edison"
344+
[27] = "event_data%5Bobject%5D%5Blast_notified_at%5D="
345+
[28] = "event_data%5Bobject%5D%5Blinks%5D%5Binvoices%5D="
346+
[29] = "event_data%5Bobject%5D%5Blinks%5D%5Bnotifications%5D="
347+
[30] = "event_data%5Bobject%5D%5Blinks%5D%5Bpatient_payment_infos%5D="
348+
[31] = "event_data%5Bobject%5D%5Blinks%5D%5Bpayments%5D="
349+
[32] = "event_data%5Bobject%5D%5Blinks%5D%5Btickets%5D="
350+
[33] = "event_data%5Bobject%5D%5Bmaiden_name%5D="
351+
[34] = "event_data%5Bobject%5D%5Bmarital_status%5D=Single"
352+
[35] = "event_data%5Bobject%5D%5Bmiddle_name%5D=E"
353+
[36] = "event_data%5Bobject%5D%5Bneeds_card_update%5D=false"
354+
[37] = "event_data%5Bobject%5D%5Bnotes%5D="
355+
[38] = "event_data%5Bobject%5D%5Bobject%5D=patient"
356+
[39] = "event_data%5Bobject%5D%5Boccupation%5D="
357+
[40] = "event_data%5Bobject%5D%5Bpharmacy_address_line_1%5D="
358+
[41] = "event_data%5Bobject%5D%5Bpharmacy_address_line_2%5D="
359+
[42] = "event_data%5Bobject%5D%5Bpharmacy_city%5D="
360+
[43] = "event_data%5Bobject%5D%5Bpharmacy_name%5D="
361+
[44] = "event_data%5Bobject%5D%5Bpharmacy_phone_number%5D="
362+
[45] = "event_data%5Bobject%5D%5Bpharmacy_state%5D="
363+
[46] = "event_data%5Bobject%5D%5Bphone_opt_out%5D=false"
364+
[47] = "event_data%5Bobject%5D%5Bpractice_id%5D=13"
365+
[48] = "event_data%5Bobject%5D%5Bprimary%5D=true"
366+
[49] = "event_data%5Bobject%5D%5Brace%5D="
367+
[50] = "event_data%5Bobject%5D%5Bremoved%5D=false"
368+
[51] = "event_data%5Bobject%5D%5Bsecondary_phone%5D=%28555%29%20555-5555"
369+
[52] = "event_data%5Bobject%5D%5Bsex%5D=Male"
370+
[53] = "event_data%5Bobject%5D%5Bsms_opt_out%5D=false"
371+
[54] = "event_data%5Bobject%5D%5Bsoft_delete%5D=0"
372+
[55] = "event_data%5Bobject%5D%5Bssn%5D=801-31-3292"
373+
[56] = "event_data%5Bobject%5D%5Bstate%5D=WA"
374+
[57] = "event_data%5Bobject%5D%5Bstatement_cycle_started_at%5D="
375+
[58] = "event_data%5Bobject%5D%5Bupdated_at%5D=2019-08-25T10%3A06%3A48.507Z"
376+
[59] = "event_data%5Bobject%5D%5Buser_id%5D="
377+
[60] = "event_data%5Bobject%5D%5Bwork_phone%5D=%28555%29%20555-5555"
378+
[61] = "event_data%5Bobject%5D%5Bzip%5D=06510"
379+
[62] = "event_type=patient_created"
380+
[63] = "id=4806"
381+
[64] = "livemode=false"
382+
```
383+
384+
Since we based our normalization rules off of the OAuth specification, we leverage the OAuth Ruby library for normalizing the parameters. The method can be viewed on GitHub at https://github.com/oauth-xx/oauth-ruby/blob/master/lib/oauth/helper.rb#L44 for further education on the normalizing process.
385+
386+
#### The Signing Key
387+
388+
Your secret Inbox Health user API key is used as the signing key to the HMAC-SHA1 hashing algorithm. We determine which API key to sign with _based on the user that created, or last updated, the webhook endpoint in the Inbox Health Partner app._ As always, keep this key secret and safe!
389+
390+
#### Calculating the Signature
391+
392+
We can now calculate the final signature by passing the base string of our request URL and normalized request parameters to the HMAC-SHA1 hashing algorithm, using the user API key as the signing key. Please note, HMAC will output in binary string, so we convert it to base64.
393+
394+
Given the following base signature
395+
396+
`https://coolcompany.com/api/v1/webhookscreated_at=2019-08-29T12%3A06%3A53.000-04%3A00&event_data%5Bobject%5D%5Baddress_line_1%5D=123%20Mansion%20Rd&event_data%5Bobject%5D%5Baddress_line_2%5D=&event_data%5Bobject%5D%5Bbalance_cents%5D=0&event_data%5Bobject%5D%5Bcell_phone%5D=%28555%29%20555-5555&event_data%5Bobject%5D%5Bcity%5D=Thimbleweed%20Park&event_data%5Bobject%5D%5Bcollection_warning_letter_date%5D=&event_data%5Bobject%5D%5Bcontact_preference%5D=both&event_data%5Bobject%5D%5Bcorrected_address_line_1%5D=&event_data%5Bobject%5D%5Bcorrected_address_line_2%5D=&event_data%5Bobject%5D%5Bcorrected_city%5D=&event_data%5Bobject%5D%5Bcorrected_state%5D=&event_data%5Bobject%5D%5Bcorrected_zip%5D=&event_data%5Bobject%5D%5Bcreated_at%5D=2019-08-24T23%3A08%3A14.691Z&event_data%5Bobject%5D%5Bdate_of_birth%5D=1987-10-07&event_data%5Bobject%5D%5Bemail%5D=weirded%40edisons.com&event_data%5Bobject%5D%5Bemail_two%5D=&event_data%5Bobject%5D%5Bemployer_name%5D=&event_data%5Bobject%5D%5Bemployment_status%5D=&event_data%5Bobject%5D%5Benterprise_id%5D=10&event_data%5Bobject%5D%5Bethnicity%5D=&event_data%5Bobject%5D%5Bfirst_name%5D=Ed&event_data%5Bobject%5D%5Bhealth_record_id%5D=12345&event_data%5Bobject%5D%5Bhome_phone%5D=&event_data%5Bobject%5D%5Bid%5D=669&event_data%5Bobject%5D%5Blanguage%5D=English&event_data%5Bobject%5D%5Blast_name%5D=Edison&event_data%5Bobject%5D%5Blast_notified_at%5D=&event_data%5Bobject%5D%5Blinks%5D%5Binvoices%5D=&event_data%5Bobject%5D%5Blinks%5D%5Bnotifications%5D=&event_data%5Bobject%5D%5Blinks%5D%5Bpatient_payment_infos%5D=&event_data%5Bobject%5D%5Blinks%5D%5Bpayments%5D=&event_data%5Bobject%5D%5Blinks%5D%5Btickets%5D=&event_data%5Bobject%5D%5Bmaiden_name%5D=&event_data%5Bobject%5D%5Bmarital_status%5D=Single&event_data%5Bobject%5D%5Bmiddle_name%5D=E&event_data%5Bobject%5D%5Bneeds_card_update%5D=false&event_data%5Bobject%5D%5Bnotes%5D=&event_data%5Bobject%5D%5Bobject%5D=patient&event_data%5Bobject%5D%5Boccupation%5D=&event_data%5Bobject%5D%5Bpharmacy_address_line_1%5D=&event_data%5Bobject%5D%5Bpharmacy_address_line_2%5D=&event_data%5Bobject%5D%5Bpharmacy_city%5D=&event_data%5Bobject%5D%5Bpharmacy_name%5D=&event_data%5Bobject%5D%5Bpharmacy_phone_number%5D=&event_data%5Bobject%5D%5Bpharmacy_state%5D=&event_data%5Bobject%5D%5Bphone_opt_out%5D=false&event_data%5Bobject%5D%5Bpractice_id%5D=13&event_data%5Bobject%5D%5Bprimary%5D=true&event_data%5Bobject%5D%5Brace%5D=&event_data%5Bobject%5D%5Bremoved%5D=false&event_data%5Bobject%5D%5Bsecondary_phone%5D=%28555%29%20555-5555&event_data%5Bobject%5D%5Bsex%5D=Male&event_data%5Bobject%5D%5Bsms_opt_out%5D=false&event_data%5Bobject%5D%5Bsoft_delete%5D=0&event_data%5Bobject%5D%5Bssn%5D=801-31-3292&event_data%5Bobject%5D%5Bstate%5D=WA&event_data%5Bobject%5D%5Bstatement_cycle_started_at%5D=&event_data%5Bobject%5D%5Bupdated_at%5D=2019-08-25T10%3A06%3A48.507Z&event_data%5Bobject%5D%5Buser_id%5D=&event_data%5Bobject%5D%5Bwork_phone%5D=%28555%29%20555-5555&event_data%5Bobject%5D%5Bzip%5D=06510&event_type=patient_created&id=4806&livemode=false`
397+
398+
and using an example API key of `api_key` as our signing key, the X-InboxHealth-Signature would be
399+
400+
`93G+w7p0GC2FB+us2KO8lT/XfZM=`
283401

284402
## FAQs
285403
Please don't hesitate to ask questions via our email, Slack or GitHub Issues. We'll update this section with common questions as we work to flesh out our documentation.
286404

287405
## Forthcoming Documentation
288-
We're currently working on more documentation, but between these initial examples and the swagger docs I hope you'll be able to get a decent start the feel of the API itself. Don't hesitate to contact us by email, GitHub Issues, or Slack for quicker responses.
406+
We're currently working on more documentation, but between these initial examples and the swagger docs I hope you'll be able to get a decent feel of the API itself. Don't hesitate to contact us by email, GitHub Issues, or Slack for quicker responses.

0 commit comments

Comments
 (0)