# Zoom Webhook URL Validation Failed in ASP.NET Core

**URL:** <https://devforum.zoom.us/t/zoom-webhook-url-validation-failed-in-asp-net-core/137029>\
**Category:** API and Webhooks\
**Tags:** webhooks, api\
**Created:** [September 2, 2025, 6:08am UTC](https://devforum.zoom.us/t/zoom-webhook-url-validation-failed-in-asp-net-core/137029 "2025-09-02T06:08:23Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![Muhmmad](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/muhmmad/32/71025_2.png) [@Muhmmad](https://devforum.zoom.us/u/Muhmmad)\
**Post date:** [September 2, 2025, 6:08am UTC](https://devforum.zoom.us/t/zoom-webhook-url-validation-failed-in-asp-net-core/137029/1 "2025-09-02T06:08:23Z")

</div>

Hi everyone,

I’m trying to integrate Zoom webhooks in my **[ASP.NET](http://ASP.NET) Core API** project, but I keep getting the error:

```auto
URL validation failed. Try again later.

```

### What I Tried

1. **Basic Controller Setup**

```auto
[ApiController]
[Route("api/[controller]")]
public class ZoomWebhookController : ControllerBase
{
    [HttpPost("HandleZoomEvent")]
    public async Task<IActionResult> HandleZoomEvent()
    {
        try
        {
            Request.EnableBuffering();
            Request.Body.Position = 0;
            using var reader = new StreamReader(Request.Body, Encoding.UTF8);
            var body = await reader.ReadToEndAsync();

            using var jsonDoc = JsonDocument.Parse(body);
            var payload = jsonDoc.RootElement;

            var eventType = payload.GetProperty("event").GetString();

            if (eventType == "endpoint.url_validation")
            {
                var plainToken = payload.GetProperty("payload").GetProperty("plainToken").GetString();

                var encryptedToken = Convert.ToBase64String(
                    SHA256.HashData(
                        Encoding.UTF8.GetBytes(plainToken + "MY_WEBHOOK_SECRET_TOKEN")
                    )
                );

                return Ok(new
                {
                    plainToken,
                    encryptedToken
                });
            }

            return Ok();
        }
        catch (Exception ex)
        {
            return BadRequest(ex.Message);
        }
    }
}

```

1. **Tried with `[FromBody]` parameter**

```auto
[HttpPost("HandleZoomEvent")]
public IActionResult HandleZoomEvent([FromBody] JsonElement payload)
{
    var eventType = payload.GetProperty("event").GetString();
    ...
}

```

1. **Also tested with dynamic model binding** but got errors because Zoom sends `plainToken` inside `payload` → `plainToken`.

### Results

- When I test locally with Postman and send Zoom’s sample payload, it works fine.

- But when I configure Zoom webhook with my API endpoint, I always get:

```auto
URL validation failed. Try again later.

```

### Question

- What’s the correct way to handle Zoom’s **endpoint.url\_validation** in [ASP.NET](http://ASP.NET) Core?

- Am I missing some required response format (maybe casing or JSON structure)?

- Does Zoom require **raw JSON** instead of [ASP.NET](http://ASP.NET) Core’s default camelCase serializer?

Any help would be appreciated

---

<div class="post-metadata">

**Author:** ![CristianEni](https://avatars.discourse-cdn.com/v4/letter/c/bb73d2/32.png) [@CristianEni](https://devforum.zoom.us/u/CristianEni)\
**Post date:** [September 2, 2025, 8:42am UTC](https://devforum.zoom.us/t/zoom-webhook-url-validation-failed-in-asp-net-core/137029/2 "2025-09-02T08:42:52Z")

</div>

```auto

public interface IZoomWebhookAuthenticationHandler
{
    Task<ZoomWebhookAuthenticationResult> AuthenticateAsync(HttpContext context, CancellationToken cancellationToken = default);
    string ComputeUrlValidationResponse(string plainToken);
}

public class ZoomWebhookAuthenticationHandler : IZoomWebhookAuthenticationHandler
{
    private readonly IOptions<ZoomConfiguration> _zoomConfig;
    private readonly ILogger<ZoomWebhookAuthenticationHandler> _logger;
    private readonly string _webhookSecret;

    public ZoomWebhookAuthenticationHandler(IOptions<ZoomConfiguration> zoomConfig, ILogger<ZoomWebhookAuthenticationHandler> logger)
    {
        _zoomConfig = zoomConfig;
        _logger = logger;
        _webhookSecret = _zoomConfig.Value.ZoomWebhookSecret
            ?? throw new InvalidOperationException("ZoomWebhookSecret configuration is missing");
    }

    public async Task<ZoomWebhookAuthenticationResult> AuthenticateAsync(HttpContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            // Enable buffering to read body multiple times
            context.Request.EnableBuffering();

            // Read the request body
            using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true);
            var body = await reader.ReadToEndAsync(cancellationToken);
            context.Request.Body.Position = 0;

            // Extract headers
            if (!context.Request.Headers.TryGetValue("x-zm-request-timestamp", out var timestampHeader))
            {
                _logger.LogWarning("Missing x-zm-request-timestamp header in Zoom webhook request");
                return ZoomWebhookAuthenticationResult.Failed("Missing timestamp header");
            }

            if (!context.Request.Headers.TryGetValue("x-zm-signature", out var signatureHeader))
            {
                _logger.LogWarning("Missing x-zm-signature header in Zoom webhook request");
                return ZoomWebhookAuthenticationResult.Failed("Missing signature header");
            }

            var timestamp = timestampHeader.ToString();
            var signature = signatureHeader.ToString();

            // Validate timestamp (optional: check if timestamp is not too old)
            if (!ValidateTimestamp(timestamp))
            {
                _logger.LogWarning("Invalid timestamp in Zoom webhook request: {Timestamp}", timestamp);
                return ZoomWebhookAuthenticationResult.Failed("Invalid timestamp");
            }

            // Verify signature
            var expectedSignature = ComputeSignature(timestamp, body);
            if (!string.Equals(signature, expectedSignature, StringComparison.Ordinal))
            {
                _logger.LogWarning("Invalid signature in Zoom webhook request. Expected: {Expected}, Received: {Received}",
                    expectedSignature, signature);
                return ZoomWebhookAuthenticationResult.Failed("Invalid signature");
            }

            _logger.LogDebug("Zoom webhook authentication successful for timestamp: {Timestamp}", timestamp);
            return ZoomWebhookAuthenticationResult.Success(body);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error during Zoom webhook authentication");
            return ZoomWebhookAuthenticationResult.Failed("Authentication error");
        }
    }

    private bool ValidateTimestamp(string timestamp)
    {
        if (!long.TryParse(timestamp, out var timestampValue))
        {
            return false;
        }

        var requestTime = DateTimeOffset.FromUnixTimeSeconds(timestampValue);
        var currentTime = DateTimeOffset.UtcNow;
        var timeDifference = Math.Abs((currentTime - requestTime).TotalMinutes);

        // Allow requests within 5 minutes (Zoom's recommended window)
        return timeDifference <= 5;
    }

    private string ComputeSignature(string timestamp, string body)
    {
        var message = $"v0:{timestamp}:{body}";
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_webhookSecret));
        var hash = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(message))).ToLowerInvariant();
        return $"v0={hash}";
    }

    public string ComputeUrlValidationResponse(string plainToken)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_webhookSecret));
        var encryptedToken = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(plainToken))).ToLowerInvariant();
        return encryptedToken;
    }
}

public class ZoomWebhookAuthenticationResult
{
    public bool IsAuthenticated { get; }
    public string? RequestBody { get; }
    public string? ErrorMessage { get; }

    private ZoomWebhookAuthenticationResult(bool isAuthenticated, string? requestBody = null, string? errorMessage = null)
    {
        IsAuthenticated = isAuthenticated;
        RequestBody = requestBody;
        ErrorMessage = errorMessage;
    }

    public static ZoomWebhookAuthenticationResult Success(string requestBody)
        => new(true, requestBody);

    public static ZoomWebhookAuthenticationResult Failed(string errorMessage)
        => new(false, errorMessage: errorMessage);
}

```

```auto
var encryptedToken = authHandler.ComputeUrlValidationResponse(urlValidationPayload.PlainToken);
var response = new ZoomUrlValidationResponse
{
    PlainToken = urlValidationPayload.PlainToken,
    EncryptedToken = encryptedToken
};

logger.Debug("URL validation successful for token: {Token}", urlValidationPayload.PlainToken);
return Task.FromResult(ZoomWebhookResult.Success(response).ToResult());

```

pls clean it up

---

<div class="post-metadata">

**Author:** ![Dr\_Serena](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/dr_serena/32/71298_2.png) [@Dr\_Serena](https://devforum.zoom.us/u/Dr_Serena)\
**Post date:** [September 19, 2025, 11:21pm UTC](https://devforum.zoom.us/t/zoom-webhook-url-validation-failed-in-asp-net-core/137029/3 "2025-09-19T23:21:50Z")

</div>

Same issues here. What gives?

---

<div class="post-metadata">

**Author:** ![system](https://us1.discourse-cdn.com/flex016/uploads/zoomdeveloper/original/3X/6/1/614bdd549b610bbaa46ff934617683a02bdaa03c.png) [@system](https://devforum.zoom.us/u/system)\
**Post date:** [September 23, 2026, 2:58pm UTC](https://devforum.zoom.us/t/zoom-webhook-url-validation-failed-in-asp-net-core/137029/4 "2026-09-23T14:58:26Z")

</div>

Closed on September 23, 2026, 2:58pm UTC

This topic was automatically closed 368 days after the last reply. New replies are no longer allowed.
