Migrating your automated webhook systems from Office 365 Connectors to Power Automate Workflows

Way back in 2016 I wrote about sending Application Insights alerts to Microsoft Teams using incoming webhooks. That was one of the first things I built when Teams launched and it’s been humming along in various forms ever since. Then in 2023 I went deeper and showed you how to authenticate those webhooks because the anonymous nature of those connector URLs was, well, a security problem. Now Microsoft is pulling the plug. Office 365 Connectors are retiring on April 30, 2026, and every migration guide I’ve seen focuses on the admin experience: clicking through Power Automate, selecting a template, done. But what about those of us who have actual code that POSTs to those URLs? What about the Azure Functions, the Logic Apps, the little C# services that have been faithfully sending MessageCards for years? That’s what we’re going to cover here.
What actually changes
First off, the old outlook.office.com/webhook/ URLs simply stop working. Gone. Dead. Your HTTP POSTs will start returning errors and your monitoring alerts won’t show up in Teams anymore. The replacement is a Power Automate Workflow with an HTTP trigger, and the URL looks completely different. Instead of the old connector format you’ll get something that looks like a Logic Apps URL, because that’s essentially what it is under the hood.
But the URL change isn’t even the biggest gotcha. The payload format changes entirely. The old connectors accepted Office 365 Connector Cards, also known as MessageCards. These were that JSON format with @type: "MessageCard" and @context: "http://schema.org/extensions" that we all got used to. The new Workflow webhooks expect Adaptive Cards wrapped in a specific envelope. If you just swap the URL and keep the same payload, you’ll get a 400 Bad Request and wonder what went wrong. I know this because that’s exactly what I did first.
The old code
Here’s what the code looked like in my alerting service. If you followed along with my earlier posts, this should look familiar:
public async Task SendAlertToTeamsLegacy(AlertPayload alert)
{
var connectorUrl = "https://outlook.office.com/webhook/your-old-connector-guid";
var legacyCard = new
{
@type = "MessageCard",
@context = "http://schema.org/extensions",
themeColor = alert.Severity == "Critical" ? "FF0000" : "FFD700",
summary = $"Alert: {alert.RuleName}",
sections = new[]
{
new
{
activityTitle = alert.RuleName,
facts = new[]
{
new { name = "Resource", value = alert.ResourceName },
new { name = "Severity", value = alert.Severity },
new { name = "Triggered at", value = alert.FiredAt.ToString("u") }
},
markdown = true
}
}
};
using var client = new HttpClient();
await client.PostAsJsonAsync(connectorUrl, legacyCard);
}
Simple. Clean. A flat JSON object, POST it to the URL, done. We didn’t even check the response because it just worked. Those were simpler times.
The new code
Now here’s what the same thing looks like targeting a Power Automate Workflow webhook. The Workflow needs to be set up in Power Automate first (pick the “When a Teams webhook request is received” template) but I’m going to skip that part because every other blog post on the internet covers it. Let’s focus on the code.
public async Task SendAlertToTeamsWorkflow(AlertPayload alert)
{
var workflowUrl = "https://prod-xx.westeurope.logic.azure.com:443/workflows/your-workflow-id/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=your-sig";
var adaptiveCard = new
{
type = "message",
attachments = new[]
{
new
{
contentType = "application/vnd.microsoft.card.adaptive",
contentUrl = (string?)null,
content = new
{
type = "AdaptiveCard",
version = "1.4",
body = new object[]
{
new
{
type = "TextBlock",
size = "Medium",
weight = "Bolder",
text = $"⚠ {alert.RuleName}",
style = "heading"
},
new
{
type = "FactSet",
facts = new[]
{
new { title = "Resource", value = alert.ResourceName },
new { title = "Severity", value = alert.Severity },
new { title = "Triggered", value = alert.FiredAt.ToString("u") }
}
}
},
msteams = new { width = "Full" }
}
}
}
};
using var client = new HttpClient();
var response = await client.PostAsJsonAsync(workflowUrl, adaptiveCard);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException(
$"Workflow webhook returned {response.StatusCode}: {body}");
}
}
There’s a lot more nesting going on. The Adaptive Card lives inside an attachments array, which lives inside a message envelope. You need to set the contentType to application/vnd.microsoft.card.adaptive or it won’t render. And notice I’m actually checking the response now. The Workflow endpoint gives you proper HTTP status codes and error messages, which is honestly an improvement over the old connector that would silently swallow malformed payloads.
The msteams property with width: "Full" is optional but I recommend it. Without it your card renders in a narrow column and wastes half the screen.
A helper for bulk migration
If you’re like me and you have MessageCard payloads scattered across multiple services, here’s a little conversion helper I wrote to make the migration less painful. This way you can centralize the Adaptive Card wrapping logic and just swap it in wherever you were building MessageCards before:
public static object ConvertMessageCardToAdaptive(string title, string themeColor,
IEnumerable<(string Name, string Value)> facts)
{
return new
{
type = "message",
attachments = new[]
{
new
{
contentType = "application/vnd.microsoft.card.adaptive",
contentUrl = (string?)null,
content = new
{
type = "AdaptiveCard",
version = "1.4",
body = new object[]
{
new
{
type = "Container",
style = themeColor == "FF0000" ? "attention" : "warning",
items = new object[]
{
new { type = "TextBlock", text = title, weight = "Bolder", size = "Medium" }
}
},
new
{
type = "FactSet",
facts = facts.Select(f => new { title = f.Name, value = f.Value }).ToArray()
}
}
}
}
}
};
}
It maps the old themeColor hex values to Adaptive Card container styles, so attention for red and warning for yellow. It’s not a perfect 1:1 mapping since Adaptive Cards don’t support arbitrary hex colors the way MessageCards did, but for alert scenarios it gets the job done.
What about authenticated webhooks
If you read my post on authenticated webhooks, you might be wondering what happens to that pattern. The good news is that the bot-proxy approach I described still works, and that’s a completely separate mechanism from Office 365 Connectors. The bot receives messages and posts them to channels using the Bot Framework, so the connector retirement doesn’t affect it at all.
Now, here’s an interesting twist. The new Workflow URLs are actually more secure out of the box than the old connector URLs. Remember how I showed you in that post that the old webhook URLs had predictable GUIDs? The Team ID, the tenant ID, all right there in the URL, and with some effort you could guess your way to a valid endpoint. The new Workflow URLs have a cryptographic signature baked into the query string (the sig parameter). It’s essentially a shared secret. It’s still not perfect (anyone who gets that URL can POST to it) but it’s a significant step up from the old structure where the GUIDs were practically an invitation to enumerate.
Of course, if you need real authentication with token validation, the bot-proxy pattern from my earlier post is still the way to go. But for most internal alerting scenarios, the signed Workflow URL is good enough.
April 30 is coming fast. Don’t wait until the last minute like I always do.