Microsoft Teams Phone as a provider

FeatureVOIPService
Shipped
August 5, 2026 at 6:37 AM UTC
Author
Kamo
Commit
6df0008

Teams is not one API, so the provider glues three control planes together: • Graph, app-only — call history (getPstnCalls), presence reads, directory and Teams device inventory. Call records are application-permission by design; Microsoft states plainly that they have no delegated form. • Graph, delegated — presence writes (Presence.ReadWrite exists only as a delegated, self-only permission) and voicemail, which lives in the member's Exchange mailbox because Teams has no voicemail resource at all. • Azure Communication Services — the in-browser softphone on the member's own Teams Phone line, via the custom Teams endpoint token exchange. This is the only supported route: Teams exposes no SIP registrar to third parties, and the Graph calling API joins as a bot rather than as the user. Capabilities are computed from what an instance is actually configured for rather than hardcoded, so an org without ACS reports voiceCalls=false and the UI hides the dialler instead of surfacing a 501 mid-call. Deliberately absent, because no Microsoft API exists at any version: SMS from a Teams number, call queues, call forwarding, auto-answer, and starting/stopping recording. Those stay on the interface defaults that raise 501. Credentials resolve per-instance first (an org registers its own Entra app — the common enterprise shape, and it needs nothing of Kamo the platform) and fall back per-field to a MICROSOFT_TEAMS row in the platform OAuth registry. Onboarding runs in two hops because the tenant must be discovered before it can be pinned: an admin signs in, the id_token's `tid` gives the real tenant, then they are sent to that tenant's /v2.0/adminconsent. Consent is confirmed by minting an app token and calling Graph — Microsoft returns admin_consent=True even on failure, and warns that the `tenant` redirect parameter is forgeable, so neither is trusted. Both Teams flows and the per-member delegated connect multiplex onto /api/voip/oauth/callback, the one path already public at the gateway, so APIService needs no new route. Change notifications ride the existing /api/voip/webhook for the same reason. Graph's handshake differs from RingCentral's — a ?validationToken query parameter echoed as text/plain — and is answered before anything else, since Graph drops notifications for ten minutes if an endpoint is slow. Subscriptions are created unfiltered on purpose: the subscription filter still targets the legacy `participants` collection that stopped returning data in June 2026. Two pre-existing defects fixed because Teams makes them load-bearing: • CallController, CapabilitiesController and CallFeaturesController resolved the provider via getPrimaryForOrg — the org's oldest active instance of ANY type. That is only ever right for a single-provider org. Teams will usually be a second provider, so every member's history, voicemail and agent state would have been served by whichever provider was created first. They now use a member-aware getForMember, mirroring what SipController already did. • Deleting an instance left member_voip_oauth_token rows behind, since there is no FK to cascade from, and they kept reporting "connected". Verified: mvn test — 133 tests, 0 failures (49 new).

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing