> **Building with AI coding agents?** If you're using an AI coding agent, install the official Scalekit plugin. It gives your agent full awareness of the Scalekit API — reducing hallucinations and enabling faster, more accurate code generation.
>
> - **Claude Code**: `/plugin marketplace add scalekit-inc/claude-code-authstack` then `/plugin install <auth-type>@scalekit-auth-stack`
> - **GitHub Copilot CLI**: `copilot plugin marketplace add scalekit-inc/github-copilot-authstack` then `copilot plugin install <auth-type>@scalekit-auth-stack`
> - **Codex**: run the bash installer, restart, then open Plugin Directory and enable `<auth-type>`
> - **Skills CLI** (Windsurf, Cline, 40+ agents): `npx skills add scalekit-inc/skills --list` then `--skill <skill-name>`
>
> `<auth-type>` / `<skill-name>`: `agent-auth`, `full-stack-auth`, `mcp-auth`, `modular-sso`, `modular-scim` — [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Invite users to organizations

The journey of bringing users into your application begins with a smooth invitation experience. User invitations are essential for growing organizations, enabling team collaboration, and maintaining secure access control. When done right, invitations create a seamless onboarding experience that sets the tone for users' entire interaction with your product.

Build applications that enable organization owners to invite users to join their organization. Scalekit takes care of sending the invite emails, verifying their email addresses, and creating the user accounts end to end. For applications that require custom invitation flows or automated user management, Scalekit provides comprehensive APIs to programmatically invite users, manage invitations, and integrate with your existing systems.

This is ideal when organization admins or workspace owners need to invite team members directly from your application's dashboard, whether individually or in bulk.
<details>
<summary>Common use cases</summary>

- **Admin dashboards**: Organization admins can invite users from a settings or team management page.
- **Bulk invitations**: Import and invite multiple users at once from CSV files or directory systems.
- **Custom workflows**: Implement approval processes or conditional invitations based on business logic.
- **Integration with existing systems**: Connect invitation flows with your CRM, HR systems, or user directories.
</details>
**Alternative ways to add users:** Beyond email invitations, Scalekit supports automated user provisioning through:
  - **[JIT (Just-In-Time) provisioning](/authenticate/auth-methods/enterprise-sso/)** - Automatically create user accounts when they sign in via SSO for the first time
  - **[SCIM provisioning](/guides/user-management/scim-provisioning/)** - Sync users from identity providers like Okta or Microsoft Entra ID
  - **[Allowed domain auto-join](/authenticate/manage-users-orgs/email-domain-rules/)** - Let users with specific email domains automatically join organizations

  This guide focuses on email-based invitations for manual and programmatic user onboarding.

## Invite users from the Scalekit dashboard

The quickest way to get started with user invitations is through the Scalekit Dashboard.

Navigate to the Users section and click the "Add User" button to invite new members to your organization. You can specify their email address, assign roles, and customize the invitation settings directly from the UI.
**Security considerations:** Scalekit invitation links include multiple security measures:
  - **Time-limited expiration** - Invitations expire after 15 days by default (configurable in dashboard)
  - **Email verification** - Users must verify their email address before gaining access
  - **One-time use tokens** - Each invitation link can only be used once to prevent unauthorized access
  - **Secure magic links** - Cryptographically signed tokens ensure only intended recipients can accept invitations

  These protections ensure that only legitimate users can join organizations, even if invitation emails are forwarded or intercepted.

## Build user invitation flows <Badge type="tip" text="API" />

For applications that require custom invitation flows or automated user management, Scalekit provides a comprehensive SDK. This allows you to programmatically invite users, manage invitations, and integrate with your existing systems.

<details>

<summary>Review the user invitation flow</summary>

```d2 pad=10
shape: sequence_diagram

User -> App: Clicks invitation link
App -> Scalekit: Redirects to initiate login endpoint
App -> Scalekit: Creates auth URL, redirects to Scalekit’s login page
User -> Scalekit: Completes authentication
Scalekit -> App: Redirects to callback URL with auth code
App -> Scalekit: Exchanges code for ID Token & user details
App -> Dashboard: Redirects user
App -> App: Decode ID Token for user details
```
</details>

1. ### Create user invitations

    To invite a user to an organization, create a user membership with their email address and the target organization ID. Scalekit handles sending the invitation email and managing the invitation process end to end.

    ```javascript title="Express.js" wrap
        // Use case: Admin invites team member from organization settings page
        // Security: Verify the organization ID belongs to the authenticated user

        try {
          // Create user account and organization membership
          const newUser = await scalekit.user.createUserAndMembership('org_xxxxxxxxxxxx',
          {
            email: "user@example.com",
            externalId: "crm-user-87425", // Your system's user ID for reference
            userProfile: {
              firstName: "John",
              lastName: "Doe"
            },
            metadata: {
              plan: "free",
              department: "Engineering"
            },
            sendInvitationEmail: true // Scalekit sends the invitation email automatically
          });

          // Invitation sent successfully - user will receive email with magic link
          console.log('User invited:', newUser.user.id);
        } catch (error) {
          console.error('Failed to invite user:', error.message);
          // Handle error: duplicate email, invalid org ID, etc.
        }
        ```
      ```python title="Flask" wrap collapse={1-4}
        from scalekit.v1.users import CreateUser, CreateUserProfile

        # Use case: Admin invites team member from organization settings page
        # Security: Verify the organization ID belongs to the authenticated user

        try:
            # Create user object with profile and metadata
            user = CreateUser(
                email="user@example.com",
                external_id="crm-user-87425", # Your system's user ID for reference
                user_profile=CreateUserProfile(
                    first_name="John",
                    last_name="Doe"
                ),
                metadata={
                    "plan": "free",
                    "department": "Engineering"
                },
                send_invitation_email=True # Scalekit sends the invitation email automatically
            )

            # Create the user and organization membership
            new_user = scalekit_client.user.create_user_and_membership(
                organization_id='org_xxxxxxxxxxxx',
                user=user
            )

            # Invitation sent successfully - user will receive email with magic link
            print(f'User invited: {new_user.user.id}')
        except Exception as error:
            print(f'Failed to invite user: {str(error)}')
            # Handle error: duplicate email, invalid org ID, etc.
        ```
      ```go title="Gin" wrap collapse={1-5}
        import (
            "context"
            "log"
            usersv1 "github.com/scalekit-inc/scalekit-sdk-go/pkg/grpc/scalekit/v1/users"
        )

        // Use case: Admin invites team member from organization settings page
        // Security: Verify the organization ID belongs to the authenticated user

        // Create user object with profile and metadata
        user := &usersv1.CreateUser{
            Email: "user@example.com",
            ExternalId: "crm-user-87425", // Your system's user ID for reference
            UserProfile: &usersv1.CreateUserProfile{
                FirstName: "John",
                LastName:  "Doe",
            },
            Metadata: map[string]string{
                "plan":       "free",
                "department": "Engineering",
            },
        }

        // Create the user and organization membership
        newUser, err := scalekitClient.User().CreateUserAndMembership(
            context.Background(),
            "org_xxxxxxxxxxxx", // The organization the user is joining
            user,
            true, // sendInvitationEmail - Scalekit sends the invitation email automatically
        )
        if err != nil {
            log.Printf("Failed to invite user: %v", err)
            // Handle error: duplicate email, invalid org ID, etc.
            return err
        }

        // Invitation sent successfully - user will receive email with magic link
        log.Printf("User invited: %s", newUser.User.Id)
        ```
      ```java title="Spring Boot" wrap collapse={1-6}
        import com.scalekit.grpc.scalekit.v1.users.*;
        import java.util.logging.Logger;

        // Use case: Admin invites team member from organization settings page
        // Security: Verify the organization ID belongs to the authenticated user

        try {
            // Build the user profile object
            CreateUserProfile profile = CreateUserProfile.newBuilder()
                .setFirstName("John")
                .setLastName("Doe")
                .build();

            // Build the user object with profile and metadata
            CreateUser createUser = CreateUser.newBuilder()
                .setEmail("user@example.com")
                .setExternalId("crm-user-87425") // Your system's user ID for reference
                .putMetadata("plan", "free")
                .putMetadata("department", "Engineering")
                .setUserProfile(profile)
                .build();

            // Create request for user and membership
            CreateUserAndMembershipRequest cuReq = CreateUserAndMembershipRequest.newBuilder()
                .setUser(createUser)
                .setSendInvitationEmail(true) // Scalekit sends the invitation email automatically
                .build();

            // Send the request to create user and membership
            CreateUserAndMembershipResponse cuResp = users.createUserAndMembership(
                "org_xxxxxxxxxxxx",
                cuReq
            );

            // Invitation sent successfully - user will receive email with magic link
            System.out.println("User invited: " + cuResp.getUser().getId());
        } catch (Exception error) {
            System.err.println("Failed to invite user: " + error.getMessage());
            // Handle error: duplicate email, invalid org ID, etc.
        }
        ```
      When you call `createUserAndMembership`, Scalekit creates the user account, adds them to the organization, and sends an invitation email with a secure magic link. The user receives this email immediately and can click the link to verify their email and access your application.

    **Parameters:**

    | Parameter | Type | Description | Required |
    | --- | --- | --- | --- |
    | `organization_id` | `string` | The ID of the organization the user is joining (starts with 'org_') | Yes |
    | `email` | `string` | The email address of the user to invite | Yes |
    | `sendInvitationEmail` | `boolean` | Set to `true` to automatically send invitation emails via Scalekit | Recommended |
    | `externalId` | `string` | Your system's user ID for cross-referencing | No |
    | `userProfile` | `object` | User profile information (`firstName`, `lastName`, etc.) | No |
    | `roles` | `string[]` | Array of role IDs to assign to the invited user | No |
    | `metadata` | `object` | Custom key-value data to associate with the user or membership | No |

2. ### Monitor invitation status

    After successfully creating an invitation, Scalekit returns a user object with membership details. Monitor the `membershipStatus` field to track invitation progress through its lifecycle:

    **Invitation Status Values:**
      - `PENDING_INVITE`: User has been invited but hasn't accepted yet - the invitation email has been sent
      - `ACTIVE`: User has accepted the invitation and can access the organization
      - `INVITE_EXPIRED`: Invitation has expired or been deactivated

    ```json title="Example invitation response" "PENDING_INVITE" {13} collapse={15-18, 22-23, 31-36} showLineNumbers=false
    {
      "user": {
        "id": "usr_01HTR0ABCXYZ",
        "environmentId": "env_01HTQZ99MMNZ",
        "createTime": "2025-06-19T15:41:22Z",
        "updateTime": "2025-06-19T15:41:22Z",
        "email": "user@example.com",
        "externalId": "crm-user-87425",
        "memberships": [
          {
            "organizationId": "org_xxxxxxxxxxxx",
            "joinTime": "2025-06-19T15:41:22Z",
            "membershipStatus": "PENDING_INVITE",
            "roles": [
              {
                "id": "role_admin",
                "name": "admin"
              }
            ],
            "primaryIdentityProvider": "IDENTITY_PROVIDER_UNSPECIFIED",
            "metadata": {
              "plan": "free",
              "department": "Engineering"
            }
          }
        ],
        "userProfile": {
          "id": "prof_01HTR0PQRMNO",
          "firstName": "John",
          "lastName": "Doe",
          "name": "John Doe",
          "locale": "en",
          "emailVerified": false,
          "phoneNumber": "",
          "metadata": {},
          "customAttributes": {}
        },
        "metadata": {
          "plan": "free",
          "department": "Engineering"
        },
        "lastLogin": null
      }
    }
    ```

    **Key fields to monitor:**
    - `membershipStatus`: Track whether the invitation is pending, active, or expired
    - `emailVerified`: Shows `false` until the user accepts the invitation and verifies their email
    - `memberships[].roles`: The roles assigned to the user in this organization
    - `metadata`: Your custom data associated with the user or membership

    Store the `user.id` in your system to track the invitation and update the user's status when they accept.

3. ### Resend user invitation

    If users haven't responded to their initial invitation or the invitation has expired, you can resend it. When an invitation expires, Scalekit automatically creates a new one with a fresh expiration timestamp. For valid invitations, Scalekit sends a reminder email instead.

    **When to resend invitations:**
    - Users haven't responded to their initial invitation
    - Original invitations have expired (after 15 days by default)
    - Users request a new invitation link
    - Users can't find the original invitation email

    Each resend operation increments the resend counter and updates the expiration timestamp if the previous invitation expired.

    ```javascript title="Express.js" wrap
        // Use case: User requests new invitation link after email expires

        try {
          // Resend invitation to the user
          const invitation = await scalekit.user.resendInvitation(
            'org_xxxxxxxxxxxx',  // Organization ID
            'usr_123456'         // User ID from the original invitation
          );

          // Invitation resent successfully with updated expiration
          console.log('Invitation resent:', invitation.invite.resent_count);
          console.log('New expiration:', invitation.invite.expires_at);
        } catch (error) {
          console.error('Failed to resend invitation:', error.message);
          // Handle error: user not found, already active, etc.
        }
        ```
      ```python title="Flask" wrap
        # Use case: User requests new invitation link after email expires

        try:
            # Resend invitation to the user
            invitation = scalekit_client.user.resend_invitation(
                organization_id='org_xxxxxxxxxxxx',  # Organization ID
                user_id='usr_123456'                 # User ID from the original invitation
            )

            # Invitation resent successfully with updated expiration
            print(f'Invitation resent: {invitation.invite.resent_count}')
            print(f'New expiration: {invitation.invite.expires_at}')
        except Exception as error:
            print(f'Failed to resend invitation: {str(error)}')
            # Handle error: user not found, already active, etc.
        ```
      ```go title="Gin" wrap
        import (
            "context"
            "log"
        )

        // Use case: User requests new invitation link after email expires

        // Resend invitation to the user
        invitation, err := scalekitClient.User().ResendInvitation(
            context.Background(),
            "org_xxxxxxxxxxxx",  // Organization ID
            "usr_123456",        // User ID from the original invitation
        )
        if err != nil {
            log.Printf("Failed to resend invitation: %v", err)
            // Handle error: user not found, already active, etc.
            return err
        }

        // Invitation resent successfully with updated expiration
        log.Printf("Invitation resent: %d", invitation.Invite.ResentCount)
        log.Printf("New expiration: %s", invitation.Invite.ExpiresAt)
        ```
      ```java title="Spring Boot" wrap
        // Use case: User requests new invitation link after email expires

        try {
            // Resend invitation to the user
            ResendInvitationResponse invitation = users.resendInvitation(
                "org_xxxxxxxxxxxx",  // Organization ID
                "usr_123456"         // User ID from the original invitation
            );

            // Invitation resent successfully with updated expiration
            System.out.println("Invitation resent: " + invitation.getInvite().getResentCount());
            System.out.println("New expiration: " + invitation.getInvite().getExpiresAt());
        } catch (Exception error) {
            System.err.println("Failed to resend invitation: " + error.getMessage());
            // Handle error: user not found, already active, etc.
        }
        ```
      **Parameters:**

    | Parameter | Description |
    | --------- | ----------- |
    | `organization_id` | Unique identifier of the organization (starts with 'org_') |
    | `id` | System-generated user ID of the user with a pending invitation (starts with 'usr_') |

    **Response:**
    ```json title="200"
    {
      "invite": {
        "created_at": "2025-07-10T08:00:00Z",
        "expires_at": "2025-12-31T23:59:59Z",
        "invited_by": "admin_998877",
        "organization_id": "org_987654321",
        "resent_at": "2025-07-15T09:30:00Z",
        "resent_count": 2,
        "status": "pending_invite",
        "user_id": "usr_123456"
      }
    }
    ```
**Response status codes:** - **200**: Success - Invitation resent. New expiry timestamp is set only if previous invitation is expired.
      - **400**: Bad Request - Invalid parameters, user already active, or invitation accepted
      - **404**: Not Found - User, organization, or invitation not found. Verify all IDs are correct.
      - **500**: Internal Server Error - Email service unavailable or system error
**Tracking resends:** Monitor the `resent_count` field to track how many times an invitation has been resent. Consider implementing rate limiting to prevent excessive resends.

    The `resendInvitation` method returns the updated invitation object with the new expiration timestamp and incremented resend counter. This helps you track the invitation lifecycle and understand user engagement with your onboarding process.

4. ### Handle invitation acceptance

    When invited users click the invitation link in their email, Scalekit automatically verifies their identity through the secure magic link embedded in the email. Once verified, Scalekit redirects them to your application's [registered initiate login endpoint](/guides/dashboard/redirects/#redirect-endpoint-types) to complete the authentication flow.

    Your application receives the user through the standard authentication callback, where you can create their session and redirect them to your dashboard. The user won't see a login page since their identity was already verified through the invitation link.

    ![](@/assets/docs/fsa/user-management/2-email-invite.png)

    **Set up the initiate login endpoint:**

    ```javascript title="Express.js" frame="terminal"
        // Handle indirect auth entry points (invitation links, magic links, etc.)
        // 1. Register this endpoint in Dashboard > Authentication > Redirect URLs
        // 2. Scalekit redirects here after verifying the invitation link

        app.get('/auth/login/initiate', (req, res) => {
          try {
            const redirectUri = 'http://localhost:3000/api/callback';
            const options = {
              scopes: ['openid', 'profile', 'email', 'offline_access']
            };

            // Create authorization URL and redirect to Scalekit
            const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options);
            res.redirect(authorizationUrl);
          } catch (error) {
            console.error('Authorization failed:', error);
            res.redirect('/login?error=auth_failed');
          }
        });
        ```
      ```python title="Flask" frame="terminal"
        from flask import redirect
        from scalekit import AuthorizationUrlOptions

        # Handle indirect auth entry points (invitation links, magic links, etc.)
        # 1. Register this endpoint in Dashboard > Authentication > Redirect URLs
        # 2. Scalekit redirects here after verifying the invitation link

        @app.route('/auth/login/initiate')
        def initiate_login():
            try:
                redirect_uri = 'http://localhost:3000/api/callback'
                options = AuthorizationUrlOptions()
                options.scopes = ['openid', 'profile', 'email', 'offline_access']

                # Create authorization URL and redirect to Scalekit
                authorization_url = scalekit_client.get_authorization_url(redirect_uri, options)
                return redirect(authorization_url)
            except Exception as error:
                print(f'Authorization failed: {str(error)}')
                return redirect('/login?error=auth_failed')
        ```
      ```go title="Gin" frame="terminal"
        // Handle indirect auth entry points (invitation links, magic links, etc.)
        // 1. Register this endpoint in Dashboard > Authentication > Redirect URLs
        // 2. Scalekit redirects here after verifying the invitation link

        func initiateLogin(c *gin.Context) {
            redirectUri := "http://localhost:3000/api/callback"
            options := scalekitClient.AuthorizationUrlOptions{
                Scopes: []string{"openid", "profile", "email", "offline_access"}
            }

            // Create authorization URL and redirect to Scalekit
            authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options)
            if err != nil {
                log.Printf("Authorization failed: %v", err)
                c.Redirect(http.StatusFound, "/login?error=auth_failed")
                return
            }

            c.Redirect(http.StatusFound, authorizationUrl.String())
        }
        ```
      ```java title="Spring Boot" frame="terminal"
        import org.springframework.web.bind.annotation.GetMapping;
        import java.net.URL;

        // Handle indirect auth entry points (invitation links, magic links, etc.)
        // 1. Register this endpoint in Dashboard > Authentication > Redirect URLs
        // 2. Scalekit redirects here after verifying the invitation link

        @GetMapping("/auth/login/initiate")
        public ResponseEntity<Void> initiateLogin() {
            try {
                String redirectUri = "http://localhost:3000/api/callback";
                AuthorizationUrlOptions options = new AuthorizationUrlOptions();
                options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access"));

                // Create authorization URL and redirect to Scalekit
                URL authorizationUrl = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options);

                return ResponseEntity.status(HttpStatus.FOUND)
                    .header("Location", authorizationUrl.toString())
                    .build();
            } catch (Exception error) {
                System.err.println("Authorization failed: " + error.getMessage());
                return ResponseEntity.status(HttpStatus.FOUND)
                    .header("Location", "/login?error=auth_failed")
                    .build();
            }
        }
        ```
      After setting up this endpoint, your invited users experience a seamless flow: they click the invitation link, Scalekit verifies their identity, redirects to your initiate login endpoint, your application creates an authorization URL, Scalekit completes the auth flow, and finally the user lands in your application with an active session. The entire process is secure and requires no password or additional verification steps from the user.
**Users in multiple organizations:** When users belong to multiple organizations, Scalekit automatically presents an organization selection interface during subsequent login flows. This allows users to choose which organization they want to access. Store the organization ID from the ID token to maintain context about which organization the user is currently working with.

---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
