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 Resource | Event Name | Event Description |
---|---|---|
company | company.create | A Company was created |
company | company.invitation_accepted | A Company accepted an invitation to onboard |
company | company.invited | A Company was invited to onboard |
payable | payable.approval_changed | A Payable received an approval |
payable | payable.created | A new Payable was created |
payable | payable.creation_failed | The creation of a Payable failed during processing - for example, due to a failure to sync with your accounting software. |
payable | payable.refreshed_from_ledger | The data for a Payable has been synced from your accounting software |
payable | payable.status_changed | The status of a Payable has changed |
payment_method | payment_method.created | A PaymentMethod has been added to a Company . The detail object will contain the Company's company_id . |
receivable | receivable.approval_changed | A Receivable received an approval |
receivable | receivable.created | A new Receivable was created |
receivable | receivable.creation_failed | The creation of a Receivable failed during processing - for example, due to a failure to sync with your accounting software. |
receivable | receivable.refreshed_from_ledger | The data for a Receivable has been synced from your accounting software |
receivable | receivable.status_changed | The status of a Receivable has changed |
tax_form | tax_form.created | A 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 Resource | Event Name | Event Description |
---|---|---|
item | item.approval_change | A Payable or Receivable received an approval |
item | item.create | A new Payable or Receivable was created |
item | item.refreshed_from_ledger | The data for a Payable or Receivable has been synced from your accounting software |
item | item.status_change | The 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
}
Key | Value Description |
---|---|
company_id | The company the resource belongs to. Should always match your company ID. |
event_name | The name of the event (e.g. payable.status_changed ) |
event_resource | The resource of the event (e.g. payable ) |
object_id | The ID of the resource (for example, the ID of the payable , which can be queried from /v1/payables/{id} ) |
detail | A 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.
- Create the
input
string by concatenating the value ofRoutable-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.
-
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. -
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. -
Compare the current time against the
Routable-Signature-Timestamp
header. If the time inRoutable-Signature-Timestamp
is in the future or more than five minutes old, then the webhook request is invalid. -
Ensure that request body can be parsed as JSON and that the JSON object received contains the
event_name
,event_resource
,company_id
andobject_id
attributes. If not, the webhook request is invalid. -
Compare the
company_id
in the JSON payload against yourcompany_id
, which must be the same company linked to the secret key used to encrypt theRoutable-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 |
---|---|
1 | sent immediately |
2 | 1 minute |
3 | 15 minutes |
4 | 1 hour |
5 | 3 hours |
6 | 6 hours |
7 | 12 hours |
8 | 24 hours |
9 | 48 hours |
Updated 3 months ago