Webhook validating issue

I’m encountering an error while validating my Zoom webhook. The message displayed is:
“URL validation failed. Try again later.”

I would like to raise a ticket for this issue. If there is an existing solution or guidance available, I would appreciate your assistance.

Below is the code I am currently using for the Zoom webhook endpoint:

Route::post(‘/zoom/test-webhook’, function (Request $request) {
$webhookSecretToken = ‘MJJP4FUUSp6q7ePLlDfupA’;

$headers = $request->headers;
$zoomSignature = $headers->get('x-zm-signature');
$zoomTimestamp = $headers->get('x-zm-request-timestamp');

if (abs(time() - (int) $zoomTimestamp) > 300) {
    return response()->json(['message' => 'Timestamp is too old.'], 400);
}

$rawBody = $request->getContent();
$computedMessage = "v0:{$zoomTimestamp}:{$rawBody}";
$computedHash = hash_hmac('sha256', $computedMessage, $webhookSecretToken);
$computedSignature = "v0={$computedHash}";

Log::info('Zoom Webhook Signature Validation', [
    'computed' => $computedSignature,
    'received' => $zoomSignature,
    'body' => $rawBody
]);

if (!hash_equals($computedSignature, $zoomSignature)) {
    return response()->json(['message' => 'Invalid signature.'], 403);
}

$data = json_decode($rawBody, true);

if (isset($data['payload']['plainToken'])) {
    $plainToken = $data['payload']['plainToken'];
    $encryptedToken = hash_hmac('sha256', $plainToken, $webhookSecretToken);

    return response()->json([
        'plainToken' => $plainToken,
        'encryptedToken' => $encryptedToken
    ], 200);
}

return response()->noContent(204);

});

Let me know if any changes are needed in this code or if there’s a configuration I might be missing.

Thank you!

Hey @That
Hope you are doing great!
Have you been able to troubleshoot this on your end?

Hello Elisa,

Yes, I’ve tried troubleshooting the issue, but I haven’t been able to identify the exact cause of the validation failure. When I click the Validate button in Zoom, I simply receive the message:
Please validate your endpoint URL.

The validation fails with the message:
URL validation failed. Try again later.

I’ve ensured that my code handles the plain token challenge as per Zoom’s documentation, and the endpoint is publicly accessible. However, the validation still doesn’t succeed.

If there’s any known solution, configuration detail, or guidance available for this issue, I’d appreciate your help. I’d also like to raise a support ticket if needed.

Thank you!

Hey @That
Here is the link to our Docs with some code samples for validation

I am also linking a sample app that might help you as a reference:

We’ve implemented the Zoom webhook exactly as per the sample you provided, including signature validation and plainToken handling—you’re welcome to review our code above to confirm.

@That Hi,

It seems like you’re facing an issue with webhook validation. Here’s a working Next.js version of the webhook verification that could be useful. You just need to convert it into Laravel for your use case.

Working Next.js Code:

import { HttpStatusCode } from 'axios';
const crypto = require('crypto');

export class WebhookVerifier {
    // Method to verify the signature
    static verifySignature({ zmSignature, zmRequestTimeStamp, body }) {
        const message = `v0:${zmRequestTimeStamp}:${JSON.stringify(body)}`;
        const hmac = crypto.createHmac('sha256', process.env.ZOOM_EVENT_VERIFICATION_SECRET);
        const computedHash = hmac.update(message).digest('hex');
        const signature = `v0=${computedHash}`;

        // Compare computed signature with Zoom signature
        return signature === zmSignature;
    }

    // Method to process the webhook verification response
    static async verifyEventEndpoint({ plainToken }) {
        try {
            const secret = process.env.ZOOM_EVENT_VERIFICATION_SECRET;

            // Encrypt plainToken
            const encryptedToken = crypto
                .createHmac('sha256', secret)
                .update(plainToken)
                .digest('hex');

            return {
                status: HttpStatusCode.Ok,
                payload: {
                    plainToken: plainToken,
                    encryptedToken: encryptedToken,
                },
            };
        } catch (error) {
            console.error(error);
            return { status: 500, message: 'Internal Server Error' };
        }
    }
}

Helper to Get Payload and Verify Signature:

class ZoomWebhookService {
    static async notification({ req, meta }) {
        try {
            const body = await req.json();
            const zmRequestTimeStamp = req.headers.get("x-zm-request-timestamp");
            const zmSignature = req.headers.get("x-zm-signature");

            if (WebhookVerifier.verifySignature({ zmSignature, zmRequestTimeStamp, body })) {
                const { event, payload } = body;

                if (event === 'endpoint.url_validation') {
                    const { plainToken } = payload;
                    return WebhookVerifier.verifyEventEndpoint({ plainToken });
                }

                console.log(`-- zoom event: ${event}  --`);

                return this.response({
                    payload: { message: "Accepted" },
                    status: HttpStatusCode.Ok,
                });
            }

            return this.response({
                payload: { message: "Unauthorized" },
                status: HttpStatusCode.Unauthorized,
            });
        } catch (error) {
            console.error("-- Webhook Error --", error.message);
            return this.response({
                payload: { message: "Internal Error" },
                status: HttpStatusCode.InternalServerError,
            });
        }
    }
}

Key Notes:

  • The code verifies the signature by using the raw body and headers to check the validity of the webhook.
  • After successful validation, it processes events like url_validation and responds accordingly.
  • This is designed for Next.js but can be easily adapted to a Laravel environment.

You can convert this code to Laravel by using Laravel’s Request class for getting the raw body and headers, and then implementing the signature verification using Laravel’s built-in cryptographic functions.

1 Like

Thanks for the feedback.
but I have no problems with the handler.
I passed the validation.

The problem is that the web hooks do not come to the server.
Not that I can’t process them.