Webhooks
- How Webhooks work
- How to verify Obkio Webhook Signatures
What you are going to learn:
Our Webhooks is an advanced feature that can be used to build integrations with third party applications. It is suitable for developers or technically savvy users with scripting or programming knowledge. Using Obkio Webhooks, you can develop custom integrations with your own web applications, services, or data warehouse.
When events occur in the Obkio systems, Webhook events are sent to the webhook endpoint URLs. The calls are always HTTP POST
with a JSON payload. The payload syntax is the following:
{
"type": "<<webhook type>>",
"created": <<webhook creation unix timestamp in seconds>>,
"data": {
<<JSON object>>
}
}
The endpoint must return a 2xx
status code as fast as possible. All the other status code (3xx
, 4xx
, 5xx
) are considered errors. The timeout for webhooks is 15 seconds
. In case of a failure, the webhook will be retried. The exact retry logic depends of the objects.
For maximum flexibility, Webhooks can be configured at multiple places in the App with different configurations. Here are the standard fields required during webhooks configuration:
Endpoint URL
: HTTPS URL that will receive the Webhook.Secret(s)
: One or more keys for Wehbooks secrets. Must be between 16 and 64 alphanumeric strings. Many secrets must be separated with commas. If a commas is entered at end of a secret, an invalid secret lenght will be raised by the application as an empty secret is detected. The secret(s) is an optional field. If it is not defined, no signature is added to the webhook events.
For the moment, here are the supported webhooks:
Obkio can sign all webhook events sent to your endpoints with one or many secrets. The signatures appear in each event's X-Obkio-Signature
HTTP header. It allows you to verify that the events were sent by Obkio rather than a third party. The signature is an HMAC SHA-256 hash built using the secret combined with details of the request. If multiple secrets are provided, multiple signatures will be generated and separated with a comma.
The header syntax is Version.Timestamp.Hash
. For now, the only valid value for the Version is v1
. The Timestamp is the current timestamp of the sender, not the created timestamp in the body. If many secrets are provided, multiple signatures are generated and separated by commas.
The Hash is calculated by concatenating: HttpMethod + HttpURL + HttpBody + Timestamp
(without any space or +
between the fields).
To verify a signature, the steps are:
- Split the signature string using the
.
character as the separator, to get the Version, Timestamp and Hash. - If version is not
v1
, discard the event. - If the timestamp is older than 5 minutes, discard the event to avoid any replay attacks.
- Concatenate the 4 fields (Method, URL, Body, Timestamp) to generate the input string.
- Generate a new Hash (HMAC SHA-256) with the secret and the generated input string.
- Compare the newly generated hash with the hash in the webhook signature.
To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures. Both examples below are using such functions.
Here is a full example of how to verify the signature using Node.JS or Python3.
- Method:
POST
- URL:
https://mycompany.com/webhooks/obkio/
- Body:
{"type":"report.completed","created":1652568497,"data":{}}
- Current Timestamp:
1652568498
- Secret:
0123456789ABCDEF
- Signature:
v1.1652568498.7f031d007010c5420e7c3c8ae7e70343f9b72e37b4f3bf6d09ab4284f5b9522b
const crypto = require('crypto');
function verifySignature(method, url, body, secret, signature) {
sigFields = signature.split('.');
const version = sigFields[0];
const timestamp = sigFields[1];
const hash = sigFields[2];
if (version !== 'v1') {
return false;
}
if (Date.now() - timestamp > 300) {
return false;
}
const payload = method + '.' + url + '.' + body + '.' + timestamp;
const expectedHash = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(hash, 'utf8'),
Buffer.from(expectedHash, 'utf8'),
);
}
const method = 'POST';
const url = 'https://mycompany.com/webhooks/obkio/';
const body = '{"type":"report.completed","created":1652568497,"data":{}}';
const secret = '0123456789ABCDEF';
const signature = 'v1.1652568498.7f031d007010c5420e7c3c8ae7e70343f9b72e37b4f3bf6d09ab4284f5b9522b';
if (verifySignature(method, url, body, secret, signature)) {
console.log('Signature is valid!');
}
import hmac
import hashlib
def verify_signature(method, url, body, secret, signature):
version, timestamp, hash = signature.split(".")
if version != "v1":
return False
if int(time.time()) - int(timestamp) > 300:
return False
secret = bytes(secret, "UTF-8")
payload = bytes(method + "." + url + "." + body + "." + timestamp, "UTF-8")
expected_hash = hmac.new(secret, payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(hash, expected_hash)
sig = "v1.1652568498.7f031d007010c5420e7c3c8ae7e70343f9b72e37b4f3bf6d09ab4284f5b9522b"
method = "POST"
url = "https://mycompany.com/webhooks/obkio/"
body = '{"type":"report.completed","created":1652568497,"data":{}}'
secret = "0123456789ABCDEF"
if verify_signature(method, url, body, secret, sig):
print("Signature is valid!")