# Microsoft Teams integration

> Announce query requests in a Teams channel and wire approvals through an HMAC-verified action endpoint with single-use action tokens.

- Documentation: Guides
- Updated: 2026-10-13
- Source: https://queryproxy.com/docs/teams-integration/
- Language: en-US
- Author: Muhammet Şafak

---
The Teams integration has two halves: **announcements** into a channel via an
incoming webhook, and an **action endpoint** your automation (an outgoing
webhook or a Power Automate flow) can call to approve or reject.

## Announcements

1. Add an **Incoming Webhook** to your Teams channel and copy its URL.
2. In QueryProxy, open **ChatOps**. A DBA pastes the webhook URL and sets the
   enabled flag; the **HMAC secret** (any strong random string — you'll use the
   same secret for the action endpoint below) can only be set by a **system
   administrator**. Both are stored encrypted. An integration cannot be saved
   without a secret, so the first save is an administrator's. The webhook URL
   must be `https://` and its host must be on the allowlist (`hooks.slack.com`,
   `*.webhook.office.com` and `*.logic.azure.com` by default; the list is shared
   across chat providers; extend it with `QUERYPROXY_CHAT_WEBHOOK_ALLOWED_HOSTS`).

> **Breaking change — the signing secret is administrator-only.** From this
> version a DBA can no longer enter or rotate it, and rotating an existing
> secret requires proving the current one. An HMAC proves that the caller knows
> the secret, not that a particular person approved: anyone who could *choose*
> the secret could sign a call approving their own request in a colleague's
> name, and the audit trail would record the colleague. On an existing install,
> rotate the secret once as an administrator after upgrading.

**Removing** an integration is administrator-only for the same reason: putting
it back needs a signing secret only an administrator can set. To switch the
integration off, a DBA unticks the **Enabled** flag and saves.

New requests are posted as a card with the requester, connection, statement
count, a SQL preview (cut at 400 characters; turn it off with
`QUERYPROXY_CHAT_INCLUDE_SQL=false`) and a **Review in QueryProxy** link. Decisions are
announced back to the channel as they happen.

Each card also carries a single-use **action token** for that request, in a
`queryproxy` envelope at the top level of the payload:

```json
{
  "queryproxy": { "request_id": 123, "action_token": "…" },
  "type": "MessageCard"
}
```

Teams renderers ignore the unknown key, so the token stays out of the visible
card; your automation reads it there and echoes it back to the action endpoint.

## The action endpoint

To approve or reject *from* Teams-side automation, call:

```
POST https://your-queryproxy-host/webhooks/teams/actions
X-QueryProxy-Timestamp: <unix seconds>
Authorization: HMAC <base64(HMAC_SHA256("{timestamp}:{raw_body}", secret))>
Content-Type: application/json
```

```json
{
  "action": "approve",
  "request_id": 123,
  "actor_id": "<AAD object id>",
  "token": "<action token>",
  "reason": "optional — used for reject"
}
```

- `action` — `approve` or `reject`.
- `request_id` — the request number from the announcement card.
- `actor_id` — the Azure AD **object id** of the approver. QueryProxy resolves
  it to a user through the **Teams ID** mapping an admin sets in
  **Teams & Users → Manage users** (an unlinked id is rejected with `422`); it is never resolved by a
  self-declared email. The resolved user must also belong to the integration's
  team (a system admin always passes this check); otherwise the call is
  refused with `403` before it ever reaches the policy. The usual rules still
  apply: they need the DBA role in the request's team and cannot decide on
  their own request. As on the web, an admin may override both rules, and an
  admin's approval that does so is flagged in the audit log.
- `token` — **required**: the single-use action token from the card's
  `queryproxy` envelope. It is bound to that one request, expires after **24
  hours** and is burned on the first decision (only once that decision is
  actually recorded, so a call the policy turns away does not use it up), so a captured call cannot be
  replayed and the shared secret alone cannot conjure a decision. A call without
  a live token (missing, expired or already spent) is refused with `422` and a
  deliberately vague message.
- `reason` — optional for `reject`; defaults to "Rejected via Teams by
  &lt;name&gt;".

> **Breaking change — `token` is mandatory.** Teams automations built against
> earlier versions must start reading it from the card's `queryproxy` envelope
> and sending it, or their calls will be refused. Requests announced before this
> version have no token at all: decide those in the web UI.

The signature is computed over `"{timestamp}:{raw_body}"` (the same
timestamped scheme as [Slack](/docs/slack-integration/)), base64-encoded, in
the `Authorization: HMAC …` header. A Power Automate flow can compute this
header directly.

The HMAC key is the secret as stored, **unless the secret is valid base64**: then
QueryProxy decodes it and signs with the decoded bytes (the convention Teams
outgoing webhooks use). Most purely alphanumeric strings are valid base64, so
sign with the decoded bytes on your side — or include a character outside the
base64 alphabet (such as `-` or `!`) to have the secret used as-is.

## Security

- The `X-QueryProxy-Timestamp` header is required and must be within **±5
  minutes** of server time, so a captured request cannot be replayed later.
- An invalid or missing HMAC (or a stale/absent timestamp) returns `401` and
  changes nothing.
- The action token makes every call answer one specific announcement, once. The
  HMAC alone proves that the caller holds the shared secret — never which person
  is acting — which is also why the secret is administrator-only.
- The approver must be pre-linked through the **Teams ID** mapping and must be a
  member of the integration's team (or a system admin) — the shared secret
  alone cannot act as an arbitrary user.
- Responses are JSON (`{"ok": true|false, "message": "…"}`); authorization
  failures return `403` with the reason, an invalid token or unlinked identity
  returns `422`, and an unknown request id or one outside the integration's
  team returns `404`.
- The endpoint is rate-limited like every webhook in QueryProxy.

> Full interactive Adaptive Card buttons (as with
> [Slack](/docs/slack-integration/)) require a Teams bot registration and
> are on the roadmap; the action endpoint covers approvals-from-Teams today.
