Tutorial9 min read

AI Meeting Summary API: How to Automatically Summarise Any Meeting

Stop summarising meetings manually. This guide covers the complete pipeline for generating AI meeting summaries from any call — the bot API, webhook handling, LLM prompt engineering, and delivery to Slack, Notion, or your CRM.

Gregnote Team

23 August 2026

Why manual meeting summaries don't scale

The end of a meeting is the worst possible time to write a summary. You're thinking about the next meeting, your open Slack messages, the action item you just committed to. The context is in your head but decaying fast.

An hour later, the summary is half as good. A day later, it's a rough outline. A week later, you're not sure what you actually decided.

AI meeting summaries work because they happen automatically, immediately after the meeting ends, from the complete transcript rather than your fading memory. The result is more accurate and more consistent than anything written by hand.

Here's how to build this pipeline for your product or team.

The architecture: four components

A complete AI meeting summary pipeline has four parts:

  1. Meeting capture — a bot joins the call and records audio
  2. Transcription — speech is converted to text with speaker labels
  3. LLM summarisation — the transcript is processed into structured output
  4. Delivery — the summary is sent to Slack, Notion, email, CRM, or wherever

Each component is a separate concern. You can swap implementations of any one without touching the others. Let's build each step.

Step 1: Capture the meeting

Use the Gregnote API to send a bot to any Google Meet or Microsoft Teams call:

const response = await fetch("https://api.gregnote.com/v1/bots", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GREGNOTE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    meeting_url: meetingUrl,
    bot_name: "Summary Bot",
    webhook_url: `${process.env.BASE_URL}/hooks/meeting`,
    metadata: {
      meeting_type: "sales_call",  // drives which prompt to use
      user_id: userId,
      slack_channel: "#deal-updates",
    }
  })
});

The bot joins within seconds. When the meeting ends, a webhook fires to your URL with the full transcript.

Step 2: Receive the transcript webhook

import crypto from "crypto";

app.post("/hooks/meeting", express.raw({ type: "*/*" }), (req, res) => { // Verify signature const sig = req.headers["x-gregnote-signature"] as string; const [t, v] = sig.split(","); const expected = crypto .createHmac("sha256", process.env.GREGNOTE_WEBHOOK_SECRET!) .update(t.slice(2) + "." + req.body) .digest("hex"); if (v.slice(3) !== expected) return res.sendStatus(401);

// Acknowledge immediately — don't block on LLM calls res.sendStatus(200);

const payload = JSON.parse(req.body.toString()); if (payload.event === "meeting.completed") { summaryQueue.add("summarise", payload); } }); ```

Step 3: Prompt engineering for great summaries

This is where the quality lives. A generic "summarise this transcript" prompt produces generic output. Prompts tailored to meeting type produce output your users actually find useful.

The transcript format to pass

function formatTranscript(segments) {
  return segments
    .map(s => `[${formatTimestamp(s.start)}] ${s.speaker}: ${s.text}`)
    .join("
");
}

function formatTimestamp(seconds) { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return ${m}:${String(s).padStart(2, "0")}; } ```

Including timestamps in the formatted transcript helps the LLM ground its output — action items attributed to a specific moment in the conversation are more reliable than those extracted from pure text.

Meeting-type-specific prompts

const PROMPTS = {
  sales_call: `
You are analysing a sales call. Extract and return JSON with:
- summary: 2-3 sentence overview of the call
- prospect_pain_points: array of specific problems they mentioned
- objections: array of concerns raised (price, timing, competition, etc.)
- action_items: array of {owner, task, due_date} — be specific
- deal_signals: object with {budget_mentioned: bool, timeline: string, decision_maker: string}
- next_step: the single most important next action
  `,

customer_success: You are reviewing a customer success call. Extract and return JSON with: - summary: 2-3 sentence overview - health_indicators: positive and negative signals about account health - product_feedback: specific feature requests or complaints - risks: any signals the customer might churn or reduce usage - action_items: array of {owner, task, due_date} - nps_signal: would you say the customer is a promoter, passive, or detractor based on tone? ,

internal_planning: You are reviewing a team planning meeting. Extract and return JSON with: - summary: what was discussed and decided - decisions: list of explicit decisions made - action_items: array of {owner, task, due_date} - open_questions: things that need more information or follow-up - blockers: anything that could slow down the work discussed ,

interview: You are reviewing a job interview. Extract and return JSON with: - candidate_strengths: specific examples of strong answers or demonstrated skills - areas_of_concern: gaps, vague answers, or concerns raised - key_examples: notable stories or examples the candidate shared - interviewer_recommendation: based on tone and content, did the interviewer seem positive, neutral, or negative? - follow_up_questions: questions that went unanswered or need clarification , }; ```

The LLM call

async function generateSummary(transcript, meetingType) {
  const formatted = formatTranscript(transcript.segments);
  const prompt = PROMPTS[meetingType] ?? PROMPTS.internal_planning;

const result = await openai.chat.completions.create({ model: "gpt-4o", messages: [ { role: "system", content: prompt }, { role: "user", content: `TRANSCRIPT:

${formatted}` } ], response_format: { type: "json_object" }, temperature: 0.2, // low temp for factual extraction });

return JSON.parse(result.choices[0].message.content); } ```

Use temperature: 0.2 for summarisation tasks. Higher temperatures introduce variability and hallucinations that don't belong in a meeting notes context. The transcript is the ground truth — the LLM's job is extraction, not creativity.

Step 4: Deliver the summary

Slack

async function sendToSlack(summary, channel, meetingType) {
  const blocks = [
    {
      type: "header",
      text: { type: "plain_text", text: `📝 Meeting Summary` }
    },
    {
      type: "section",
      text: { type: "mrkdwn", text: summary.summary }
    },
  ];

if (summary.action_items?.length) { blocks.push({ type: "section", text: { type: "mrkdwn", text: "*Action items* " + summary.action_items .map(a => • *${a.owner}*: ${a.task}${a.due_date ? (by ${a.due_date}) : ""}) .join(" ") } }); }

await slack.chat.postMessage({ channel, blocks }); } ```

Notion

async function sendToNotion(summary, pageId) {
  const blocks = [
    {
      object: "block",
      type: "heading_2",
      heading_2: { rich_text: [{ type: "text", text: { content: "Summary" } }] }
    },
    {
      object: "block",
      type: "paragraph",
      paragraph: { rich_text: [{ type: "text", text: { content: summary.summary } }] }
    },
    // ... action items, decisions, etc.
  ];

await notion.blocks.children.append({ block_id: pageId, children: blocks }); } ```

Handling edge cases

Short meetings and small talk

Meetings under 3 minutes often don't have meaningful content — they're scheduling calls, quick check-ins, or dropped connections. Add a minimum duration check:

if (payload.duration_seconds < 180) {
  // Too short to summarise meaningfully — log but skip LLM
  return;
}

Empty or single-speaker transcripts

If the transcript has one or zero speakers, it's likely a no-show. Gregnote fires bot.no_show instead of meeting.completed in this case on Pro — but as a defensive check:

const uniqueSpeakers = new Set(segments.map(s => s.speaker)).size;
if (uniqueSpeakers < 2) {
  // Don't summarise a monologue — handle as no-show
  return;
}

LLM rate limits and failures

LLM APIs are occasionally slow or unavailable. Wrap your LLM call in retry logic with exponential backoff, and put the whole processing job in a durable queue so a failure doesn't lose the transcript.

Cost per meeting summary

For a typical 30-minute sales call: - Gregnote capture: ~£0.25 (0.5 hrs × £0.50/hr) - GPT-4o summarisation: ~£0.03 (roughly 4,000 tokens in, 400 out) - Total: ~£0.28 per meeting

For a team with 50 meetings per week: roughly £14/week, or ~£60/month. That's the cost of one hour of a junior employee's time — for saving the whole team hours every week.

Further reading

For the full SaaS product architecture built around this pipeline, see building meeting intelligence into your SaaS product. For prompt-level improvements to transcription quality, see 10 ways to improve meeting transcription accuracy.

Try it yourself

API key in 30 seconds. Free credit on sign-up. No card required.