Teams incoming webhook returns HTTP 200 but no message appears
Symptom
A monitoring or automation job posts to a Teams incoming webhook. The client is happy:
$ curl -sS -i -H 'Content-Type: application/json' \
-d '{"text":"certificate renewed"}' \
'https://example.webhook.office.com/webhookb2/…'
HTTP/1.1 200 OK
acme.sh reports send success. Nothing arrives in the channel. No error, no bounce, no entry anywhere.
The proof is in the response headers
Look past the status line:
x-nanoproxy: 1
x-proxyerrorhresult: 0x80070036
x-proxyerrormessage: The network is busy.
That is not a transient condition despite what it says. Microsoft retired Office 365 connectors, and the
endpoints now answer 200 at a proxy while discarding the payload. Any client that only checks the status
code will report success forever, which is why this failure survives in cron jobs and alerting scripts for
months.
If you see those three headers, stop debugging your JSON. The endpoint is dead.
Fix: Power Automate “Workflows” webhook
In the target Teams channel: … → Workflows → “Post to a channel when a webhook request is received”.
You get a URL on *.powerplatform.com/powerautomate/... or *.logic.azure.com. A successful POST there
answers 202 Accepted, not 200 — a useful signal that you are hitting the new stack.
The trigger expects an Adaptive Card, not the old MessageCard:
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.4",
"body": [
{ "type": "TextBlock", "text": "certificate renewed", "wrap": true }
]
}
}
]
}
Send the old {"text":"…"} body to a Workflows URL and you get an accepted request with an empty card, or
a flow run failure — depending on how the trigger was configured. Check the run history in Power Automate
when a message does not show up; unlike the retired connector, this stack actually tells you what happened.
If you use acme.sh
The bundled notify/teams.sh hook already sends the Adaptive Card format, so you do not need to patch it —
only repoint it at the new URL:
export TEAMS_WEBHOOK_URL='https://…powerplatform.com/powerautomate/…'
acme.sh --set-notify --notify-hook teams
Then force a test notification and confirm it lands in the channel. Do not trust the exit code alone; that is exactly what hid the breakage in the first place.
Make this class of failure visible
The generalisable lesson is that 200 from a proxy is not delivery confirmation. For any notification
path that matters:
- assert on the expected status (
202here), not on “not an error” - log the response headers of failures at least once a day’s worth of runs, so a silent migration shows up
- add one end-to-end canary that a human sees — a daily message to a low-noise channel beats discovering three months later that alerting has been mute.