Skip to content

Microsoft Teams Source

What it ingests

Two things, on one connector:

  1. Channel messages and thread replies from the teams you list — the conversational decisions that happen between meetings. The Slack analogue.
  2. Meeting transcripts with participants — optional, off by default, and commercial Microsoft 365 only (not GCC-High / DoD). The Google Meet analogue.

Not ingested: 1:1 and group chats, Outlook mail, and calendar. Private chats are deliberately out of scope — reading them requires Chat.Read.All, which grants an application access to every private message in the tenant. That is a much larger consent than Slack's channel-scoped bot, and this source is the Slack analogue.

Documents live in the SharePoint / OneDrive source, and scheduled meetings live in the Outlook Calendar source. Both reuse the same credential (see below). Outlook mail is still a planned follow-on.

Credential

References a microsoft_graph credential by id. Unlike the other sources, the secret is a small JSON object describing an Entra ID (Azure AD) app registration, because Graph's app-only flow needs all three values together:

json
{
  "tenant_id": "72f988bf-86f1-41af-91ab-2d7cd011db47",
  "client_id": "a1b2c3d4-e5f6-4789-8abc-def012345678",
  "client_secret": "abc8Q~…"
}
bash
curl -sS -X POST https://your-corveil-host/api/credentials \
  -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
  -d '{"type":"microsoft_graph","name":"contoso-graph","secret":"{\"tenant_id\":\"…\",\"client_id\":\"…\",\"client_secret\":\"…\"}"}'

The credential is named for Graph, not for Teams, on purpose: one app registration serves every Graph surface. The SharePoint / OneDrive and Outlook Calendar sources reuse this same vault row — a tenant that has connected Teams adds either by consenting additional permissions on the existing registration, not by creating another one.

client_secret is the Value, not the Secret ID

The Azure portal shows a Secret ID (a GUID) next to the secret Value. Corveil rejects the GUID at write time, because it is the single most common setup mistake and otherwise only fails hours later at the first poll. The Value is displayed once, at creation — if you did not copy it, create a new secret.

Config fields

KeyRequiredNotes
team_idsyesComma-separated Microsoft 365 group IDs. In Teams: ⋯ → Get link to team, then copy the groupId parameter. Pasting a groupId=<guid> fragment works too. A team name is rejected — Graph addresses teams by id.
channelsnoComma-separated channel display names to limit ingestion to (e.g. General, Engineering). Blank ingests every channel in each listed team.
cloudnoglobal (default) for commercial Microsoft 365 (including GCC), usgov for GCC-High (L4), or usgovdod for DoD (L5). See the sovereign-cloud note below.
meeting_transcriptsnotrue to also ingest meeting transcripts. Off by default because it needs an extra permission and a PowerShell policy step. Commercial tenants only — rejected together with cloud: usgov or usgovdod, see below.
member_allowlistnoComma-separated emails. Only meetings organized by these members are fetched. Transcripts only.
workspace_domainsnoComma-separated email domains, narrowing transcript organizers. Transcripts only.
reply_lookback_hoursnoHow far back each poll re-scans threads for late replies. Default 48, max 720. See "Why replies need a lookback" below.

Provider-side setup

1. Create the app registration

  1. In the Microsoft Entra admin centerApp registrationsNew registration. Name it something recognizable (e.g. Corveil Teams sensor). No redirect URI is needed — this is an app-only (daemon) integration.
  2. Copy the Application (client) ID and Directory (tenant) ID from the Overview page.
  3. Certificates & secretsNew client secret. Copy the Value immediately.

Under API permissionsAdd a permissionMicrosoft GraphApplication permissions:

PermissionWhy
ChannelMessage.Read.AllRead channel messages and replies
Team.ReadBasic.AllResolve each configured team id to its name
Channel.ReadBasic.AllList the channels in each team
User.Read.AllResolve message authors and @-mentions to email addresses
OnlineMeetingTranscript.Read.AllOnly if meeting_transcripts is on

Then click Grant admin consent for <tenant>. That single click is the whole gate for channel messages.

No approval form, no metering

Older guides describe a "protected API" request form at aka.ms/teamsgraph/requestaccess, and a model=A/model=B billing parameter with per-message charges. Both are gone: the approval process was deprecated on 18 May 2023, and Teams APIs were de-metered on 25 August 2025 (the model parameter is now ignored when supplied). No Azure subscription needs to be attached.

User.Read.All is required even for message-only ingestion. Graph's chatMessage.from carries only an object id and a display name — never an email — so without it no Teams event can be linked to a Corveil user. It is the exact counterpart of Slack's users:read.email scope.

3. Meeting transcripts only: grant an application access policy

App-only calls to a specific user's meetings additionally require a Teams application access policy. This is not consented in the portal; it is a PowerShell step, run once with the MicrosoftTeams module:

powershell
Connect-MicrosoftTeams
New-CsApplicationAccessPolicy -Identity Corveil-Teams -AppIds "<client-id>" `
  -Description "Corveil Teams sensor"
Grant-CsApplicationAccessPolicy -PolicyName Corveil-Teams -Global

-Global skips users with an explicit policy

A global grant applies to every user in the tenant except any who already have an explicit policy assignment. If some users are covered by another policy, assign per user instead: Get-CsOnlineUser | Grant-CsApplicationAccessPolicy -PolicyName Corveil-Teams. Propagation can take up to 30 minutes.

4. GCC-High / DoD tenants

L4 and L5 are different hosts. Set cloud to usgov for GCC-High (graph.microsoft.us) or usgovdod for DoD (dod-graph.microsoft.us). Both authenticate against login.microsoftonline.us, and each requests the token scope matching its own host — a token minted for one resource is rejected by the other and by the commercial endpoint. Plain GCC (not GCC-High) uses the commercial endpoints, so those tenants stay on global.

This source reads messages one channel at a time rather than through Graph's bulk getAllMessages Export API specifically so that it works in these clouds: the Export API is documented as Global-cloud only. The per-channel endpoint is available in every national cloud, at the cost of throughput.

Meeting transcripts are not available in GCC-High / DoD

Setting meeting_transcripts to true together with cloud: usgov or cloud: usgovdod is rejected — the source will not save, and an existing source configured that way reports the error rather than polling.

This is a Microsoft limitation on discovery, not a missing download API, and it applies to both GCC-High (L4, graph.microsoft.us) and DoD (L5, dod-graph.microsoft.us) — they are different hosts, but the same gap.

Graph has no "list every meeting in the tenant" endpoint. The commercial path uses getAllTranscripts scoped to an organizer. Microsoft documents that as Not supported in US Government in Teams API implementation differences in national clouds (the organizer-bulk APIs: list transcripts by organizer and transcript delta). That page overrides the per-API national-cloud badge, which still renders ✅ for getAllTranscripts in GCC-High and DoD because the table is generated from a shared include.

Accepting the combination would give you a healthy, green source that ingests channel messages and zero transcripts, forever, with nothing in its status line — so the source refuses it instead.

Set meeting_transcripts to false. Channel message ingestion is unaffected and remains fully supported in both sovereign clouds. Scheduled meetings still ingest in those clouds via the Outlook Calendar source — that is a different API (calendarView), which Microsoft marks ✅ for both L4 and L5.

A later gov transcript path is possible: only discovery is missing. Downloading a transcript you already have the meeting id for (list transcripts, get content) is ✅ in GCC-High and DoD. calendarView returns onlineMeeting.joinUrl on those same hosts, which is the join between a calendar event and an onlineMeetings id. Corveil has not wired that path yet, so meeting_transcripts stays rejected in both government clouds.

Wire-up

POST /api/sensors

json
{
  "sensor_type": "microsoftteams",
  "name": "Contoso Teams",
  "config": {
    "team_ids": "0fcd8f4d-1111-2222-3333-444455556666",
    "channels": "General, Engineering"
  },
  "credentials": { "graph_credential": "<microsoft_graph credential id>" }
}

With transcripts enabled:

json
{
  "sensor_type": "microsoftteams",
  "name": "Contoso Teams",
  "config": {
    "team_ids": "0fcd8f4d-1111-2222-3333-444455556666",
    "meeting_transcripts": "true",
    "workspace_domains": "contoso.com"
  },
  "credentials": { "graph_credential": "<microsoft_graph credential id>" }
}

Verify

Run Test connection. It resolves every configured team and lists its channels, so a mistyped team id or a missing consent fails here rather than silently ingesting nothing.

A partially-working configuration is reported without stopping the source: if one team of three cannot be read, the other two keep ingesting and the source's status line names the offending team and the likely cause. Sources only begin polling once you confirm their bindings.

Once polling, new channel messages appear as conversation events and transcripts as meeting events. A newly-added channel is seeded on the first poll and emits nothing — history is loaded by backfill, not by the first tick, so adding a channel does not ingest years of chatter at once.

Notes and limits

Why replies need a lookback. Posting a reply to a Teams channel message does not update the root message's lastModifiedDateTime. A cursor that only tracked the newest root would therefore never notice a late reply on an older thread. Each poll pages back reply_lookback_hours (default 48) but still emits only messages newer than the cursor, so late replies are caught without re-ingesting settled conversation. A reply that lands more than the lookback window after its thread went quiet is not picked up; raise the value if your teams routinely revive old threads.

Throttling. Graph allows roughly one request per second per channel for an app in a tenant, which the source paces itself against. A 429 skips that channel for the tick and resumes after the server's Retry-After — no messages are lost, since the cursor only advances over what was actually read.

Transcript formats. Transcripts are fetched as WebVTT so speaker attribution is preserved, and rendered to speaker-tagged plain text. If your tenant forbids speaker attribution (Set-CsTeamsMeetingConfiguration), the source automatically retries in the unattributed plain-text format rather than losing the meeting. If your tenant disables Graph transcript access entirely, the source reports it as a credential/policy error naming the cmdlet to fix it — turn off meeting_transcripts to keep ingesting channel messages meanwhile.

Meeting discovery is organizer-scoped. Graph has no "list every meeting in the tenant" endpoint, so transcripts are found by asking for each Corveil org member's organized meetings. A member whose Corveil login email differs from their Entra user principal name is skipped with a warning. Use member_allowlist / workspace_domains to narrow the fan-out. The endpoint this relies on (getAllTranscripts) is commercial-cloud only, which is why meeting_transcripts cannot be combined with either government cloud — see above. That is not the same as calendar ingest: the Outlook Calendar source runs in GCC-High and DoD today; it does not download Teams transcripts.