Skip to main content
POST
/
admin
/
invite
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"
  }'
{
  "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"
}
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
string
required
Email address to send the invitation to.
data
object
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
redirect_to
string
URL to redirect to after the invitation is accepted. If not provided, uses the default redirect URL.
options
object
Additional invitation options.Available options:
  • captcha_token - Captcha token for verification
  • data - Additional app metadata

Response

user
object
Created user object with invitation status
message_id
string
Unique identifier for the sent invitation email
{
  "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

{
  "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

React Admin Invitation Form

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&#10;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);
  });
});
I