diff --git a/docs.json b/docs.json
index ec7b7e8..29f453d 100644
--- a/docs.json
+++ b/docs.json
@@ -177,7 +177,8 @@
"embedding/embed-snippet-generator",
"embedding/prefill-booking-form-embed",
"embedding/utm-tracking-embed",
- "embedding/embed-auto-forward-query-params"
+ "embedding/embed-auto-forward-query-params",
+ "embedding/onboarding-embed"
]
},
{
diff --git a/embedding/onboarding-embed.mdx b/embedding/onboarding-embed.mdx
new file mode 100644
index 0000000..ae9c841
--- /dev/null
+++ b/embedding/onboarding-embed.mdx
@@ -0,0 +1,276 @@
+---
+title: "Onboarding embed"
+description: "Embed Cal.com account creation and onboarding directly in your app so users never leave your site."
+---
+
+The onboarding embed lets you add Cal.com account creation, onboarding, and OAuth authorization directly inside your application. Your users create a Cal.com account, set up their profile, connect a calendar, and grant your app access — all without leaving your site.
+
+
+ 
+
+
+## Prerequisites
+
+Before using the onboarding embed, you need:
+
+- An **OAuth client ID** from the Cal.com team. Fill out [this form](https://i.cal.com/forms/4052adda-bc79-4a8d-9f63-5bc3bead4cd3) to get started.
+- A **redirect URI** registered on your OAuth client that shares the same origin (scheme + domain + port) as the page hosting the embed.
+- The `@calcom/atoms` React package installed in your project.
+
+```bash
+npm install @calcom/atoms
+```
+
+## How it works
+
+The component opens a dialog containing an iframe that runs Cal.com's onboarding flow. Because the iframe runs on Cal.com's domain with a first-party session, no third-party cookies are needed.
+
+The flow automatically detects where the user is:
+
+- **No session** — starts at signup/login, then profile setup, calendar connection, and OAuth consent.
+- **Session with incomplete onboarding** — resumes from where the user left off.
+- **Session with complete onboarding** — skips straight to OAuth consent.
+
+After the user grants access, you receive an authorization code that you exchange for access and refresh tokens.
+
+## Two modes
+
+The component supports two modes for receiving the authorization code:
+
+- **Callback mode** — provide an `onAuthorizationAllowed` callback to receive the code directly. No page navigation occurs.
+- **Redirect mode** — omit the callback and the browser navigates to your `redirectUri` with the code as a query parameter.
+
+### Callback mode
+
+Provide `onAuthorizationAllowed` to receive the authorization code directly. The dialog closes and your callback fires after the user authorizes — no page reload.
+
+```tsx
+import { OnboardingEmbed } from "@calcom/atoms";
+import { useState } from "react";
+
+function App() {
+ const [state] = useState(() => crypto.randomUUID());
+
+ return (
+ {
+ fetch("/api/cal/exchange", {
+ method: "POST",
+ body: JSON.stringify({ code, state }),
+ });
+ }}
+ onError={(error) => console.error(error.code, error.message)}
+ onClose={() => console.log("Dialog dismissed")}
+ />
+ );
+}
+```
+
+### Redirect mode
+
+Omit `onAuthorizationAllowed` and the browser navigates to your `redirectUri` after the user completes onboarding and grants access:
+
+```
+https://your-app.com/cal/callback?code=AUTHORIZATION_CODE&state=YOUR_STATE
+```
+
+```tsx
+import { OnboardingEmbed } from "@calcom/atoms";
+import { useState } from "react";
+
+function App() {
+ const [state] = useState(() => crypto.randomUUID());
+
+ return (
+ console.error(error.code, error.message)}
+ />
+ );
+}
+```
+
+## Props
+
+| Prop | Type | Required | Description |
+|------|------|----------|-------------|
+| `oAuthClientId` | `string` | Yes | Your OAuth client ID. |
+| `host` | `string` | No | Cal.com host URL. Defaults to `https://app.cal.com`. |
+| `theme` | `"light"` or `"dark"` | No | Theme for the embedded UI. Defaults to `"light"`. |
+| `user` | `{ email?, name?, username? }` | No | Prefill user details in signup and profile steps. |
+| `authorization` | See below | Yes | OAuth authorization parameters. |
+| `onAuthorizationAllowed` | `({ code }) => void` | No | Called with the authorization code on success. Enables callback mode. If omitted, enables redirect mode. |
+| `onError` | `(error) => void` | No | Called when an error occurs. |
+| `onAuthorizationDenied` | `() => void` | No | Called when the user declines authorization. If omitted, the browser redirects with `error=access_denied`. |
+| `onClose` | `() => void` | No | Called when the user dismisses the dialog. |
+| `trigger` | `ReactNode` | No | Custom trigger element. Defaults to a "Continue with Cal.com" button. |
+
+### Authorization props
+
+| Prop | Type | Required | Description |
+|------|------|----------|-------------|
+| `redirectUri` | `string` | Yes | One of the redirect URIs registered on your OAuth client. Must share the same origin as the page hosting the embed. |
+| `scope` | `string[]` | Yes | OAuth scopes to request. Must be a subset of scopes registered on the OAuth client. |
+| `state` | `string` | Yes | A unique CSRF token. Generate one per session and verify it matches when you receive the authorization code. |
+| `codeChallenge` | `string` | For public clients | PKCE code challenge (S256 method). Required for public OAuth clients that cannot store a client secret. |
+
+
+ If the user signs up via Google, the `user` prop values are ignored — name, email, and username come from the Google account instead.
+
+
+## Theming and custom trigger
+
+The `theme` prop controls the appearance of the trigger button, the onboarding steps, and the authorization page.
+
+| Light theme (default) | Dark theme |
+|---|---|
+|  |  |
+
+You can replace the default button with your own element using the `trigger` prop:
+
+```tsx
+Connect calendar}
+ {/* ...other props */}
+/>
+```
+
+## Step-by-step walkthrough
+
+Here is what the user sees when they click the trigger button:
+
+
+
+ The dialog opens with a login form. Existing users sign in with email or Google. New users click "Create account" to sign up. The `user.email` prop prefills the email field.
+
+
+ 
+
+
+
+
+ After signing up, the user sets their display name. The `user.name` prop prefills this field.
+
+
+ 
+
+
+
+
+ The user can connect a calendar (such as Google Calendar) or skip this step.
+
+
+ 
+
+
+
+
+ The user reviews the permissions your app requests and clicks "Allow". The displayed permissions correspond to the `scope` you passed to the component.
+
+
+ 
+
+
+
+
+ Your `onAuthorizationAllowed` callback fires with the authorization code, or the browser redirects to your `redirectUri`.
+
+
+
+## Public clients (PKCE)
+
+If your OAuth client cannot safely store a client secret (for example, a browser-only app), use PKCE to secure the authorization code exchange. Generate a `code_verifier`, derive a `code_challenge`, and pass it to the component:
+
+```tsx
+import { OnboardingEmbed } from "@calcom/atoms";
+import { useMemo, useState } from "react";
+
+async function generatePkce() {
+ const array = new Uint8Array(32);
+ crypto.getRandomValues(array);
+ const codeVerifier = btoa(String.fromCharCode(...array))
+ .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+
+ const digest = await crypto.subtle.digest(
+ "SHA-256",
+ new TextEncoder().encode(codeVerifier)
+ );
+ const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
+ .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+
+ return { codeVerifier, codeChallenge };
+}
+
+export function MyApp() {
+ const state = useMemo(() => crypto.randomUUID(), []);
+ const [pkce, setPkce] = useState<{
+ codeVerifier: string;
+ codeChallenge: string;
+ } | null>(null);
+
+ useMemo(() => {
+ generatePkce().then(setPkce);
+ }, []);
+
+ if (!pkce) return null;
+
+ return (
+ {
+ const res = await fetch(
+ "https://api.cal.com/v2/auth/oauth2/token",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ client_id: "your_client_id",
+ code_verifier: pkce.codeVerifier,
+ grant_type: "authorization_code",
+ code,
+ redirect_uri: "https://your-app.com/cal/callback",
+ }),
+ }
+ );
+ const { access_token, refresh_token } = await res.json();
+ }}
+ />
+ );
+}
+```
+
+## Error handling
+
+The `onError` callback receives an error with a `code` and `message`:
+
+| Code | What it means |
+|------|---------------|
+| `INVALID_PROPS` | Required props are missing or invalid (for example, the `oAuthClientId` does not exist or the `redirectUri` does not match a registered URI). |
+| `SIGNUP_FAILED` | Account creation failed. |
+| `ONBOARDING_FAILED` | An error occurred during one of the onboarding steps. |
+| `AUTHORIZATION_FAILED` | OAuth consent failed. |
+| `STATE_MISMATCH` | The `state` in the response did not match what you provided. This could indicate a CSRF attack. |
+| `UNKNOWN` | An unexpected error occurred. |
+
+In redirect mode, errors are passed as query parameters on your `redirectUri`:
+
+```
+https://your-app.com/cal/callback?error=SIGNUP_FAILED&state=YOUR_STATE
+```
diff --git a/images/onboarding-step-authorize.png b/images/onboarding-step-authorize.png
new file mode 100644
index 0000000..46ba0f7
Binary files /dev/null and b/images/onboarding-step-authorize.png differ
diff --git a/images/onboarding-step-calendar.png b/images/onboarding-step-calendar.png
new file mode 100644
index 0000000..59607d3
Binary files /dev/null and b/images/onboarding-step-calendar.png differ
diff --git a/images/onboarding-step-login.png b/images/onboarding-step-login.png
new file mode 100644
index 0000000..f4b9258
Binary files /dev/null and b/images/onboarding-step-login.png differ
diff --git a/images/onboarding-step-profile.png b/images/onboarding-step-profile.png
new file mode 100644
index 0000000..872498b
Binary files /dev/null and b/images/onboarding-step-profile.png differ
diff --git a/images/onboarding-step-signup-form.png b/images/onboarding-step-signup-form.png
new file mode 100644
index 0000000..19b32fe
Binary files /dev/null and b/images/onboarding-step-signup-form.png differ
diff --git a/images/onboarding-step-signup.png b/images/onboarding-step-signup.png
new file mode 100644
index 0000000..eda64a6
Binary files /dev/null and b/images/onboarding-step-signup.png differ
diff --git a/images/onboarding-step-success.png b/images/onboarding-step-success.png
new file mode 100644
index 0000000..85e4afb
Binary files /dev/null and b/images/onboarding-step-success.png differ
diff --git a/images/onboarding-trigger-custom.png b/images/onboarding-trigger-custom.png
new file mode 100644
index 0000000..a6bd01b
Binary files /dev/null and b/images/onboarding-trigger-custom.png differ
diff --git a/images/onboarding-trigger-dark.png b/images/onboarding-trigger-dark.png
new file mode 100644
index 0000000..f5476e5
Binary files /dev/null and b/images/onboarding-trigger-dark.png differ
diff --git a/images/onboarding-trigger-light.png b/images/onboarding-trigger-light.png
new file mode 100644
index 0000000..ee406a6
Binary files /dev/null and b/images/onboarding-trigger-light.png differ