Webhooks

Learn how to receive events from Rupa using webhooks.


Webhooks allows your application to be notified when interesting events happen in connected practitioner accounts in Rupa. A webhook is an endpoint on your server that receives requests containing these events from Rupa.

When an interesting event occurs, we create a new Event object object. The state of that resource at the time of the change is embedded in the event's data field. For example, when a new result for an order comes in, we create an order.new_result event containing an Order.

If an event affects your application, a POST request will be made to your endpoint with a body comprised of the Event object. Your endpoint needs to return a 2xx status code. If doesn't, we'll retry the webhook for up to three days with an exponential back off.

For the list of possible event types, see Types of events.

Overview

To start receiving webhook events, follow these steps:

  1. Create a webhook endpoint. You'll create an endpoint on your server that will receives events from Rupa.
  2. Verify the signature. Events received should be verified for their authenticity.
  3. Handle the event. Then you'll process the event and update your system accordingly.
  4. Send test event. Finally, you can send a test event to verify it works correctly.

1. Create an endpoint

Add a new endpoint to your server and make sure it’s publicly accessible so we can send unauthenticated POST requests. Send the URL of this endpoint to a member of the Rupa team so we can configure it on your OAuth application.

Here's an example of an event payload for an Order awaiting payment from the patient.

{
  "created_at": "2021-01-01T00:00:00-08:00",
  "data": {
    "object": {
      "attributes": {
        "date_canceled": null,
        "date_completed": null,
        "date_paid": null,
        "date_submitted": null,
        "notes_to_patient": null,
        "payer": "patient",
        "status": "Pending Payment",
        "total_price": 0
      },
      "id": "ord_e3lMmlN",
      "relationships": {
        "line_items": { "data": [], "meta": { "count": 0 } },
        "ordered_tests": {
          "data": [{ "id": "ordts_DOdwXaW", "type": "OrderedTest" }],
          "meta": { "count": 1 }
        },
        "patient": { "data": { "id": "pat_arR5Nk7", "type": "Patient" } },
        "patients_practitioner": {
          "data": { "id": "pra_AEj0dj6", "type": "Practitioner" }
        },
        "signing_practitioner": {
          "data": { "id": "pra_AEj0dj6", "type": "Practitioner" }
        }
      },
      "type": "order"
    },
    "included": [
      {
        "attributes": {
          "cost": 10000,
          "description": "",
          "title": "A Lab Test",
          "type": "Lab Test"
        },
        "id": "ordlneitm_86ahf7",
        "relationships": { "ordered_test": { "data": null } },
        "type": "order_line_item"
      },
      {
        "attributes": {
          "date_results_received_from_lab": null,
          "requisition": null,
          "results_hl7": null,
          "results_pdf": "https://s3...com/..."
        },
        "id": "ordtst_a4h888a",
        "relationships": {
          "lab_test": {
            "data": { "id": "lbtst_88fh1ka", "type": "lab_test" }
          }
        },
        "type": "ordered_test"
      },
      {
        "attributes": {
          "address": {
            "street_1": "123 Fake St",
            "street_2": null,
            "city": "Fake Town",
            "state": "Fake",
            "zip": "00000"
          },
          "first_name": "Katherine",
          "last_name": "Johnson",
          "ordering_authorization_status": {
            "physician_authorization_approved": false
          },
          "phone": "+5551234567",
          "primary_practitioner_type": {
            "name": "Dr"
          },
          "titled_full_name": "Dr. Katherine Johnson",
          "verification_status": { "email": true }
        },
        "id": "prac_afv8112",
        "relationships": {
          "clinic": { "data": { "id": "clnc_12mf781", "type": "clinic" } }
        },
        "type": "practitioner"
      }
    ]
  },
  "id": "evt_0gBg5Oa",
  "type": "order.new_result"
}

2. Verify the signature

Rupa signs the webhook events it sends to your endpoint by including a signature in each event’s Rupa-Signature header. This allows you to verify that the events were sent by Rupa, not by a third party.

Before you can verify signatures, you'll need your OAuth application's WEBHOOK_SIGNING_SECRET you received from a Rupa team member.

The Rupa-Signature header included in each signed event contains a timestamp and a signature. The timestamp is prefixed by t=, and the signature is prefixed by v1=.

Rupa-Signature:
t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Rupa generates signatures using a hash-based message authentication code (HMAC) with SHA-256.

To verify the signature in the payload, follow these steps:

Step 1. Extract the timestamp and signature from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

The value for the prefix t corresponds to the timestamp, and v1 corresponds to the signature.

Step 2: Prepare the signed_payload string

The signed_payload string is created by concatenating:

  • The timestamp (as a string)
  • The character .
  • The actual JSON payload (i.e. the request body)

Step 3: Determine the expected signature

Compute an HMAC with the SHA256 hash function. Use your application's client secret as the key, and use the signed_payload string as the message.

Step 4: Compare the signatures

Compare the signature in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use a constant-time string comparison to compare the expected signature to the received signature.

Example

Let's run through an example using Python. Suppose:

  1. We have an application with a client_secret equal to
0zpeyOEn4rA7MCupRuNo3WEzbk0S4G5XVcClU6sSyIrPphueNRusJ9wppZTnVLEjlQohFrEWmXGQfvALH0Pp57CboqydmaBQdGI5saBYZEabdvTrYpkbrQad2MbNt46O
  1. We receive a request with the following body: '{"test": "data"}'.
  2. The Rupa-Signature header is equal to: t=1625785323,v1=496c0d8436d7401542b343462d2c0c00cea0fe64770bcbecb354995c3a0258f2.

So to compute the expected signature, we:

  1. Extract the timestamp: 1625785323.
  2. Prepare the signed_payload: '1625785323.{"test": "data"}'.
  3. Compute the HMAC:
import hashlib
import hmac

signature = hmac.new(
  key=client_secret.encode("utf-8"),
  msg=signed_payload.encode("utf-8"),
  digestmod=hashlib.sha256
).hexdigest()
  1. This gives us 496c0d8436d7401542b343462d2c0c00cea0fe64770bcbecb354995c3a0258f2, which is equal to the value sent in Rupa-Signature and so therefore is a valid request.

3. Handle the event

After verifying the request, read the Event object from the request body.

After you've read the event object and processed it accordingly, you'll need to return a successful 200 response. If doesn't, we'll retry the webhook for up to three days with an exponential back off.

4. Test the webhook

With your webhook created, you should test it by Triggering an event.

Best practices

Consider these best practices when building your integration.

Handle unknown event types

We might add new event types at any point, so your integration should handle only those you're interested and ignore the rest. You still return as 200 response to any received event.

Handle duplicate events

Typically we send a single request for any Event created in Rupa, but to ensure you're integration handles duplicates events, you should build it with idempotency in mind. Every Event contains a unique id that can be used to keep track of previously processed events.

Next steps

Next, check out Results Syncing to learn how to sync results to your application.