Invitations
Invitations allow you to add new members to your organization. This guide covers creating invitations, managing pending invites, and what happens when someone accepts an invitation.
Who Can Invite Members?
Only Owners and Admins can create invitations. Editors and Viewers cannot invite new members.
Invitation Workflow
The invitation process follows these steps:
1. Admin/Owner creates invitation
↓
2. System generates unique invite link
↓
3. Inviter shares link with invitee
↓
4. Invitee clicks link and signs in
↓
5. System verifies email matches
↓
6. Member added to organizationCreating an Invitation
Step 1: Navigate to Members
Go to Settings → Members in your organization.
Step 2: Click “Invite Member”
Click the Invite Member button at the top of the member list.
Step 3: Enter Invitation Details
Fill in the invitation form:
| Field | Description | Required |
|---|---|---|
| The email address of the person you’re inviting | Yes | |
| Role | The role they’ll have when they join | Yes (defaults to Editor) |
Step 4: Send the Invitation
Click Send Invitation to create the invite. You’ll see a success screen with:
- Confirmation message
- The invite link
- A copy button
Step 5: Share the Link
Copy the invite link and share it with the invitee via:
- Slack or Teams
- Any other communication channel
Note: The system does not automatically send emails. You need to share the link manually.
Invitation Link Format
Invitation links follow this pattern:
https://app.llmx.io/invite/{token}The token is a unique, random identifier that:
- Cannot be guessed
- Expires after 7 days
- Can only be used once
- Is tied to the specified email address
Managing Pending Invitations
View and manage invitations that haven’t been accepted yet.
Viewing Pending Invitations
- Go to Settings → Invitations tab
- See all pending invitations with:
- Invitee email
- Assigned role
- Date sent
- Expiration status
Resending an Invitation
If an invitation is about to expire or the invitee lost the link:
- Find the invitation in the list
- Click the Resend button
- The expiration is extended by 7 days
- A new link is generated (the old link still works too)
Revoking an Invitation
To cancel an invitation before it’s accepted:
- Find the invitation in the list
- Click the Revoke button
- Confirm the cancellation
Revoked invitations:
- Cannot be accepted anymore
- Are marked as “Cancelled” in the list
- Can be cleaned up by creating a new invitation if needed
Invitation Expiration
Invitations automatically expire after 7 days from creation.
What Happens When an Invitation Expires?
- The invite link stops working
- The invitee sees an “Invitation expired” message
- The invitation is marked as “Expired” in your list
Handling Expired Invitations
- Go to Settings → Invitations
- Find the expired invitation
- Click Resend to extend the expiration, or
- Revoke it and create a new invitation
Accepting an Invitation
When someone receives your invitation link, here’s what they experience:
For Users with an Account
- Click the invitation link
- Sign in if not already authenticated
- See the invitation details (organization name, role)
- Click Accept Invitation
- Get redirected to the organization workspace
For New Users
- Click the invitation link
- Create an account using the same email address
- See the invitation details
- Click Accept Invitation
- Get redirected to the organization workspace
Email Verification
The invitation email must match the user’s authenticated email address.
Why?
- Prevents invitation hijacking
- Ensures the right person joins
- Maintains organizational security
What if emails don’t match?
- The user sees an error message
- They cannot accept the invitation
- They need to sign in with the correct email, or
- You need to create a new invitation with their actual email
Invitation Statuses
Invitations can have one of four statuses:
| Status | Description |
|---|---|
| Pending | Waiting to be accepted (active and valid) |
| Accepted | The invitee has joined the organization |
| Expired | The 7-day window has passed |
| Cancelled | An admin revoked the invitation |
Best Practices
Use Accurate Email Addresses
Always verify the invitee’s email address before sending. The email matching requirement prevents accidental acceptance by the wrong person.
Set Appropriate Roles
Consider the invitee’s responsibilities when choosing their role:
- Viewer for stakeholders who only need to review
- Editor for team members who create content
- Admin for managers who help run the team
- Owner sparingly, only for key personnel
Clean Up Expired Invitations
Periodically review your invitations list and clean up expired or cancelled invitations by revoking and recreating as needed.
Communicate Expiration
When sharing invitation links, let invitees know they expire in 7 days. This encourages prompt action.
For Developers
Invitation Model
class InvitationStatus(str, Enum):
PENDING = "pending"
ACCEPTED = "accepted"
EXPIRED = "expired"
CANCELLED = "cancelled"
class InvitationResponse(BaseModel):
id: str
email: str
role: OrgRole
status: InvitationStatus
token: str # UUID for invite link
org_id: str
org_name: str
org_slug: str
invited_by: str # User ID
invited_by_name: str
created_at: datetime
expires_at: datetime
accepted_at: Optional[datetime]
accepted_by: Optional[str]Token Generation
Invitation tokens are UUIDs (v4), providing:
- 128 bits of randomness
- Practically impossible to guess
- No sequential patterns
import uuid
token = str(uuid.uuid4())
# Example: "550e8400-e29b-41d4-a716-446655440000"Expiration Logic
from datetime import datetime, timedelta
INVITATION_VALIDITY_DAYS = 7
def create_invitation(email: str, role: OrgRole) -> Invitation:
return Invitation(
token=str(uuid.uuid4()),
email=email,
role=role,
status=InvitationStatus.PENDING,
created_at=datetime.utcnow(),
expires_at=datetime.utcnow() + timedelta(days=INVITATION_VALIDITY_DAYS)
)
def is_expired(invitation: Invitation) -> bool:
return datetime.utcnow() > invitation.expires_at
def resend_invitation(invitation: Invitation) -> Invitation:
invitation.expires_at = datetime.utcnow() + timedelta(days=INVITATION_VALIDITY_DAYS)
return invitationEmail Matching
Acceptance requires case-insensitive email matching:
def accept_invitation(token: str, user_email: str):
invitation = await get_invitation_by_token(token)
if invitation.email.lower() != user_email.lower():
raise HTTPException(403, "Email does not match invitation")
if invitation.status != InvitationStatus.PENDING:
raise HTTPException(400, "Invitation is no longer valid")
if is_expired(invitation):
raise HTTPException(400, "Invitation has expired")
# Add user to organization
await add_member(invitation.org_id, user_id, invitation.role)
# Update invitation status
invitation.status = InvitationStatus.ACCEPTED
invitation.accepted_at = datetime.utcnow()
invitation.accepted_by = user_idCollection Group Query
Finding invitations by token uses a Firestore collection group query across all organizations:
async def get_invitation_by_token(token: str) -> Invitation:
query = db.collection_group("invitations").where("token", "==", token)
results = await query.get()
if not results:
raise HTTPException(404, "Invitation not found")
return results[0].to_dict()