• Alerts
  • Node.js
  • Getting started

Microsoft Teams webhook in Node.js: send alerts with Workflows and Adaptive Cards

Send alerts with a Microsoft Teams webhook from Node.js after the connector retirement: create a Workflows URL, post Adaptive Cards and catch silent failures.

Author
Extenify Team
Published
Reading time
12 min read
Four vintage doorbell buttons mounted in a column on a weathered wall beside a brown wooden door frame

A Microsoft Teams webhook used to be the easiest alert channel there was: add the Incoming Webhook connector to a channel, copy a webhook.office.com URL and POST some JSON. That connector is gone. Microsoft finished retiring Office 365 Connectors in Teams in May 2026, and every webhook now runs through the Workflows app, which is built on Power Automate and behaves differently in ways that matter for monitoring.

This guide shows how to send alerts to a Microsoft Teams webhook from Node.js today: how to create the Workflows URL, the exact payload it expects, a sender with retries and size checks, how to migrate old MessageCard payloads, and the failure modes that make a Teams alert disappear without an error. Everything here was checked against Microsoft's documentation on October 15, 2026.

Key takeaways

  • Office 365 Connectors in Teams were switched off between May 18 and May 22, 2026. Old webhook.office.com URLs no longer post anything.
  • Create the replacement in Teams with Workflows, using a template such as "Send webhook alerts to a channel". The URL is a Power Platform address, not an Office one.
  • Send {"type": "message", "attachments": [...]} with an Adaptive Card inside. A plain {"text": "..."} body only works if your flow is built to read it.
  • A successful response means the flow accepted the request, not that a message appeared. Check the flow's run history when alerts go missing.
  • A workflow belongs to a person, can be turned off after 14 days of errors or 90 days without a trigger, and stops posting if its owner leaves. Add a co-owner.

What happened to the Microsoft Teams incoming webhook

Microsoft announced the retirement in July 2024 and extended it several times. The official retirement post on the Microsoft 365 Developer Blog carries the whole timeline:

Date What changed
August 15, 2024 Creating new connectors was blocked
January 31, 2025 Existing connector URLs had to be updated to keep posting
February 5, 2026 Workflows gained MessageCard support and posting to shared channels; deadline moved to April 30, 2026
April 14, 2026 Final notice: connectors "will be progressively disabled"
May 18 to 22, 2026 Rollout of the shutdown; "After these dates, Office 365 Connectors will no longer function."

A second change hit early Workflows adopters. Power Automate moved HTTP and Teams webhook trigger URLs off the logic.azure.com domain to Power Platform hosts under api.powerplatform.com, and reports of the change put the end of the old URLs at November 30, 2025. If a Teams alert you set up in 2024 went quiet, check which of the two URLs it uses before anything else.

Developers are not thrilled about the new model. A Hacker News commenter summed it up in April 2026: "Why do I have to create a weird 'workflow' to receive webhooks in a chat channel?" The answer is that a workflow is a small automation with an owner, permissions and a run history, and once you treat it that way it is reliable enough for alerts.

Create a Microsoft Teams webhook with Workflows

Microsoft's guide to incoming webhooks in Teams describes the template route:

  1. In Teams, open the channel that should receive alerts and select More options (...) next to it.
  2. Select Workflows and search for a webhook template. For a channel, the templates include Send webhook alerts to a channel, Send webhook alerts from specific people to a channel and Send webhook alerts from people in an org to a channel. Matching templates exist for chats.
  3. Pick the team and channel, then select Save.
  4. Copy the webhook URL from the workflow's details page.

According to Microsoft Support, "Using these templates and the Teams webhook trigger does not require a premium license to use."

The template you choose sets who may call the URL. The Teams connector reference explains that the trigger can accept requests from anyone, from any user in your tenant, or only from specific users. For a server that sends alerts, Anyone is the practical choice, and the reference warns: "Do not pass an authentication token header if you selected the 'Anyone' authentication option, or POST requests to the trigger will fail." The other two options require an access token with every request, which a monitoring script usually does not have.

With Anyone, the URL itself is the credential. Treat it like an API key: keep it in an environment variable or secret store, never in client code or a public repository, and recreate the workflow if it leaks. A Stack Overflow question asks exactly this, "Must MS Teams workflow URL be kept secure?", and the accepted answer compares it to a bearer token: whoever holds it can post.

The payload a Teams Workflows webhook expects

The When a Teams webhook request is received trigger documents one body format for cards. The type must be message, and attachments holds one or more Adaptive Cards:

{
  "type": "message",
  "attachments": [
    {
      "contentType": "application/vnd.microsoft.card.adaptive",
      "contentUrl": null,
      "content": {
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "type": "AdaptiveCard",
        "version": "1.2",
        "body": [
          { "type": "TextBlock", "text": "New 1-star review on Chrome", "weight": "Bolder", "wrap": true }
        ]
      }
    }
  ]
}

The stock templates loop over attachments and post each card. Microsoft's quick example posts {"text": "Hello from a webhook workflow!"}, but that only renders if your flow reads a text field; with a template, community threads report failed runs complaining that attachments is null. Send the envelope above and you avoid the problem.

Adaptive Cards give you a richer layout than the old connector cards. For alerts, three elements cover almost everything: TextBlock for the headline and summary, FactSet for label and value pairs such as "Rating: 4.6 to 4.3", and an Action.OpenUrl button that links to the dashboard or listing. Microsoft's overview of cards in Teams notes that all native Adaptive Card elements except Action.Submit work with incoming webhooks. The Adaptive Cards documentation hub includes a designer for previewing a card before you code it.

Send a Microsoft Teams webhook alert from Node.js

The sender below uses the built-in fetch in Node.js 18 and later, so it needs no dependencies. It builds the card, refuses payloads above the size limit, and retries on throttling and server errors.

// teams-alert.mjs
const MAX_BYTES = 28 * 1024; // Teams fails messages above roughly 28 KB

export function buildAlertCard({ title, summary, facts = [], url, linkLabel = 'Open' }) {
  const body = [{ type: 'TextBlock', text: title, weight: 'Bolder', size: 'Medium', wrap: true }];
  if (summary) body.push({ type: 'TextBlock', text: summary, wrap: true, isSubtle: true });
  if (facts.length) {
    body.push({
      type: 'FactSet',
      facts: facts.map(([label, value]) => ({ title: label, value: String(value) })),
    });
  }
  const card = {
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    type: 'AdaptiveCard',
    version: '1.2',
    body,
  };
  if (url) card.actions = [{ type: 'Action.OpenUrl', title: linkLabel, url }];
  return {
    type: 'message',
    attachments: [{ contentType: 'application/vnd.microsoft.card.adaptive', contentUrl: null, content: card }],
  };
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function sendTeamsAlert(webhookUrl, payload, { attempts = 4 } = {}) {
  const body = JSON.stringify(payload);
  const size = Buffer.byteLength(body);
  if (size > MAX_BYTES) throw new Error(`Teams payload is ${size} bytes, keep it under ${MAX_BYTES}`);

  for (let attempt = 1; attempt <= attempts; attempt++) {
    let res;
    try {
      res = await fetch(webhookUrl, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body,
        signal: AbortSignal.timeout(10_000),
      });
    } catch (err) {
      if (attempt === attempts) throw err;
      await sleep(2 ** attempt * 500);
      continue;
    }
    if (res.ok) return { status: res.status }; // accepted by the flow, not yet posted
    const retryable = res.status === 429 || res.status >= 500;
    if (!retryable || attempt === attempts) {
      const detail = (await res.text().catch(() => '')).slice(0, 300);
      throw new Error(`Teams webhook failed with ${res.status}: ${detail}`);
    }
    const retryAfter = Number(res.headers.get('retry-after'));
    await sleep(retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500);
  }
}

Using it for a store alert looks like this:

import { buildAlertCard, sendTeamsAlert } from './teams-alert.mjs';

const card = buildAlertCard({
  title: 'Rating dropped on the Chrome Web Store',
  summary: 'Three 1-star reviews since yesterday mention the 4.2.0 update.',
  facts: [
    ['Rating', '4.6 to 4.4'],
    ['New reviews', 5],
    ['Version', '4.2.0'],
  ],
  url: 'https://chromewebstore.google.com/detail/your-extension-id',
  linkLabel: 'Read the reviews',
});

await sendTeamsAlert(process.env.TEAMS_WEBHOOK_URL, card);

Three details are deliberate. Timeouts and network errors are retried, which can produce a duplicate card if the first request did reach Teams, so include something unique such as the date or version in the title to make duplicates obvious. Client errors other than 429 are not retried, because a malformed card or a deleted workflow will fail the same way every time. And the size check runs before the request: Microsoft's documentation says that when a message exceeds about 28 KB the action fails with "Request Entity too large", which in a workflow shows up as a failed run rather than an error in your code.

Migrate an old MessageCard payload

Code written for the retired connector usually sends a MessageCard with @type, sections and potentialAction. Since February 2026, Workflows accept MessageCard payloads, so the old JSON may post, but the retirement post lists the catch: "Interactive cards are not supported for MessageCard-formatted payloads." Buttons simply do not render.

Converting is mostly a mapping exercise:

MessageCard Adaptive Card
title, text TextBlock elements
sections[].facts (name, value) FactSet with facts (title, value)
potentialAction with OpenUri actions with Action.OpenUrl
themeColor No direct equivalent; use a Container with a style such as attention
summary No direct equivalent

The URL changes too. Any code that validates webhook hosts by checking for webhook.office.com or office.com will reject a Workflows URL, so update the allowlist along with the payload. The Stack Overflow thread "Converting Teams Webhook with payload type MessageCard" shows how many teams hit this in the same week.

Why a Teams webhook can accept a request and post nothing

The trigger answers as soon as the flow accepts the request, and the posting happens afterwards inside Power Automate. If the card is invalid, the channel no longer exists or the flow lacks permission, your code sees success and the channel sees nothing. When alerts go missing, open the workflow in the Workflows app and read its run history; a failed run shows the step and the error.

The flow itself can also stop. Power Automate's limits documentation lists two rules that matter for alerting:

  • Flows with errors: "A cloud flow that has a trigger or actions that fail continuously is turned off" after 14 days. A sender that keeps posting a broken card will eventually switch its own channel off.
  • Flows without trigger activity: "A cloud flow that isn't triggered within a 90 day period might be turned off." Owners and co-owners are notified 30 days before, and flows owned by users with premium or capacity licenses are exempt. An alert channel for rare events, such as store takedowns, can hit this. A weekly summary sent through the same workflow keeps it alive.

Ownership is the third trap. Microsoft notes that "Workflows are linked only to specific users" and can become orphaned when the owner leaves. Add a co-owner when you create the workflow, ideally a shared admin account.

Limits to design around

Limit Value Source
Message size About 28 KB per message Teams connector reference
Request rate More than four requests per second is throttled Incoming webhook guide
Trigger calls 4,500 invoke calls per 5 minutes on the Low tier, 45,000 on others Teams connector reference
Mentions 20 users and 20 tags per message Teams connector reference
Private channels Posting a card to a private channel is listed as not supported Teams connector reference

The private channel row is in flux. The retirement post said private channel selection would roll out by April 20, 2026, while the connector reference still lists posting to private channels as unsupported, and Microsoft's guide says the Flow bot cannot post in private channels yet but a workflow can post on behalf of a user. Test with the channel you plan to use. That choice also explains a common question, "How to change the poster's name": the Flow bot posts as "via Workflows", while the user option posts under the owner's name. Custom bot names and icons are not available for webhook posts.

For a monitoring system the rate limits mean one thing: send digests, not streams. Group the day's changes into one card per product, the approach we recommend in store change alerts your team will actually read, and you will never come near them.

How Teams compares with other alert channels

If you send the same alerts to several places, the differences are worth knowing:

Channel Setup Format Main gotcha
Microsoft Teams Workflows template, owned by a user Adaptive Card in a message envelope Silent failures and flows that get turned off
Slack Incoming webhook in a Slack app Block Kit One webhook per channel
Discord Channel integration Embeds Rate limits per webhook, accidental pings
Telegram Bot token and chat ID HTML or Markdown text Finding the chat ID

Our guides cover the others in the same style: Discord webhooks in Node.js, Telegram bot notifications and App Store review alerts in Slack.

What to send to Teams

A webhook is only as useful as the alerts behind it. For extension and app teams, the changes worth a Teams card are the ones in our weekly Chrome extension metrics scorecard: a rating drop, a burst of low-star reviews, a release that is not reaching users, a takedown. Mobile teams can add crash and ANR rates from the Google Play Developer Reporting API to the same card.

If you would rather not build the collection side, Extenify handles extension review monitoring across the Chrome Web Store, Edge Add-ons, Firefox Add-ons, Google Play and the App Store. It captures public store listing data every day and sends one digest of what changed to email, Slack, Discord or Telegram. Every plan starts with a 7-day free trial, and the pricing page lists what each plan includes.

Frequently asked questions

Do Office 365 connector webhooks still work in Teams?

No. Microsoft disabled Office 365 Connectors in Teams in a rollout from May 18 to May 22, 2026, and said that after those dates they "will no longer function." Replace each one with a Workflows webhook.

Why does my Teams webhook return success but nothing shows up in the channel?

The trigger accepts the request before the flow posts the card. Open the workflow's run history to see the failed step. The usual causes are a body without the attachments array, an invalid Adaptive Card, a deleted channel or a flow that was turned off.

Do I need a Power Automate premium license for a Teams webhook?

No. Microsoft says the webhook templates and the Teams webhook trigger do not require a premium license. A premium license does exempt the flow from being turned off after 90 days without a trigger.

Can a Teams Workflows webhook post to a private channel?

Microsoft's documentation is inconsistent. The connector reference lists posting to private channels as not supported, and the incoming webhook guide says the Flow bot cannot post there yet while posting as a user can. Test with your channel before relying on it.

How do I stop messages showing "via Workflows"?

Choose the option to post as a user instead of the Flow bot when you set up the workflow. The message then appears under the owner's name. Custom names and icons for webhook posts are not available.

Is a Teams workflow URL a secret?

Yes, when the trigger allows anyone to call it. Anyone with the URL can post to your channel, so store it like an API key and recreate the workflow if it leaks.

Summary

The Microsoft Teams webhook still works for alerts, but it is now a Workflows flow rather than a connector. Create it from a webhook template, keep the URL secret, send a message envelope with Adaptive Cards, and retry only throttling and server errors. Then design around what a flow is: check the run history when alerts go quiet, keep the flow triggered at least every few weeks, add a co-owner, and send digests instead of a stream of single events.

Image credits

Put every store in one dashboard tonight.

Sign in with Google, paste one store link, and your unified view is live in under a minute. Then 7 days of Pro free, no credit card required, and you never touch your extension's code.

Start free trial