Docs

Build a custom sign-up flow to handle application invitations

Warning

This guide is for users who want to build a custom user interface using the Clerk API. To use a prebuilt UI, you should use Clerk's Account Portal pages or prebuilt components.

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.

app/sign-up/[[...sign-up]]/page.tsx
'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>
    </>
  );
}
index.html
<!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>
main.js
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));
      }
    });
}
index.html
<!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>

Feedback

What did you think of this content?