[BUG] Server-to-Server OAuth webhook URL validation rejects correct HMAC responses (200 OK, byte-for-byte verified)

I’m hitting the same pattern several other developers have reported on this forum (threads 143028, 142690, 102257, 74014, 123600): a fresh Server-to-Server OAuth app’s event subscription URL validation fails with URL validation failed. Try again later. even though my endpoint is returning 200 OK with the correct HMAC-SHA256 encryptedToken response in ~60ms. This looks like the same server-side state issue Chun Siong has flagged before…I’ve exhausted everything controllable on my side and need an engineer to look at our app’s validation state.

App details

  • App type: Server-to-Server OAuth, Account-level, Intend to publish: No

  • App created today (May 12, 2026 UTC) on a Pro Named Host account

  • Webhook URL under test is on my own domain, behind Caddy + Cloudflare proxy, hitting an n8n workflow

What my endpoint returns

  • HTTP Status: 200

  • Content-Type: application/json (stripped charset, Vary, Etag, CSP after first failure)

  • Body: {"plainToken":"<echoed>","encryptedToken":"<HMAC-SHA256(secret_token, plainToken) hex>"}

  • Median response time: 60ms

  • HMAC implementation: crypto.createHmac('sha256', secretToken).update(plainToken).digest('hex') – matches the reference code Chun Siong has posted in earlier threads

Verification

For one specific validation attempt your platform made to my endpoint today:

  • I captured the exact plainToken your validator sent

  • Computed HMAC-SHA256(secret_token, plainToken) independently in Python’s hmac lib

  • Confirmed byte-for-byte match against what my n8n workflow returned

  • Confirmed 200 status code and ~53ms response time at the proxy layer

  • Marketplace UI still showed URL validation failed. Try again later. for that exact request

Every Zoom Marketplace validation request (User-Agent Zoom Marketplace/1.0a) hitting my endpoint over the last 60 minutes has received a matching 200 + correct payload. Both reverse proxy access logs and n8n execution logs confirm this.

I can share X-Zm-Request-Id values + timestamps privately for the specific attempts you’d want to trace on your side.

What I’ve already tried

  1. Stripped Content-Security-Policy: sandbox from response headers (n8n’s default)

  2. Stripped charset=utf-8 from Content-Type so it’s a clean application/json

  3. Stripped Vary: Accept-Encoding and Etag headers

  4. Changed the webhook URL path to a brand-new slug – fresh URL with no prior validation history – same rejection

  5. Deleted the event subscription entirely and recreated it from scratch on the new URL – same rejection

  6. Hard-refreshed the browser to clear any UI cache

  7. Verified Zoom services are reported Operational on status page

Ask

Could someone please look at this app’s server-side validation state and either:

  1. Tell me what your validator is actually rejecting (I’m out of variables to test client-side), OR

  2. Reset whatever cached state is blocking validation so we can move forward

The endpoint is live and I can supply the app ID, account ID, request IDs, headers, or a specific timestamped attempt privately for diagnosis any time.

Thanks.

-Brett

@Brett1 I just tested on this python code, you might want to change it a little as this saves the webhook to a txt file for demo purpose.

I’m using my webhook secret in .env file, this is hosted over a full qualified domain name with SSL cert.

Tag if me this code doesn’t work for you

import json
import hashlib
import hmac
from flask import Flask, request, jsonify
import os
from dotenv import load_dotenv

Load environment variables from .env file, this will try to load these values from your .env file

load_dotenv()

oauth_secret_token = os.getenv(“OAUTH_SECRET_TOKEN”)

app = Flask(_name_)

def handle_request(request):

if request.method == 'POST':
    # This block handles POST requests

    # Get the raw POST data from the request
    input_data = request.data

    # Decode the JSON data
    try:
        data = json.loads(input_data)
    except json.JSONDecodeError:
        return "Invalid JSON data", 400

    # Check if the event type is "endpoint.url_validation"
    if data.get('event') == 'endpoint.url_validation':
        # Check if the payload contains the "plainToken" property
        payload = data.get('payload', {})
        plain_token = payload.get('plainToken')
        if plain_token is not None:
            # Hash the plainToken using HMAC-SHA256
            encrypted_token = hmac.new(
                oauth_secret_token.encode('utf-8'),
                plain_token.encode('utf-8'),
                hashlib.sha256
            ).hexdigest()

            # Create the response JSON object
            response = {
                "plainToken": plain_token,
                "encryptedToken": encrypted_token
            }

            # Set the response content type to JSON
            return response, 200
        else:
            # Payload is missing the "plainToken" property
            return "Payload is missing 'plainToken' property.", 400
    else:
        # Invalid event type
        message = "Success."
        
        # Save the POST body to a text file
        with open('webhook.txt', 'w') as file:
            file.write(str(request.get_json()))

        return message, 200

elif request.method == 'GET':
    # This block handles GET requests

    # Read the content of the text file
    try:
        with open('webhook.txt', 'r') as file:
            file_content = file.read()
        return file_content, 200
    except FileNotFoundError:
        return "File not found.", 404

else:
    # Unsupported HTTP method
    return "Unsupported HTTP method.", 405