> ## Documentation Index
> Fetch the complete documentation index at: https://miniti.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# webhooks

> POST meeting data as json to any endpoint when a meeting is saved or updated

miniti can POST a meeting payload as json to any url when a meeting is saved, when insights are regenerated, or when a saved transcript is trimmed. works with zapier, make, n8n, and any custom endpoint.

## configure

1. open **settings** (`⌘,`)
2. go to the webhooks section
3. paste a url into **webhook url**
4. leave it empty to disable

miniti sends requests directly from your device. no backend sits in the middle.

## events

| event             | fires when                                                                      |
| ----------------- | ------------------------------------------------------------------------------- |
| `meeting.saved`   | you save a meeting from a live / stopped session                                |
| `meeting.updated` | you regenerate insights on an already-saved meeting, or trim a saved transcript |

## request

* method: `POST`
* headers: `Content-Type: application/json`
* timeout: 10 seconds
* retries: none (fire-and-forget)
* signing: none (the webhook url is the shared secret - keep it private)

<Warning>
  webhooks are unsigned. anyone with your webhook url can send payloads to it. use a hard-to-guess url path, or put a reverse proxy in front of your endpoint that checks a secret token from the url.
</Warning>

## payload

<CodeGroup>
  ```json meeting.saved theme={null}
  {
    "event": "meeting.saved",
    "meeting": {
      "id": "3F9A2B1C-4D5E-4F6A-9B7C-0D1E2F3A4B5C",
      "title": "acme discovery call",
      "date": "2026-04-20T14:00:00Z",
      "end_time": "2026-04-20T14:32:17Z",
      "duration_seconds": 1937,
      "language": "en",
      "summary": "brief overview of the meeting...",
      "action_items": [
        "send follow-up email to jane",
        "schedule product demo next tuesday"
      ],
      "key_decisions": [
        "proceed with 30-day pilot"
      ],
      "topics": [
        "pricing",
        "onboarding",
        "integrations"
      ],
      "discussion_flow": [
        "introductions",
        "pain points",
        "product walkthrough",
        "next steps"
      ],
      "notes": "jane seems to own budget. mentioned competitor x.",
      "meddpicc": {
        "metrics": "targeting 40% lift in pipeline",
        "economic_buyer": "jane (vp ops)",
        "decision_criteria": "ease of onboarding, support sla",
        "decision_process": "pilot then exec review",
        "paper_process": "security + legal (2-week turnaround)",
        "identified_pain": "manual data entry across tools",
        "champion": "jane",
        "competition": "competitor x"
      },
      "training": {
        "talk_ratio_you": 0.42,
        "duration_minutes": 32.3,
        "speakers": [
          {
            "speaker": "you",
            "is_you": true,
            "word_count": 1843,
            "words_per_minute": 143.2,
            "fillers_per_minute": 2.1,
            "total_fillers": 68,
            "fillers": { "um": 24, "uh": 18, "like": 26 },
            "longest_monologue_words": 312,
            "questions_asked": 14,
            "avg_words_per_turn": 21.4
          }
        ]
      },
      "questions": [
        {
          "question": "what would make the pilot a clear success?",
          "type": "clarify",
          "context": "jane mentioned 'success' twice but never quantified it"
        }
      ],
      "speaker_count": 2,
      "speaker_names": {
        "speaker 1": "jane doe"
      },
      "transcript": [
        {
          "speaker": "you",
          "text": "thanks for making time today...",
          "timestamp": 0.42
        },
        {
          "speaker": "jane doe",
          "text": "happy to, let me share some context...",
          "timestamp": 3.88
        }
      ],
      "calendar_event_id": "abc123@google.com",
      "attendees": [
        { "email": "jane@acme.com", "name": "jane doe", "domain": "acme.com" }
      ]
    }
  }
  ```

  ```json meeting.updated theme={null}
  {
    "event": "meeting.updated",
    "meeting": {
      "id": "3F9A2B1C-4D5E-4F6A-9B7C-0D1E2F3A4B5C",
      "title": "acme discovery call",
      "date": "2026-04-20T14:00:00Z",
      "end_time": "2026-04-20T14:32:17Z",
      "duration_seconds": 1937,
      "language": "en",
      "transcript_edited_at": "2026-04-20T15:02:44Z",
      "transcript_revision": 2,
      "insights_stale": true,
      "summary": "",
      "action_items": [],
      "key_decisions": [],
      "topics": [],
      "discussion_flow": [],
      "notes": "...",
      "speaker_count": 2,
      "transcript": [ { "speaker": "you", "text": "...", "timestamp": 0.42 } ]
    }
  }
  ```
</CodeGroup>

### field notes

* `meddpicc`, `training`, `questions`, `attendees`, and `speaker_names` are **omitted** when empty - do not assume these keys are always present
* `speaker_names` is a map from the underlying diarization key to the resolved real name (e.g. `"jane doe"`). the key is whatever miniti uses internally to identify that speaker - it is **not guaranteed to be a human-readable `"speaker N"` string**, so treat it as an opaque id and look up names by key rather than parsing it. see [speaker names](/docs/features/speaker-names) for how miniti resolves them.
* `transcript[].speaker` is the **display label** for that segment: the resolved real name when one is available, otherwise the display form of the diarization label, or `"you"` for your own mic. do not assume it matches the `speaker_names` key.
* `calendar_event_id` is present only if the meeting was linked to a google calendar event
* `transcript[].timestamp` is seconds from the start of the recording
* `transcript_edited_at` is present after a saved transcript has been trimmed
* `transcript_revision` increments each time the saved transcript is edited
* `insights_stale` is true when transcript edits have cleared generated insights and the meeting has not been regenerated yet
* after transcript trimming, generated insight fields may be empty until the user regenerates insights
* `date` and `end_time` are ISO 8601 utc
* keys are always json-sorted - downstream consumers can rely on stable ordering

## matching records in other systems

* use `meeting.id` as the external id and idempotency key for the meeting. `meeting.updated` keeps the same id, so downstream systems can replace the existing record instead of creating a duplicate.
* if your system groups meetings by customer or company, match against `meeting.attendees[].domain`. this is available when the meeting came from google calendar attendees.
* use `meeting.calendar_event_id` when you need to link back to the source google calendar event.
* action items do not have their own ids today. if your downstream system needs one row per action item, derive a stable id from `meeting.id` plus the action item index or a hash of the action item text.

## receiver requirements

* respond with **any 2xx** status - miniti logs non-2xx as failures but does not retry
* respond within **10 seconds** or miniti will time out
* accept a payload up to \~a few mb for long meetings (transcript size dominates)

## recipes

* **zapier / make / n8n** - use their built-in "webhook by url" trigger and map fields from the `meeting` object
* **slack** - forward summary + action items to a channel by transforming the payload in a small middle function
* **notion / airtable** - create a row per `meeting.saved` event, mapping `id`, `title`, `summary`, `action_items`

## see also

* [attio integration](/docs/integrations/attio) - native push for attio users
* [markdown export](/docs/features/markdown-export) - file-based alternative that works offline
