Webhooks

    What you are going to learn:

  • How Webhooks work
  • How to verify Obkio Webhook Signatures

What are Webhooks?
What are Webhooks?

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.

How do Webhooks work?
How do Webhooks work?

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.

Webhook Configuration
Webhook Configuration

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.
  • Signature Secret(s): One or more secrets keys for Wehbooks Signatures. Must be between 16 and 64 alphanumeric strings. Many secrets must be separated with commas. The signature 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:

Webhook Signatures
Webhook Signatures

Obkio can sign all webhook events sent to your endpoints with one or many signatures. 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 signature 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).

Signature Verification
Signature Verification

To verify a signature, the steps are:

  1. Split the signature string using the . character as the separator, to get the Version, Timestamp and Hash.
  2. If version is not v1, discard the event.
  3. If the timestamp is older than 5 minutes, discard the event to avoid any replay attacks.
  4. Concatenate the 4 fields (Method, URL, Body, Timestamp) to generate the input string.
  5. Generate a new Hash (HMAC SHA-256) with the signature secret and the generated input string.
  6. 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

Node.JS code example
Node.JS code example

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!');
}

Python3 code example
Python3 code example

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!")