Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c284d0a
Add initial docs for SignInFuture and SignUpFuture
dstaley Oct 7, 2025
6ed9df4
Add legacy folder for auth custom flows
dstaley Oct 8, 2025
8f9e564
Move email-password code samples to partials
dstaley Oct 8, 2025
0664f60
Ticket sign ups
dstaley Oct 8, 2025
f91be3f
Email password MFA
dstaley Oct 8, 2025
4f71b20
Add sdks for google one tap
dstaley Oct 8, 2025
fbb28c0
Add version callout
dstaley Oct 8, 2025
db3dc14
Remove docs that don't use legacy APIs
dstaley Oct 8, 2025
8ddbd36
Add SDK filter to sign-out page
dstaley Oct 8, 2025
564424e
Add email/sms otp guide
dstaley Oct 8, 2025
022de2e
Remove links from manifest
dstaley Oct 8, 2025
cf23eab
Replace partials with inline code blocks
dstaley Oct 9, 2025
1ff6324
Add SSO documentation
dstaley Oct 9, 2025
2a6da3e
Update API docs for SignInFuture and SignUpFuture
dstaley Oct 14, 2025
f868d8a
Fix line breaks
dstaley Oct 14, 2025
8cd114e
Add hooks docs and legacy docs
dstaley Oct 15, 2025
3832e90
Remove link
dstaley Oct 15, 2025
6428b2f
Add legacy warning
dstaley Oct 15, 2025
6d3a1f2
Merge branch 'main' into ds.feat/custom-flow-apis
dstaley Nov 7, 2025
871d19f
Add legal acceptance flow and swap dashboard links
dstaley Nov 7, 2025
7202a79
Add docs for Errors
dstaley Nov 7, 2025
ff431d7
Remove placeholders
dstaley Nov 7, 2025
2688bf2
Update placeholders
dstaley Nov 7, 2025
f8ea163
Fix link
dstaley Nov 7, 2025
123021d
Split operation fields for errors
dstaley Nov 13, 2025
4199341
Merge branch 'main' into ds.feat/custom-flow-apis
dstaley Nov 17, 2025
2790395
Clarify identity of signIn and signUp
dstaley Nov 17, 2025
0d15781
Fix anchor reference
dstaley Nov 17, 2025
2be33df
remove frontmatter; add callout
alexisintech Nov 17, 2025
9155d61
Add SSOCallback transfer flow
dstaley Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/_partials/custom-flows/future-api-callout.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
> [!IMPORTANT]
> The APIs described here are stable, and will become the default in the next major version of `clerk-js`.
12 changes: 12 additions & 0 deletions docs/_partials/custom-flows/sso-connections-legacy.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
The following example **will both sign up _and_ sign in users**, eliminating the need for a separate sign-up page. However, if you want to have separate sign-up and sign-in pages, the sign-up and sign-in flows are equivalent, meaning that all you have to do is swap out the `SignIn` object for the `SignUp` object using the [`useSignUp()`](/docs/reference/hooks/use-sign-up) hook.

The following example:

1. Accesses the [`SignIn`](/docs/reference/javascript/sign-in) object using the [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook.
1. Starts the authentication process by calling [`SignIn.authenticateWithRedirect(params)`](/docs/reference/javascript/sign-in#authenticate-with-redirect). This method requires a `redirectUrl` param, which is the URL that the browser will be redirected to once the user authenticates with the identity provider.
1. Creates a route at the URL that the `redirectUrl` param points to. The following example names this route `/sso-callback`. This route should either render the prebuilt [`<AuthenticateWithRedirectCallback/>`](/docs/reference/components/control/authenticate-with-redirect-callback) component or call the [`Clerk.handleRedirectCallback()`](/docs/reference/javascript/clerk#handle-redirect-callback) method if you're not using the prebuilt component.

The following example shows two files:

1. The sign-in page where the user can start the authentication flow.
1. The SSO callback page where the flow is completed.
11 changes: 4 additions & 7 deletions docs/_partials/custom-flows/sso-connections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ The following example **will both sign up _and_ sign in users**, eliminating the
The following example:

1. Accesses the [`SignIn`](/docs/reference/javascript/sign-in) object using the [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook.
1. Starts the authentication process by calling [`SignIn.authenticateWithRedirect(params)`](/docs/reference/javascript/sign-in#authenticate-with-redirect). This method requires a `redirectUrl` param, which is the URL that the browser will be redirected to once the user authenticates with the identity provider.
1. Creates a route at the URL that the `redirectUrl` param points to. The following example names this route `/sso-callback`. This route should either render the prebuilt [`<AuthenticateWithRedirectCallback/>`](/docs/reference/components/control/authenticate-with-redirect-callback) component or call the [`Clerk.handleRedirectCallback()`](/docs/reference/javascript/clerk#handle-redirect-callback) method if you're not using the prebuilt component.

The following example shows two files:

1. The sign-in page where the user can start the authentication flow.
1. The SSO callback page where the flow is completed.
1. Starts the authentication process by calling [`SignIn.sso(params)`](/docs/reference/javascript/sign-in-future#sso). This method requires the following params:
- `redirectUrl`: The URL that the browser will be redirected to once the user authenticates with the identity provider if no additional requirements are needed, and a session has been created
- `redirectCallbackUrl`: The URL that the browser will be redirected to once the user authenticates with the identity provider if additional requirements are needed
1. Creates a route at the URL that the `redirectCallbackUrl` param points to. The following example re-uses the `/sign-in` route, which should be written to handle when a sign-in attempt is in a non-complete status such as `needs_second_factor`.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ description: Learn how to use the Clerk API to build a custom flow for handling

<Include src="_partials/custom-flows-callout" />

<If sdk={["nextjs", "react", "expo", "js-frontend", "react-router", "tanstack-react-start"]}>
> [!IMPORTANT]
> This guide applies to the following Clerk SDKs:
>
> - `@clerk/react` v6 or higher
> - `@clerk/nextjs` v7 or higher
> - `@clerk/expo` v3 or higher
> - `@clerk/react-router` v3 or higher
> - `@clerk/tanstack-react-start` v0.26.0 or higher
>
> If you're using an older version of one of these SDKs, or are using the legacy API, refer to the [legacy API documentation](/docs/guides/development/custom-flows/authentication/legacy/application-invitations).
</If>

When a user visits an [invitation](/docs/guides/users/inviting) link, Clerk first checks whether a custom redirect URL was provided.

**If no redirect URL is specified**, the user will be redirected to the appropriate Account Portal page (either [sign-up](/docs/guides/customizing-clerk/account-portal#sign-up) or [sign-in](/docs/guides/customizing-clerk/account-portal#sign-in)), or to the custom sign-up/sign-in pages that you've configured for your application.
Expand All @@ -19,236 +32,74 @@ Once the user visits the invitation link and is redirected to the specified URL,

For example, if the redirect URL was `https://www.example.com/accept-invitation`, the URL that the user would be redirected to would be `https://www.example.com/accept-invitation?__clerk_ticket=.....`.

To create a sign-up flow using the invitation token, you need to extract the token from the URL and pass it to the [`signUp.create()`](/docs/reference/javascript/sign-up#create) method, as shown in the following example. The following example also demonstrates how to collect additional user information for the sign-up; you can either remove these fields or adjust them to fit your application.
To create a sign-up flow using the invitation token, you need to call the [`signUp.ticket()`](/docs/reference/javascript/sign-up-future#ticket) method, as shown in the following example. The following example also demonstrates how to collect additional user information for the sign-up; you can either remove these fields or adjust them to fit your application.

<Tabs items={["Next.js", "JavaScript"]}>
<Tabs items={["Next.js"]}>
<Tab>
```tsx {{ filename: 'app/accept-invitation/page.tsx', collapsible: true }}
'use client'

import * as React from 'react'
import { useSignUp, useUser } from '@clerk/nextjs'
import { useSearchParams, useRouter } from 'next/navigation'
import { useRouter } from 'next/navigation'

export default function Page() {
const { isSignedIn, user } = useUser()
const { signUp, errors, fetchStatus } = useSignUp()
const router = useRouter()
const { isLoaded, signUp, setActive } = useSignUp()
const [firstName, setFirstName] = React.useState('')
const [lastName, setLastName] = React.useState('')
const [password, setPassword] = React.useState('')

// Handle signed-in users visiting this page
// This will also redirect the user once they finish the sign-up process
React.useEffect(() => {
if (isSignedIn) {
router.push('/')
}
}, [isSignedIn])

// Get the token from the query params
const token = useSearchParams().get('__clerk_ticket')

// If there is no invitation token, restrict access to this page
if (!token) {
return <p>No invitation token found.</p>
}
const handleSubmit = async (formData: FormData) => {
const firstName = formData.get('firstName') as string
const lastName = formData.get('lastName') as string
const password = formData.get('password') as string

// Handle submission of the sign-up form
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()

if (!isLoaded) return

try {
if (!token) return null

// Create a new sign-up with the supplied invitation token.
// Make sure you're also passing the ticket strategy.
// After the below call, the user's email address will be
// automatically verified because of the invitation token.
const signUpAttempt = await signUp.create({
strategy: 'ticket',
ticket: token,
firstName,
lastName,
password,
await signUp.ticket({
firstName,
lastName,
password,
})
if (signUp.status === 'complete') {
await signUp.finalize({
navigate: () => {
router.push('/')
},
})

// If the sign-up was completed, set the session to active
if (signUpAttempt.status === 'complete') {
await setActive({ session: signUpAttempt.createdSessionId })
} else {
// If the sign-up status is not complete, check why. User may need to
// complete further steps.
console.error(JSON.stringify(signUpAttempt, null, 2))
}
} catch (err) {
console.error(JSON.stringify(err, null, 2))
}
}

if (signUp.status === 'complete' || isSignedIn) {
return null
}

return (
<>
<h1>Sign up</h1>
<form onSubmit={handleSubmit}>
<form action={handleSubmit}>
<div>
<label htmlFor="firstName">Enter first name</label>
<input
id="firstName"
type="text"
name="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
<input id="firstName" type="text" name="firstName" />
{errors.fields.firstName && <p>{errors.fields.firstName.message}</p>}
</div>
<div>
<label htmlFor="lastName">Enter last name</label>
<input
id="lastName"
type="text"
name="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
<input id="lastName" type="text" name="lastName" />
{errors.fields.lastName && <p>{errors.fields.lastName.message}</p>}
</div>
<div>
<label htmlFor="password">Enter password</label>
<input
id="password"
type="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<input id="password" type="password" name="password" />
{errors.fields.password && <p>{errors.fields.password.message}</p>}
</div>
<div id="clerk-captcha" />
<div>
<button type="submit">Next</button>
<button type="submit" disabled={fetchStatus === 'fetching'}>
Next
</button>
</div>
</form>
</>
)
}
```
</Tab>

<Tab>
<CodeBlockTabs options={["index.html", "main.js"]}>
```html {{ filename: 'index.html', collapsible: true }}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clerk + JavaScript App</title>
</head>
<body>
<div id="signed-in"></div>

<div id="task"></div>

<div id="sign-up">
<h2>Sign up</h2>
<form id="sign-up-form">
<label for="firstName">Enter first name</label>
<input name="firstName" id="firstName" />
<label for="lastName">Enter last name</label>
<input name="lastName" id="lastName" />
<label for="password">Enter password</label>
<input name="password" id="password" />
<button type="submit">Continue</button>
</form>
</div>

<script type="module" src="/src/main.js" async crossorigin="anonymous"></script>
</body>
</html>
```

```js {{ filename: 'main.js', collapsible: true }}
import { Clerk } from '@clerk/clerk-js'

const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

const clerk = new Clerk(pubKey)
await clerk.load()

if (clerk.isSignedIn) {
// Mount user button component
document.getElementById('signed-in').innerHTML = `
<div id="user-button"></div>
`

const userbuttonDiv = document.getElementById('user-button')

clerk.mountUserButton(userbuttonDiv)
} else if (clerk.session.currentTask) {
// Check for pending tasks and display custom UI to help users resolve them
// See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks
switch (clerk.session.currentTask.key) {
case 'choose-organization': {
document.getElementById('app').innerHTML = `
<div id="task"></div>
`

const taskDiv = document.getElementById('task')

clerk.mountTaskChooseOrganization(taskDiv)
}
}
} else {
// Get the token from the query parameter
const param = '__clerk_ticket'
const token = new URL(window.location.href).searchParams.get(param)

// Handle the sign-up form
document.getElementById('sign-up-form').addEventListener('submit', async (e) => {
e.preventDefault()

const formData = new FormData(e.target)
const firstName = formData.get('firstName')
const lastName = formData.get('lastName')
const password = formData.get('password')

try {
// Start the sign-up process using the ticket method
const signUpAttempt = await clerk.client.signUp.create({
strategy: 'ticket',
ticket: token,
firstName,
lastName,
password,
})

// If sign-up was successful, set the session to active
if (signUpAttempt.status === 'complete') {
await clerk.setActive({
session: signUpAttempt.createdSessionId,
navigate: async ({ session }) => {
if (session?.currentTask) {
// Check for tasks and navigate to custom UI to help users resolve them
// See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks
console.log(session?.currentTask)
return
}

await router.push('/')
},
})
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(JSON.stringify(signUpAttempt, null, 2))
}
} catch (err) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
}
})
}
```
</CodeBlockTabs>
</Tab>
</Tabs>
Loading