Microsoft Graph Webhook Validation: Return validationToken
Pass the Microsoft Graph webhook validation handshake by returning validationToken as text/plain, then verify clientState and forward change notifications.
Microsoft Graph validates every webhook notification URL before it creates a subscription. It sends an HTTP POST with validationToken in the query string, and your endpoint must return the URL-decoded token as the complete text/plain response within ten seconds.
An ordinary webhook receiver that returns { "ok": true } will fail this handshake. A Webhook Relay input function can answer it at the public edge while forwarding real Microsoft Graph change notifications to your public or private application.
The Microsoft Graph validation request
The request looks like this:
POST /your-webhook?validationToken=Validation%3A+Testing+client+application HTTP/1.1
The successful response must look like:
HTTP/1.1 200 OK
Content-Type: text/plain
Validation: Testing client application
Microsoft's change-notification delivery documentation specifies the POST, 200, text/plain, decoded token and ten-second deadline. Treat the token as opaque text. Do not wrap it in JSON, add quotes, or return the encoded query value.
1. Create the public input
Create a Webhook Relay bucket and copy its HTTPS input URL. Add your application endpoint as an output:
- For a public API, use a public destination.
- For
localhostor a private service, use the relay agent.
The same stable input URL handles both the one-time validation request and later notifications.
2. Answer validationToken in a JavaScript function
Create a function and attach it to the bucket input, not the output:
const validationToken = r.query.validationToken
if (r.method === "POST" && validationToken) {
r.setResponseStatus(200)
r.setResponseHeader("Content-Type", "text/plain")
r.setResponseBody(validationToken)
r.stopForwarding()
return
}
r.query.validationToken is the parsed, URL-decoded query value. The function returns it directly and stops the validation request from reaching the downstream application. Requests without the token continue to the normal outputs.
Do not use the validation token as a persistent secret. It proves that Microsoft Graph can reach and read the callback response; it is not the value used to authenticate subsequent notifications.
3. Create the Graph subscription
A subscription request varies by resource, but the key webhook fields look like this:
{
"changeType": "created,updated",
"notificationUrl": "https://example.hooks.webhookrelay.com",
"resource": "/users/{user-id}/messages",
"expirationDateTime": "2026-09-07T10:00:00Z",
"clientState": "a-random-secret-value"
}
Use an expiration time supported by the selected resource and renew the subscription before it expires. Store clientState as a secret generated for this subscription.
When Graph creates the subscription, open the Webhook Relay request log. You should see the validation POST with a 200 plain-text response. If subscription creation fails, compare the recorded response body byte-for-byte with the decoded query value.
4. Validate real change notifications
The notification body contains a value array. Each notification should have the clientState you supplied during subscription creation:
const payload = JSON.parse(r.body)
const expectedClientState = cfg.get("GRAPH_CLIENT_STATE")
const notifications = Array.isArray(payload.value) ? payload.value : []
const valid = notifications.length > 0 && notifications.every(function (item) {
return item.clientState === expectedClientState
})
if (!valid) {
r.setResponseStatus(403)
r.setResponseBody("invalid clientState")
r.stopForwarding()
return
}
Add GRAPH_CLIENT_STATE as a function configuration value. Do not hard-code the real value in shared source.
The validation check can live in the same input function after the handshake branch. Your application should still authorize resource access independently and follow Microsoft guidance for validating tokens included with rich notifications.
5. Respond quickly and process asynchronously
Microsoft Graph considers a 2xx response within three seconds a timely delivery for ordinary notifications. If processing may take longer, acknowledge the event and put the work on a queue. The Microsoft documentation describes 202 Accepted for queued processing and retries for slow or unsuccessful endpoints.
Avoid doing slow directory, mail or database calls before acknowledging the notification. Webhook Relay can forward quickly, but a synchronous destination still controls total response time.
Troubleshooting
Subscription creation returns InvalidRequest
Inspect the validation request. Confirm the input function is attached, the method is POST, and the body is exactly the decoded token.
The response is JSON
JSON.stringify(validationToken) adds quotes and fails validation. Pass the token string directly to r.setResponseBody and set Content-Type to text/plain.
The token contains spaces or punctuation
Use r.query.validationToken, which gives the parsed query value. Do not copy a percent-encoded value from r.rawQuery into the response.
Validation works, but notifications are rejected
Compare the configured GRAPH_CLIENT_STATE with every item in the notification array. Also confirm the output function was not attached to the input by mistake and that the application accepts batched notifications.
Notifications arrive more than once
Design the consumer to be idempotent. Use the subscription ID plus resource data or another stable event identity to avoid applying the same change twice. See webhook retries and idempotency.
Microsoft Graph is one example of a provider validation handshake. Meta uses a different GET challenge flow, covered in the WhatsApp Cloud API webhook guide. For reusable response patterns, see the transformation cookbook.
