Interview scheduling is a coordination nightmare. Recruiters spend hours cross-referencing interviewer calendars, candidates wait days for a confirmed time 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 tutorial builds a pipeline that eliminates all of that. Candidates pick their own slot from a scheduling page, Nylas distributes interviews across your team using round-robin, conferencing links are generated automatically, and Notetaker joins every call to record and transcribe. After the interview, you get a structured transcript, summary, and action items through a webhook. The recruiter never touches a calendar.
What you will build
Section titled “What you will build”By the end of this tutorial, you will have a working interview scheduling pipeline with these components:
- A Scheduler Configuration with round-robin distribution across multiple interviewers
- Automatic conferencing (Google Meet, Microsoft Teams, or Zoom) attached to every booking
- Notetaker integration that joins each interview and generates a transcript, summary, and action items
- A scheduling page (Nylas-hosted or embedded in your careers site) where candidates book their own slot
- Webhook handlers that notify your ATS when bookings happen and when recordings are ready
The flow is straightforward. A candidate visits the scheduling page, picks a time, and confirms. Scheduler checks all interviewer calendars, picks the interviewer who was booked least recently, and creates the event. Notetaker joins the call at the scheduled time, records everything, and delivers the processed media afterward.
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)
New to Nylas? Start with the quickstart guide to set up your app and connect a test account before continuing here.
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)
Nylas blocks requests to ngrok URLs. Use VS Code port forwarding or Hookdeck to expose your local server during development.
Round-robin requires every participant to have a valid grant under the same Nylas application. Scheduler reads each interviewer’s calendar to compute availability. If a grant expires or is revoked, Scheduler skips that interviewer during assignment. Check your grants regularly in the Nylas Dashboard.
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".
For performance, keep your participant list under 10 interviewers. If you have a larger panel, split them into separate Configurations by interview stage or role.
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.
Set conferencing.provider to match your video platform. Use "Google Meet" for Google Workspace, "Microsoft Teams" for Microsoft 365, or "Zoom Meeting" for Zoom. The autocreate object generates a unique meeting link for every booking.
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", 200Your webhook endpoint must respond quickly with a 200 status code. Nylas retries failed deliveries, but if your endpoint is consistently unreachable, notifications are dropped. Respond immediately and process asynchronously. See Using webhooks with Nylas for details.
Retrieve 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, })Media URLs expire after 60 minutes. Download the files immediately when you receive the webhook. If you miss the window, fetch fresh URLs from the Notetaker Media endpoint. Nylas retains media files for 14 days, then permanently deletes them.
Things to know
Section titled “Things to know”Here are practical considerations for running this pipeline in production.
Round-robin does not guarantee strict alternation
Section titled “Round-robin does not guarantee strict alternation”Max-fairness distributes interviews based on booking count for a given Configuration. It does not track assignments across Configurations. If Sarah is in two interview panels, her total load could still be uneven. Also, if one interviewer blocks out every Friday, candidates who book on Fridays never get assigned to them. Encourage interviewers to keep calendars accurate.
Timezone handling for remote candidates
Section titled “Timezone handling for remote candidates”Scheduler displays time slots in the candidate’s local timezone, detected from their browser. The Configuration’s default_open_hours are defined in a specific timezone, so set these to match your team’s working hours. If your interviewers span multiple timezones, use participant-level open_hours instead. See Managing availability for details.
Lobby and waiting room issues
Section titled “Lobby and waiting room issues”Notetaker joins as a non-signed-in participant. If nobody admits the bot within 10 minutes, it times out with a failed_entry state and you lose the recording. For fully automated pipelines:
- Google Meet: Set meetings to “Anyone with the link can join”
- Microsoft Teams: Enable “Anonymous users can join a meeting” in Teams admin
- Zoom: Disable the waiting room for Scheduler-created meetings
Media retention and long hiring cycles
Section titled “Media retention and long hiring cycles”Nylas deletes media files after 14 days. Hiring cycles often run longer. Build an automated download pipeline that triggers on the notetaker.media webhook and stores files in your own infrastructure. Do not rely on Nylas-hosted URLs for long-term access.
Cancellation and rescheduling
Section titled “Cancellation and rescheduling”When a candidate cancels, Scheduler fires a booking.cancelled webhook and removes the calendar event. There is no in-place reschedule — candidates cancel and rebook. Build your ATS integration to handle both, and link records by the candidate’s email address to maintain an audit trail.
Recording consent
Section titled “Recording consent”The scheduling page shows a consent message if show_ui_consent_message is true, and the bot sends a chat message shortly after joining. That said, recording laws vary by jurisdiction. Some require explicit opt-in consent before recording starts. Review the requirements for your jurisdiction. The messages Notetaker sends are informational, not legal consent mechanisms.
Zoom with round-robin
Section titled “Zoom with round-robin”If you use Zoom with round-robin under a single grant, restrictive Zoom settings can prevent participants from joining meetings created by a different account. See the Zoom troubleshooting documentation for workarounds.
What’s next
Section titled “What’s next”You now have a working pipeline where candidates self-schedule, interviewers are assigned automatically, and every conversation is recorded and transcribed. Here are ways to extend it:
- Meeting types in Scheduler for details on max-fairness vs. max-availability and other options
- Scheduler and Notetaker integration for the full reference on Notetaker settings within Scheduler
- Managing availability to configure open hours, buffer times, and exclusion dates for your interviewers
- Customize Scheduler to brand the scheduling page, add custom fields (like “role applied for”), and control the booking flow
- Handling Notetaker media files for details on transcript format, recording specs, and storage strategies
- Webhook notification schemas for full payload references for
booking.created,booking.cancelled, andnotetaker.media