# Troubleshooting

Fixes for the problems people hit integrating clndr.pro, from 401 Missing API key and empty slot lists to dates a day off, client-component errors and embeds that don't resize.

Find the symptom, check the cause, apply the fix. If none of these match, email [hello@devsforfun.com](mailto:hello@devsforfun.com) with the request URL, the status code, the `error` message and roughly when it happened. Never include the full key.

## Keys and requests

**401 Missing API key, but I am sending one**

The request went to `https://clndr.pro`, which redirects to `https://www.clndr.pro`. HTTP clients drop the `Authorization` header when a redirect changes host, so the key never arrives.

Use `https://www.clndr.pro/api/v1` everywhere. If it's the SDK, upgrade: `@clndr-pro/sdk` before 0.1.5 and `@clndr-pro/react` before 0.2.1 default to the bare domain. If you can't upgrade yet, set the base URL yourself:

```ts
new Clndr({ apiKey: process.env.CLNDR_SECRET_KEY!, baseUrl: 'https://www.clndr.pro' });
```

```tsx
<ClndrProvider publishableKey={key} baseUrl="https://www.clndr.pro">
```

The other cause is a hand-written `fetch` with an empty key: `process.env.X` was `undefined` (see the next entry), so the header said `Bearer undefined`, or wasn't set. The SDK doesn't get that far: `new Clndr({ apiKey: undefined })` throws ``Clndr: `apiKey` is required``, `new Clndr(undefined)` throws a `TypeError`, and `ClndrProvider` without a key throws ``ClndrProvider: either `publishableKey` or `client` is required``.

**401 Invalid API key**

clndr.pro has no key with that value. Check, in order: the env var holds the whole key (they're 73 characters: `clndr_sk_` or `clndr_pk_` plus 64 hex characters); the deployment you're testing has the variable (Vercel keeps Production, Preview and Development separately); the key wasn't deleted in the dashboard.

In Next.js, a secret key read inside a `'use client'` file is always `undefined` (with the SDK that throws when the client is built, before any request), and a `NEXT_PUBLIC_` variable added after the last build isn't in the bundle until you rebuild.

**401 API key has been revoked**

Someone revoked it under **API keys**. Create a new one and deploy it. Revoked keys can't be switched back on.

**403 API key is missing required scope(s): bookings:read**

A publishable key called an endpoint only secret keys can use: listing, reading, confirming or cancelling bookings. Move that call to your server and use the secret key there. [Book through a server action](/tutorials/server-actions) and [Approve bookings in your admin](/tutorials/approvals-dashboard) show the shape.

**403 This booking page does not belong to the API key owner**

The `bookingPageId` you sent belongs to a different clndr.pro account than the key. It happens when you copy a page id from one account and a key from another, or mix up staging and production keys. Fetch the page with the same key (`GET /booking-pages/{slug}`) and use its `bookingPage.id`.

**404 Booking page not found**

Either the slug is wrong (it's the last part of the page's URL, lowercase, like `intro-call`) or the page is switched off. [List booking pages](/api-reference/operations/listBookingPages) shows every page on the account with its `slug` and `is_active`.

**429 Rate limit exceeded**

The key used up its window: 300 requests a minute for a secret key, 120 for a publishable one, shared by everyone using that key. Wait `Retry-After` seconds. If a page fires a slots request on every render, fix the effect's dependencies; if traffic is genuinely high, fetch slots on your server and cache them briefly. See [Errors and rate limits](/errors#rate-limits).

**CORS error in the browser console**

The API allows every origin, so a CORS error almost always hides something else. The usual one: the request went to `clndr.pro` and the browser refused to follow the redirect on a preflight. Use `https://www.clndr.pro`. If the URL is right, open the Network tab: a failed request that isn't from clndr.pro (an ad blocker, a proxy) shows up there.

## Slots and dates

**The slot list is empty**

Go through these in order:

1. The host has no availability that weekday. Check [**Availability**](https://www.clndr.pro/availability), and check which **Hours** the booking page uses.
2. The day is past the page's **Bookable up to** limit.
3. The day is in the past, or it's today and every slot has started.
4. Bookings and calendar events fill the day. Synced Google Calendar events block slots like bookings do.
5. You asked for the wrong day. `date` is a day in the host's timezone, as `YYYY-MM-DD`.

**Slots are a day off, or show at odd hours**

Two separate things cause this.

clndr.pro returns UTC times. Showing them without converting (slicing `"2026-10-15T08:00:00.000Z"` to `08:00`) puts every slot at its UTC time. Format them with `toLocaleTimeString()` or `Intl.DateTimeFormat`, which use the guest's timezone.

Separately, `date` means the host's day. A guest far from the host can see some of those slots fall on their previous or next day. That's correct; group slots by the guest's local date if you show a calendar. [How booking works](/concepts#timezones-worked-through) has a worked example.

Older SDKs add a third cause: before `@clndr-pro/sdk` 0.1.5, `getSlots(slug, date)` sent a `Date` as a full UTC timestamp, which can land on the neighbouring day. Upgrade, or pass the day as a `YYYY-MM-DD` string.

**A booking failed with 409**

`This time slot is no longer available` means someone booked an overlapping time between your slots request and the guest's submit, or the guest double-submitted. `This time slot is not available` (publishable keys only, and what a publishable key usually gets for a taken slot too) means the `startTime`/`endTime` you sent isn't an open slot right now: it's in the past, beyond `max_days_ahead`, outside the host's hours, a different length than the page's duration, or no longer free. Send a slot's `start` and `end` exactly as the slots endpoint returned them.

Either way, fetch the slots again and let the guest pick. If the error came after a timed-out first attempt, the first attempt may have worked: see [Retrying](/errors#retrying).

## React and Next.js

**createContext only works in Client Components**

A clndr.pro component or hook was imported into a Server Component. Import `@clndr-pro/react` only in files that start with `'use client'`, and keep `ClndrProvider` in its own client file rendered from your layout (see the [quickstart](/quickstart)). Versions before 0.2.1 also left the `'use client'` marker out of the published bundle, so upgrade.

**require('@clndr-pro/sdk') fails with Cannot find module**

Versions before `@clndr-pro/sdk` 0.1.5 and `@clndr-pro/react` 0.2.1 pointed their CommonJS entry at a file that wasn't published. ES module imports (`import { Clndr } from '@clndr-pro/sdk'`) always worked. Upgrade, or switch the file to `import`.

**useClndr must be used inside ClndrProvider**

The component isn't under the provider. Render `<Providers>` around `{children}` in `app/layout.tsx`, or wrap just the booking section in `ClndrProvider`.

**The date picker and slot buttons are unstyled**

That's the default: the components ship plain HTML so they don't fight your CSS. Pass your own components and class names, as in [Styling and slots](/react/styling).

## Embeds

**Clndr is not defined**

Your code ran before `embed.js` loaded. Call `Clndr` from the script's `onload` (or `onReady` with `next/script`), as in [Embed script](/embed/script#inline).

**The embed never resizes or reports bookings, or stays blank**

Load the script and the iframe from `https://www.clndr.pro`. From the bare `clndr.pro` the frame ends up on another origin, so its messages are ignored: no resizing, no callbacks. A blank frame on a `team` page means the guest isn't signed in to clndr.pro inside the frame, which browsers block for third-party iframes; team pages can't be embedded.

**The iframe doesn't resize**

Only the embed script, or your own message listener, resizes the frame. Check the listener compares `event.origin` with `https://www.clndr.pro` exactly, with the `www`. See [Sizing](/embed/iframe#sizing).

**A private page says Booking page not found**

Private pages need their access code: `config.accessCode` in the script, `?c=` on the iframe URL. The code must belong to that page and not have expired.

## Emails and calendar

**The guest didn't get a confirmation email**

On an approval page that's expected: the guest gets an email when the host confirms in the dashboard, not when they book. Confirming through the API sends no clndr.pro email (Google sends an invite if the page has Meet links). On a direct page, check the address on the booking under **Meetings**, then the guest's spam folder.

**The booking has no Meet link**

Meet links need **Add a Google Meet link** on the booking page and the host's Google Calendar connected. For approval pages the link appears once the booking is confirmed. If it's on and still missing, read the booking's `sync_status` and `sync_failure_reason`: `Google Calendar is not connected` means the host needs to reconnect Google in the dashboard. Those two fields aren't updated when you confirm through the API, so for a booking you approved with `PATCH`, an empty `google_calendar_event_id` is the sign that no event was created.
