Developing Webhooks

📘

What's a webhook, anyway?

Whereas the typical usage of the Routable API allows your application to make outbound calls to trigger actions such as making a payment when you need to. However, what happens when a vendor accepts a payment or your accountant creates a payable on the Routable Dashboard? Your application might need to know about these changes as well. We don't want to constantly poll for changes, as this would trigger rate limiting and significantly impact your application's performance.

Webhooks are scripts that you create and host on your site, that allow your application to receive incoming calls informing your application of changes made to your data from other sources. Routable will notify your webhook script whenever a change happens to your data, whether the change occurred in a vendor workflow, the Routable Dashboard, or in your application(s).

Webhook Events

We support one webhook per account and can call your webhook for the following events:

Event ResourceEvent NameEvent Description
companycompany.createdA Company was created
companycompany.invitation_acceptedA Company accepted an invitation to onboard
companycompany.invitedA Company was invited to onboard
payablepayable.approval_changedA Payable received an approval
payablepayable.createdA new Payable was created
payablepayable.creation_failedThe creation of a Payable failed during processing - for example, due to a failure to sync with your accounting software.
payablepayable.refreshed_from_ledgerThe data for a Payable has been synced from your accounting software
payablepayable.status_changedThe status of a Payable has changed
payment_methodpayment_method.createdA PaymentMethod has been added to a Company. The detail object will contain the Company's company_id.
receivablereceivable.approval_changedA Receivable received an approval
receivablereceivable.createdA new Receivable was created
receivablereceivable.creation_failedThe creation of a Receivable failed during processing - for example, due to a failure to sync with your accounting software.
receivablereceivable.refreshed_from_ledgerThe data for a Receivable has been synced from your accounting software
receivablereceivable.status_changedThe status of a Receivable has changed
tax_formtax_form.createdA TaxForm has been added to a Company. The detail object will contain the Company's company_id.

Deprecated Webhook Events

The following webhook events are deprecated and should be replaced in listener implementations with the corresponding payable and receivable webhook events:

Event ResourceEvent NameEvent Description
itemitem.approval_changeA Payable or Receivable received an approval
itemitem.createA new Payable or Receivable was created
itemitem.refreshed_from_ledgerThe data for a Payable or Receivable has been synced from your accounting software
itemitem.status_changeThe status of a Payable or Receivable has changed

📘

What's an "item"?

Items refer to all payments made through Routable, whether they are an outgoing payment (Payable) or an incoming payment (Receivable).

Payload Structure

You will receive a request with the following JSON payload structure as the body.

{
  "company_id":"53e47d2e-a82c-4dca-9cf2-45af6040bc6c",
  "event_name":"payable.created",
  "event_resource":"payable",
  "object_id":"f116a4bb-ea1e-4578-ba82-af22c435b108",
  "detail": null
}
KeyValue Description
company_idThe company the resource belongs to. Should always match your company ID.
event_nameThe name of the event (e.g. payable.status_changed)
event_resourceThe resource of the event (e.g. payable)
object_idThe ID of the resource (for example, the ID of the payable, which can be queried from /v1/payables/{id})
detailA JSON object present in some webhook events to provide additional information about the event. Its structure will vary depending on which webhook event is triggered.

To obtain more details about the resource that was modified, make a follow-up API call. For example, if the event is related to a Payable, a call to Retrieve a Payable with the ID found in object_id will return the correct resource.

In addition to the request body, the incoming request will also have two relevant headers: Routable-Signature-Timestamp, which is a timestamp in GMT, and Routable-Signature, which is a hash of the request body encrypted with your webhook secret token. Both headers will be used in your webhook validation algorithm.

📘

Retrieving Past Webhook Events

If you missed some webhook events and need to get more information about them, the List Webhook Events endpoint provides this functionality.

Requirements

Your endpoint needs to do the following:

  • Use HTTPS for a secure connection
  • Respond within 2 seconds with a 200 OK status code and no body or cookies in the response
  • If the webhook does not pass our signature verification or other required validation, return a 401 Unauthorized status code and no body or cookies.

🚧

Preventing timeout errors

It is strongly recommended that your webhook code itself not perform any processing of the webhook request's data. Ideally, the webhook code would place the incoming request into a queue such as RabbitMQ or Redis for later, asynchronous processing by a separate script.

If you use a serverless platform such as AWS Lambda to host your webhooks, these systems have a "spin-up time" when they get their first new request after a pause. This can occasionally cause webhooks to take more than two seconds to return and cause paused webhooks. Contact Routable's Developer Experience team if you need a hand handling this use case.

Signature Verification

The Routable-Signature header is generated with HMAC-SHA256 on an input, encrypted using the secret token you provided during your webhook setup as a key. Make sure you store this key in a secure, non-public location on your server, and do not commit it to source control! We use this signature to validate that the request came from Routable and is suitable to be acted upon by your application.

  1. Create the input string by concatenating the value of Routable-Signature-Timestamp, a period (.), and the webhook request payload (the JSON body).

📘

Important

Use the raw JSON body you receive in your input string, as some programming languages use different semantics in JSON rendering that can cause your signature validation not to match.

  1. Compute the HMAC with SHA-256 using your webhook secret as the key and the input as the message. Convert this value to a string in base-16, hexadecimal format if your language's encryption function does not produce one natively.

  2. Compare the computed string with the Routable-Signature header. If the values are not equal, then the webhook request has failed signature validation and the request is invalid.

  3. Compare the current time against the Routable-Signature-Timestamp header. If the time in Routable-Signature-Timestamp is in the future or more than five minutes old, then the webhook request is invalid.

  4. Ensure that request body can be parsed as JSON and that the JSON object received contains the event_name, event_resource, company_id and object_id attributes. If not, the webhook request is invalid.

  5. Compare the company_id in the JSON payload against your company_id, which must be the same company linked to the secret key used to encrypt the Routable-Signature. If the values are not equal, then the webhook request is invalid.

On any invalid webhook request, your webhook code must return a status of 401 Unauthorized. On a successful request, return a 200 OK. Neither reply may contain any body or set any cookies.

Sample

We've included some sample implementations below...

import hashlib
import hmac
from datetime import datetime, timezone
import json


def is_routable_webhook_valid(routable_signature, routable_signature_timestamp, request_body):
    secret = MY_ROUTABLE_SECRET

    # Check signature
    message = bytes(f"{routable_signature_timestamp}.", "utf-8") + request_body
    key = bytes(secret, "utf-8")
    our_signature = hmac.new(key, message, hashlib.sha256).hexdigest()
    if our_signature != routable_signature:
        return False

    # Check timestamp
    now = datetime.now(timezone.utc)
    timestamp = datetime.fromisoformat(routable_signature_timestamp)
    if ((now - timestamp).total_seconds() / 60.0) > 5:
        return False  
    if (now - timestamp).total_seconds() < 0:
        return False
      
    # Verify company id
    try:
        request_json = json.loads(request_body)
        my_company_id = MY_ROUTABLE_COMPANY_ID
        if "company_id" not in request_json or request_json["company_id"] != my_company_id:
            return False
    except:
        return False

    return True
const crypto = require('crypto');

const hmac = crypto.createHmac('sha256', secret);

// Ingest the timestamp from the Routable-Signature-Timestamp header
hmac.update("2021-05-25T20:34:17.042353+00:00");

// Add a period between them
hmac.update(".");

// Ingest the body
hmac.update('{"company_id": "bf24af31-531f-41a0-abc3-11c92958c31b", "event_name": "item.create", "event_resource": "item", "object_id": "f116a4bb-ea1e-4578-ba82-af22c435b108"}');

// Compute the signature
const signature = hmac.digest("hex");
# Note: This example is for Ruby on Rails only.
module V1
  class RoutableEventsController < ActionController::API
    include ActionController::Rendering

    def event
      head :unauthorized and return if params[:routable_event] == {}

      routable_signature = request.headers['Routable-Signature']
      routable_signature_timestamp = request.headers['Routable-Signature-timestamp']
      request_body = routable_event_params.to_json.gsub('":', '": ').gsub('",', '", ')

      # construct hmac input string
      input = "#{routable_signature_timestamp}.#{request_body}"
      key = Routable.webhook_secret
      data = input

      digest = OpenSSL::Digest::SHA256.new
      hmac = OpenSSL::HMAC.hexdigest(digest, key, data)
     
      # return 401 if the hmac doesn't match the signature
      head :unauthorized and return if hmac != routable_signature

      # return 401 if the timestamp is more than 5 minutes old or is in the future
      now = Time.zone.now
      timestamp_time = routable_signature_timestamp.to_time
      head :unauthorized and return if now < timestamp_time || (now - timestamp_time) / 60 > 5

      # return 401 if the company_id doesn't match
      head :unauthorized and return if Routable.company_id != routable_event_params[:company_id]

      # process the routable event here...

      head :ok
    end

    private

    def routable_event_params
      params.require(:routable_event).permit(:company_id, :event_name, :event_resource, :object_id)
    end
  end
end

Pausing and Disabling Webhooks

🚧

If your webhooks are paused or disabled...

You will stop receiving webhook requests and will need to re-deploy your webhook to re-enable requests.

If you return something other than the following status codes, your webhooks will be paused immediately. This includes on your sandbox account, so please ensure you're catching errors and returning a 401 Unauthorized during your testing.

  • 200 OK
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout

Webhooks can also be paused if any of the other required configuration steps described above are not successfully implemented.

Retry Schedule

We will retry webhooks that return the following non-pausing errors above up to nine times according to the schedule below. The Routable-Signature-Timestamp will be updated each time, so your validation method should still only be accepting webhook requests less than five minutes old.

If, after the final retry attempt below, your webhook is still failing with a "permitted" status code, your webhooks will be paused and the failed request will not be retried again.

Retry #Interval Since Initial Event
1sent immediately
21 minute
315 minutes
41 hour
53 hours
66 hours
712 hours
824 hours
948 hours