Send user invitations via email with customizable invitation links and user data. This endpoint allows administrators to invite new users to join the platform with pre-configured roles and metadata.
This endpoint requires service role authentication. Only use the service role key on your backend servers, never in client-side code.
curl -X POST "http://localhost:8080/admin/invite" \
-H "Authorization: Bearer service_role_key_here" \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com",
"data": {
"first_name": "John",
"last_name": "Doe",
"role": "team_member"
},
"redirect_to": "https://yourapp.com/welcome"
}'
Request Body
Email address to send the invitation to.
User metadata to pre-populate when the invitation is accepted. Common fields:
first_name - User’s first name
last_name - User’s last name
role - User’s role in the system
team_id - Team or organization ID
department - User’s department
URL to redirect to after the invitation is accepted. If not provided, uses the default redirect URL.
Additional invitation options. Available options:
captcha_token - Captcha token for verification
data - Additional app metadata
Response
Created user object with invitation status
Unique identifier for the sent invitation email
200 - Invitation Sent Successfully
{
"user" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"aud" : "authenticated" ,
"role" : "authenticated" ,
"email" : "newuser@example.com" ,
"phone" : null ,
"email_confirmed_at" : null ,
"phone_confirmed_at" : null ,
"invited_at" : "2023-01-01T00:00:00Z" ,
"confirmation_sent_at" : "2023-01-01T00:00:00Z" ,
"app_metadata" : {
"provider" : "email" ,
"providers" : [ "email" ]
},
"user_metadata" : {
"first_name" : "John" ,
"last_name" : "Doe" ,
"role" : "team_member"
},
"created_at" : "2023-01-01T00:00:00Z" ,
"updated_at" : "2023-01-01T00:00:00Z"
},
"message_id" : "msg_1234567890abcdef"
}
Error Responses
400 - Invalid Email
401 - Unauthorized
409 - User Already Exists
429 - Rate Limited
{
"code" : 400 ,
"msg" : "Invalid email format" ,
"details" : "Please provide a valid email address"
}
Invitation Email Template
The invitation email includes:
<! DOCTYPE html >
< html >
< head >
< title > You're Invited to Join Strike </ title >
</ head >
< body >
< div style = "max-width: 600px; margin: 0 auto; padding: 20px;" >
< h1 > You're Invited! </ h1 >
< p > Hi {{.FirstName}}, </ p >
< p > You've been invited to join Strike as a {{.Role}}. </ p >
< a href = "{{.InvitationURL}}"
style = "background-color: #0D9373; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;" >
Accept Invitation
</ a >
< p > Or copy and paste this link into your browser: </ p >
< p > {{.InvitationURL}} </ p >
< p > This invitation will expire in 7 days. </ p >
< p > Welcome to the team! </ p >
< p > Best regards, < br > The Strike Team </ p >
</ div >
</ body >
</ html >
Implementation Examples
import { useState } from 'react' ;
function AdminInviteForm ({ onInviteSent }) {
const [ formData , setFormData ] = useState ({
email: '' ,
firstName: '' ,
lastName: '' ,
role: 'user' ,
teamId: '' ,
department: ''
});
const [ loading , setLoading ] = useState ( false );
const [ error , setError ] = useState ( '' );
const [ success , setSuccess ] = useState ( false );
const handleSubmit = async ( e ) => {
e . preventDefault ();
setLoading ( true );
setError ( '' );
setSuccess ( false );
try {
const response = await fetch ( '/api/admin/invite' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . REACT_APP_SERVICE_ROLE_KEY } ` ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
email: formData . email ,
data: {
first_name: formData . firstName ,
last_name: formData . lastName ,
role: formData . role ,
team_id: formData . teamId ,
department: formData . department
},
redirect_to: 'https://yourapp.com/welcome'
}),
});
if ( ! response . ok ) {
const errorData = await response . json ();
throw new Error ( errorData . msg || 'Failed to send invitation' );
}
const result = await response . json ();
setSuccess ( true );
onInviteSent ?.( result );
// Reset form
setFormData ({
email: '' ,
firstName: '' ,
lastName: '' ,
role: 'user' ,
teamId: '' ,
department: ''
});
} catch ( err ) {
setError ( err . message );
} finally {
setLoading ( false );
}
};
const handleInputChange = ( field , value ) => {
setFormData ( prev => ({
... prev ,
[field]: value
}));
};
return (
< div className = "admin-invite-form" >
< h2 > Invite New User </ h2 >
{ error && (
< div className = "error-message" >
{ error }
</ div >
) }
{ success && (
< div className = "success-message" >
✅ Invitation sent successfully!
</ div >
) }
< form onSubmit = { handleSubmit } className = "invite-form" >
< div className = "form-row" >
< div className = "form-group" >
< label htmlFor = "firstName" > First Name </ label >
< input
type = "text"
id = "firstName"
value = { formData . firstName }
onChange = { ( e ) => handleInputChange ( 'firstName' , e . target . value ) }
required
/>
</ div >
< div className = "form-group" >
< label htmlFor = "lastName" > Last Name </ label >
< input
type = "text"
id = "lastName"
value = { formData . lastName }
onChange = { ( e ) => handleInputChange ( 'lastName' , e . target . value ) }
required
/>
</ div >
</ div >
< div className = "form-group" >
< label htmlFor = "email" > Email Address </ label >
< input
type = "email"
id = "email"
value = { formData . email }
onChange = { ( e ) => handleInputChange ( 'email' , e . target . value ) }
placeholder = "user@example.com"
required
/>
</ div >
< div className = "form-row" >
< div className = "form-group" >
< label htmlFor = "role" > Role </ label >
< select
id = "role"
value = { formData . role }
onChange = { ( e ) => handleInputChange ( 'role' , e . target . value ) }
>
< option value = "user" > User </ option >
< option value = "team_member" > Team Member </ option >
< option value = "team_lead" > Team Lead </ option >
< option value = "admin" > Admin </ option >
</ select >
</ div >
< div className = "form-group" >
< label htmlFor = "department" > Department </ label >
< input
type = "text"
id = "department"
value = { formData . department }
onChange = { ( e ) => handleInputChange ( 'department' , e . target . value ) }
placeholder = "Engineering"
/>
</ div >
</ div >
< div className = "form-group" >
< label htmlFor = "teamId" > Team ID (optional) </ label >
< input
type = "text"
id = "teamId"
value = { formData . teamId }
onChange = { ( e ) => handleInputChange ( 'teamId' , e . target . value ) }
placeholder = "team_123"
/>
</ div >
< button type = "submit" disabled = { loading } >
{ loading ? 'Sending Invitation...' : 'Send Invitation' }
</ button >
</ form >
</ div >
);
}
export default AdminInviteForm ;
Bulk User Invitation
import { useState } from 'react' ;
function BulkInviteForm () {
const [ csvData , setCsvData ] = useState ( '' );
const [ loading , setLoading ] = useState ( false );
const [ results , setResults ] = useState ([]);
const handleBulkInvite = async ( e ) => {
e . preventDefault ();
setLoading ( true );
setResults ([]);
// Parse CSV data
const lines = csvData . trim (). split ( ' \n ' );
const headers = lines [ 0 ]. split ( ',' ). map ( h => h . trim ());
const invitations = [];
for ( let i = 1 ; i < lines . length ; i ++ ) {
const values = lines [ i ]. split ( ',' ). map ( v => v . trim ());
const invitation = {};
headers . forEach (( header , index ) => {
invitation [ header ] = values [ index ];
});
invitations . push ( invitation );
}
// Send invitations
const inviteResults = [];
for ( const invitation of invitations ) {
try {
const response = await fetch ( '/api/admin/invite' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . REACT_APP_SERVICE_ROLE_KEY } ` ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
email: invitation . email ,
data: {
first_name: invitation . first_name ,
last_name: invitation . last_name ,
role: invitation . role || 'user' ,
department: invitation . department
},
redirect_to: 'https://yourapp.com/welcome'
}),
});
if ( response . ok ) {
const result = await response . json ();
inviteResults . push ({
email: invitation . email ,
success: true ,
userId: result . user . id
});
} else {
const errorData = await response . json ();
inviteResults . push ({
email: invitation . email ,
success: false ,
error: errorData . msg
});
}
} catch ( err ) {
inviteResults . push ({
email: invitation . email ,
success: false ,
error: err . message
});
}
}
setResults ( inviteResults );
setLoading ( false );
};
return (
< div className = "bulk-invite-form" >
< h2 > Bulk User Invitation </ h2 >
< div className = "csv-instructions" >
< h3 > CSV Format </ h3 >
< p > Use the following format for your CSV data: </ p >
< pre >
email,first_name,last_name,role,department
john@example.com,John,Doe,team_member,Engineering
jane@example.com,Jane,Smith,admin,Marketing
</ pre >
</ div >
< form onSubmit = { handleBulkInvite } className = "bulk-form" >
< div className = "form-group" >
< label htmlFor = "csvData" > CSV Data </ label >
< textarea
id = "csvData"
value = { csvData }
onChange = { ( e ) => setCsvData ( e . target . value ) }
placeholder = "email,first_name,last_name,role,department john@example.com,John,Doe,team_member,Engineering"
rows = { 10 }
required
/>
</ div >
< button type = "submit" disabled = { loading } >
{ loading ? 'Sending Invitations...' : 'Send All Invitations' }
</ button >
</ form >
{ results . length > 0 && (
< div className = "results" >
< h3 > Invitation Results </ h3 >
< div className = "results-summary" >
< span className = "success-count" >
✅ { results . filter ( r => r . success ). length } successful
</ span >
< span className = "error-count" >
❌ { results . filter ( r => ! r . success ). length } failed
</ span >
</ div >
< div className = "results-list" >
{ results . map (( result , index ) => (
< div key = { index } className = { `result-item ${ result . success ? 'success' : 'error' } ` } >
< span className = "email" > { result . email } </ span >
{ result . success ? (
< span className = "status" > ✅ Invited (ID: { result . userId } ) </ span >
) : (
< span className = "error" > ❌ { result . error } </ span >
) }
</ div >
)) }
</ div >
</ div >
) }
</ div >
);
}
export default BulkInviteForm ;
Node.js Backend Handler
const express = require ( 'express' );
const { body , validationResult } = require ( 'express-validator' );
const router = express . Router ();
// Middleware to verify service role
const verifyServiceRole = ( req , res , next ) => {
const authHeader = req . headers . authorization ;
const token = authHeader ?. split ( ' ' )[ 1 ];
if ( ! token || token !== process . env . SERVICE_ROLE_KEY ) {
return res . status ( 401 ). json ({
code: 401 ,
msg: 'Invalid service role key' ,
details: 'Please provide a valid service role key'
});
}
next ();
};
router . post ( '/admin/invite' , [
verifyServiceRole ,
body ( 'email' ). isEmail (). normalizeEmail (),
body ( 'data' ). optional (). isObject (),
body ( 'redirect_to' ). optional (). isURL ()
], async ( req , res ) => {
try {
// Check validation errors
const errors = validationResult ( req );
if ( ! errors . isEmpty ()) {
return res . status ( 400 ). json ({
code: 400 ,
msg: 'Invalid request data' ,
details: errors . array ()
});
}
const { email , data , redirect_to , options } = req . body ;
// Call Strike Auth Service
const response = await fetch ( ` ${ process . env . AUTH_SERVICE_URL } /admin/invite` , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . SERVICE_ROLE_KEY } ` ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
email ,
data ,
redirect_to: redirect_to || ` ${ process . env . APP_URL } /welcome` ,
options
}),
});
const result = await response . json ();
if ( ! response . ok ) {
return res . status ( response . status ). json ( result );
}
// Log invitation
console . log ( `User invited: ${ email } , role: ${ data ?. role || 'user' } ` );
res . json ( result );
} catch ( error ) {
console . error ( 'Invitation error:' , error );
res . status ( 500 ). json ({
code: 500 ,
msg: 'Internal server error' ,
details: 'Please try again later'
});
}
});
module . exports = router ;
Use Cases
Team Member Invitation
Invite new team members with specific roles:
{
"email" : "developer@example.com" ,
"data" : {
"first_name" : "Alex" ,
"last_name" : "Johnson" ,
"role" : "developer" ,
"team_id" : "engineering_team" ,
"department" : "Engineering" ,
"start_date" : "2023-02-01"
},
"redirect_to" : "https://yourapp.com/onboarding"
}
Client Invitation
Invite external clients with limited access:
{
"email" : "client@company.com" ,
"data" : {
"first_name" : "Sarah" ,
"last_name" : "Wilson" ,
"role" : "client" ,
"company" : "Wilson Corp" ,
"access_level" : "read_only"
},
"redirect_to" : "https://yourapp.com/client-portal"
}
Admin Invitation
Invite administrators with elevated privileges:
{
"email" : "admin@example.com" ,
"data" : {
"first_name" : "Michael" ,
"last_name" : "Brown" ,
"role" : "admin" ,
"permissions" : [ "user_management" , "system_config" ],
"department" : "IT"
},
"redirect_to" : "https://yourapp.com/admin-setup"
}
Security Features
Service Role Authentication : Requires valid service role key
Email Validation : Validates email format and uniqueness
Rate Limiting : Prevents invitation spam
Invitation Expiration : Invitations expire after 7 days
Audit Logging : Logs all invitation activities
Best Practices
Include meaningful user data in invitations
Use appropriate redirect URLs for different user types
Set reasonable expiration times for invitations
Track invitation status and follow up if needed
Provide clear invitation emails with next steps
Include contact information for support
Customize invitation content based on user role
Handle expired invitations gracefully
Validate all user data before processing
Implement proper access controls for invitation sending
Monitor for suspicious invitation patterns
Use secure redirect URLs (HTTPS only)
Testing
Unit Tests
describe ( 'POST /admin/invite' , () => {
test ( 'should send invitation to new user' , async () => {
const inviteData = {
email: 'newuser@example.com' ,
data: {
first_name: 'John' ,
last_name: 'Doe' ,
role: 'team_member'
}
};
const response = await request ( app )
. post ( '/admin/invite' )
. set ( 'Authorization' , 'Bearer valid_service_role_key' )
. send ( inviteData )
. expect ( 200 );
expect ( response . body . user . email ). toBe ( 'newuser@example.com' );
expect ( response . body . user . user_metadata . first_name ). toBe ( 'John' );
expect ( response . body . message_id ). toBeDefined ();
});
test ( 'should reject invitation for existing user' , async () => {
await request ( app )
. post ( '/admin/invite' )
. set ( 'Authorization' , 'Bearer valid_service_role_key' )
. send ({
email: 'existing@example.com' ,
data: { first_name: 'Test' }
})
. expect ( 409 );
});
test ( 'should reject invalid service role' , async () => {
await request ( app )
. post ( '/admin/invite' )
. set ( 'Authorization' , 'Bearer invalid_key' )
. send ({
email: 'test@example.com' ,
data: { first_name: 'Test' }
})
. expect ( 401 );
});
});