The endpoint that will receive data from the Verkada webhook is required to have the following secure configuration:
- HTTPS endpoint with SSL certificate signed by Trusted CAs (list)
- Handle concurrent requests
- Responds in less than 2000 ms
- Responds with JSON
- HTTP Response codes: 200 for success, 4XX for errors/invalid requests, 500 to try request later
- Accepts and responds with JSON
Your webhook can be configured in Organization Settings. You can add a new webhook to Command if you are an organization admin. Navigate to the Verkada API tab under Organization Settings.
This will prompt users to name their Command Webhook setup, as well as directly provide the URL and shared secret. By default, all Command Webhooks will have full scope access. Users can refine the webhook's scope by limiting the data pushed to their defined URL to specific events relevant to their application. In the example below, a user is configuring their webh ook specifically for license plate recognition events.
## Verifying Signature
Verkada includes a timestamp in the Verkada-Signature header sent to your endpoints. This allows you to verify the events were sent by Verkada and not by a third party (ex: replay attack). This timestamp is verified as part of the signed payload and cannot be changed by a potential attacker without invalidating the signature.
You can set this up in the following way
-
Extract the timestamp and signatures from the header
Split the header, using the|
character as the separator, to get a list of tokens. The first token of the string is a timestamp and the second one is the signature. -
Prepare the signed_payload string
The signed_payload string is created by concatenating:- The actual JSON payload (i.e., the request body)
- The character
|
- The timestamp (as a string)
-
Determine expected signature:
Compute HMAC with SHA256 hash function. Use shared secret (ask your Org Admin) as the key and signed_payload string as the message. -
Compare the signatures:
Compare the signature (or signatures) in the header to the expected signature. For equality match, compute difference between current and received timestamp and check whether this satisfies your tolerance threshold. To protect against timing attacks, use constant-time string comparison to compare the expected signature to each of the received signatures.
try:
timestamp, signature = request.headers['Verkada-Signature'].split('|')
except KeyError:
raise ApiError(message='Missing signature', code=400)
# Check if request was sent in last minute
if time.time() - int(timestamp) > 60:
raise ApiError(message='Expired', code=403)
to_hash = b'|'.join((request.get_data() or b'', timestamp.encode()))
if not compare_digest(signature.encode(), hmac.new(
shared_secret, msg=to_hash, digestmod=hashlib.sha256
).hexdigest().encode()):
raise ApiError(message='Invalid signature', code=403)
- Compare the signatures:
Compare the signature (or signatures) in the header to the expected signature. For equality match, compute difference between current and received timestamp and check whether this satisfies your tolerance threshold. To protect against timing attacks, use constant-time string comparison to compare the expected signature to each of the received signatures.
try:
timestamp, signature = request.headers['Verkada-Signature'].split('|')
except KeyError:
raise ApiError(message='Missing signature', code=400)
# Check if request was sent in last minute
if time.time() - int(timestamp) > 60:
raise ApiError(message='Expired', code=403)
to_hash = b'|'.join((request.get_data() or b'', timestamp.encode()))
if not compare_digest(signature.encode(), hmac.new(
shared_secret, msg=to_hash, digestmod=hashlib.sha256
).hexdigest().encode()):
raise ApiError(message='Invalid signature', code=403)