Skip to main content
The MeetCost Teams app and Outlook add-in are a single page that serves three surfaces from one deployment: an Outlook appointment compose pane that shows a live estimate while the organiser writes the invite, a Teams meeting side panel that shows what the meeting has cost once Graph publishes its call record, and a Teams personal tab with a manual estimator for planning. All three surfaces send meeting context to your organisation’s own MeetCost backend; no salary, rate, or cost figure ever reaches Microsoft.

How the app works

When loaded in Outlook, the page detects Office.context.mailbox and enters compose mode. The Office JS API fires RecipientsChanged and AppointmentTimeChanged events as the organiser edits the invite; each change debounces into a single POST to /api/v1/estimate. The organiser sees the cost estimate update in the task pane in real time without any interaction.

Prerequisites

  • A deployed MeetCost backend reachable over HTTPS.
  • A Microsoft Entra ID app registration for the Teams client (delegated openid and email permissions only — this registration is for identity, not for Graph API calls).
  • A separate Entra app registration on the server side with application permissions User.Read.All and CallRecords.Read.All (see Directory sync below).
  • Hosting for the static files in clients/teams-app/ (or serve them from the MeetCost service itself).

Installation

1

Host the static files

Serve clients/teams-app/ over HTTPS. If you serve it from the MeetCost service itself, the client falls back to the page’s own origin and no config.js is needed.Otherwise copy the example config and set the backend address:
The token source does not need to be configured for most deployments: inside Teams the client automatically uses microsoftTeams.authentication.getAuthToken(), and elsewhere it fetches /auth/token on the same origin.
2

Vendor the Teams JS SDK

Download the Teams JS SDK bundle into vendor/teams-js.min.js:
Loading from a CDN at runtime means that CDN controls the file. Vendoring removes that dependency.
3

Register the client app in Entra ID

In the Entra admin centre, register a new application for the Teams client. It needs only delegated permissions:
  • openid
  • email
MeetCost verifies the token the Teams SDK already holds for the user — it does not call Graph on the user’s behalf. Note the Application (client) ID from this registration; you will need it in the manifest and for MEETCOST_OIDC_AUDIENCE.
4

Edit the manifest

Edit clients/teams-app/manifest.json. Replace every occurrence of meetcost.your-company.example with your actual hostname, generate a GUID for the id field, and fill in webApplicationInfo with your Entra app details:
The full manifest schema version is 1.17. The configurableTabs entry points at meetingSidePanel and meetingDetailsTab; the staticTabs entry covers the personal tab.
5

Package and upload the app

Zip manifest.json, icon-color.png (192×192), and icon-outline.png (32×32) together, then upload the zip in the Teams admin centre under Apps → Manage apps → Upload. No Microsoft review is needed for an internal deployment.
6

Set the server environment variables

On your MeetCost backend, configure the Microsoft adapter:When these variables are set, graph.microsoft.com and login.microsoftonline.com are automatically added to the backend’s egress allowlist.
7

Install the Outlook add-in (optional)

outlook-addin/manifest.xml is a separate sideload for Outlook. Replace the same placeholders and upload it through the Microsoft 365 admin centre. The add-in requests only ReadItem — the narrowest permission that can see an attendee list — and never writes to the mailbox.

Authentication

Inside Teams, the client calls microsoftTeams.authentication.getAuthToken() to obtain the user’s Microsoft-issued OIDC token. The backend verifies this token against the OIDC issuer:
Set MEETCOST_OIDC_AUDIENCE to your client-side Entra app ID so the backend validates the aud claim in the token. MeetCost has no accounts and no passwords.

Directory sync

The Microsoft directory adapter uses the client credentials flow against your tenant to obtain an access token, then pages through GET https://graph.microsoft.com/v1.0/users selecting id, mail, userPrincipalName, displayName, department, jobTitle, and accountEnabled. Each user’s department field becomes their team_id for reports grouping.

Required Graph application permissions

Grant these application permissions (not delegated) to the server-side Entra app registration:
These permissions require admin consent in your tenant. The client-side registration (used for the Teams app identity) does not need these permissions.

Graph webhook: call record notifications

When a Teams meeting ends, Graph publishes a callRecord resource and sends a change notification to your configured endpoint. Set up a subscription pointing at:
Set the subscription’s clientState to the same value as MEETCOST_GRAPH_WEBHOOK_CLIENT_STATE.

Validation handshake

When Graph first validates a new subscription URL, it sends a POST request to the endpoint with a validationToken query parameter. The MeetCost backend echoes the token as a plain-text 200 response within 10 seconds. This is handled automatically.

Notification handling

On each notification, the backend:
  1. Checks that clientState matches MEETCOST_GRAPH_WEBHOOK_CLIENT_STATE. Any notification that fails this check is discarded.
  2. Reads the call record ID from the notification body.
  3. Fetches the full call record from GET https://graph.microsoft.com/v1.0/communications/callRecords/{id}?$expand=sessions using server credentials.
  4. Builds a MeetingRecord from the sessions.
The notification body is used only to learn which meeting ended. Attendance is fetched from Graph with server credentials, so a forged notification cannot inject a cost figure.

Attendance collection: session overlap handling

Graph reports attendance as a list of sessions between endpoints. A person who drops and rejoins contributes multiple sessions. The Microsoft adapter takes the union of a person’s session spans, not their sum, so overlapping or adjacent sessions are not double-counted:
Participants who appear in the record but never connected (no sessions) contribute zero minutes rather than being dropped — no-shows are a meaningful signal in meeting reports and must not be hidden.

Local development

Serve the folder directly, without Teams or Outlook:
When both SDKs are absent the page falls back to the manual estimator. Set window.MEETCOST_BACKEND_URL in config.js to your local backend.

Security model

No pay data to Microsoft

The app sends attendee addresses and times. The backend returns a cost total already redacted for the caller. No salary or rate figure travels to Microsoft infrastructure.

clientState validation

Every Graph notification is checked against MEETCOST_GRAPH_WEBHOOK_CLIENT_STATE. Notifications that fail the check are silently discarded.

Separated app registrations

The client-side registration (identity only) is separate from the server-side registration (directory and call records). A compromised client token cannot be used to read Graph data.

ReadItem only in Outlook

The Outlook add-in requests ReadItem, the minimum permission to read an attendee list. It never writes to the mailbox or calendar.