The notes from a sales call are only useful if they reach the room. Everyone says “I’ll send a recap” and half the time it never goes out. If a bot already joined the meeting, recorded it, and produced a summary, the recap should send itself the moment the call ends.
This recipe wires that up. When a meeting bot finishes processing, a webhook fires, your code fetches the summary and transcript, and a recap email goes out through a connected mailbox, no human in the loop. It builds on the media fetch covered in Get a meeting transcript and recording.
How do you email a meeting summary automatically?
Section titled “How do you email a meeting summary automatically?”You email a recap in three steps: subscribe to the notetaker.media webhook so you know when processing finishes, fetch the meeting media with the Notetaker API to get the summary and transcript, then send the recap through a connected mailbox. The whole chain runs server-side with no human action, and it works for any meeting a bot joined on Google Meet, Zoom, or Microsoft Teams.
Timing is the reason this is webhook-driven. Media isn’t ready when the call ends, only after the bot finishes transcribing, usually 2 to 5 minutes later, so polling wastes calls. The webhook tells you the exact moment the summary exists.
Trigger on the notetaker.media webhook
Section titled “Trigger on the notetaker.media webhook”The notetaker.media webhook carries a state field, and it fires for more than one state, including processing and error states, not just success. Only state of available means the media is ready to download, so guard on it before you act. The payload also carries the notetaker id and the grant_id, the two values you need to fetch the media. Acknowledge with a 200 OK within 10 seconds, then do the work.
The handler below captures the IDs and hands off to the send step. Acknowledging before fetching keeps the download and send, which can take a few seconds, from timing out the webhook window. Dedupe on the notetaker ID, because webhook delivery is at-least-once.
import jsonfrom fastapi import FastAPI, Request, BackgroundTasks, HTTPExceptionapp = FastAPI()
@app.post("/webhooks/notetaker")async def on_media(request: Request, background_tasks: BackgroundTasks): raw = await request.body() # Reject forged events before acting (see Receive real-time webhooks for the check). if not verify_nylas_signature(raw, request.headers.get("X-Nylas-Signature")): raise HTTPException(status_code=401) payload = json.loads(raw) if payload["type"] == "notetaker.media": obj = payload["data"]["object"] if obj.get("state") == "available": # also fires for processing/errors # Hand off everything, including the attendee lookup, to run after the 200 ack. background_tasks.add_task(send_recap, obj["grant_id"], obj["id"]) return {"ok": True}The webhook challenge handshake and signature check are the same as any Nylas webhook, covered in Receive real-time webhooks.
Fetch the summary and transcript links
Section titled “Fetch the summary and transcript links”With the IDs in hand, call GET /v3/grants/{grant_id}/notetakers/{notetaker_id}/media to get the meeting’s media objects. The response returns a summary and a transcript object, each with a pre-authenticated url. Those links expire after one hour (ttl of 3,600 seconds), so download the content as part of the send rather than emailing the raw link.
The function below pulls the media and downloads the summary JSON. Downloading the file rather than forwarding the URL matters because a recipient who opens the email 90 minutes later would otherwise hit a dead link. The full media object and its retention behavior are documented in Get a meeting transcript and recording.
import os, requestsNYLAS = "https://api.us.nylas.com"HEADERS = {"Authorization": f"Bearer {os.environ['NYLAS_API_KEY']}"}
def get_summary(grant_id, notetaker_id): media = requests.get( f"{NYLAS}/v3/grants/{grant_id}/notetakers/{notetaker_id}/media", headers=HEADERS).json()["data"] summary = requests.get(media["summary"]["url"]).json() # link valid for 1 hour return summaryCompose and send the recap email
Section titled “Compose and send the recap email”Send the recap through the host’s own mailbox with POST /v3/grants/{grant_id}/messages/send, the same endpoint used for any outbound mail. Because it sends as the host, the recap threads naturally and replies land back in their inbox. Drop the summary into the body and offer the transcript as a follow-up, so the email stays scannable in the 10 seconds most people give it.
The function below sends a formatted recap to the meeting attendees. Sending from the host’s address rather than a no-reply alias is what gets the recap opened, since it looks like a normal message from a colleague. For the full send field reference, see Send email without SMTP.
def send_recap(grant_id, notetaker_id): attendees = lookup_attendees(notetaker_id) # from your own meeting record summary = get_summary(grant_id, notetaker_id) body = f"<p>{summary}</p><p>Reply here for the full transcript.</p>" requests.post( f"{NYLAS}/v3/grants/{grant_id}/messages/send", headers=HEADERS, json={ "to": [{"email": a} for a in attendees], "subject": "Recap: today's meeting", "body": body, }, ).raise_for_status()Send to attendees who aren’t connected accounts
Section titled “Send to attendees who aren’t connected accounts”The host has a grant, but the prospect on the other end of the call doesn’t, and you still want them to get the recap. Sending through the host’s mailbox already reaches external attendees as ordinary recipients, so one messages/send covers the whole invite list. The host’s per-account send limits apply, near 500 messages a day on Gmail consumer accounts and 2,000 on Workspace.
When no host mailbox should be on the email, for example a system-generated recap from your product, send from a verified domain instead. That path uses transactional send, which needs no grant and no per-user mailbox, so the recap comes from your application rather than a person.
Things to know about emailing recaps
Section titled “Things to know about emailing recaps”The one-hour link expiry is the detail that bites people. The url on every media object is a signed link valid for 3,600 seconds, so a job that fetches now and sends the link for later delivery will ship a dead link. Download the summary and transcript content when the webhook fires, then attach or inline it.
Media only exists after processing completes, which is why this is webhook-driven rather than tied to the call’s end time. A 30-minute call typically reaches media_available a few minutes after everyone hangs up. If you call the media endpoint while the media is still processing you get a 404, and once the files are deleted past the retention window you get a 410, so treat the webhook as the trigger and don’t poll.
What’s next
Section titled “What’s next”- Get a meeting transcript and recording for the full media object and downloads
- Send email without SMTP for the send request reference
- Send transactional email from a domain for recaps with no host mailbox
- Receive real-time webhooks for the webhook handshake and retries
- Notetaker API for bot creation and meeting capture