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.