← All integrations
Webhooks
Developer reference
Live
Included on
Pro, Team & Enterprise

Webhooks

Send meeting summaries, transcripts, and action items to any app that can receive an HTTP request. No connector to wait for, and nothing to install.

Set up a webhook

A webhook is a URL you own. TeamsMaestro posts to it when something happens, and your app decides what to do next.

Create a subscription in your TeamsMaestro settings, give it the destination URL, and pick the events you care about. You can filter by meeting title, host, or participants, including the group values @external, @internal, and @domain(...). Changing a filter only affects future events. It never re-evaluates deliveries already queued.

API reference

The base URL is https://teams-app.maestrolabs.com. Webhooks you create in the TeamsMaestro applications live under /integrations/webhook/, and their delivery history under /integrations/webhook-attempts/.

The delivery request

POST https://hooks.example.com/teamsmaestro
Content-Type: application/json
X-Webhook-ID: evt_cbb2f97b-6625-48c7-bf84-faaead03f299
X-Webhook-Event: meeting.transcribed
X-Webhook-Version: 20260806
X-Webhook-Timestamp: 1785875231
X-Webhook-Signature: t=1785875231,v1=73ca72de3f8f...

X-Webhook-ID is the delivery id and is identical across retries, so use it as your idempotency key. It matches id in the body and public_id in the delivery log. X-Webhook-Event lets you route the request before parsing the body. X-Webhook-Version duplicates version in the body, and X-Webhook-Timestamp duplicates the t= value inside the signature so freshness can be checked without parsing it.

Headers are not covered by the signature. Route on X-Webhook-Version, but treat the body's version as authoritative for anything security relevant.

Return any 2xx within ten seconds. A non-2xx status, a timeout, or a connection failure all count as failures and are retried. Acknowledge first and process afterwards, since a meeting.transcribed body carries the full transcript and is unlikely to be parsed and stored inside the timeout on a cold start.

Verifying the signature

X-Webhook-Signature is t=<timestamp>,v1=<hmac>, where the HMAC is computed over the timestamp and the raw body and rendered as lowercase hexadecimal. The v1 identifies the signing scheme and has nothing to do with the payload version.

HMAC-SHA256(secret, "{timestamp}.{raw_request_body}")

Sign the raw bytes of the request body, before any JSON parsing. Re-serializing a parsed object produces different bytes and the signature will not match. The timestamp is part of the signed content, so a captured request cannot be re-stamped and rejecting old timestamps also rejects replays. A five minute window is enough, since the timestamp is regenerated on every attempt and does not need to span the retry schedule.

Delivery and retries

Delivery is at least once. A failed delivery is retried up to three times in total, one per sweep cycle and so roughly five minutes apart, across a window of about fifteen minutes, after which it is abandoned. There is no backoff, and one redirect is followed.

POST /integrations/webhook/

Create a webhook. This is the only response that includes secret. Authentication on this surface is Authorization: Token <jwt>, issued by a TeamsMaestro application. The scheme is Token, not Bearer, and a Bearer credential is rejected with 401 before any other check, so a partner integration cannot reach these endpoints.

The body takes url (required, at most 2048 characters, must resolve to a public address), scope (required, at least one of meeting.transcribed and meeting.summarized), name (optional, falling back to Webhook: <n>), filter_match (optional, all or any, default all) and filters (optional array).

{
  "id": "0022c696-b12d-40e8-9c65-ba6f50d9bc38",
  "name": "Webhook: 1",
  "url": "https://hooks.example.com/teamsmaestro",
  "scope": ["meeting.summarized"],
  "created_by": "owner@example.com",
  "source": null,
  "status": "active",
  "filter_match": "all",
  "filters": [],
  "payload_version": 20260806,
  "created_at": "2026-08-04T19:12:44.104382Z",
  "updated_at": "2026-08-04T19:12:44.104382Z",
  "secret": "a3f1..."
}

secret is 64 hexadecimal characters and is returned on create only. source is null for a first-party webhook. status is one of active, inactive, disabled and expired, and only active delivers. payload_version, status, source, created_by and the timestamps are read-only, so an unrelated PATCH cannot change the contract.

Every operation here requires an active plan with webhooks and, for a member of an organization, that the organization has not disabled member-created webhooks.

GET /integrations/webhook/

A bare, unpaginated array. Omit source for first-party webhooks, or pass zapier or power-automate for a partner platform's. An unrecognized value returns 400. GET /integrations/webhook/{id}/ returns one webhook without its secret and is not narrowed by source.

PUT and PATCH /integrations/webhook/{id}/

First-party webhooks only; a partner-created webhook returns 404. filters is written as a complete set: omitting it leaves the conditions unchanged, an array replaces them, and an empty array clears them.

DELETE /integrations/webhook/{id}/

Returns 204 No Content. First-party webhooks only.

Activate, deactivate and rotate

PATCH /integrations/webhook/{id}/activate/
PATCH /integrations/webhook/{id}/deactivate/
PATCH /integrations/webhook/{id}/regenerate_secret/
GET   /integrations/webhook/{id}/secret/

The first two move a webhook between active and inactive and return it updated, or 409 when it is disabled or expired. Events already queued for a deactivated webhook are cancelled rather than delivered. regenerate_secret issues a new signing secret and returns the webhook including it; it takes effect on the next delivery, so accept both secrets for a short period after rotating. secret returns the current one and is not narrowed by source.

Partner-created webhooks are read-only here

An account may also hold webhooks created through Zapier or Power Automate, which carry a source. Those are visible through GET, including their signing secret, but PUT, PATCH, DELETE, activate, deactivate and regenerate all return 404 for them. The restriction is keyed on the HTTP method, which is why reading a partner webhook's secret works while rotating it does not.

POST /integrations/webhook/send_sample_event/

POSTs a sample payload for each requested event type and reports the outcome of each. Nothing is persisted, retried, or filtered. Rate limited to 10 requests per minute.

The body takes webhook (optional, a stored webhook's id, which signs with that webhook's own secret and supplies any omitted url or scope), plus url and scope, which are required unless webhook is given. A webhook id belonging to another account returns 404.

{
  "signed_with": "ephemeral",
  "secret": "b71c...",
  "results": [
    {
      "event": "meeting.transcribed",
      "delivery_id": "evt_54f1c640-6c05-4147-9568-14b91bf67689",
      "payload": {},
      "result": "succeeded",
      "response_status_code": 200,
      "response_body": "ok",
      "error_message": ""
    }
  ]
}

signed_with is webhook_secret or ephemeral, and secret is the single-use secret, null when a stored webhook signed the delivery.

GET /integrations/webhook-attempts/

Every delivery attempt against the account's webhooks, with its outcome. Read-only and append-only, and it covers webhooks from every source, partner-created subscriptions included.

The query takes search (meeting title, webhook name, or a delivery id, the prefixed evt_<uuid> form included), result (succeeded, failed or cancelled), webhook (a uuid), since and until, latest_only (default true, one row per delivery), include_payload (default true), page and page_size (default 20, maximum 100).

This is the only paginated endpoint, and the wrapper key is prev, not previous.

{
  "count": 143,
  "next": "https://teams-app.maestrolabs.com/integrations/webhook-attempts/?page=2",
  "prev": null,
  "results": [
    {
      "id": 4821,
      "attempt_number": 2,
      "attempt_count": 2,
      "result": "failed",
      "response_status_code": 500,
      "response_body": "{\"error\":\"internal\"}",
      "error_message": "",
      "created_at": "2026-08-04T20:32:11.104382Z",
      "event_id": "cbb2f97b-6625-48c7-bf84-faaead03f299",
      "public_id": "evt_cbb2f97b-6625-48c7-bf84-faaead03f299",
      "event": "meeting.transcribed",
      "event_status": "retrying",
      "payload": {},
      "payload_version": 20260806,
      "meeting_id": "14f65e8d-7b09-40d3-bf87-2d32be878686",
      "meeting_title": "Acme Renewal Sync",
      "webhook_id": "0022c696-b12d-40e8-9c65-ba6f50d9bc38",
      "webhook_name": "Webhook: 1"
    }
  ]
}

event_id identifies the delivery and is stable across retries, while id identifies one attempt row. attempt_count can be lower than attempt_number, since a cancellation records a row without consuming an attempt. response_body keeps JSON in full and truncates other content at 2000 characters. GET /integrations/webhook-attempts/{id}/ returns one attempt in the same shape, and query filters do not apply to a detail read.

Delivery statuses

event_status describes the delivery and result describes one attempt. A delivery is pending while queued, in_progress while a request is in flight, and retrying after a failed attempt. The terminal states are succeeded when the receiving endpoint returned 2xx, abandoned when all three attempts are used, and cancelled when the webhook was not active and nothing was sent. A meeting excluded by a subscription's conditions records no entry at all.

error_message is populated only when no HTTP response was received: a receiving endpoint that exceeded ten seconds, a DNS, TCP or TLS failure, a host that resolves to a private or reserved address, a host that stopped resolving, a bad URL scheme, or a URL with no hostname.

Events

Two events fire today. Both carry the meeting id, title, and platform, plus the data specific to that event. Example payloads are below.

meeting.transcribed

A meeting transcript has finished processing.

{
  "id": "evt_cbb2f97b-6625-48c7-bf84-faaead03f299",
  "event": "meeting.transcribed",
  "version": 20260806,
  "sent_at": "2026-08-04T20:27:11.592354Z",
  "webhook_id": "wh_0022c696-b12d-40e8-9c65-ba6f50d9bc38",
  "account": {
    "type": "individual",
    "email": "owner@example.com",
    "name": "Alex Rivera",
    "organization": null
  },
  "data": {
    "meeting": {
      "id": "mtg_14f65e8d-7b09-40d3-bf87-2d32be878686",
      "title": "TeamsMaestro Webhook set-up",
      "platform": "teams",
      "url": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_SAMPLE#sample",
      "started_at": "2026-08-04T19:18:55.099132Z",
      "ended_at": "2026-08-04T20:04:59.099132Z",
      "duration_seconds": 2764,
      "organizer": {
        "name": "Alex Rivera",
        "email": "owner@example.com",
        "is_external": false,
        "company": "example.com"
      },
      "attendees": [
        {
          "name": "Alex Rivera",
          "email": "owner@example.com",
          "is_external": false,
          "company": "example.com"
        },
        {
          "name": "Sam Okafor",
          "email": "sam@northwind.example",
          "is_external": true,
          "company": "northwind.example"
        }
      ]
    },
    "transcript": {
      "language": "en",
      "duration_seconds": 21,
      "words_count": 60,
      "speakers": [
        "Alex Rivera",
        "Sam Okafor"
      ],
      "text": "[00:00:00] Alex Rivera: Let's get TeamsMaestro connected so our meeting notes land in the tools we already use.\n[00:00:07] Sam Okafor: I can take that one. Do we start from the TeamsMaestro side or from the automation tool?\n[00:00:14] Start in TeamsMaestro, then choose the meeting events you want sent out.\n[00:00:19] Alex Rivera: Once it's connected we should get a notification for every meeting that gets transcribed or summarized.",
      "segments": [
        {
          "text": "Let's get TeamsMaestro connected so our meeting notes land in the tools we already use.",
          "speaker": "Alex Rivera",
          "language": "en",
          "start": 0.0,
          "end": 5.6
        },
        {
          "text": "I can take that one. Do we start from the TeamsMaestro side or from the automation tool?",
          "speaker": "Sam Okafor",
          "language": "en",
          "start": 7.1,
          "end": 12.3
        },
        {
          "text": "Start in TeamsMaestro, then choose the meeting events you want sent out.",
          "language": "en",
          "start": 14.4,
          "end": 18.3
        },
        {
          "text": "Once it's connected we should get a notification for every meeting that gets transcribed or summarized.",
          "speaker": "Alex Rivera",
          "language": "en",
          "start": 19.8,
          "end": 26.1
        }
      ]
    }
  }
}
meeting.summarized

The AI summary and action items for a meeting are ready.

{
  "id": "evt_cbb2f97b-6625-48c7-bf84-faaead03f299",
  "event": "meeting.summarized",
  "version": 20260806,
  "sent_at": "2026-08-04T20:27:11.592354Z",
  "webhook_id": "wh_0022c696-b12d-40e8-9c65-ba6f50d9bc38",
  "account": {
    "type": "individual",
    "email": "owner@example.com",
    "name": "Alex Rivera",
    "organization": null
  },
  "data": {
    "meeting": {
      "id": "mtg_14f65e8d-7b09-40d3-bf87-2d32be878686",
      "title": "TeamsMaestro Webhook set-up",
      "platform": "teams",
      "url": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_SAMPLE#sample",
      "started_at": "2026-08-04T19:18:55.099132Z",
      "ended_at": "2026-08-04T20:04:59.099132Z",
      "duration_seconds": 2764,
      "organizer": {
        "name": "Alex Rivera",
        "email": "owner@example.com",
        "is_external": false,
        "company": "example.com"
      },
      "attendees": [
        {
          "name": "Alex Rivera",
          "email": "owner@example.com",
          "is_external": false,
          "company": "example.com"
        },
        {
          "name": "Sam Okafor",
          "email": "sam@northwind.example",
          "is_external": true,
          "company": "northwind.example"
        }
      ]
    },
    "summary": {
      "template": "basic",
      "template_id": null,
      "text": "## What we covered\n- Getting TeamsMaestro connected so meeting notes reach the tools we already work in\n- Who owns the setup, and what we should expect to see once it is live\n\n## How the connection works\n- Connect the TeamsMaestro account from the automation tool and approve access\n- Choose which meeting events to listen for: transcribed, summarized, or both\n- Point the connection at wherever the notes should end up\n\n## Next steps\n- Run a test meeting and check the notes come through\n- Sam Okafor to confirm the destination before we roll it out to the team"
    },
    "action_items": [
      {
        "id": "act_1993326d-4d84-4298-b901-beea9a0dd3fb",
        "title": "Run a test meeting and check the notes come through",
        "status": "done",
        "assignee": {
          "name": "Alex Rivera",
          "email": "owner@example.com"
        }
      },
      {
        "id": "act_7c0be4f1-2a55-4a7e-b0c1-3d9f5e2a1b88",
        "title": "Confirm where the notes should land before rolling this out",
        "status": "todo",
        "assignee": {
          "name": "Alex Rivera",
          "email": "owner@example.com"
        }
      }
    ]
  }
}

Frequently asked questions

Do I need to verify the signature?

Strongly recommended for production use. Subscriptions created directly in TeamsMaestro get a signing secret and can verify with the HMAC scheme above. Subscriptions created through a partner integration, such as Zapier, Power Automate, or a custom OAuth app, do not expose a secret, so treat the delivery URL itself as the credential instead.

What happens if my endpoint is temporarily unreachable?

TeamsMaestro retries up to 3 times, roughly 5 minutes apart, across about 15 minutes, then abandons the delivery. There is no backoff, since the interval is fixed to the retry sweep cycle rather than an increasing delay.

Can I limit which meetings trigger a delivery?

Yes. Filter on meeting title, host, or participants, including the @external, @internal, and @domain(...) group values. Changing a filter only affects future events. It never re-evaluates or cancels deliveries already queued.