Skip to main content
Because your webhook endpoint is a public URL, anyone could send requests to it. To confirm that a request genuinely came from Openlayer and was not tampered with, every delivery is signed. Your endpoint should verify the signature before processing the payload. Openlayer follows the Standard Webhooks specification, so you can verify signatures with any compatible library.

Signature headers

Every webhook request includes the following headers:
HeaderDescription
webhook-idA unique identifier for the message. It stays constant across retries of the same event.
webhook-timestampThe Unix timestamp (in seconds) of the delivery attempt.
webhook-signatureA space-delimited list of signatures, each prefixed with its version (for example, v1,…).
Requests are also sent with Content-Type: application/json and a User-Agent of Openlayer-Webhooks/1.0.

How the signature is computed

The signature is an HMAC-SHA256 over the webhook id, timestamp, and the raw request body, joined with periods:
signed_content = "{webhook-id}.{webhook-timestamp}.{raw_body}"
The key is your subscription’s signing secret with the whsec_ prefix removed and the remainder Base64-decoded. The result is Base64-encoded and prefixed with v1, to form the value sent in the webhook-signature header:
signature = "v1," + base64(HMAC_SHA256(base64decode(secret_without_prefix), signed_content))
Verify against the raw request body exactly as received. Parsing the JSON and re-serializing it can change the bytes (key order, whitespace) and cause verification to fail.
The Standard Webhooks libraries handle signature construction, Base64 decoding, constant-time comparison, and timestamp checks for you. Pass the signing secret returned when you created the subscription.
# pip install standardwebhooks
from standardwebhooks import Webhook

# The secret returned when the subscription was created, e.g. "whsec_..."
secret = "YOUR_WEBHOOK_SIGNING_SECRET"

def handle_request(raw_body: bytes, headers: dict):
    wh = Webhook(secret)
    # Raises an error if the signature or timestamp is invalid.
    payload = wh.verify(raw_body, {
        "webhook-id": headers["webhook-id"],
        "webhook-timestamp": headers["webhook-timestamp"],
        "webhook-signature": headers["webhook-signature"],
    })
    # payload is the verified, parsed event.
    return payload

Verify manually

If you prefer not to add a dependency, you can reproduce the signature yourself and compare it to the header using a constant-time comparison.
import base64
import hashlib
import hmac

def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
    webhook_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]

    signed_content = f"{webhook_id}.{timestamp}.{raw_body.decode('utf-8')}"
    key = base64.b64decode(secret.removeprefix("whsec_"))
    digest = hmac.new(key, signed_content.encode("utf-8"), hashlib.sha256).digest()
    expected = base64.b64encode(digest).decode("utf-8")

    # The header may contain multiple space-delimited signatures.
    for part in headers["webhook-signature"].split(" "):
        version, _, value = part.partition(",")
        if version == "v1" and hmac.compare_digest(value, expected):
            return True
    return False

Replay protection

The webhook-timestamp header lets you reject stale requests. Compare it to the current time and discard requests whose timestamp is outside a tolerance window (for example, more than 5 minutes old). The Standard Webhooks libraries perform this check for you.

Idempotency

Because deliveries are retried, your endpoint may receive the same event more than once. The webhook-id header is constant across retries of the same event, so you can use it as an idempotency key — record the IDs you’ve already processed and skip duplicates.