Skip to content
Constructing the Teams Join Meeting URL Without the Graph
Albert-Jan Schot
Albert-Jan Schot

· 4 min read

Post

Constructing the Teams Join Meeting URL Without the Graph

When you build a Teams meeting tab, the SDK hands you the meeting’s identity but not the link to actually join it. That sounded like a small gap, right until I needed that URL somewhere downstream, like passing it to a bot that has to enter the meeting on your behalf from a Microsoft Teams tab…

The obvious solution is a Graph call to the onlineMeeting resource, which does have a joinWebUrl property. And that works, but it comes with a cost: you need the right permissions, you need the right token, and you are adding a network round-trip to something you could have computed locally. It turns out the meetup-join deep link is deterministic given three values you already have in context. So you can skip the Graph call entirely.

What the TeamsJS SDK gives you

Inside a meeting tab, you initialize the SDK and ask for context:

await teamsJs.app.initialize();
const context = await teamsJs.app.getContext();

Three fields from that context object are all you need:

PropertySourceRole
Thread IDcontext.chat.idIdentifies the meeting’s chat/thread
Tenant IDcontext.user.tenant.idScopes the join to the right AAD tenant
Object IDcontext.user.idCurrent user’s AAD object ID (so the user that opened the tab)

The meetup-join URL format Teams uses is: https://teams.microsoft.com/l/meetup-join/{threadId}/0?context={context}

Where {threadId} is the URL-encoded chat.id, the /0 is an unused message-ID placeholder, and {context} is a URL-encoded JSON object carrying the tenant and user identifiers.

Put it together and you get:

const encodedThreadId = encodeURIComponent(chatId);
const contextParam = encodeURIComponent(
  JSON.stringify({ Tid: tenantId, Oid: currentUserId })
);
const joinUrl =
  `https://teams.microsoft.com/l/meetup-join/${encodedThreadId}/0` +
  `?context=${contextParam}`;

That joinUrl is valid the moment the tab has its context. No extra calls, no extra permissions.

The Oid caveat

There is one thing worth being honest about. The Oid in this URL is the current user’s AAD object ID not necessarily the organizer’s. In the normal happy path, when the tab is opened by the meeting organizer, this works exactly as you would expect. But if someone else opens the tab first and you rely on Oid to identify the organizer, you will have the wrong value.

For the use case we built this for, a note-taker bot that joins the meeting on behalf of whoever triggered it, this is fine. The bot just needs a valid join URL, and Teams accepts any authenticated user’s Oid in that context field. But if your downstream logic cares specifically about who organized the meeting, you would need to handle that separately, either by requiring the organizer to be the one triggering the flow, or by fetching the organizer ID from Graph once at a setup point where you control the context.

Fallback behaviour

Not every context has all three fields populated. The guard logic we applied is straightforward:

  • No chat.id → cannot build the URL at all. Hard error, logged, surface it to the user.
  • No user.tenant.id → same. Without a tenant scope the URL is meaningless.
  • No user.id → build the URL without Oid, so context = { Tid: tenantId }. Teams still accepts it in most cases, so this is a soft fallback rather than a hard stop, and it is logged as a warning.

The Oid-less URL is less precise but functional. Whether that is acceptable depends on what you do with it downstream.

Why not just use Graph?

The onlineMeeting resource does expose joinWebUrl and joinUrl, and the Microsoft documentation will point you there as the canonical source. And for some scenarios that is the right call if you need to read other meeting metadata at the same time, or if you are already holding a Graph token with the right scopes, the overhead is not a big deal.

But if all you need is the join URL and you are running on the client inside a tab that already has context, the Graph approach requires either OnlineMeetings.Read delegated permission or OnlineMeetings.Read.All application permission. That is a scope conversation with whoever manages your app registration, a token exchange, and a network call for a value you can derive locally in three lines. The deterministic nature of the meetup-join format means there is nothing to look up.

Albert-Jan Schot

Albert-Jan Schot

CTO, Microsoft MVP & FastTrack Recognized Solution Architect

I am Albert-Jan Schot, CTO at Blis Digital, Microsoft MVP, and FastTrack Recognized Solution Architect focused on Microsoft 365, Azure, and AI agents. I help teams turn complex Microsoft Cloud challenges into practical architecture decisions and shipped outcomes.

Copilot Studio Microsoft 365 Agent Flows

Zuid Holland, Netherlands

Related Posts