Webhook Signature Verification

Verify the authenticity of webhook calls

All webhook calls include a JWT signature that you can use to verify the authenticity and integrity of the event.

Signature Header

Lirium's webhook POST requests include an HTTP header:

X-JWT-SIGNATURE: <jwt-token>

The JWT is signed using the RS512 algorithm.

JWT Structure

{
  "iss": "[SIGNING_KEY_ID]",
  "iat": 1646758802,
  "digest": "[REQUEST_BODY_DIGEST]"
}
ClaimDescription
issSigning key ID: lirium-sandbox or lirium-production
iatUnix timestamp when the JWT was generated
digestHex-encoded SHA-256 hash of the request body

Verification Steps

  1. Extract the JWT from the X-JWT-SIGNATURE header
  2. Decode and verify the JWT using Lirium's public key
  3. Compute the SHA-256 hash of the request body
  4. Compare the computed hash with the digest claim
  5. Optionally validate the iat timestamp is recent

Python Example

from jose import jwt
from hashlib import sha256
import flask

# Get the JWT from the request header
token = flask.request.headers.get("X-JWT-SIGNATURE")

# Verify and decode the JWT
decoded = jwt.decode(token, signing_public_key, algorithms=["RS512"])

# Compute the digest of the request body
digest = sha256(flask.request.data).hexdigest()

# Verify the digest matches
assert decoded.get("digest") == digest

# Verify the issuer matches your environment
assert decoded.get("iss") == "lirium-sandbox"  # or "lirium-production"

Public Keys

Get the public keys for signature verification from Webhooks Public Keys.

Example JWT

Decoded payload:

{
  "iss": "lirium-sandbox",
  "iat": 1646758802,
  "digest": "075b83598e4e147f240f5b15694b42063741b677548c89c9a4f100b5a36263f5"
}

Encoded JWT:

eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJsaXJpdW0tc2FuZGJveCIsImlhdCI6MTY0Njc1ODgwMiwiZGlnZXN0IjoiMDc1YjgzNTk4ZTRlMTQ3ZjI0MGY1YjE1Njk0YjQyMDYzNzQxYjY3NzU0OGM4OWM5YTRmMTAwYjVhMzYyNjNmNSJ9.ONcsg4ZWPKVuXC_nGWk7zfKuoqWgfV4edxDeweL7S4cSkj9SdnxZnxSBdPFMzeIaPAETnFNc-6Pxcd919pYPIx0M155yEBP4SHwmsflh93vrjdoUZYB2sD3XL6xeiCQ0KuwRp_yHpICO9JzCto4XsbvZ_wmhI1wIl-hDnlPDfdbL--rV-usbx1EPXoS67v3oTV2YtdvtoO5QZAy7relxyA9hLEpsKv_Duf_RYDiNkGEl_aZrKu8fQEFf0CUZkbqsbv1Xt-VBavk1A3muGXJfOHjBxbT5er0_Mkn7pj5T1o4XiRLlz-WhYjwsKaewpvsdoFVt_ggFPOwcBYaOLKPx5Xfapt7WtsxSH3gds9PzUGj9mi4lIm-wCYjIn7tb9iimwfhkNm_6himIflLGCQ1eu3n1UzirnX5OvlmBOutXkHddWxzp1xBn5PuWWRMMHGWg-tZcUl5x4cV9kkkse9FmheHkstjf0Ham3ePCydlMp5r0vjrgsXbHaC3iLSf_3al7wpkLXogWiVVbqAkoLYpy3nCTZ_i0btnn5GwcRW2Gx5-ILNoxLe49lMD_le-iSEb9VaC7n8lyZYw3WTXdN1a0RAAO3RbkT8UahqTxfH_iqWbbRcr1Uep92OKTt3xbaCqvyGMOSLSO-nlnXrLgyZHllG0K59wUwQjp2GcMVOljoNU