Inviting Members
Add team members to your organization with email invitations and role assignments.
Overview
LinkForty's team invitation system allows you to:
- Send email invitations to colleagues
- Assign roles before members join
- Track pending, accepted, and declined invitations
- Set automatic expiration (7 days)
- Manage invitations through dashboard or API
Key Concepts:
- Invitation - Email invite with unique token and assigned role
- Pending - Invitation sent but not yet responded to
- Accepted - Member joined the organization
- Declined - Invitation rejected by recipient
- Expired - Invitation passed 7-day validity period
Sending Invitations
Via Dashboard
- Go to Settings → Team
- Click "Invite Member"
- Enter email address
- Select role (Admin, Member, or Viewer)
- Click "Send Invitation"
The recipient receives an email with an invitation link.
Via API
curl -X POST https://api.linkforty.com/api/organizations/org_abc123/invitations \
-H "Authorization: Bearer $LINKFORTY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "sarah@example.com",
"role": "member"
}'
Response:
{
"id": "inv_xyz789",
"email": "sarah@example.com",
"role": "member",
"token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"expires_at": "2024-03-22T14:30:00Z",
"invitation_url": "https://app.linkforty.com/invitations/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}
Requires: Admin or Owner role
Note: Cannot assign "Owner" role via invitation. Only existing Owners can transfer ownership.
Invitation Flow
1. Admin/Owner sends invitation
↓
2. Email sent to recipient
↓
3. Recipient clicks link
↓
4. Logs in or creates account
↓
5. Accepts or declines invitation
↓
6. [If accepted] Added to organization
Managing Invitations
Viewing Pending Invitations
Dashboard:
- Go to Settings → Team
- Click "Pending Invitations" tab
- See all sent invitations with status
API:
curl https://api.linkforty.com/api/organizations/org_abc123/invitations \
-H "Authorization: Bearer $LINKFORTY_API_KEY"
Response:
[
{
"id": "inv_123",
"email": "sarah@example.com",
"role": "member",
"status": "pending",
"expires_at": "2024-03-22T14:30:00Z",
"created_at": "2024-03-15T14:30:00Z",
"invited_by_name": "John Doe",
"invited_by_email": "john@example.com"
},
{
"id": "inv_456",
"email": "mike@example.com",
"role": "admin",
"status": "accepted",
"accepted_at": "2024-03-16T10:15:00Z",
"expires_at": "2024-03-22T09:00:00Z",
"created_at": "2024-03-15T09:00:00Z",
"invited_by_name": "John Doe",
"invited_by_email": "john@example.com"
}
]
Canceling Invitations
Dashboard:
- Go to Settings → Team → "Pending Invitations"
- Click "Cancel" next to invitation
- Confirm cancellation
API:
curl -X DELETE https://api.linkforty.com/api/organizations/org_abc123/invitations/inv_xyz789 \
-H "Authorization: Bearer $LINKFORTY_API_KEY"
Response:
{
"message": "Invitation cancelled successfully"
}
Requires: Admin or Owner role
Why Cancel:
- Sent to wrong email
- Role assignment incorrect (need to resend with different role)
- Person no longer joining team
Accepting Invitations
Via Dashboard
- Click invitation link from email
- Log in (or create account if new user)
- Review organization details
- Click "Accept Invitation"
- Redirected to organization dashboard
Via API
Step 1: Get invitation details
curl https://api.linkforty.com/api/invitations/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Response:
{
"id": "inv_123",
"email": "sarah@example.com",
"role": "member",
"status": "pending",
"expires_at": "2024-03-22T14:30:00Z",
"created_at": "2024-03-15T14:30:00Z",
"organization": {
"id": "org_abc123",
"name": "Acme Marketing Team",
"slug": "acme-marketing-team-x7y9z2"
},
"invited_by": {
"name": "John Doe",
"email": "john@example.com"
}
}
Step 2: Accept invitation
curl -X POST https://api.linkforty.com/api/invitations/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6/accept \
-H "Authorization: Bearer $LINKFORTY_API_KEY"
Response:
{
"message": "Invitation accepted successfully",
"organization": {
"id": "org_abc123",
"name": "Acme Marketing Team",
"slug": "acme-marketing-team-x7y9z2"
}
}
Note: Requires authentication. Email must match invitation email.
Declining Invitations
Via Dashboard
- Click invitation link from email
- Log in
- Review organization details
- Click "Decline Invitation"
Via API
curl -X POST https://api.linkforty.com/api/invitations/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6/decline \
-H "Authorization: Bearer $LINKFORTY_API_KEY"
Response:
{
"message": "Invitation declined"
}
Invitation Statuses
| Status | Description | Can Resend? |
|---|---|---|
| pending | Sent, awaiting response | No (must cancel first) |
| accepted | Member joined organization | N/A |
| declined | Recipient rejected invitation | Yes |
| expired | 7 days passed without response | Yes |
Common Scenarios
1. Inviting Multiple Team Members
Bulk invite via API:
const emails = [
{ email: 'sarah@example.com', role: 'member' },
{ email: 'mike@example.com', role: 'admin' },
{ email: 'emma@example.com', role: 'viewer' },
];
for (const invite of emails) {
await fetch(`https://api.linkforty.com/api/organizations/org_abc123/invitations`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(invite),
});
// Wait 100ms between requests to avoid rate limits
await new Promise(resolve => setTimeout(resolve, 100));
}
2. Resending Expired Invitations
Check for expired invitations:
async function resendExpiredInvitations(orgId: string) {
const response = await fetch(
`https://api.linkforty.com/api/organizations/${orgId}/invitations`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const invitations = await response.json();
// Find expired invitations
const expired = invitations.filter(inv => inv.status === 'expired');
// Resend each one
for (const inv of expired) {
// Cancel old invitation
await fetch(
`https://api.linkforty.com/api/organizations/${orgId}/invitations/${inv.id}`,
{
method: 'DELETE',
headers: { 'Authorization': `Bearer ${API_KEY}` },
}
);
// Send new invitation
await fetch(
`https://api.linkforty.com/api/organizations/${orgId}/invitations`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: inv.email,
role: inv.role,
}),
}
);
}
}
3. Client Stakeholder Access
Invite client with viewer role:
curl -X POST https://api.linkforty.com/api/organizations/org_abc123/invitations \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "client@nike.com",
"role": "viewer"
}'
Viewer role gives read-only access - perfect for clients monitoring campaign performance.
4. Agency Onboarding New Team Member
Workflow:
async function onboardNewMember(orgId: string, email: string) {
// 1. Send invitation
const inviteResponse = await fetch(
`https://api.linkforty.com/api/organizations/${orgId}/invitations`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
role: 'member',
}),
}
);
const invitation = await inviteResponse.json();
console.log(`✅ Invitation sent to ${email}`);
console.log(`📧 Invitation link: ${invitation.invitation_url}`);
console.log(`⏰ Expires: ${new Date(invitation.expires_at).toLocaleDateString()}`);
return invitation;
}
TypeScript Examples
1. Check Invitation Status
interface Invitation {
id: string;
email: string;
role: 'admin' | 'member' | 'viewer';
status: 'pending' | 'accepted' | 'declined' | 'expired';
expires_at: string;
created_at: string;
invited_by_name: string;
invited_by_email: string;
accepted_at?: string;
}
async function getInvitationStatus(
orgId: string,
email: string
): Promise<Invitation | null> {
const response = await fetch(
`https://api.linkforty.com/api/organizations/${orgId}/invitations`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const invitations: Invitation[] = await response.json();
return invitations.find(inv => inv.email === email) || null;
}
// Usage
const status = await getInvitationStatus('org_abc123', 'sarah@example.com');
if (status) {
console.log(`Status: ${status.status}`);
console.log(`Role: ${status.role}`);
console.log(`Expires: ${new Date(status.expires_at).toLocaleDateString()}`);
} else {
console.log('No invitation found');
}
2. Track Invitation Acceptance Rate
async function getInvitationMetrics(orgId: string) {
const response = await fetch(
`https://api.linkforty.com/api/organizations/${orgId}/invitations`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const invitations: Invitation[] = await response.json();
const total = invitations.length;
const accepted = invitations.filter(inv => inv.status === 'accepted').length;
const pending = invitations.filter(inv => inv.status === 'pending').length;
const declined = invitations.filter(inv => inv.status === 'declined').length;
const expired = invitations.filter(inv => inv.status === 'expired').length;
return {
total,
accepted,
pending,
declined,
expired,
acceptanceRate: total > 0 ? ((accepted / total) * 100).toFixed(1) + '%' : '0%',
};
}
// Usage
const metrics = await getInvitationMetrics('org_abc123');
console.table(metrics);
3. Auto-Cleanup Expired Invitations
async function cleanupExpiredInvitations(orgId: string) {
const response = await fetch(
`https://api.linkforty.com/api/organizations/${orgId}/invitations`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const invitations: Invitation[] = await response.json();
// Find invitations older than 30 days
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const oldInvitations = invitations.filter(inv => {
const createdAt = new Date(inv.created_at);
return createdAt < thirtyDaysAgo &&
(inv.status === 'expired' || inv.status === 'declined');
});
// Delete old invitations
for (const inv of oldInvitations) {
await fetch(
`https://api.linkforty.com/api/organizations/${orgId}/invitations/${inv.id}`,
{
method: 'DELETE',
headers: { 'Authorization': `Bearer ${API_KEY}` },
}
);
}
return {
deleted: oldInvitations.length,
emails: oldInvitations.map(inv => inv.email),
};
}
Invitation Email
Recipients receive an email with:
Subject: You've been invited to join [Organization Name] on LinkForty
Content:
- Inviter's name and organization name
- Assigned role (Admin, Member, or Viewer)
- Call-to-action button with invitation link
- Expiration notice (7 days)
Email Template Variables:
{organizationName}- Organization being joined{inviterName}- Person who sent invitation{role}- Assigned role{invitationUrl}- Unique acceptance link
Limits and Restrictions
Member Limits by Plan
| Plan | Max Members | Max Pending Invitations |
|---|---|---|
| Free | 1 | 0 (upgr. required) |
| Pro | 25 | 10 |
| Enterprise | Unlimited | Unlimited |
Invitation Rules
- ✅ Can invite to roles: Admin, Member, Viewer
- ❌ Cannot invite as Owner (must transfer ownership after joining)
- ✅ Can invite email not yet registered
- ❌ Cannot invite existing organization member
- ✅ Can have multiple pending invitations
- ❌ Cannot send duplicate invitations to same email
- ✅ Invitations expire after 7 days
- ✅ Can resend after cancellation or expiration
Troubleshooting
"User is already a member"
Cause: Email address is already in the organization
Fix:
- Check team members list
- If wrong role, change role instead of re-inviting
- If duplicate email, verify spelling
"Invitation already sent"
Cause: Pending invitation exists for this email
Fix:
- Check pending invitations
- Cancel existing invitation first
- Send new invitation
"Invitation has expired"
Cause: More than 7 days passed since invitation sent
Fix:
- Admin cancels expired invitation
- Admin sends new invitation
- Recipient accepts within 7 days
"Member limit reached"
Cause: Organization hit plan's member limit
Fix:
- Check usage:
GET /api/organizations/:id/usage - Remove inactive members
- Upgrade plan
"Email doesn't match"
Cause: Logged-in user email differs from invitation email
Fix:
- Log out
- Create account with invited email
- Accept invitation
Best Practices
1. Use Descriptive Role Assignments
✅ Good:
- Assign Admin to team leads who manage settings
- Assign Member to content creators and marketers
- Assign Viewer to clients and stakeholders
❌ Bad:
- Making everyone Admin "just in case"
- Assigning Member when Viewer is sufficient
2. Track Invitation Status
// Weekly invitation audit
async function auditInvitations(orgId: string) {
const metrics = await getInvitationMetrics(orgId);
if (metrics.pending > 5) {
console.warn(`⚠️ ${metrics.pending} pending invitations - follow up?`);
}
if (parseInt(metrics.acceptanceRate) < 50) {
console.warn('⚠️ Low acceptance rate - check email deliverability');
}
return metrics;
}
3. Clean Up Old Invitations
// Monthly cleanup
setInterval(async () => {
await cleanupExpiredInvitations('org_abc123');
}, 30 * 24 * 60 * 60 * 1000); // 30 days
4. Provide Context in Custom Emails
If using API to send custom emails, include:
- Why they're being invited
- What they'll be working on
- Who they'll be collaborating with
- What access they'll have (based on role)
Security
Email Verification
- ✅ Token-based - Unique 32-character token per invitation
- ✅ Email matching - Must log in with invited email
- ✅ Expiration - 7-day validity period
- ✅ Single-use - Token invalidated after acceptance/decline
Access Control
- ✅ Only Admins and Owners can send invitations
- ✅ Only invited email can accept invitation
- ✅ Tokens are cryptographically secure (nanoid)
- ✅ Invitation details hidden after expiration
Related Guides
- Organizations - Creating and managing organizations
- Roles & Permissions - Understanding access control
- Projects - Organizing work with projects
Next Steps
- Invite your first team member
- Assign appropriate roles based on responsibilities
- Track invitation acceptance
- Set up regular invitation audits