splitforms.com
All articles/ INTEGRATIONS9 MIN READPublished May 11, 2026

Send Contact Form Submissions to MS Teams 2026 Guide

Send contact form submissions to Microsoft Teams in 2026 — incoming webhook, adaptive card formatting, mentions, and a reliable no-code path that just works.

✶ Written by
splitforms.com / blog

Founder of splitforms — the form backend API for developers. Writes about form UX, anti-spam, and shipping web apps without backend code.

The actual problem: email is the wrong inbox for forms

If your team lives in Microsoft Teams all day, routing contact form submissions to a personal Gmail or Outlook inbox is friction. Someone has to notice the email, screenshot it, paste it into Teams, then start a thread. By the time a response goes out, the lead has churned.

The fix is direct: post every new submission straight into a Teams channel. The whole team sees it, anyone can claim it, replies happen in a thread, and there's a permanent searchable record. Microsoft built incoming webhooks specifically for this — but the documentation is scattered across three deprecated formats and the new Workflows successor, so most tutorials are out of date.

This guide covers the path that works in 2026: incoming webhook (or Workflows endpoint), Adaptive Card v1.5 payload, optional @mentions, and retry logic that survives the inevitable 429. We'll show both the manual code path and the no-code option through splitforms, which sends the right payload by default.

If you want a more general primer on webhooks before diving in, read send form data to a webhook first — same plumbing, different destination. If Slack is your target instead of Teams, the sibling guide is send form submissions to Slack.

Step 1: Create the incoming webhook in your Teams channel

Teams webhooks are scoped per channel, not per team. Pick the channel that should receive submissions — usually #leads, #sales, or #support.

  1. Open Microsoft Teams. Navigate to the channel.
  2. Click the ••• menu on the channel name → Manage channelConnectors. (In tenants where classic connectors are off, you'll see Workflows instead — the steps are nearly identical.)
  3. Search for Incoming Webhook and click Add, then Configure.
  4. Name it something descriptive like splitforms — contact form. Upload an icon if you want (a 192x192 PNG works).
  5. Click Create. Microsoft generates a long URL that starts with https://[tenant].webhook.office.com/webhookb2/.... Copy it. You can't see it again later.
  6. Click Done.

Treat this URL like a password. Anyone who has it can post any message into that channel — there's no auth on the request itself, just URL secrecy. Don't commit it to Git, don't expose it to the browser, and rotate it if someone leaves the team.

If your tenant is on Workflows only

Microsoft is migrating tenants from classic connectors to Power Automate Workflows. The flow is: •••Workflows → search Post to a channel when a webhook request is received → pick channel → save. The output URL ends in logic.azure.com instead of webhook.office.com, but the payload format is the same Adaptive Card JSON.

Adaptive Card v1.5: the JSON schema that actually renders

Teams supports two message formats. The legacy MessageCard format still works but Microsoft has marked it for sunset, and it can't render mentions, action sets, or column layouts properly on the new Teams desktop client. Use Adaptive Cards v1.5. Here's the minimum viable payload for a form submission:

{
  "type": "message",
  "attachments": [
    {
      "contentType": "application/vnd.microsoft.card.adaptive",
      "contentUrl": null,
      "content": {
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "type": "AdaptiveCard",
        "version": "1.5",
        "body": [
          {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": "New contact form submission"
          },
          {
            "type": "FactSet",
            "facts": [
              { "title": "Name",    "value": "Jane Cooper" },
              { "title": "Email",   "value": "jane@example.com" },
              { "title": "Subject", "value": "Demo request" }
            ]
          },
          {
            "type": "TextBlock",
            "text": "Hi — interested in your enterprise plan. Can we book a 30 min call this week?",
            "wrap": true
          }
        ],
        "actions": [
          {
            "type": "Action.OpenUrl",
            "title": "Reply in dashboard",
            "url": "https://splitforms.com/dashboard/submissions"
          }
        ]
      }
    }
  ]
}

Three things people miss when hand-rolling this:

  • The outer wrapper must be { "type": "message", "attachments": [...] }. Posting the AdaptiveCard object directly returns 200 but renders nothing.
  • contentType must be exactly application/vnd.microsoft.card.adaptive. Typos here silently fall back to a plain text block.
  • version should be 1.5 for new code. Teams desktop supports up to 1.5 as of 2026-05; older mobile clients gracefully degrade.

The no-code path: splitforms → Teams

Hand-rolling the Adaptive Card JSON, signing the request, queueing retries, and storing the webhook URL securely is maybe two hours of work if you've done it before, and a full day if you haven't. The splitforms integration does it in three clicks:

  1. Sign up at splitforms.com/login. Free tier covers 1,000 submissions per month and webhooks are free (not paywalled like on Formspree).
  2. In the dashboard, open Integrations → Microsoft Teams. Paste the webhook URL you copied from Teams.
  3. Pick a template (Default, Minimal, Detailed with attachments, or Custom JSON for the Adaptive Card body). Save.

Drop the standard splitforms form into your site:

<form action="https://splitforms.com/api/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
  <input type="text"  name="name"    required />
  <input type="email" name="email"   required />
  <textarea           name="message" required></textarea>
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />
  <button type="submit">Send</button>
</form>

Every submission now triggers an email and a Teams card. No code changes required when you swap webhook URLs later, no secrets in your repo, no broken cron job to maintain. Framework-specific drop-ins are documented at /forms/nextjs, /forms/react, and /forms/astro.

MessageCard (legacy) vs Adaptive Card v1.5: pick correctly

You'll see both formats in Microsoft's docs and most third-party tutorials still use MessageCard. Don't copy them for new work. Here's the head-to-head:

CapabilityMessageCard (legacy)Adaptive Card v1.5
Renders in 2026 TeamsYes (degraded)Yes (native)
@mentions of usersNoYes (via msteams.entities)
Action buttonsLimited (HttpPOST, OpenUri)Full action set (OpenUrl, Submit, ToggleVisibility)
Column layoutNoYes (ColumnSet)
MarkdownPartialFull (in TextBlock)
Image / icon supportSection imageImage element, anywhere
Microsoft's 2026 stanceDeprecated, no new featuresActive, recommended

If you're maintaining an old MessageCard integration that still works, it'll keep working — Microsoft hasn't flipped the kill switch. But anything new should be Adaptive Card. splitforms generates Adaptive Card v1.5 by default.

@mentions: how to actually page a human

Posting a card is one thing — making sure someone reads it before the lead goes cold is another. Adaptive Cards support @mentions through a small extension block. Here's the pattern:

{
  "type": "message",
  "attachments": [{
    "contentType": "application/vnd.microsoft.card.adaptive",
    "content": {
      "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
      "type": "AdaptiveCard",
      "version": "1.5",
      "body": [
        {
          "type": "TextBlock",
          "text": "New lead — <at>Alex</at> you're on call",
          "wrap": true
        }
      ],
      "msteams": {
        "entities": [
          {
            "type": "mention",
            "text": "<at>Alex</at>",
            "mentioned": {
              "id": "alex@yourcompany.onmicrosoft.com",
              "name": "Alex Chen"
            }
          }
        ]
      }
    }
  }]
}

The id field is the user's Azure AD UPN (User Principal Name) — usually their work email. The <at>Alex</at> token in the body is what Teams replaces with the visible mention pill, and it must match the text field in the entities array exactly. Get either wrong and the mention silently fails — you'll see the literal <at>Alex</at> text in the channel.

Channel-wide mentions (@channel, @team) don't work from incoming webhooks — those require a bot identity. If you need them, route the submission through a Power Automate flow that posts as a bot.

Action buttons that do real work

The card's actions array gives you up to six buttons. Two action types matter for form submissions:

Action.OpenUrl

The reliable workhorse. Opens any URL in the user's browser. Use it for "Reply in dashboard", "Open in CRM", "Mark as junk" (linking to a tagged URL), or pre-filled mailto: links:

{
  "type": "Action.OpenUrl",
  "title": "Reply via email",
  "url": "mailto:jane@example.com?subject=Re%3A%20Demo%20request"
}

Action.Submit (only with a bot)

If you have a Teams bot installed in the channel, Action.Submit posts a JSON payload back to it — perfect for "Assign to me" or "Snooze 1 hour". Without a bot listening, the button errors out. Most teams don't need this. Stick to Action.OpenUrl.

One small UX tip: limit yourself to two or three actions per card. Adaptive Cards collapses additional actions into an overflow menu on mobile, and people just don't click them.

Error handling and retry logic

Teams webhooks fail. Not often, but enough that a naive integration will silently lose submissions. The four failure modes:

CodeMeaningRight response
400Malformed JSON or wrong contentTypeDo NOT retry. Log and alert. Fix the payload.
404Webhook URL revoked or wrongDo NOT retry. Alert someone to rotate.
429Rate limited (4 req/sec or channel cap)Retry with exponential backoff. Honor Retry-After header.
5xxMicrosoft transient errorRetry up to 5 times with exponential backoff.

If you're rolling your own, here's the minimum viable retry loop:

async function postToTeams(payload, attempt = 0) {
  const res = await fetch(WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
  if (res.ok) return;
  if (res.status === 400 || res.status === 404) {
    console.error("Permanent failure", res.status);
    return; // don't retry
  }
  if (attempt >= 5) throw new Error("Max retries");
  const retryAfter = Number(res.headers.get("retry-after")) || 2 ** attempt;
  await new Promise(r => setTimeout(r, retryAfter * 1000));
  return postToTeams(payload, attempt + 1);
}

splitforms handles all of this server-side. Submissions are queued in durable storage, retried with jittered exponential backoff (2s, 4s, 8s, 16s, 32s), and surfaced in the dashboard as "Delivery: failed — last error: 429" if every retry burns. You don't lose the lead even if Teams is down for an hour.

Test the integration without spamming your channel

Before pointing your live contact form at the webhook, sanity-check the payload. Two tricks:

  1. Adaptive Card Designer. Open https://adaptivecards.io/designer/, paste your card body, switch the host to "Microsoft Teams". You'll see exactly how it renders before any HTTP request goes out.
  2. Test channel. Create a #splitforms-test channel, attach a separate webhook, and target that during dev. Once it looks right, swap to the production webhook URL.

To simulate a submission, splitforms has a "Send test payload" button on the Teams integration page that fires a sample submission through the same code path your real forms use. If that lands correctly, real forms will too. For broader troubleshooting on why a form fails end-to-end, see contact form not working.

Message templates worth stealing

The default splitforms template is fine for most teams, but a few patterns earn their keep. Pick the one that matches how your team actually triages submissions.

The lead-routing template

For sales teams that need to grab leads fast: lead name in the header, three facts (email, company, message preview), and two buttons — "Open in CRM" and "Reply via email". Mention the on-call rep's UPN so their phone buzzes. Don't include the entire submission body — too much vertical space pushes the next card off screen and people stop scrolling.

The support triage template

For support inboxes: short header ("New support request"), a single FactSet with severity (parsed from a dropdown field), and one action button linking to the submission in your help desk. Skip mentions — channel-wide alerts on every ticket train the team to mute the channel within a week.

The detailed-audit template

For low-volume but high-stakes forms (enterprise demo requests, partnership inquiries): every field as a fact row, IP and country in a smaller container at the bottom, and a copy-paste-ready summary in a code block. People who need this template usually only get one or two submissions per day, so the verbosity is fine.

splitforms' dashboard ships all three as one-click presets. You can also drop in raw Adaptive Card JSON and use field tokens like {{ name }} or {{ message }} for full control.

Security: don't leak the webhook URL

Teams incoming webhooks have no request authentication. The URL is the credential. Anyone who has it can post anything to your channel — phishing messages, spam, fake leads. A few rules:

  • Never put the webhook URL in client-side code. Browser-visible JavaScript means the URL is public. Always proxy through a server.
  • Never commit it to Git. Use environment variables (TEAMS_WEBHOOK_URL) or a secrets manager. Add it to .gitignore-tracked files like .env.local.
  • Rotate it when staff leaves. Anyone who saw the URL still has it. Go to the channel connector, delete the existing webhook, create a new one, update splitforms.
  • Use one webhook per source. Don't reuse the same Teams webhook for your CRM, your alerts, and your form. If one leaks you have to rotate all of them.

With splitforms, the webhook URL is stored encrypted in our database and never appears in your form HTML, so the browser-side leak vector is gone by default. We also surface a "last delivered" timestamp per integration so you can spot a silently failing webhook before someone notices submissions stopped showing up in chat.

Next steps

FAQ

Are Microsoft Teams incoming webhooks being retired?

Microsoft has announced that classic Office 365 connector incoming webhooks are deprecated in favor of Workflows (Power Automate) with the equivalent "Post to a channel when a webhook request is received" trigger. The endpoint format changes, but the request body (Adaptive Card JSON) is identical. If you set up a brand new integration in 2026, use the Workflows path — it's drop-in compatible with the same Adaptive Card payload splitforms sends.

Why does my Adaptive Card render as a plain text block?

Three common causes: (1) you posted a raw object instead of wrapping it in `attachments[0]` with the correct `contentType`; (2) your `$schema` URL is wrong or your `version` is higher than what Teams supports (stick to 1.4 or 1.5); (3) you sent the card to a Workflow that's set to "text only" mode. Open the Teams Adaptive Card Designer, paste your JSON, and confirm it renders there before blaming the webhook.

Can I @mention a real Teams user from an incoming webhook?

Yes, but only if you use Adaptive Cards (not the legacy MessageCard format) and you include both an `msteams.entities` block with the user's AAD object ID and a matching `<at>name</at>` token in the card body. Channel mentions and "@team" only work from bot accounts, not webhooks. For an oncall rotation, mention the on-call user's UPN — splitforms can template this dynamically per submission.

How is this different from Power Automate flows?

Power Automate flows are the modern Microsoft path for the same outcome: a webhook trigger fires, the flow posts a card to a channel. They're slightly slower (cold start can add 2-3 seconds) but more flexible — you can route by form field, branch, call Graph API, etc. splitforms talks to both. If you need conditional routing, build the flow; if you just need "new submission → card in channel", incoming webhook is faster and simpler.

What's the rate limit on Teams incoming webhooks?

Microsoft documents 4 requests per second per webhook, and the channel itself has a soft cap of roughly 30 messages per second across all sources. For form submissions you'll never hit this — even a viral landing page rarely sees 4 submissions per second sustained. If you ever do, splitforms' queue absorbs the burst and trickles them out at safe pace. You'll see a 429 response only if you misuse the webhook from custom code without backoff.

Can I post submissions to a private channel?

Yes. Incoming webhooks are scoped per channel, not per team — when you add the webhook connector, you pick the exact channel (private or public). The webhook URL only posts to that one channel. If you need to post to multiple channels, create one webhook per channel and configure two webhook destinations in splitforms. Each gets its own retry budget.

Will action buttons in the card actually do anything?

`Action.OpenUrl` always works — it opens a link in the user's default browser (great for "Reply in dashboard" or "Open in CRM"). `Action.Submit` only works if the channel has a bot installed that can receive the submit payload; from a plain incoming webhook there's nothing listening to the submit, so the button will fail silently. Stick to `Action.OpenUrl` unless you've built a bot.

How do I keep the webhook URL out of my Git repo?

Don't put it in code at all. With splitforms you paste the webhook URL once into the dashboard integration page — it lives in our database, encrypted at rest, and your form HTML never sees it. If you're rolling your own backend instead, store it in an environment variable (`TEAMS_WEBHOOK_URL`) and reference it server-side. Never expose a Teams webhook URL to the browser; anyone with the URL can post anything to your channel.

About the author
✻ ✻ ✻

Get your free contact form API key in 60 seconds.

1,000 free form submissions per month. No credit card. No SDK, no PHP, no plugin. Drop one POST endpoint in your form and submissions land in your inbox.

Generate access key →Read the docs
v0.1 · founders pricing locked in · early access open