Webhook Verification

This document explains how to securely verify incoming webhooks from Stay AI using JSON Web Tokens (JWT). Verifying webhooks is critical to ensuring requests are authentic and have not been tampered with or replayed.

Overview

Stay AI signs each webhook request with a JWT, provided in the x-retextion-webhook-token header. This token:

  • Is signed using a merchant-specific API key (Creating an API Key in Stay)
  • Includes a required iat (issued at) field
  • Does not include an exp (expiration) field

Webhook verification requires:

  • Looking up the correct API key using the shop domain.
  • Decoding and verifying the JWT using that key.
  • Ensuring the iat is recent (e.g., within the last 10 minutes).

Headers

Webhook requests include the following headers:

Python Implementation (Flask Example)

from flask import Blueprint, request, jsonify
import jwt
import time

# Register Blueprint for webhook verification
webhook_verification_bp = Blueprint('webhook_verification', __name__)

@webhook_verification_bp.route('/webhooks/verify', methods=['POST'])
def handle_webhook_verification():
    token = request.headers.get('x-retextion-webhook-token')
    shop_domain = request.headers.get('x-retextion-webhook-shop')
    api_key = get_api_key_for_shop(shop_domain)
    
    if not token or not api_key:
        return jsonify({"error": "Missing token or API key"}), 400
    
    try:
        # Decode the JWT and require 'iat'
        decoded = jwt.decode(
            token,
            api_key,
            algorithms=["HS256"],
            options={"require": ["iat"]}
        )
        
        # Check token age (10-minute window)
        now = int(time.time())
        age = now - decoded['iat']
        if age > 600:
            return jsonify({"error": "Token too old"}), 401
        
    except jwt.InvalidTokenError as e:
        return jsonify({"error": "Invalid token", "details": str(e)}), 401
    
    return jsonify({"status": "Webhook verified"}), 200

def get_api_key_for_shop(shop_domain):
    """
    Replace this with your actual API key lookup logic.
    This should return the secret key used to sign the JWT for the shop.
    """
    return 'STAY_AI_API_KEY'

Best Practices

Validate token freshness: The iatclaim is required. Tokens older than 10 minutes should be rejected.

Never trust unsigned payloads: Always verify the JWT signature before using any token data.