Webhook signatures can be used to verify the authenticity and integrity of a received webhook notification. By verifying the webhook signature merchants can protect themselves from malicious parties sending tampered webhook data to the merchant’s webhook endpoint.

Configuration

When configuring a webhook subscription you can generate a secret that will be used to sign the webhooks content. Please head over to the Webhook subscriptions dashboard, select the subscription, select the menu, and select Generate secret. Once created, the secret can be copied from the table and used to verify webhooks.

Please note it may take 30 seconds for the signature to appear in webhook notifications.

Signature verification

When a secret is enabled for a webhook, the following HTTP headers will be sent with every webhook message.

  • X-Gr4vy-Webhook-Timestamp: UNIX timestamp in seconds used to generate the signature.
  • X-Gr4vy-Webhook-Signatures: comma-separated list of signatures for each of the active secrets.
  • X-Gr4vy-Webhook-ID: unique reference to webhook across retries which acts as the idempotency value.

In order to verify the webhook content the following steps must be followed.

  1. Append the timestamp header and payload contents ({timestamp}.{payload}).
  2. Compute the HMAC SHA256 value using the webhook subscription secret.
  3. Make sure the computed value matches at least one of the signature header values.
  4. (Optional) Check that timestamp is not too old, in order to prevent replay attacks.
Python
import hashlib
import hmac

secret = "super-secret-value"

def verify_signature(request):
    payload = request.data.decode("utf-8")
    signature_header = request.headers.get("X-Gr4vy-Webhook-Signatures", None)
    timestamp_header = request.headers.get("X-Gr4vy-Webhook-Timestamp", None)

    if not signature_header or not timestamp_header:
        raise Exception("Missing header values")

    signatures = signature_header.split(",")
    expected_signature = hmac.new(
        key=secret.encode("utf-8"),
        msg=f"{timestamp}.{payload}".encode(),
        digestmod=hashlib.sha256,
    ).hexdigest()


    if expected_signature not in signatures:
        raise Exception("No matching signature found")

Bear in mind that extracting the payload value must include all the formatting as it is received. For example, if the payload is a JSON object, it must be formatted as a string with all the spaces and line breaks. If you are using a JSON library to parse the payload, make sure to use the original vlaue received.

Secret rotation

In the webhook subscriptions page the secret can also be rotated. Secret rotation allows you to safely change the secret used to sign webhooks. It is possible to set a custom rotation period during which our system will send signatures for both the old secret and the new one. After this period, only the new secret will be used to sign webhooks.

To rotate a secret, please following these steps.

  1. Generate a new secret using the Rotate secret… menu in the webhook subscriptions page.
  2. Update your webhook endpoint to use the new secret.
  3. Verify the webhooks are processed using the new secret.
  4. Wait for the old secret to expire after the rotation period.
Please note it may take 30 seconds for the new signatures to appear in webhook notifications.

Idempotency

The X-Gr4vy-Webhook-ID header is a unique reference to the webhook across any retries. For example, you may successfully process a webhook and return response but due to network problems our system did not receive the response. We will then retry that request and your code must know that the webhook was already processed.

If the webhook handling is idempotent itself this might not be a real problem but still a waste of resources. You can store the latest webhooks processed extracting the id from X-Gr4vy-Webhook-ID to avoid processing them again.