After you send a Notetaker bot to a call, there’s no clean signal for when the meeting actually ends or when the transcript finishes processing. Polling the media endpoint on a timer means you either check too often and waste requests, or check too rarely and make your users wait. Processing time also varies with meeting length, so a fixed interval never fits every call. Webhooks remove the guesswork: Nylas pushes a notification the instant a recording or transcript becomes available, and your handler reacts in one step.
This recipe wires up two Notetaker triggers so your app reacts to state changes and grabs media as soon as it’s ready, instead of polling.
Subscribe to Notetaker events
Section titled “Subscribe to Notetaker events”Two triggers cover the full lifecycle. The notetaker.meeting_state event reports state changes as the bot joins, records, and leaves, and notetaker.media fires once the recording and transcript finish processing and are ready to download. Subscribing to both means you track the meeting live and still get a clean “files are ready” signal at the end.
Create the webhook with one POST /v3/webhooks/ request. Pass your HTTPS endpoint as webhook_url, the two triggers in trigger_types, and an optional notification_email_addresses list that Nylas alerts if delivery starts failing. The endpoint must return 200 OK within 10 seconds.
curl --request POST \ --url 'https://api.us.nylas.com/v3/webhooks/' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --data-raw '{ "trigger_types": ["notetaker.media", "notetaker.meeting_state"], "webhook_url": "https://yourapp.com/webhooks/nylas", "description": "Notetaker transcript-ready notifications", "notification_email_addresses": ["[email protected]"] }'const webhook = await nylas.webhooks.create({ requestBody: { triggerTypes: ["notetaker.media", "notetaker.meeting_state"], webhookUrl: "https://yourapp.com/webhooks/nylas", description: "Notetaker transcript-ready notifications", },});webhook = nylas.webhooks.create( request_body={ "trigger_types": ["notetaker.media", "notetaker.meeting_state"], "webhook_url": "https://yourapp.com/webhooks/nylas", "description": "Notetaker transcript-ready notifications", })When you create the webhook, the API sends a GET request with a challenge query parameter that your endpoint echoes back to confirm the URL. For the full create flow and the challenge handshake, see using webhooks with Nylas.
What the notetaker.media payload contains
Section titled “What the notetaker.media payload contains”The notetaker.media event arrives when processing finishes, with status and state set to available. The payload’s data.object carries the bot identifiers plus a media object holding signed download URLs. You don’t get the file bytes in the webhook; you get the IDs you need to fetch them. Across all Notetaker events, the payload shape stays the same except for the inner state and media fields.
The two IDs you extract are data.object.id (the Notetaker ID) and data.object.grant_id. Together they build the media endpoint URL in the next step. The media object also reports recording_duration in seconds and recording_file_format, so your handler can branch on file type before downloading.
{ "specversion": "1.0", "type": "notetaker.media", "source": "/nylas/notetaker", "id": "<WEBHOOK_ID>", "time": 1737500935555, "data": { "application_id": "<NYLAS_APPLICATION_ID>", "object": { "id": "<NOTETAKER_ID>", "grant_id": "<NYLAS_GRANT_ID>", "object": "notetaker", "status": "available", "state": "available", "media": { "recording": "<SIGNED_URL>", "recording_duration": "1800", "recording_file_format": "mp4", "thumbnail": "<SIGNED_URL>", "transcript": "<SIGNED_URL>", "summary": "<SIGNED_URL>", "action_items": "<SIGNED_URL>" } } }}React to a ready transcript
Section titled “React to a ready transcript”When the notetaker.media event lands, call GET /v3/grants/<NYLAS_GRANT_ID>/notetakers/<NOTETAKER_ID>/media using the two IDs from the payload to get fresh signed URLs for the recording, transcript, summary, and action items. The signed links the endpoint returns expire after 3,600 seconds (one hour), so fetch and store the files right away rather than queuing the work for later.
The handler below verifies the trigger type, pulls the IDs, and requests the media. Return 200 OK fast and move the download to a background job if it’s slow, since Nylas treats anything past the 10-second window as a failed delivery.
app.post("/webhooks/nylas", async (req, res) => { const { type, data } = req.body; res.sendStatus(200); // ack within 10 seconds
if (type === "notetaker.media") { const media = await nylas.notetakers.downloadMedia({ identifier: data.object.grant_id, notetakerId: data.object.id, }); await storeTranscript(media); // links expire after 3,600 seconds }});@app.post("/webhooks/nylas")def nylas_webhook(payload: dict): if payload["type"] == "notetaker.media": obj = payload["data"]["object"] media = nylas.notetakers.get_media( identifier=obj["grant_id"], notetaker_id=obj["id"], ) store_transcript(media) # links expire after 3,600 seconds return "", 200Things to know about Notetaker webhooks
Section titled “Things to know about Notetaker webhooks”These details decide whether your handler stays reliable in production. Each one maps to a real field in the notetaker.meeting_state payload or a delivery behavior shared by every Nylas webhook. Plan for all four before you ship; ordering and retries cause most of the surprises.
Meeting state values
Section titled “Meeting state values”The notetaker.meeting_state payload carries both a top-level state and a more specific meeting_state field. When state is connecting, meeting_state is waiting_for_entry. When state is attending, it’s recording_active, the signal that recording started. When state is failed_entry, meeting_state is one of 12 error reasons: error, internal_error, bad_meeting_code, bad_meeting_link, sign_in_required, entry_denied, no_response, cannot_join, meeting_capacity_reached, admission_timeout, network_error, or cancelled. Branch on these to retry, alert, or re-invite the bot.
Verify the signature
Section titled “Verify the signature”Every notification carries an X-Nylas-Signature header: a hex-encoded HMAC-SHA256 of the raw request body, signed with your webhook_secret. Recompute the HMAC over the unmodified body and reject any request that doesn’t match before you act on it. See secure a webhook for the verification details and where the webhook_secret comes from, and webhook best practices for retries and compressed delivery.
Retries and idempotency
Section titled “Retries and idempotency”Nylas retries delivery when your endpoint doesn’t return 200 OK, so the same notetaker.media event can arrive more than once. Each payload includes a webhook_delivery_attempt counter and a stable top-level id. Key your processing on that id (or on the Notetaker ID) so a duplicate delivery doesn’t trigger a second download or a duplicate database row.
Ordering isn’t guaranteed
Section titled “Ordering isn’t guaranteed”Webhooks aren’t guaranteed to arrive in the order events happened, so a later meeting_state change can land before an earlier one. Trust the time field on each payload rather than arrival order, and treat notetaker.media as the authoritative “ready” signal instead of inferring readiness from a sequence of state events.
What’s next
Section titled “What’s next”- Get the transcript and recording to download finished files once the media event fires
- Notetaker API guide for the full bot lifecycle
- Using webhooks with Nylas for setup, the challenge handshake, and failure handling
- Using Nylas Notetaker for meeting settings, summaries, and media handling