errno

Teams incoming webhook returns HTTP 200 but no message appears

· tested on Microsoft Teams, retired O365 connector webhook, acme.sh notify hook, curl 8.x

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:

teams microsoft365 webhook monitoring