How the app works
- Outlook compose
- Teams meeting side panel
- Manual estimator
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
openidandemailpermissions 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.AllandCallRecords.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 The token source does not need to be configured for most deployments: inside Teams the client automatically uses
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: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 Loading from a CDN at runtime means that CDN controls the file. Vendoring removes that dependency.
vendor/teams-js.min.js: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:
openidemail
MEETCOST_OIDC_AUDIENCE.4
Edit the manifest
Edit The full manifest schema version is
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: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 callsmicrosoftTeams.authentication.getAuthToken() to obtain the user’s Microsoft-issued OIDC token. The backend verifies this token against the OIDC issuer:
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 throughGET 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:Graph webhook: call record notifications
When a Teams meeting ends, Graph publishes acallRecord resource and sends a change notification to your configured endpoint. Set up a subscription pointing at:
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 avalidationToken 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:- Checks that
clientStatematchesMEETCOST_GRAPH_WEBHOOK_CLIENT_STATE. Any notification that fails this check is discarded. - Reads the call record ID from the notification body.
- Fetches the full call record from
GET https://graph.microsoft.com/v1.0/communications/callRecords/{id}?$expand=sessionsusing server credentials. - Builds a
MeetingRecordfrom the sessions.
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:Local development
Serve the folder directly, without Teams or Outlook: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.