How to Set Up and Test Microsoft Teams Webhooks
Teams webhooks serve two distinct purposes and they work in opposite directions. Incoming Webhooks let your application push messages into a Teams channel. Outgoing Webhooks let Teams call your server when someone @mentions your bot in a channel. The setup, the payload format, and the security model are completely different for each.
This guide covers both: how to configure them in the Teams admin UI, what the wire format looks like, and how to test your integration before you ship it.
Incoming Webhooks: sending alerts into Teams
Incoming Webhooks are a Connector feature built into Teams channels. You configure one through the channel settings, and Teams gives you a URL. Any HTTP POST to that URL drops a message into the channel.
To set one up, open the channel, click the three-dot menu, and choose "Connectors". Find "Incoming Webhook" in the list, click "Configure", give it a name, and copy the generated webhook URL. It will look something like this:
https://yourorg.webhook.office.com/webhookb2/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx@xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/IncomingWebhook/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
That URL is the only credential you need. Anyone with it can post to the channel, so keep it out of version control.
Teams accepts two JSON card formats in the POST body. The legacy format is MessageCard, and the newer format is Adaptive Card. A minimal MessageCard that posts a simple alert:
{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": "0076D7",
"summary": "Deployment succeeded",
"sections": [{
"activityTitle": "Production deploy complete",
"activitySubtitle": "app-v2.4.1 is live",
"facts": [
{ "name": "Environment", "value": "production" },
{ "name": "Duration", "value": "3m 12s" }
]
}]
}
To test it immediately from the command line:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"@type":"MessageCard","@context":"http://schema.org/extensions","summary":"Test","text":"Hello from curl"}' \
"https://yourorg.webhook.office.com/webhookb2/..."
A successful request returns a 1 response body with HTTP 200. Any error in your JSON structure returns an HTTP 400 with a plain-text error message that usually identifies the problem.
Outgoing Webhooks: Teams calling your server
Stop reading raw JSON
Payloader shows you what your Microsoft Teams webhook actually did — in plain English. See the event, amount, status, and more at a glance.
Start free trial →Outgoing Webhooks work in the other direction. You register a public URL in the Teams Admin Center, and when someone @mentions your webhook bot in a channel, Teams makes an HTTP POST to that URL. Your server has to respond within 5 seconds with a message for Teams to display.
To create one, go to Teams Admin Center > Teams apps > Manage apps, then use the "Outgoing webhooks" option in a team's Apps tab. You provide a name, a callback URL, and Teams gives you a security token. Hold on to that token.
When someone types @YourBot check status in a channel, Teams POSTs a JSON payload to your URL:
{
"type": "invoke",
"name": "actionableMessage/executeAction",
"serviceUrl": "https://smba.trafficmanager.net/amer/",
"channelId": "msteams",
"from": {
"id": "29:1abc...",
"name": "Ada Lovelace"
},
"conversation": {
"id": "19:[email protected]"
},
"text": "<at>YourBot</at> check status",
"timestamp": "2026-03-01T10:30:00.000Z"
}
Your server must respond with JSON in this shape:
{
"type": "message",
"text": "All systems operational."
}
If your server does not respond within 5 seconds with a valid 200, Teams shows the user an error. There is no retry.
Testing Incoming Webhooks
Because you are the one calling the Teams webhook URL, testing Incoming Webhooks is mostly about verifying your message card JSON is valid. The two most common failure modes are malformed JSON and using an unsupported field for your card version.
Start with the curl command above to confirm the URL is active and accepts your base payload. Then iterate on the card structure, adding sections, facts, and actions until the Teams channel displays what you expect.
If you want to capture and review the exact payload your application sends (for example, the JSON your deployment pipeline generates), point it at Payloader first instead of the Teams URL. Payloader is a webhook inspector that gives you a public URL, captures every request, and shows you the full headers and body. You can verify the payload looks exactly right before you start sending it into a real Teams channel.
Testing Outgoing Webhooks with Payloader
Testing Outgoing Webhooks is harder because you need a public URL before Teams will let you save the configuration. Your localhost is not reachable from Teams' servers.
Use Payloader to solve the registration problem:
- Create a new endpoint in Payloader and copy the URL.
- Paste that URL as the callback URL when creating your Outgoing Webhook in Teams.
- In a Teams channel, @mention your webhook bot name to trigger it.
- Open Payloader and inspect the incoming request.
You will see the full request exactly as Teams sends it: the Authorization header containing the HMAC token, the Content-Type, and the complete JSON body. This is the exact data your server will need to handle. Understanding the actual payload structure before you write the handler saves significant debugging time.
Note that because Payloader does not respond with a valid message JSON, Teams will show an error to the user in the channel. That is expected during inspection. Once you have seen what you need, proceed to the forwarding step below.
Forwarding to localhost
Once you know the payload structure, you need to run your handler locally against real Teams requests. Configure Payloader to forward incoming requests to your local development server, for example http://localhost:3000/webhook/teams.
When someone @mentions your bot in Teams, the request flows from Teams to Payloader to your local machine. Payloader logs the full request and the response your server returns, so you can see both sides of the exchange in one place.
If your local handler throws an error or times out, you can use the Replay button in Payloader to resend the exact same request without having to type another @mention in Teams. This matters especially for the HMAC verification step below, where the token value stays constant across replays.
HMAC verification
Teams signs every Outgoing Webhook request using the security token it showed you when you created the webhook. The signature arrives in the Authorization header with the format HMAC <base64-encoded-hash>.
Teams computes the signature by running HMAC-SHA256 over the raw request body, using the UTF-16LE-encoded bytes of your security token as the key. Both the message and key encoding matter. This differs from most other webhook signatures that use UTF-8.
Verification in Node.js:
const crypto = require("crypto");
function verifyTeamsSignature(rawBody, authHeader, securityToken) {
if (!authHeader || !authHeader.startsWith("HMAC ")) {
return false;
}
const receivedHash = authHeader.slice(5); // strip "HMAC "
// Teams uses the UTF-16LE bytes of the token as the HMAC key
const keyBuffer = Buffer.from(securityToken, "base64");
const computedHash = crypto
.createHmac("sha256", keyBuffer)
.update(rawBody)
.digest("base64");
return crypto.timingSafeEqual(
Buffer.from(computedHash),
Buffer.from(receivedHash)
);
}
And in Python:
import hmac
import hashlib
import base64
def verify_teams_signature(raw_body: bytes, auth_header: str, security_token: str) -> bool:
if not auth_header or not auth_header.startswith("HMAC "):
return False
received_hash = auth_header[5:] # strip "HMAC "
# The security token is base64-encoded; decode it to get the raw key bytes
key = base64.b64decode(security_token)
computed = hmac.new(key, raw_body, hashlib.sha256).digest()
computed_b64 = base64.b64encode(computed).decode("utf-8")
return hmac.compare_digest(computed_b64, received_hash)
A few things that trip developers up here. First, the key is the base64-decoded value of the security token Teams gives you, not the token string itself. Second, you must hash the exact raw bytes of the request body. If your framework parses the JSON before your middleware runs and you re-serialize it, the bytes will not match and every request will fail verification. Third, use a constant-time comparison function (timingSafeEqual or hmac.compare_digest) to avoid timing side-channels.
Capturing requests in Payloader before writing your verification code is useful here because you can see the exact Authorization header value Teams sends and test your hashing logic against the real token, not a value you constructed yourself.
Once your handler verifies the signature correctly and returns a message response within 5 seconds, your Outgoing Webhook integration is production-ready.