Webhook Verification

Validating webhook request signatures

Unit21 provides the ability to verify incoming webhooks by HMAC-SHA256 with a random 20-character key that is unique per-webhook.

This key is visible to Agents in the Unit21 web app when configuring webhooks:

1646

You can copy the secret key by using the two buttons next to it.

Sample request headers

{
  "content-length": "452",
  "content-type": "application/json",
  "unit21-signature": "t=1582702424,s0=cbe7c96a57abff5e43e1b4b394f85ff6100d062f5bf551142d0527ae3213cfcc"
}
{
  "unit21_id": 123,
  "object_type": "ALERT",
  "alert_id": null,
  "alert_type": "kyc",
  "change": "CLOSED",
  "change_time": 1570062682,
  "disposition": "FALSE_POSITIVE",
  "status": "CLOSED",
  "changed_by": "[email protected]",
  "title": "Sample alert title",
  "description": "Sample alert description",
  "start_date": 1570052582,
  "end_date": 1570062582,
  "entities": [{"entity_id": "entity-886313e1", "entity_type": "user", "unit21_id": 46, "resolution": null}, {"entity_id": "entity-91a31re2", "entity_type": "user", "unit21_id": 72, "resolution": "false_positive"}],
  "events": [{"event_id": "event-1063e4e3e1", "event_type": "transaction", "unit21_id": 111, "resolution": null}],
  "instruments": [{"instrument_id": "instrument-4112950a", "instrument_type": "wallet", "unit21_id": 401, "resolution": null}],
  "triggered_by_rules": [{"unit21_id": 6, "rule_id": null}],
  "assigned_to": "[email protected]",
  "tags": ["rule_type:layering"],
  "custom_data": {"internal_priority": 3}
}

Verifying requests

Requests can be verified by using the Unit21-Signature HTTP header, which is sent in the format: unit21-signature: t=<timestamp>,s0=<signature> where:
--<timestamp> is an integer representing Unix time when the payload was sent, and
--<signature> is a signature generated as follows:

  • Take the timestamp value from t and prepend it to the request body with a .
  • Generate a signature using HMAC-SHA256 with the secret key provided in the application console

Given t=100 and a request body of '{"foo": "bar", "baz": "foo"}' the value of s0 would be the signature of 100.{"foo": "bar", "baz": "foo"} with the secret key.

Sample Python code to validate a signature:

import hashlib
import hmac

def verify_signature(secret_key: str, signature_header: str, request_body: str) -> bool:
	# Extract the timestamp from signature_header
  timestamp = signature_header.split(",")[0].split("=")[1]
	# Extract the signature from signature_header
	signature = signature_header.split(",")[1].split("=")[1]
	
	# Construct the expected signature using the timestamp and request_body
	signature_payload = f"{timestamp}.{request_body}"
	expected_signature = hmac.new(
		key=str.encode(secret_key),
		msg=str.encode(signature_payload),
		digestmod=hashlib.sha256,
	).hexdigest()

	# Confirm that the signature is as expected
	return signature == expected_signature


u21_secret_key = "5b010867f0aeaa8c75b6"
body = '{"foo": "bar", "baz": "foo"}'
header = "t=1676417774,s0=1de43c487e72e51b74b83216cde0c6f6c990f3254585e855c71ec235473578bc"

assert verify_signature(
  secret_key=u21_secret_key, 
  signature_header=header,
  request_body=body,
)

A deeper look:

The unit21-signature header contains a timestamp and a signature. The signature (s0) is a hex code generated by using a per-endpoint secret key found on your unit21 webhooks dashboard as a hash key and a message created by concatenating the following in order:

  • timestamp (t) seen in the signature as a string
  • The . character
  • and the actual request body as an un-prettified JSON string

The sample on the right would be represented by this pre-hashed string:

1582702424.{"unit21_id": 123, "object_type": "ALERT", "alert_id": null, "alert_type": "kyc", "change": "CLOSED", "change_time": 1570062682, "disposition": "FALSE_POSITIVE", "status": "CLOSED", "changed_by": "[email protected]", "title": "Sample alert title", "description": "Sample alert description", "start_date": 1570052582, "end_date": 1570062582, "entities": [{"entity_id": "entity-886313e1", "entity_type": "user", "unit21_id": 46}, {"entity_id": "entity-91a31re2", "entity_type": "user", "unit21_id": 72, "resolution": "false_positive"}], "events": [{"event_id": "event-1063e4e3e1", "event_type": "transaction", "unit21_id": 111}], "instruments": [{"instrument_id": "instrument-4112950a", "instrument_type": "wallet", "unit21_id": 401}], "triggered_by_rules": [{"unit21_id": 6, "rule_id": null}], "assigned_to": "[email protected]", "tags": ["rule_type:layering"], "custom_data": {"internal_priority": 3}}

Using secret key 4acff285d1de621a4077 outputs the s0 signature:

c2c035b8759cb142441e7c3a40a7571adbdbb03a63d84c117c6772432e7c1bd9

Note that the secret key and concatenated body are used as UTF-8 encoded strings when generating the HMAC and the digest algorithm is SHA256