Minimal TypeScript/Express proof-of-concept for posting notifications to Microsoft Teams via a Teams app bot using Bot Framework.
Implemented:
POST /api/v1/messagesPOST /api/v1/messages/preview(validate + render payload only, no Bot Framework delivery)GET /api/v1/health- API key auth + required
X-User-Entra-Idheader - Explicit
teamId + channelIdtargeting - Content kinds:
texttemplatewithtemplate=generic|github|sysdig|uptime|db_backup|argocd
Deferred:
- Graph membership checks and cache
- Additional content kinds/templates
- Node.js 20+
- npm
- Working Teams bot credentials (
BOT_ID,BOT_SECRET) - Teams app already installed in the target team/channel
Create .env from .env.example and fill values:
cp .env.example .envRequired values:
CONNECTOR_API_KEYBOT_IDBOT_SECRET
Optional/defaulted:
PORT(default3000)BOT_SERVICE_URL(defaulthttps://smba.trafficmanager.net/teams)TENANT_IDorBOT_TOKEN_TENANT(defaultbotframework.com; set this to your tenant ID for single-tenant bots)LOG_LEVEL(defaultinfo)
Install dependencies:
npm installRun in dev mode:
npm run devBuild and run production-style:
npm run build
npm startBuild image:
docker build -t teams-connector-poc .Run container:
docker run --rm -p 3000:3000 --env-file .env teams-connector-pocStart with compose:
npm run compose:upStop and remove containers:
npm run compose:downcurl http://localhost:3000/api/v1/healthcurl -X POST http://localhost:3000/api/v1/messages \
-H "Authorization: Bearer ${CONNECTOR_API_KEY}" \
-H "X-User-Entra-Id: ${USER_ENTRA_ID}" \
-H "Content-Type: application/json" \
-d '{
"target": {
"teamId": "00000000-0000-0000-0000-000000000000",
"channelId": "19:abc123@thread.tacv2"
},
"content": {
"kind": "text",
"text": "<b>Test:</b> Deployment complete."
}
}'curl -X POST http://localhost:3000/api/v1/messages/preview \
-H "Authorization: Bearer ${CONNECTOR_API_KEY}" \
-H "X-User-Entra-Id: ${USER_ENTRA_ID}" \
-H "Content-Type: application/json" \
-d '{
"target": {
"teamId": "00000000-0000-0000-0000-000000000000",
"channelId": "19:abc123@thread.tacv2"
},
"content": {
"kind": "template",
"template": "sysdig",
"data": {
"severity": "high",
"alertName": "CPU saturation"
}
}
}'curl -X POST http://localhost:3000/api/v1/messages \
-H "Authorization: Bearer ${CONNECTOR_API_KEY}" \
-H "X-User-Entra-Id: ${USER_ENTRA_ID}" \
-H "Content-Type: application/json" \
-d '{
"target": {
"teamId": "00000000-0000-0000-0000-000000000000",
"channelId": "19:abc123@thread.tacv2"
},
"content": {
"kind": "template",
"template": "generic",
"data": {
"title": "Maintenance Window",
"severity": "warning",
"body": "DB maintenance in 30 minutes.",
"url": "https://status.example.com"
}
}
}'curl -X POST http://localhost:3000/api/v1/messages \
-H "Authorization: Bearer ${CONNECTOR_API_KEY}" \
-H "X-User-Entra-Id: ${USER_ENTRA_ID}" \
-H "Content-Type: application/json" \
-d '{
"target": {
"teamId": "00000000-0000-0000-0000-000000000000",
"channelId": "19:abc123@thread.tacv2"
},
"content": {
"kind": "template",
"template": "github",
"data": {
"event": "opened",
"title": "PR #123: Improve alert rendering",
"repo": "bcgov/devx-teams-connector",
"author": "octocat",
"url": "https://github.com/bcgov/devx-teams-connector/pull/123",
"body": "Adds support for additional adaptive card templates."
}
}
}'curl -X POST http://localhost:3000/api/v1/messages \
-H "Authorization: Bearer ${CONNECTOR_API_KEY}" \
-H "X-User-Entra-Id: ${USER_ENTRA_ID}" \
-H "Content-Type: application/json" \
-d '{
"target": {
"teamId": "00000000-0000-0000-0000-000000000000",
"channelId": "19:abc123@thread.tacv2"
},
"content": {
"kind": "template",
"template": "sysdig",
"data": {
"severity": "high",
"alertName": "CPU saturation",
"scope": "prod-cluster",
"description": "Sustained CPU > 90% for 5 minutes",
"timestamp": "2026-02-22T12:00:00Z",
"url": "https://app.sysdig.com/#/alerts"
}
}
}'curl -X POST http://localhost:3000/api/v1/messages \
-H "Authorization: Bearer ${CONNECTOR_API_KEY}" \
-H "X-User-Entra-Id: ${USER_ENTRA_ID}" \
-H "Content-Type: application/json" \
-d '{
"target": {
"teamId": "00000000-0000-0000-0000-000000000000",
"channelId": "19:abc123@thread.tacv2"
},
"content": {
"kind": "template",
"template": "uptime",
"data": {
"status": "degraded",
"service": "payments-api",
"responseTimeMs": 620,
"downSince": "2026-02-22T11:40:00Z",
"url": "https://status.example.com/payments-api"
}
}
}'curl -X POST http://localhost:3000/api/v1/messages \
-H "Authorization: Bearer ${CONNECTOR_API_KEY}" \
-H "X-User-Entra-Id: ${USER_ENTRA_ID}" \
-H "Content-Type: application/json" \
-d '{
"target": {
"teamId": "00000000-0000-0000-0000-000000000000",
"channelId": "19:abc123@thread.tacv2"
},
"content": {
"kind": "template",
"template": "db_backup",
"data": {
"status": "success",
"database": "users",
"duration": "2m 03s",
"size": "1.2 GB",
"message": "Backup completed",
"container": "backup-job-1"
}
}
}'curl -X POST http://localhost:3000/api/v1/messages \
-H "Authorization: Bearer ${CONNECTOR_API_KEY}" \
-H "X-User-Entra-Id: ${USER_ENTRA_ID}" \
-H "Content-Type: application/json" \
-d '{
"target": {
"teamId": "00000000-0000-0000-0000-000000000000",
"channelId": "19:abc123@thread.tacv2"
},
"content": {
"kind": "template",
"template": "argocd",
"data": {
"event": "sync_failed",
"application": "platform-registry-prod",
"syncStatus": "OutOfSync",
"healthStatus": "Degraded",
"revision": "f4e5d6c",
"target": "abc123-prod",
"timestamp": "2026-02-22T12:00:00Z",
"message": "ComparisonError: failed to sync Deployment/api-gateway",
"url": "https://argocd.example.com/applications/platform-registry-prod"
}
}
}'Run all tests:
npm testUse the included script for quick local smoke tests.
Show help:
npm run send:test -- --helpSend text:
npm run send:test -- --type text --message "Hello from script"Preview mode (does not post to Bot Framework):
npm run send:test -- --preview --type template --template sysdig --severity high --alertName "CPU saturation"Send generic template:
npm run send:test -- --type template --template generic --title "Maintenance Window" --body "DB maintenance in 30 minutes." --severity warningSend GitHub template:
npm run send:test -- --type template --template github --event opened --title "PR #123" --repo "bcgov/devx-teams-connector" --author "octocat" --url "https://github.com/bcgov/devx-teams-connector/pull/123"Send Sysdig template:
npm run send:test -- --type template --template sysdig --severity high --alertName "CPU saturation" --scope "prod-cluster" --timestamp "2026-02-22T12:00:00Z"Send uptime template:
npm run send:test -- --type template --template uptime --status degraded --service "payments-api" --responseTimeMs 620 --downSince "2026-02-22T11:40:00Z"Send DB backup template:
npm run send:test -- --type template --template db_backup --status success --database "users" --duration "2m 03s" --size "1.2 GB" --container "backup-job-1"Send Argo CD template:
npm run send:test -- --type template --template argocd --event sync_failed --application "platform-registry-prod" --syncStatus OutOfSync --healthStatus Degraded --revision "f4e5d6c" --targetName "abc123-prod" --url "https://argocd.example.com/applications/platform-registry-prod"Script env vars:
CONNECTOR_API_KEYUSER_ENTRA_IDTEAM_IDCHANNEL_IDCONNECTOR_URL(optional, defaulthttp://localhost:3000)
AUTH_FAILED: CheckAuthorizationheader and API key value.- Docker-specific
AUTH_FAILED: If your.envvalues are quoted (for exampleCONNECTOR_API_KEY="abc123"), restart with the latest build of this service (it now normalizes quoted env values for Docker--env-filecompatibility). BACKEND_UNAVAILABLE: Verify bot credentials and outbound access to Microsoft token/Bot Framework endpoints.Authorization has been denied for this request: Use the correct token tenant for your bot registration (TENANT_IDfor single-tenant bots, or defaultbotframework.comfor multi-tenant bots).DELIVERY_FAILED: Confirm the Teams app bot is installed in the target team/channel and channel ID is correct.RATE_LIMITED: Retry after theRetry-Afterheader value.