Build a custom sign-up flow to handle application invitations
Inviting users to your Clerk application begins with creating an invitation for an email address. Once the invitation is created, an email with an invitation link will be sent to the user's email address. When the user visits the invitation link, they will be redirected to the application's sign up page and their email address will be automatically verified.
To learn how to create an invitation, see the Invitations guide.
This guide will demonstrate how to build a custom sign-up flow to handle application invitations.
Create the sign-up flow
Once the user visits the invitation link and is redirected to the specified URL, an invitation token will be appended to the URL. 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
method.
The following example demonstrates how to create a new sign-up using the invitation token. If there is no invitation token in the URL, the user will be shown a message indicating that they need an invitation to sign up for the application.
'use client';
import * as React from 'react';
import { useSignUp } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
export default function Page() {
const { isLoaded, signUp, setActive } = useSignUp();
const [firstName, setFirstName] = React.useState('');
const [lastName, setLastName] = React.useState('');
const router = useRouter();
// Get the token from the query parameter
const param = '__clerk_ticket';
const ticket = new URL(window.location.href).searchParams.get(param);
// If there is no invitation token, restrict access to the sign-up page
if (!ticket) {
return <p>You need an invitation to sign up for this application.</p>;
}
// Handle submission of the sign-up form
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isLoaded) return;
try {
if (!ticket) return null;
// Optional: collect additional user information
const firstName = 'John';
const lastName = 'Doe';
// 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,
firstName,
lastName,
});
// If verification was completed, set the session to active
// and redirect the user
if (signUpAttempt.status === 'complete') {
await setActive({ session: signUpAttempt.createdSessionId });
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: any) {
console.error(JSON.stringify(err, null, 2));
}
};
// Display the initial sign-up form to capture optional sign-up info
return (
<>
<h1>Sign up</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="firstName">Enter first name</label>
<input
id="firstName"
type="text"
name="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
</div>
<div>
<label htmlFor="lastName">Enter last name</label>
<input
id="lastName"
type="text"
name="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
</div>
<div>
<button type="submit">Next</button>
</div>
</form>
</>
);
}
<!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="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" />
<button type="submit">Continue</button>
</form>
</div>
<script
type="module"
src="/src/main.js"
async
crossorigin="anonymous"
></script>
</body>
</html>
import { Clerk } from '@clerk/clerk-js';
// Initialize Clerk with your Clerk publishable key
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY');
await clerk.load();
if (clerk.user) {
// Mount user button component
document.getElementById('signed-in').innerHTML = `
<div id="user-button"></div>
`;
const userbuttonDiv = document.getElementById('user-button');
clerk.mountUserButton(userbuttonDiv);
} else {
// Get the token from the query parameter
const param = '__clerk_ticket';
const ticket = 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');
try {
// Start the sign-up process using the ticket method
const signUpAttempt = await clerk.client.signUp.create({
strategy: 'ticket',
ticket,
firstName,
lastName,
});
// If sign-up was successful, set the session to active
if (signUpAttempt.status === 'complete') {
await clerk.setActive({ session: signUpAttempt.createdSessionId });
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(JSON.stringify(signUpAttempt, null, 2));
}
} catch (error) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(error, null, 2));
}
});
}
<!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="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" />
<button type="submit">Continue</button>
</form>
</div>
<script
async
crossorigin="anonymous"
data-clerk-publishable-key="YOUR_PUBLISHABLE_KEY"
src="https://YOUR_FRONTEND_API_URL/npm/@clerk/clerk-js@latest/dist/clerk.browser.js"
type="text/javascript"
></script>
<script>
window.addEventListener('load', async function () {
await Clerk.load();
if (Clerk.user) {
// Mount user button component
document.getElementById('signed-in').innerHTML = `
<div id="user-button"></div>
`;
const userbuttonDiv = document.getElementById('user-button');
Clerk.mountUserButton(userbuttonDiv);
} else {
// Get the token from the query parameter
const param = '__clerk_ticket';
const ticket = 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');
try {
// Start the sign-up process using the ticket method
const signUpAttempt = await Clerk.client.signUp.create({
strategy: 'ticket',
ticket,
firstName,
lastName,
});
// If sign-up was successful, set the session to active
if (signUpAttempt.status === 'complete') {
await Clerk.setActive({ session: signUpAttempt.createdSessionId });
} else {
// If the status is not complete, check why. User may need to
// complete further steps.
console.error(JSON.stringify(signUpAttempt, null, 2));
}
} catch (error) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(error, null, 2));
}
});
}
});
</script>
</body>
</html>