Interview scheduling is a coordination nightmare. Recruiters cross-reference interviewer calendars by hand, candidates wait days for a confirmed slot, and when the call finally happens nobody records it consistently. The debrief devolves into “I think they said something about distributed systems” because the notes are incomplete or missing entirely.
This recipe builds a pipeline that removes all of that. Candidates pick their own slot from a scheduling page, Scheduler distributes the load across your panel with round-robin, every booking gets a conferencing link, and Notetaker joins each call. After the interview, a webhook delivers a structured transcript, summary, and action items mapped to your hiring scorecard.
The pipeline
Section titled “The pipeline”candidate visits scheduling page ─▶ Scheduler picks interviewer (max-fairness or max-availability) │ ▼ event created on interviewer's calendar with conferencing link │ booking.created webhook ─▶ create record in ATS │ interview happens, Notetaker joins │ notetaker.media webhook ─▶ download transcript + summary + action items │ ▼ attach to candidate's ATS recordBy the end you’ll have a Configuration with round-robin distribution, automatic conferencing (Google Meet, Microsoft Teams, or Zoom), Notetaker on every call, a scheduling page (Nylas-hosted or embedded), and webhook handlers wired into your ATS.
Before you begin
Section titled “Before you begin”Make sure you have the following before starting this tutorial:
- A Nylas account with an active application
- A valid API key from your Nylas Dashboard
- At least one connected grant (an authenticated user account) for the provider you want to work with
- Node.js 18+ or Python 3.8+ installed (depending on which code samples you follow)
You also need the following for this tutorial:
- Connected grants for each interviewer on your hiring panel, each with calendar access
- A conferencing provider configured for at least one grant (Google Meet, Microsoft Teams, or Zoom). See Adding conferencing to bookings for setup.
- Notetaker enabled on your Nylas plan
- A publicly accessible HTTPS endpoint to receive webhook notifications (use VS Code port forwarding or Hookdeck for local development)
Create a round-robin scheduling configuration
Section titled “Create a round-robin scheduling configuration”Create a Configuration that defines your interview panel, meeting duration, and round-robin distribution method.
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/scheduling/configurations' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "requires_session_auth": false, "participants": [ { "name": "Sarah Kim", "email": "[email protected]", "is_organizer": true, "availability": { "calendar_ids": ["primary"] }, "booking": { "calendar_id": "primary" } }, { "name": "Marcus Johnson", "email": "[email protected]", "is_organizer": false, "availability": { "calendar_ids": ["primary"] }, "booking": { "calendar_id": "primary" } }, { "name": "Priya Patel", "email": "[email protected]", "is_organizer": false, "availability": { "calendar_ids": ["primary"] }, "booking": { "calendar_id": "primary" } } ], "availability": { "duration_minutes": 45, "availability_rules": { "availability_method": "max-fairness" } }, "event_booking": { "title": "Interview with {{invitee_name}}" } }'A few things to note:
availability_method: "max-fairness"distributes interviews evenly. Scheduler assigns candidates to whichever interviewer was booked least recently. If Sarah conducted the last two interviews, Marcus or Priya gets the next one.{{invitee_name}}is a template variable. Scheduler replaces it with the candidate’s name from the booking form, so events show “Interview with Dana Chen” instead of a generic title.- Each participant needs both
availability.calendar_ids(calendars to check for conflicts) andbooking.calendar_id(calendar to create the event on).
Choose between max-fairness and max-availability
Section titled “Choose between max-fairness and max-availability”Scheduler offers two round-robin strategies:
max-fairnesskeeps the interview count balanced. Good for teams where equal distribution matters. The tradeoff is fewer time slots shown to candidates, because Scheduler only offers times when the least-booked interviewer is free.max-availabilityshows the most possible time slots by assigning the interview to whichever interviewer is free at the chosen time. High-volume recruiting teams tend to prefer this because candidates see more options and book faster.
To switch, change availability_method to "max-availability".
Add conferencing and Notetaker
Section titled “Add conferencing and Notetaker”Update the Configuration to attach automatic conferencing and enable Notetaker.
curl --request PUT \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/scheduling/configurations/<CONFIGURATION_ID>' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "event_booking": { "title": "Interview with {{invitee_name}}", "conferencing": { "provider": "Google Meet", "autocreate": {} } }, "scheduler": { "notetaker_settings": { "enabled": true, "show_ui_consent_message": true, "notetaker_name": "Interview Recorder", "meeting_settings": { "video_recording": true, "audio_recording": true, "transcription": true, "summary": true, "summary_settings": { "custom_instructions": "Summarize the candidate interview. Focus on technical skills demonstrated, communication quality, and any concerns raised. Note which topics were covered and which were skipped." }, "action_items": true, "action_items_settings": { "custom_instructions": "List follow-up items: topics to probe in the next round, references to check, skills to verify, and hiring committee discussion points." } } } } }'const configuration = await nylas.scheduler.configurations.update({ identifier: process.env.NYLAS_GRANT_ID, configurationId: "<CONFIGURATION_ID>", requestBody: { eventBooking: { title: "Interview with {{invitee_name}}", conferencing: { provider: "Google Meet", autocreate: {} }, }, scheduler: { notetakerSettings: { enabled: true, showUiConsentMessage: true, notetakerName: "Interview Recorder", meetingSettings: { videoRecording: true, audioRecording: true, transcription: true, summary: true, summarySettings: { customInstructions: "Summarize the candidate interview. Focus on technical skills demonstrated, communication quality, and any concerns raised.", }, actionItems: true, actionItemsSettings: { customInstructions: "List follow-up items: topics to probe in the next round, references to check, skills to verify.", }, }, }, }, },});configuration = nylas.scheduler.configurations.update( identifier=os.environ["NYLAS_GRANT_ID"], configuration_id="<CONFIGURATION_ID>", request_body={ "event_booking": { "title": "Interview with {{invitee_name}}", "conferencing": {"provider": "Google Meet", "autocreate": {}}, }, "scheduler": { "notetaker_settings": { "enabled": True, "show_ui_consent_message": True, "notetaker_name": "Interview Recorder", "meeting_settings": { "video_recording": True, "audio_recording": True, "transcription": True, "summary": True, "summary_settings": { "custom_instructions": "Summarize the candidate interview. Focus on technical skills demonstrated, communication quality, and any concerns raised." }, "action_items": True, "action_items_settings": { "custom_instructions": "List follow-up items: topics to probe in the next round, references to check, skills to verify." }, }, }, }, },)The custom instructions make a real difference for hiring workflows. Generic summaries are too vague for interview debriefs. By telling Notetaker to focus on technical skills and follow-up items, you get output that maps directly to your hiring scorecard.
Host the scheduling page
Section titled “Host the scheduling page”With the Configuration ready, give candidates a way to book. Nylas supports two approaches.
Option 1: Nylas-hosted page
Section titled “Option 1: Nylas-hosted page”Add a slug to your Configuration and Nylas hosts the page at book.nylas.com/<slug>. No frontend work required.
curl --request PUT \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/scheduling/configurations/<CONFIGURATION_ID>' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "slug": "interview-engineering-team" }'Your scheduling page is now live at https://book.nylas.com/interview-engineering-team. Drop that link into recruiter emails, your careers page, or your ATS’s candidate communication templates.
Option 2: Embedded scheduling component
Section titled “Option 2: Embedded scheduling component”Embed the scheduling UI directly in your careers site using the <nylas-scheduling> web component.
<script src="https://cdn.jsdelivr.net/npm/@nylas/react@latest/dist/cdn/nylas-scheduling/nylas-scheduling.es.js"></script>
<nylas-scheduling configuration-id="<CONFIGURATION_ID>"></nylas-scheduling>import { NylasScheduling } from "@nylas/react";
function InterviewBookingPage({ configurationId }) { return <NylasScheduling configurationId={configurationId} />;}The component handles date selection, time slots, the booking form, and confirmation. Notetaker consent and round-robin settings apply automatically. For styling options, see Customize Scheduler.
Handle booking webhooks
Section titled “Handle booking webhooks”Subscribe to booking.created, booking.cancelled, and notetaker.media so your system tracks the full interview lifecycle.
curl --request POST \ --url 'https://api.us.nylas.com/v3/webhooks/' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "trigger_types": [ "booking.created", "booking.cancelled", "notetaker.media" ], "description": "Interview pipeline webhooks", "webhook_url": "https://your-app.example.com/webhooks/nylas", "notification_email_addresses": ["[email protected]"] }'When a candidate books, Nylas sends a booking.created notification with the event details:
{ "specversion": "1.0", "type": "booking.created", "source": "/nylas/passthru", "id": "<WEBHOOK_ID>", "time": 1725895310, "data": { "application_id": "<NYLAS_APPLICATION_ID>", "grant_id": "<NYLAS_GRANT_ID>", "object": { "booking_id": "<BOOKING_ID>", "configuration_id": "<CONFIGURATION_ID>", "object": "booking", "booking_info": { "event_id": "<EVENT_ID>", "start_time": 1719842400, "end_time": 1719844500, "title": "Interview with Dana Chen", "location": "https://meet.google.com/abc-defg-hij" } } }}Build a handler that routes booking events to your ATS:
const express = require("express");const crypto = require("crypto");const app = express();app.use(express.json());
function verifySignature(req, secret) { const signature = req.headers["x-nylas-signature"]; const digest = crypto .createHmac("sha256", secret) .update(JSON.stringify(req.body)) .digest("hex"); return signature === digest;}
app.get("/webhooks/nylas", (req, res) => { res.status(200).send(req.query.challenge);});
app.post("/webhooks/nylas", async (req, res) => { res.status(200).send("OK");
if (!verifySignature(req, process.env.NYLAS_WEBHOOK_SECRET)) return;
const { type, data } = req.body;
if (type === "booking.created") { const booking = data.object; await fetch("https://your-ats.example.com/api/interviews", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ booking_id: booking.booking_id, event_id: booking.booking_info.event_id, candidate_name: booking.booking_info.title.replace( "Interview with ", "", ), start_time: new Date( booking.booking_info.start_time * 1000, ).toISOString(), end_time: new Date(booking.booking_info.end_time * 1000).toISOString(), meeting_link: booking.booking_info.location, status: "scheduled", }), }); }
if (type === "booking.cancelled") { const booking = data.object; await fetch( `https://your-ats.example.com/api/interviews/${booking.booking_id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "cancelled" }), }, ); }});
app.listen(3000, () => console.log("Webhook server running on port 3000"));import hmac, hashlib, json, os, requestsfrom flask import Flask, request
app = Flask(__name__)
def verify_signature(req, secret): signature = req.headers.get("x-nylas-signature", "") body = json.dumps(req.json, separators=(",", ":")) digest = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(signature, digest)
@app.route("/webhooks/nylas", methods=["GET"])def challenge(): return request.args.get("challenge", ""), 200
@app.route("/webhooks/nylas", methods=["POST"])def handle_webhook(): if not verify_signature(request, os.environ["NYLAS_WEBHOOK_SECRET"]): return "Invalid signature", 401
payload = request.json event_type = payload.get("type") booking = payload.get("data", {}).get("object", {})
if event_type == "booking.created": info = booking["booking_info"] requests.post("https://your-ats.example.com/api/interviews", json={ "booking_id": booking["booking_id"], "event_id": info["event_id"], "candidate_name": info["title"].replace("Interview with ", ""), "start_time": info["start_time"], "end_time": info["end_time"], "meeting_link": info["location"], "status": "scheduled", })
if event_type == "booking.cancelled": requests.patch( f"https://your-ats.example.com/api/interviews/{booking['booking_id']}", json={"status": "cancelled"}, )
return "OK", 200Retrieve interview recordings and transcripts
Section titled “Retrieve interview recordings and transcripts”After each interview ends, Notetaker processes the recording and sends a notetaker.media webhook. The payload includes download URLs for the recording, transcript, summary, and action items.
Add a notetaker.media handler to the same webhook server:
// Add this inside the existing POST handler
if (type === "notetaker.media" && data.object.state === "available") { const { media, id: notetakerId } = data.object;
const [summaryRes, actionItemsRes, transcriptRes] = await Promise.all([ fetch(media.summary), fetch(media.action_items), fetch(media.transcript), ]);
const summary = await summaryRes.json(); const actionItems = await actionItemsRes.json(); const transcript = await transcriptRes.json();
// Look up the linked calendar event const notetakerRes = await fetch( `https://api.us.nylas.com/v3/grants/${process.env.NYLAS_GRANT_ID}/notetakers/${notetakerId}`, { headers: { Authorization: `Bearer ${process.env.NYLAS_API_KEY}` } }, ); const notetaker = await notetakerRes.json(); const eventId = notetaker.data.event?.event_id;
// Store interview artifacts in your ATS await fetch("https://your-ats.example.com/api/interview-recordings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ event_id: eventId, notetaker_id: notetakerId, recording_url: media.recording, transcript, summary, action_items: actionItems, processed_at: new Date().toISOString(), }), });}# Add this inside the existing POST handler
if event_type == "notetaker.media": obj = payload.get("data", {}).get("object", {}) if obj.get("state") != "available": return "OK", 200
media = obj["media"] notetaker_id = obj["id"]
summary = requests.get(media["summary"]).json() action_items = requests.get(media["action_items"]).json() transcript = requests.get(media["transcript"]).json()
notetaker = requests.get( f"https://api.us.nylas.com/v3/grants/{os.environ['NYLAS_GRANT_ID']}/notetakers/{notetaker_id}", headers={"Authorization": f"Bearer {os.environ['NYLAS_API_KEY']}"}, ).json()
event_id = notetaker.get("data", {}).get("event", {}).get("event_id")
requests.post("https://your-ats.example.com/api/interview-recordings", json={ "event_id": event_id, "notetaker_id": notetaker_id, "recording_url": media["recording"], "transcript": transcript, "summary": summary, "action_items": action_items, })Things to know
Section titled “Things to know”- Round-robin doesn’t guarantee strict alternation. Max-fairness counts bookings per Configuration — not across Configurations. If Sarah is on two interview panels, her total load can still skew. If one interviewer blocks every Friday, Friday candidates never get assigned to her. Calendar hygiene matters.
- Notetaker times out at the lobby. The bot joins as a non-signed-in participant. If nobody admits it within 10 minutes, it gives up with
failed_entryand you lose the recording. For automated pipelines, configure: Google Meet “Anyone with the link can join”; Teams “Anonymous users can join”; Zoom waiting room disabled. - Don’t ship without an automated media download. Nylas deletes media after 14 days. Hiring cycles often run longer. Build a download pipeline that triggers on
notetaker.mediaand stores files in your own infrastructure — don’t rely on Nylas-hosted URLs for long-term access. - There’s no in-place reschedule. When a candidate cancels, Scheduler fires
booking.cancelledand removes the event. The candidate has to rebook. Link ATS records by the candidate’s email address so cancel-then-rebook flows still tie back to the same person. - Recording consent is your problem. The
show_ui_consent_messageflag and the in-meeting chat message are informational, not legal consent mechanisms. Some jurisdictions require explicit opt-in before recording. Get this reviewed by counsel before you go live. - Zoom round-robin under a single grant has gotchas. Restrictive Zoom account settings can block participants from joining meetings created by a different account. See Zoom troubleshooting for workarounds.
- Custom Notetaker instructions move the needle. Generic summaries are useless for hiring debriefs. Tell Notetaker exactly what to extract — technical skills demonstrated, areas to probe in the next round, references to check. The output then maps directly to your scorecard.
Next steps
Section titled “Next steps”- Scheduling with notetaking — the simpler one-on-one version of this pipeline
- Automate meeting follow-ups — close the loop with an automated recap email after each interview
- Auto-log meeting notes to your CRM — same Notetaker pattern, different destination
- Meeting types in Scheduler — max-fairness vs. max-availability and other options
- Managing availability — open hours, buffer times, and exclusion dates
- Customize Scheduler — brand the page, add custom fields like “role applied for”, control the booking flow
- Handling Notetaker media files — transcript format, recording specs, storage strategies