Invitation Integration Recipes

Complete invitation workflow, frontend acceptance, email, and consumer administration examples.

AuthOS release 0.8.2 API v1 Latest-only documentation
Updated Jul 15, 2026 Edit this page
On this page

Complete Workflow Example

Admin Invites User

# 1. Admin creates invitation
curl -X POST https://sso.example.com/api/organizations/acme-corp/invitations \
  -H "Authorization: Bearer {admin_jwt}" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "newuser@example.com",
    "role": "member"
  }'

# Response includes plaintext token for email
{
  "invitation": { ... },
  "inviter": { ... },
  "token": "550e8400-e29b-41d4-a716-446655440000"
}

User Receives Email

Email contains:

  • Invitation details (organization name, role, inviter)
  • Direct acceptance link: https://sso.example.com/invitations/accept?token=550e8400-e29b-41d4-a716-446655440000
  • Expiration date

User Accepts Invitation

# Option 1: Click email link (redirects to frontend)
# User clicks: https://sso.example.com/invitations/accept?token=ABC123
# Frontend displays invitation details
# Frontend calls accept API

# Option 2: Direct API call
curl -X POST https://sso.example.com/api/invitations/accept \
  -H "Content-Type: application/json" \
  -d '{
    "token": "550e8400-e29b-41d4-a716-446655440000"
  }'

# Success: User is now a member

Admin Monitors Invitations

# List all invitations
curl -X GET https://sso.example.com/api/organizations/acme-corp/invitations \
  -H "Authorization: Bearer {admin_jwt}"

# Cancel unused invitation
curl -X POST https://sso.example.com/api/organizations/acme-corp/invitations/{invitation_id} \
  -H "Authorization: Bearer {admin_jwt}"

Integration Examples

Frontend Invitation Acceptance Page

// React example
import { useSearchParams } from 'react-router-dom';
import { useState, useEffect } from 'react';

function AcceptInvitationPage() {
  const [searchParams] = useSearchParams();
  const token = searchParams.get('token');
  const [invitation, setInvitation] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Fetch invitation details (you'd need to create this endpoint)
    // or decode from JWT token if you implement that
  }, [token]);

  const handleAccept = async () => {
    try {
      const response = await fetch('/api/invitations/accept', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token })
      });

      if (response.ok) {
        // Redirect to login or dashboard
        window.location.href = '/dashboard';
      } else {
        const data = await response.json();
        setError(data.error);
      }
    } catch (err) {
      setError('Failed to accept invitation');
    }
  };

  const handleDecline = async () => {
    try {
      const response = await fetch('/api/invitations/decline', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token })
      });

      if (response.ok) {
        window.location.href = '/';
      }
    } catch (err) {
      setError('Failed to decline invitation');
    }
  };

  return (
    <div>
      <h1>Invitation to Join Organization</h1>
      {error && <div className="error">{error}</div>}
      {invitation && (
        <div>
          <p>You've been invited to join <strong>{invitation.organization.name}</strong></p>
          <p>Role: {invitation.role}</p>
          <button onClick={handleAccept}>Accept</button>
          <button onClick={handleDecline}>Decline</button>
        </div>
      )}
    </div>
  );
}

Email Template

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Invitation to Join {{organization_name}}</title>
</head>
<body>
  <h1>You're Invited!</h1>

  <p>{{inviter_name}} ({{inviter_email}}) has invited you to join <strong>{{organization_name}}</strong> as a {{role}}.</p>

  <p>
    <a href="{{base_url}}/invitations/accept?token={{token}}"
       style="background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">
      Accept Invitation
    </a>
  </p>

  <p>Or copy and paste this link into your browser:</p>
  <p>{{base_url}}/invitations/accept?token={{token}}</p>

  <p><small>This invitation expires on {{expires_at}}.</small></p>

  <p><small>If you don't want to join this organization, you can safely ignore this email.</small></p>
</body>
</html>

Example Consumer-App Admin Component

// Vue.js example
<template>
  <div class="invitations-manager">
    <h2>Pending Invitations</h2>

    <form @submit.prevent="createInvitation">
      <input v-model="newInvite.email" placeholder="Email" required>
      <select v-model="newInvite.role" required>
        <option value="member">Member</option>
        <option value="admin">Admin</option>
        <option value="owner">Owner</option>
      </select>
      <button type="submit">Send Invitation</button>
    </form>

    <table>
      <thead>
        <tr>
          <th>Email</th>
          <th>Role</th>
          <th>Status</th>
          <th>Expires</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="invitation in invitations" :key="invitation.invitation.id">
          <td>{{ invitation.invitation.email }}</td>
          <td>{{ invitation.invitation.role }}</td>
          <td>{{ invitation.invitation.status }}</td>
          <td>{{ formatDate(invitation.invitation.expires_at) }}</td>
          <td>
            <button
              v-if="invitation.invitation.status === 'pending'"
              @click="cancelInvitation(invitation.invitation.id)">
              Cancel
            </button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      invitations: [],
      newInvite: { email: '', role: 'member' }
    };
  },
  async mounted() {
    await this.loadInvitations();
  },
  methods: {
    async loadInvitations() {
      const response = await fetch(
        `/api/organizations/${this.orgSlug}/invitations`,
        { headers: { Authorization: `Bearer ${this.token}` } }
      );
      this.invitations = await response.json();
    },
    async createInvitation() {
      const response = await fetch(
        `/api/organizations/${this.orgSlug}/invitations`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${this.token}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(this.newInvite)
        }
      );

      if (response.ok) {
        this.newInvite = { email: '', role: 'member' };
        await this.loadInvitations();
      }
    },
    async cancelInvitation(invitationId) {
      const response = await fetch(
        `/api/organizations/${this.orgSlug}/invitations/${invitationId}`,
        {
          method: 'POST',
          headers: { Authorization: `Bearer ${this.token}` }
        }
      );

      if (response.ok) {
        await this.loadInvitations();
      }
    },
    formatDate(dateStr) {
      return new Date(dateStr).toLocaleDateString();
    }
  }
};
</script>