To use Calendly with V0, embed the Calendly widget directly in V0-generated React components using the react-calendly package for scheduling, and create a Next.js API route at app/api/calendly/route.ts to fetch events and invitees via Calendly's v2 API. Store your Calendly Personal Access Token in Vercel Dashboard → Settings → Environment Variables as CALENDLY_ACCESS_TOKEN. The embed widget works client-side; the API route fetches booking data server-side.
Adding Scheduling to Your V0 App with Calendly
Calendly is the most widely used scheduling tool for B2B workflows — from sales demos to onboarding calls to consulting bookings. For founders building V0 apps, there are two distinct ways to integrate Calendly: embedding the hosted scheduling widget directly in your pages (zero backend required), or fetching event and invitee data through Calendly's v2 API to build custom booking dashboards and reporting views.
The embed approach is the fastest path to a working scheduler. Using the react-calendly package, you can drop a full Calendly booking interface into any React component V0 generates. The widget loads Calendly's hosted UI inside your app — your visitors pick a time, fill in their details, and receive confirmation emails, all without leaving your page. This approach requires no API routes, no server setup, and no handling of booking logic in your code.
The API approach is for when you need to read booking data — list upcoming events, check who booked a particular slot, build a dashboard for your team, or trigger post-booking actions. Calendly's v2 API provides endpoints for event types, scheduled events, and invitees. All calls must happen server-side to protect your Personal Access Token. A Next.js API route running on Vercel handles this securely. For real-time notifications when bookings happen, Calendly also supports webhooks that fire on event creation, cancellation, and rescheduling.
Integration method
V0 generates your booking UI and scheduling pages as React components, while a Next.js API route fetches Calendly event and invitee data from the Calendly v2 API using a Personal Access Token. For embedding the booking widget directly, the react-calendly package renders Calendly's hosted scheduling interface inline in your V0 app without a backend call.
Prerequisites
- A V0 account with a Next.js project at v0.dev
- A Calendly account (free tier supports basic scheduling; paid plans enable team features and webhooks)
- Your Calendly Personal Access Token from Calendly Settings → Integrations → API & Webhooks
- A Vercel account with your V0 project deployed via GitHub
- At least one Event Type configured in your Calendly account with a scheduling link
Step-by-step guide
Embed the Calendly Widget with react-calendly
Embed the Calendly Widget with react-calendly
The fastest way to add Calendly scheduling to a V0 app is the inline embed — no API routes, no backend setup, just a React component that renders Calendly's hosted booking UI directly in your page. The react-calendly npm package provides React-friendly wrappers around Calendly's widget script. Add react-calendly to your project by including it in package.json dependencies. Then create a client component (it must have 'use client' at the top because it renders browser-based JavaScript) that uses the InlineWidget component from react-calendly. Pass your Calendly scheduling URL as the url prop — this is the link from your Calendly event type, like https://calendly.com/your-name/30min. The InlineWidget renders inside your page as an iframe-based booking experience. It automatically loads the correct event type, shows available time slots, and handles the full booking flow including invitee name and email collection. After the visitor books, Calendly sends them a confirmation email and calendar invite automatically. For V0 integration, you can either hardcode your Calendly URL in the component or make it configurable via an environment variable (NEXT_PUBLIC_CALENDLY_URL). Using an environment variable is useful if you have different scheduling links for different environments or team members. Since the URL is not a secret, using the NEXT_PUBLIC_ prefix is appropriate here. One V0-specific note: V0 may generate the InlineWidget without the 'use client' directive. If you see a 'window is not defined' error after deployment, add 'use client' at the top of the component file or use Next.js's dynamic import with ssr: false to defer loading until the browser.
Create a 'Schedule a Meeting' page at /book. The page should have a clean header with the title 'Book a Free Consultation' and a subtitle explaining what to expect. Below the header, add a Calendly inline widget using the InlineWidget component from react-calendly with styles={{ minWidth: '320px', height: '700px' }}. This is a client component so add 'use client' at the top.
Paste this in V0 chat
1'use client';23import { InlineWidget } from 'react-calendly';45interface CalendlyEmbedProps {6 url?: string;7 primaryColor?: string;8 textColor?: string;9 backgroundColor?: string;10}1112export function CalendlyEmbed({13 url = process.env.NEXT_PUBLIC_CALENDLY_URL || 'https://calendly.com/your-name/30min',14 primaryColor = '0066cc',15 textColor = '4d5055',16 backgroundColor = 'ffffff',17}: CalendlyEmbedProps) {18 return (19 <div className="w-full">20 <InlineWidget21 url={url}22 styles={{ minWidth: '320px', height: '700px' }}23 pageSettings={{24 primaryColor,25 textColor,26 backgroundColor,27 hideLandingPageDetails: false,28 hideEventTypeDetails: false,29 }}30 />31 </div>32 );33}Pro tip: Use Calendly's 'hide_gdpr_banner=1' query parameter appended to your scheduling URL to remove the GDPR consent banner if your app handles consent separately.
Expected result: A Calendly booking widget renders inline on your page. Visitors can select a time, enter their details, and receive a booking confirmation without leaving your app.
Generate a Calendly Personal Access Token
Generate a Calendly Personal Access Token
To fetch booking data through Calendly's v2 API, you need a Personal Access Token (PAT). Unlike OAuth, a PAT is a static credential tied to your Calendly account — simpler for single-user or internal tool use cases where you do not need users to authenticate with their own Calendly accounts. Log into your Calendly account and navigate to Account Settings → Integrations → API & Webhooks. In the Personal Access Tokens section, click Create New Token. Give it a descriptive name like 'V0 App Integration' so you can identify it later. Calendly will generate a long token string — copy it immediately as it is only shown once. This PAT authenticates API requests as your Calendly account, giving you access to your event types, scheduled events, and invitee records. For team use cases where you need to access multiple users' calendars, you would instead use Calendly's OAuth flow — but that requires users to grant permission through a consent screen and is significantly more complex to implement. Calendly's PAT does not have granular permission scopes — it grants full read/write access to your account's data. This is another reason to keep it strictly server-side. Never put it in a NEXT_PUBLIC_ variable, in a V0 chat prompt, or in any file that gets committed to your GitHub repository.
Pro tip: Calendly Personal Access Tokens do not expire automatically, but you should rotate them periodically or immediately if your repository is ever accidentally exposed in a public state.
Expected result: You have a Calendly Personal Access Token saved securely, ready to add as a Vercel environment variable.
Create the Calendly Events API Route
Create the Calendly Events API Route
Now build the server-side API route that fetches scheduled events from Calendly's v2 API. Create app/api/calendly/route.ts in your project. This route will run as a Vercel serverless function and proxy Calendly API calls, keeping your PAT out of the browser. Calendly's v2 API uses a two-step pattern for most data fetches: first, get your user URI (your unique Calendly identity) by calling the /users/me endpoint, then use that URI to filter events by their organizer. Without specifying the user URI, the events endpoint returns nothing useful. The scheduled events endpoint supports date range filtering with min_start_time and max_start_time query parameters, which accept ISO 8601 datetime strings. For a dashboard showing this week's meetings, you would pass the start of the current week and the end of the week. The API also paginates results — use the pagination.next_page_token from the response to fetch subsequent pages if you have more events than the per_page limit. For each event, you may want to fetch the associated invitees (the people who booked). This is a separate API call to /scheduled_events/{event_uuid}/invitees. The event UUID appears in the event URI from the scheduled events response — extract it with a simple split('/').pop(). Consider whether you need per-event invitee data or just the event list to avoid making excessive API calls.
Add a Next.js API route at app/api/calendly/route.ts that uses the Calendly v2 API. Fetch the current user's URI from https://api.calendly.com/users/me, then fetch upcoming scheduled events for this week using CALENDLY_ACCESS_TOKEN from environment variables. Return a JSON array of events with id, name, start_time, end_time, and event_type fields.
Paste this in V0 chat
1import { NextRequest, NextResponse } from 'next/server';23const CALENDLY_BASE_URL = 'https://api.calendly.com';45async function calendlyFetch(path: string, token: string) {6 const response = await fetch(`${CALENDLY_BASE_URL}${path}`, {7 headers: {8 Authorization: `Bearer ${token}`,9 'Content-Type': 'application/json',10 },11 });1213 if (!response.ok) {14 throw new Error(`Calendly API error: ${response.status} ${response.statusText}`);15 }1617 return response.json();18}1920export async function GET(request: NextRequest) {21 const token = process.env.CALENDLY_ACCESS_TOKEN;2223 if (!token) {24 return NextResponse.json(25 { error: 'CALENDLY_ACCESS_TOKEN is not configured' },26 { status: 500 }27 );28 }2930 try {31 // Step 1: Get the current user's URI32 const userResponse = await calendlyFetch('/users/me', token);33 const userUri = userResponse.resource.uri;3435 // Step 2: Fetch scheduled events for this week36 const now = new Date();37 const weekStart = new Date(now);38 weekStart.setHours(0, 0, 0, 0);39 const weekEnd = new Date(now);40 weekEnd.setDate(weekEnd.getDate() + 7);41 weekEnd.setHours(23, 59, 59, 999);4243 const params = new URLSearchParams({44 user: userUri,45 min_start_time: weekStart.toISOString(),46 max_start_time: weekEnd.toISOString(),47 status: 'active',48 count: '50',49 });5051 const eventsResponse = await calendlyFetch(`/scheduled_events?${params}`, token);5253 const events = eventsResponse.collection.map((event: {54 uri: string;55 name: string;56 status: string;57 start_time: string;58 end_time: string;59 event_type: string;60 location?: { type: string; join_url?: string };61 }) => ({62 id: event.uri.split('/').pop(),63 name: event.name,64 status: event.status,65 startTime: event.start_time,66 endTime: event.end_time,67 eventType: event.event_type,68 location: event.location?.type || 'unknown',69 joinUrl: event.location?.join_url || '',70 }));7172 return NextResponse.json({73 events,74 total: eventsResponse.collection.length,75 });76 } catch (error: unknown) {77 console.error('Calendly API error:', error);78 return NextResponse.json(79 { error: 'Failed to fetch Calendly events' },80 { status: 500 }81 );82 }83}Pro tip: Cache the user URI result — it never changes for a given token. Store it in a module-level variable or use Vercel's Edge Config to avoid an extra API call on every request.
Expected result: The API route returns a JSON array of upcoming Calendly events for the current week, including meeting names, start and end times, and location details.
Add Environment Variables in Vercel
Add Environment Variables in Vercel
Your API route reads CALENDLY_ACCESS_TOKEN from environment variables. Add this to Vercel so it is available when your serverless function runs. Open Vercel Dashboard → your project → Settings → Environment Variables. Add CALENDLY_ACCESS_TOKEN with the PAT you generated. Set the scope to Production, Preview, and Development. Do not add the NEXT_PUBLIC_ prefix — this token must remain server-side only. If you are also configuring the embed widget with a Calendly URL, add NEXT_PUBLIC_CALENDLY_URL with your scheduling link (e.g., https://calendly.com/your-name/30min). This one can and should have the NEXT_PUBLIC_ prefix since the URL is not sensitive and your client-side CalendlyEmbed component needs to read it. After saving variables, trigger a redeployment by pushing any commit or clicking Redeploy. Environment variable changes require a new deployment. For local development, add both variables to your .env.local file — the non-NEXT_PUBLIC_ variable will only be accessible server-side, while the NEXT_PUBLIC_ one will be available everywhere.
Pro tip: If you have multiple team members, each with their own Calendly account, you can use Calendly's round-robin event types which handle assignment automatically rather than managing multiple tokens.
Expected result: Vercel Dashboard shows both CALENDLY_ACCESS_TOKEN and NEXT_PUBLIC_CALENDLY_URL saved. After redeployment, the API route returns real event data and the embed widget shows your actual available times.
Set Up Calendly Webhooks for Real-Time Booking Notifications
Set Up Calendly Webhooks for Real-Time Booking Notifications
For applications that need to react immediately when someone books, cancels, or reschedules a meeting, Calendly provides webhooks. When a booking event occurs, Calendly sends a POST request to your specified URL with the event details. Create a dedicated webhook handler at app/api/calendly/webhook/route.ts. Calendly webhooks support four event types: invitee.created (new booking), invitee.canceled (cancellation), routing_form_submission.created (form submission), and invitee_no_show.created. For most use cases, invitee.created and invitee.canceled are the important ones. Calendly webhook payloads include the event URI and invitee URI. To get full details (email, name, answers to custom questions), you may need to make a follow-up API call to Calendly's invitee endpoint using the invitee URI from the webhook payload. To register a webhook subscription, use Calendly's API: POST to https://api.calendly.com/webhook_subscriptions with your webhook URL and the event types you want to subscribe to. Calendly will send a one-time verification request to confirm your endpoint is reachable before activating the subscription. Your webhook handler should respond with a 200 status within 10 seconds. Note that Calendly webhook subscriptions are only available on paid Calendly plans (Standard and above). The free tier does not include webhook access. If your use case is budget-sensitive, an alternative is polling the events endpoint on a schedule using Vercel Cron Jobs.
Add a webhook endpoint at app/api/calendly/webhook/route.ts that handles POST requests from Calendly. Parse the request body, log the event type and invitee details, and return a 200 response. Handle invitee.created events by extracting the invitee name, email, and scheduled start time from the payload.
Paste this in V0 chat
1import { NextRequest, NextResponse } from 'next/server';23interface CalendlyWebhookPayload {4 event: string;5 payload: {6 event_type: {7 name: string;8 slug: string;9 };10 invitee: {11 name: string;12 email: string;13 uri: string;14 };15 scheduled_event: {16 uri: string;17 start_time: string;18 end_time: string;19 name: string;20 };21 cancel_url?: string;22 reschedule_url?: string;23 };24 created_at: string;25}2627export async function POST(request: NextRequest) {28 try {29 const body: CalendlyWebhookPayload = await request.json();3031 console.log('Calendly webhook received:', body.event);3233 switch (body.event) {34 case 'invitee.created': {35 const { invitee, scheduled_event } = body.payload;36 console.log(`New booking: ${invitee.name} (${invitee.email}) for ${scheduled_event.name}`);37 console.log(`Start time: ${scheduled_event.start_time}`);38 // TODO: Send confirmation email, create CRM record, notify Slack, etc.39 break;40 }4142 case 'invitee.canceled': {43 const { invitee, scheduled_event } = body.payload;44 console.log(`Cancellation: ${invitee.name} canceled ${scheduled_event.name}`);45 // TODO: Update database, send cancellation email, free up resource46 break;47 }4849 default:50 console.log('Unhandled Calendly event:', body.event);51 }5253 return NextResponse.json({ received: true });54 } catch (error: unknown) {55 console.error('Calendly webhook error:', error);56 return NextResponse.json(57 { error: 'Webhook processing failed' },58 { status: 500 }59 );60 }61}Pro tip: Calendly does not sign webhook payloads with a secret by default, so anyone could POST to your webhook URL. Add a secret token query parameter to your webhook URL (e.g., /api/calendly/webhook?secret=your-secret) and verify it in the handler for basic security.
Expected result: The webhook handler receives Calendly events and logs booking details. Vercel logs show incoming booking notifications when test events are triggered from the Calendly Dashboard.
Common use cases
Inline Booking Page Embed
A consultant adds a Calendly scheduling widget directly to their V0-generated portfolio or services page. Visitors can book a discovery call without leaving the site. The react-calendly InlineWidget renders Calendly's full booking UI inside the page — no API route needed, just a client component with the scheduling URL.
Create a 'Book a Call' section for a consulting services page. Include a heading 'Schedule a Free Discovery Call', a brief description of what the call covers (30 minutes, video or phone), and then render the Calendly widget using InlineWidget from react-calendly. The Calendly URL should come from a prop or environment variable.
Copy this prompt to try it in V0
Sales Team Booking Dashboard
A sales team builds an internal dashboard that shows all upcoming booked meetings across team members. The dashboard fetches scheduled events from a Next.js API route that calls Calendly's v2 API, displaying invitee names, meeting times, event types, and booking notes in a sortable table.
Build an upcoming meetings dashboard that fetches from /api/calendly/events. Display a table with columns for Meeting Title, Invitee Name, Invitee Email, Start Time, and Duration. Add a filter dropdown to show meetings for Today, This Week, or This Month. Show a loading skeleton while data is fetching.
Copy this prompt to try it in V0
Post-Booking Action Trigger
A SaaS company automatically sends a custom welcome email and creates a CRM record when someone books an onboarding call. A Next.js API route receives Calendly webhook notifications on booking creation, extracts the invitee's details, and triggers downstream actions like sending emails or updating a database.
Create a booking confirmation page at /booking-confirmed that shows a personalized message. It should display the meeting time, a calendar add link, and a checklist of what to prepare. The page should look polished with a checkmark icon and the company branding.
Copy this prompt to try it in V0
Troubleshooting
Calendly embed shows 'window is not defined' error on Vercel deployment
Cause: The react-calendly InlineWidget uses browser APIs and cannot run during Next.js server-side rendering. The component needs to be rendered only in the browser.
Solution: Add 'use client' at the top of the component file containing InlineWidget. Alternatively, import the component with Next.js dynamic import: const InlineWidget = dynamic(() => import('react-calendly').then(m => m.InlineWidget), { ssr: false }).
1'use client';2// OR use dynamic import in the parent component:3import dynamic from 'next/dynamic';4const InlineWidget = dynamic(5 () => import('react-calendly').then((m) => m.InlineWidget),6 { ssr: false }7);Calendly API returns 401 Unauthorized
Cause: The CALENDLY_ACCESS_TOKEN environment variable is missing or the token was revoked.
Solution: Verify the token exists in Vercel Dashboard → Settings → Environment Variables. If the token was deleted or rotated, generate a new Personal Access Token in Calendly Settings → Integrations → API & Webhooks and update the Vercel environment variable. Redeploy after updating.
API route returns an empty events array even though meetings exist in Calendly
Cause: The events endpoint requires the user URI as a filter parameter. Without it, Calendly returns no results rather than returning all events.
Solution: Ensure your API route first calls /users/me to get the user URI, then passes that URI as the 'user' query parameter to the /scheduled_events endpoint. Also verify the min_start_time and max_start_time date range covers the period your events are scheduled in.
1// Must fetch user URI first, then use it as a filter2const userResponse = await calendlyFetch('/users/me', token);3const userUri = userResponse.resource.uri;4// Then filter events by user5const params = new URLSearchParams({6 user: userUri,7 min_start_time: weekStart.toISOString(),8 status: 'active',9});Best practices
- Use the InlineWidget embed for simple booking flows — it requires no API route, no token, and no backend infrastructure.
- Never expose your Calendly Personal Access Token client-side; always use it only in Next.js API routes without the NEXT_PUBLIC_ prefix.
- Cache the /users/me response since the user URI never changes — avoid making this extra API call on every request.
- Filter events by date range when fetching from the scheduled_events endpoint; fetching all events without a filter is wasteful and slow.
- Use environment variables for your Calendly scheduling URL (NEXT_PUBLIC_CALENDLY_URL) so you can update it without code changes.
- For post-booking automations, webhooks are more reliable than polling — use Calendly webhooks on paid plans rather than repeated API calls.
- Add error boundary components around Calendly embeds so a slow widget load does not break your entire page.
- Test your booking flow in Calendly's sandbox mode before sharing publicly — Calendly allows test booking with yourself using the preview link.
Alternatives
Doodle is an alternative if your scheduling need is group availability polling rather than individual 1:1 booking — Doodle lets participants vote on time slots rather than picking from a calendar.
Acuity Scheduling (by Squarespace) is an alternative if you need service-based booking with intake forms, payments at time of booking, and package management for appointments.
ScheduleOnce (now OnceHub) is an alternative if you need enterprise-grade meeting routing with complex rules for assigning meetings to specific sales team members based on territory or workload.
Frequently asked questions
Can I embed Calendly in a V0 app without any backend code?
Yes. The react-calendly InlineWidget renders Calendly's full booking interface client-side with no API route needed. Add 'use client' to your component, import InlineWidget from react-calendly, and pass your scheduling URL as a prop. The entire booking flow — time selection, form, confirmation email — happens through Calendly's hosted infrastructure.
Does V0 have a built-in Calendly integration?
No, V0 does not have a native one-click Calendly integration. You embed it manually using react-calendly for the widget or build a Next.js API route for data access. V0 can generate the surrounding UI and the API route boilerplate when you describe what you need in the V0 chat.
How do I show only specific event types in the Calendly embed?
Pass the URL of the specific event type to the InlineWidget's url prop. In Calendly, each event type has its own unique URL (e.g., https://calendly.com/your-name/strategy-call or https://calendly.com/your-name/30min). Use that specific URL rather than your main scheduling page URL to show only that event type in the embed.
Can I customize the Calendly widget's colors to match my V0 app?
Yes, the InlineWidget accepts a pageSettings prop where you can set primaryColor, textColor, and backgroundColor as hex strings (without the # character). For example: pageSettings={{ primaryColor: '0066cc', backgroundColor: 'f8f9fa', textColor: '333333' }}. The Calendly embed will apply these colors to its UI elements.
Are Calendly webhooks available on the free plan?
No, Calendly webhook subscriptions require a paid plan (Standard at $10/month per user or higher). On the free plan, you can use the API to poll for new events, but there is no push notification. If real-time booking notifications are important and budget is a concern, consider using Zapier's free tier which can bridge Calendly events to your webhook endpoint.
How do I fetch the email address of someone who booked a meeting?
Invitee details including email are available through the Calendly API's invitees endpoint. After fetching a scheduled event, extract the event URI and call GET /scheduled_events/{event_uuid}/invitees with your access token. The response includes each invitee's name, email, and answers to any custom questions you added to the event type.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation