Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for Microsoft Teams By James Joyner IV · · 8 min read Last reviewed Jul 2026

Microsoft Teams Error: 'Subscription validation request failed' — Cause, Fix, and Troubleshooting Guide

Quick answer

Fix the Microsoft Graph 'Subscription validation request failed' (HTTP 400): echo the raw decoded validationToken as text/plain with a 200 and no extra bytes.

  • #microsoft-teams
  • #troubleshooting
  • #errors
  • #graph-api
Free toolkit

Stuck on this Microsoft Teams error? Get the free incident triage checklist

A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.

What this error means

When you create a Microsoft Graph change-notification subscription with POST /subscriptions, Graph first sends a validation request to your notificationUrl to prove you own the endpoint. That request includes a validationToken query parameter, and your endpoint must respond 200 OK with Content-Type: text/plain and a body equal to the exact decoded validationToken — nothing more. If the response content or format is wrong, subscription creation fails with HTTP 400 and the message Subscription validation request failed.

The literal error body from the create call:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": {
    "code": "InvalidRequest",
    "message": "Subscription validation request failed. ..."
  }
}

The validation request Graph sends to your endpoint looks like this:

POST {notificationUrl}?validationToken=Validation%3A+TestToken...
Content-Type: text/plain
Content-Length: 0

And the only response Graph accepts:

HTTP/1.1 200 OK
Content-Type: text/plain

Validation: TestToken...

This guide covers the content/format mismatch: wrong body, JSON wrapping, leftover URL-encoding, extra whitespace, or the wrong Content-Type. It is distinct from the validation timeout case (endpoint too slow to answer), which is covered separately.

Where it surfaces

  • POST /subscriptions returns 400 with a message containing Subscription validation request failed.
  • Your endpoint logs show a validationToken query param arriving but creation still fails.
  • The endpoint returns 200, yet Graph rejects it — pointing at the response body or headers.
  • Validation succeeds with a raw handler but fails once routed through a JSON API framework.
  • The echoed value differs from the token because it is still URL-encoded or JSON-wrapped.
  • Manual curl replays of the token echo look right visually but include a trailing newline or charset-only content type.

Identity and permission causes

  • JSON-wrapped response. A framework serializes the string to {"value":"..."} or adds quotes; Graph needs the raw token text, not JSON.
  • Wrong Content-Type. Responding application/json (or omitting text/plain) fails validation even when the body text is correct.
  • URL-encoded token echoed back. Returning the token still percent-encoded (for example Validation%3A) instead of the decoded value.
  • Extra bytes. A trailing newline, leading whitespace, a BOM, or surrounding quotes make the body unequal to the expected token.
  • Non-200 status. Auth middleware, a redirect, or a 401/302 on the validation POST prevents the 200 OK Graph requires.
  • Missing the validation branch. Treating the validation POST as a normal notification (parsing a JSON body that is empty) and returning the wrong thing.

Tracing the failed authorization

Log the raw query string and the exact bytes you return. Verify the response has status 200, Content-Type: text/plain, and a body byte-for-byte equal to the decoded token.

Replay the validation handshake with curl, sending a known token and inspecting the response headers and body precisely:

TOKEN='Validation: TestToken 12345'
curl -si -X POST \
  "https://your.endpoint.example.com/notifications?validationToken=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))' "$TOKEN")"

Check three things in the output: the status line is HTTP/1.1 200, the content-type is text/plain (a bare charset addition is fine, application/json is not), and the body exactly equals $TOKEN. Confirm there are no stray bytes:

curl -s -X POST "https://your.endpoint.example.com/notifications?validationToken=$(...)" | xxd | tail

A trailing 0a (newline), leading spaces, 22 quote bytes, or a leading ef bb bf BOM all indicate extra bytes that will fail the equality check.

Resolution

Echo the raw, decoded validationToken as text/plain with a 200 and no extra bytes. Detect the validation request by the presence of the validationToken query parameter and short-circuit before any JSON/notification handling.

Node.js (Express):

app.post("/notifications", (req, res) => {
  const token = req.query.validationToken;
  if (token) {
    res.set("Content-Type", "text/plain");
    return res.status(200).send(token); // raw decoded token, nothing else
  }
  // ...handle real notifications, respond 202...
});

C# (ASP.NET Core):

[HttpPost("/notifications")]
public IActionResult Notifications([FromQuery] string validationToken)
{
    if (!string.IsNullOrEmpty(validationToken))
        return Content(validationToken, "text/plain"); // no JSON wrapping, 200
    // ...process notifications, return Accepted()...
}

Do not JsonConvert/res.json() the token, do not add quotes, and do not append a newline. Read the framework-decoded query value (do not re-decode or re-encode it). Make sure auth middleware allows the unauthenticated validation POST through so it returns 200, and that the endpoint answers well within the validation window. Once the echo is correct, re-issue POST /subscriptions.

Hardening access

  • Raw text only. The body must equal the decoded token exactly — no JSON envelope, no quotes.
  • text/plain is required. application/json fails even with the right characters.
  • Zero extra bytes. Trailing newlines, BOMs, and leading/trailing whitespace break equality.
  • Use the decoded value. Frameworks decode the query param for you; echoing the still-encoded string fails.
  • Let the validation POST through. Auth gates or redirects that block a 200 cause the failure just as a wrong body would.
  • Different from the timeout case. If the token is correct but the endpoint is slow, you get the validation timeout failure instead — fix latency there, content here.
Free download · 368-page PDF

Fixed it? Get 500 Microsoft Teams & DevOps AI prompts — free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.

Did this fix your issue?

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.