Gareth Wilson Gareth Wilson

Guide to Azure Event Grid Webhooks: Features and Best Practices

Published


Azure Event Grid delivers events from Azure services and your own applications to a webhook endpoint you control: a blob is created, a resource is deleted, a subscription is validated. If you're routing Azure events to your own service, webhooks are how they arrive.

This guide covers how Event Grid webhooks work, the two endpoint validation handshakes, how to authenticate deliveries when nothing is signed, and the best practices for production.

What are Azure Event Grid webhooks?

Event Grid is a managed pub/sub service rather than a single-vendor webhook sender, and that changes what "verifying a webhook" means. Event Grid does not sign the request body. Trust comes from two places instead. First, an ownership handshake at subscription time proves you control the endpoint before Event Grid sends anything. Second, a credential you configure yourself authenticates each delivery: a header, a query parameter, or a Microsoft Entra ID token.

Azure Event Grid webhook features

FeatureDetails
ConfigurationAzure portal, az eventgrid event-subscription create, ARM/Bicep
Signature headerNone. Event Grid signs nothing
Endpoint validationMicrosoft.EventGrid.SubscriptionValidationEvent echo, or a CloudEvents OPTIONS preflight
Delivery authenticationStatic delivery-property header, query-parameter client secret, or Microsoft Entra ID bearer token
Delivery headersaeg-subscription-name, aeg-delivery-count, aeg-event-type, aeg-metadata-version, aeg-data-version, aeg-output-event-id
Payload shapeEvent Grid schema (JSON array) or CloudEvents v1.0 (single JSON object)
TransportHTTPS only
DeliveryAt-least-once, unordered, 30-second response timeout

Common events

Event Grid is a broker, so most event types belong to the Azure service publishing them rather than to Event Grid itself. Custom topics carry whatever types you define.

EventFires when
Microsoft.Storage.BlobCreated / Microsoft.Storage.BlobDeletedA blob is written or removed
Microsoft.Resources.ResourceWriteSuccess / ResourceDeleteSuccessAn Azure resource is created, updated or deleted
Microsoft.ContainerRegistry.ImagePushedA container image is pushed
Microsoft.EventGrid.SubscriptionValidationEventAn event subscription is created or updated
Microsoft.EventGrid.SubscriptionDeletedEventAn event subscription is deleted
Microsoft.EventGrid.MQTTClientSessionConnectedAn MQTT client session connects to an Event Grid namespace

The envelope depends on the delivery schema you chose. CloudEvents v1.0 gives you a single JSON object with specversion, type, source, id, time, subject and data. Event Grid schema gives you a JSON array whose elements carry eventType, eventTime, topic, subject, data, dataVersion and metadataVersion.

See Azure Event Grid webhook payloads in action. Inspect and replay sample Event Grid payloads in the Hookdeck Console — no account or setup required.

Setting up Azure Event Grid webhooks

Create an event subscription against a topic, system topic or domain, pointing at your HTTPS endpoint:

az eventgrid event-subscription create \
  --name my-webhook-subscription \
  --source-resource-id "$TOPIC_ID" \
  --endpoint "https://example.com/webhooks/azure-event-grid" \
  --endpoint-type webhook \
  --event-delivery-schema cloudeventschemav1_0

The --event-delivery-schema flag decides both your payload shape and which handshake you'll get, so choose it before you write the handler. Event Grid only delivers to HTTPS endpoints, and self-signed certificates aren't supported for validation.

Securing Azure Event Grid webhooks

There is no signature to verify, so securing an Event Grid endpoint is two jobs: answer the right handshake, and check a credential on every delivery.

The two handshakes

Which one fires depends on the delivery schema, and they are alternatives rather than a sequence.

Event Grid schema sends a POST whose body is a single-element array containing a Microsoft.EventGrid.SubscriptionValidationEvent. Echo data.validationCode back:

const VALIDATION = "Microsoft.EventGrid.SubscriptionValidationEvent";

app.post("/webhooks/azure-event-grid", express.json(), (req, res) => {
  const events = Array.isArray(req.body) ? req.body : [req.body];

  // Only validate subscriptions you created. Otherwise anyone who learns this
  // URL can point their own subscription at it and self-validate.
  const subscription = req.get("aeg-subscription-name");
  if (!EXPECTED_SUBSCRIPTIONS.includes(subscription)) return res.sendStatus(403);

  const validation = events.find((e) => e.eventType === VALIDATION);
  if (validation) {
    // Must be 200. Event Grid does not accept 202 for the handshake.
    return res.status(200).json({ validationResponse: validation.data.validationCode });
  }

  res.sendStatus(200); // acknowledge fast
  events.forEach((e) => queue.add(e)); // branch on eventType, dedupe on id, async
});

CloudEvents v1.0 replaces that with an HTTP OPTIONS abuse-protection preflight. Consent is signalled by the response headers, not the status code:

app.options("/webhooks/azure-event-grid", (req, res) => {
  const origin = req.get("WebHook-Request-Origin");
  if (!origin) return res.sendStatus(400);

  res.set("WebHook-Allowed-Origin", origin); // or "*"
  res.set("WebHook-Allowed-Rate", "120");
  res.sendStatus(200);
});

Authenticating each delivery

The handshake proves you own the endpoint once. It does nothing about someone replaying requests at your URL afterwards, which is why you also need a credential. The simplest is a static delivery-property header you set on the subscription and compare in constant time:

import hmac
import os

# Comma-separated so you can accept the old and new secret during rotation.
ACCEPTED = [s for s in os.environ["EVENT_GRID_SECRETS"].split(",") if s]

def check_secret(received: str) -> bool:
    if not ACCEPTED:
        return False  # fail closed: "unset" must never mean "allow anything"
    # No early return, so timing doesn't reveal which secret matched.
    matched = False
    for candidate in ACCEPTED:
        if hmac.compare_digest(received or "", candidate):
            matched = True
    return matched

hmac.compare_digest here is a constant-time string comparison, not an HMAC. Nothing is being hashed, because there is nothing to hash.

Make Azure Event Grid webhooks production-ready. Hookdeck Event Gateway answers the handshake, checks your credential, deduplicates, and durably queues every Azure event.

Azure Event Grid webhook limitations and pain points

There is no signature

The Problem: Developers reach for a familiar HMAC verification template and write a crypto.createHmac check against a header Event Grid never sends. The handler then rejects every delivery, or silently accepts anything.

Why It Happens: Almost every other webhook provider signs its payloads, so the absence is easy to miss.

Workarounds:

  • Don't write a signature verifier. Answer the handshake and check a configured credential instead.

How Hookdeck Can Help: Hookdeck supports Event Grid's authentication methods without pretending there's a signature to check.

202 acknowledges a delivery but fails the handshake

The Problem: Event Grid treats 200, 201, 202, 203 and 204 as successful deliveries, but the validation response must be 200. A handler that returns 202 everywhere passes deliveries and never completes validation.

Why It Happens: The two paths have different rules, and Microsoft documents them on separate pages.

Workarounds:

  • Return 200 specifically for the validation response, within 30 seconds.

How Hookdeck Can Help: Hookdeck answers the handshake with the exact response Event Grid expects.

Handshake failure is total and silent

The Problem: If validation never completes, the subscription is never activated and no event is ever sent. There's no failed delivery in any log, because nothing was delivered. You're debugging an absence.

Why It Happens: Validation gates activation, so a broken handshake produces silence rather than errors.

Workarounds:

  • Check the subscription's provisioning state. AwaitingManualAction means you returned 200 without echoing the code; GET the validationUrl within 10 minutes to finish manually. That URL uses port 553, which firewalls routinely block.

How Hookdeck Can Help: Hookdeck completes the handshake for you, and every delivery attempt is visible in the request log.

Two payload shapes, two discriminators

The Problem: Event Grid schema delivers a JSON array keyed on eventType. CloudEvents delivers a single object keyed on type. A handler written for one breaks on the other, and the choice is per-subscription configuration rather than a default you can assume.

Why It Happens: Event Grid supports both its own proprietary format and the CloudEvents standard.

Workarounds:

  • Normalise both shapes on the way in, and always loop, because batching can put up to 5,000 events in one array.

How Hookdeck Can Help: Hookdeck's transformations normalise either shape before your service sees it.

Rotating a query-parameter secret drops deliveries

The Problem: If you carry the secret in the endpoint URL's query string, updating your service and the subscription are two separate steps. Deliveries fail in the gap.

Why It Happens: The secret lives in the subscription's endpoint URL, which only Azure can change.

Workarounds:

  • Accept the old and new secret concurrently, update the subscription, then retire the old one. Note that query parameters aren't returned when you read a subscription back unless you pass --include-full-endpoint-url.

How Hookdeck Can Help: Hookdeck holds the credential at the edge, so rotating it doesn't touch your application.

Best practices

Answer the handshake your delivery schema actually uses

Implement both paths if you support both schemas, and gate them on aeg-subscription-name so an attacker who learns your URL can't self-validate.

Return 200 for validation, and acknowledge fast

Event Grid waits 30 seconds for a response before queueing a retry. Return quickly and defer work to a queue. See why to process webhooks asynchronously.

Authenticate every delivery, and fail closed

Compare your credential in constant time, and reject when it's unconfigured rather than accepting anything.

Dedupe on the event id

Delivery is at-least-once and unordered, so duplicates and out-of-order events are normal. aeg-delivery-count tells you which attempt you're on. See our guide to webhook idempotency.

Conclusion

Azure Event Grid webhooks have no payload signature. Security rests on completing the right endpoint validation handshake, either the SubscriptionValidationEvent echo for Event Grid schema or the CloudEvents OPTIONS preflight, and then authenticating every delivery with a header, query-parameter secret or Entra ID token you configure yourself. Return 200 for validation specifically, guard on aeg-subscription-name, handle both payload shapes, and dedupe on the event id.

Hookdeck answers the handshake, checks your credential, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

Get started with Hookdeck for free and handle Azure Event Grid webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.