Search code examples
pythonsquarespace

Implement Acuity (Squarespace scheduling) webhook signature validation in python


The Acuity webhook documentation describes the steps to verify the source of the webhook is from Acuity.

First compute the base64 HMAC-SHA256 signature of the notification using the request's body as the message and your API key as the shared secret. Then compare this signature to the request header X-Acuity-Signature. If they match, the notification is authentic.

And then it provides some code samples for PHP, JavaScript, and Ruby. These samples help clarify a bit, but use variables without describing their source, leaving me to guess what they refer to. I haven't been able to verify messages.

Here's the code I'm using:

acuity_api_key = 'redacted'
key_bytes = acuity_api_key.encode('utf-8')

signature = request.headers['x-acuity-signature']

message = request.body
message_bytes = message.encode('utf-8')

signed_body = base64.b64encode(
    hmac.new(key_bytes, message_bytes, hashlib.sha256).digest()
).decode('utf-8')

if signature != signed_body:
    raise RuntimeError('Failed to validate message signature')

Solution

  • I actually had 2 problems.

    1. The webhook receiver I have set up is an AWS Function URL, backed by a Lambda. For that setup, the message body is base64 encoded, and needs to be decoded before passing to the signing check. See the following code.
    acuity_api_key = 'redacted'
    key_bytes = acuity_api_key.encode('utf-8')
    
    signature = event['headers']['x-acuity-signature']
    
    encoded_message = event['body']
    if event.get('isBase64Encoded', True):
        decoded_message_bytes = base64.b64decode(encoded_message)
    else:
        decoded_message_bytes = encoded_message.encode('utf-8')
    
    signed_body = base64.b64encode(
        hmac.new(key_bytes, decoded_message_bytes, hashlib.sha256).digest()
    ).decode('utf-8')
    
    if signature != signed_body:
        raise RuntimeError('Failed to validate message signature')
    
    1. The API key I was using for my personal Squarespace account, and I was relying on the 'static webhook' configuration. It is documented that it uses the "main admin's API key". I had assumed the API key displayed in the Squarespace scheduling's 'Integrations' tab was admin key for the integration (vs my API key from some OAuth flow). Actually, Squarespace users see different API keys when they visit the Integrations page. I set up "Dynamic Webhooks", and used my own API key instead. I expect the Squarespace "owner" account API key would have worked.

    2. I continued to have problems with order.completed hook verification. Despite having signed up for the hook with my (non-owner) credentials, the hook was still being signed with the credentials from the Squarespace owner account for the site. I've opened a support request about that particular issue.