--- title: Invitation API description: Invite users to organizations with flexible invitation types icon: mail --- # Invitation API Create and manage invitations to join organizations. Supports both single-use and reusable invitations. ## Overview KohakuHub invitation system supports: - **Email-specific invitations**: Sent to a specific email address - **Reusable invitations**: Can be used multiple times (link sharing) - **Unlimited invitations**: No usage limit (perfect for public organizations) - **Expiration control**: Set custom expiration (1-365 days) - **Role assignment**: Specify member role when creating invitation --- ## Endpoints ### Create Organization Invitation Create an invitation for users to join an organization. **Endpoint:** `POST /api/invitations/org/{org_name}/create` **Parameters:** | Parameter | Type | Location | Required | Description | |-----------|------|----------|----------|-------------| | `org_name` | string | path | Yes | Organization name | **Request Body:** ```json { "email": "alice@example.com", "role": "member", "max_usage": null, "expires_days": 7 } ``` | Field | Type | Required | Description | Default | |-------|------|----------|-------------|---------| | `email` | string (email) | No | Recipient email (null for reusable invites) | null | | `role` | string | Yes | Member role: `visitor`, `member`, or `admin` | - | | `max_usage` | integer | No | Max uses: `null` (single-use), `-1` (unlimited), or positive number | null | | `expires_days` | integer | Yes | Days until expiration (1-365) | 7 | **Authentication:** Required (admin or super-admin) **Response:** ```json { "success": true, "token": "abc123xyz789...", "invitation_link": "http://localhost:28080/invite/abc123xyz789...", "expires_at": "2025-01-22T10:30:00Z", "max_usage": null, "is_reusable": false } ``` **Invitation Types:** 1. **Single-use (default)**: `max_usage: null` - Can be used once, then expired 2. **Limited reusable**: `max_usage: 5` - Can be used up to 5 times 3. **Unlimited**: `max_usage: -1` - Can be used unlimited times (until expiration) **Email Behavior:** - If `email` is provided and SMTP is configured, invitation email is sent automatically - If `email` is null, invitation link can be shared manually (for reusable invites) - Email-specific invitations check if user is already a member before creating **Error Responses:** | Status | Description | |--------|-------------| | 400 | Invalid role, user already a member, or invalid parameters | | 403 | Not authorized to invite members | | 404 | Organization not found | **Example:** ```python import requests BASE_URL = "http://localhost:28080" TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} # Single-use email invitation response = requests.post( f"{BASE_URL}/api/invitations/org/myorg/create", headers=headers, json={ "email": "alice@example.com", "role": "member", "expires_days": 7 } ) invite = response.json() print(f"Invitation sent to alice@example.com") # Reusable link (5 uses) response = requests.post( f"{BASE_URL}/api/invitations/org/myorg/create", headers=headers, json={ "role": "member", "max_usage": 5, "expires_days": 30 } ) invite = response.json() print(f"Share this link: {invite['invitation_link']}") # Unlimited public invite response = requests.post( f"{BASE_URL}/api/invitations/org/myorg/create", headers=headers, json={ "role": "visitor", "max_usage": -1, "expires_days": 365 } ) invite = response.json() print(f"Public invite link: {invite['invitation_link']}") ``` --- ### Get Invitation Details Get invitation information without accepting it. **Endpoint:** `GET /api/invitations/{token}` **Parameters:** | Parameter | Type | Location | Required | Description | |-----------|------|----------|----------|-------------| | `token` | string | path | Yes | Invitation token | **Authentication:** Optional **Response:** ```json { "action": "join_org", "org_name": "myorg", "role": "member", "email": "alice@example.com", "inviter_username": "admin", "expires_at": "2025-01-22T10:30:00Z", "is_expired": false, "is_available": true, "error_message": null, "max_usage": null, "usage_count": 0, "is_reusable": false } ``` **Field Descriptions:** | Field | Type | Description | |-------|------|-------------| | `action` | string | Invitation action type (`join_org` or `register_account`) | | `org_name` | string | Organization name | | `role` | string | Assigned role | | `email` | string | Target email (null for reusable invites) | | `inviter_username` | string | User who created the invitation | | `expires_at` | string | ISO 8601 expiration timestamp | | `is_expired` | boolean | Whether invitation has expired | | `is_available` | boolean | Whether invitation can be used | | `error_message` | string | Reason if not available (null if available) | | `max_usage` | integer | Maximum uses (null = single-use, -1 = unlimited) | | `usage_count` | integer | Number of times used | | `is_reusable` | boolean | Whether invitation can be used multiple times | **Availability Conditions:** An invitation is available if ALL of the following are true: - Not expired (`expires_at` > now) - Usage count < max_usage (or max_usage is -1 for unlimited) - For single-use invitations: not yet used **Example:** ```python response = requests.get( f"{BASE_URL}/api/invitations/{token}" ) invite = response.json() if invite["is_available"]: print(f"Valid invite to join {invite['org_name']} as {invite['role']}") else: print(f"Invite not available: {invite['error_message']}") if invite["is_reusable"]: print(f"Used {invite['usage_count']} / {invite['max_usage']} times") ``` --- ### Accept Invitation Accept an invitation and join the organization. **Endpoint:** `POST /api/invitations/{token}/accept` **Parameters:** | Parameter | Type | Location | Required | Description | |-----------|------|----------|----------|-------------| | `token` | string | path | Yes | Invitation token | **Authentication:** Required **Response:** ```json { "success": true, "message": "You have successfully joined myorg as a member", "org_name": "myorg", "role": "member" } ``` **Behavior:** - Adds authenticated user to organization with specified role - Increments usage count for reusable invitations - Marks single-use invitations as used - Checks if user is already a member (returns error if so) **Error Responses:** | Status | Description | |--------|-------------| | 400 | Invitation expired, max usage reached, or already a member | | 401 | Authentication required | | 404 | Invitation not found | **Example:** ```python # User accepts invitation response = requests.post( f"{BASE_URL}/api/invitations/{token}/accept", headers={"Authorization": f"Bearer {USER_TOKEN}"} ) result = response.json() print(result["message"]) ``` --- ### List Organization Invitations List all invitations for an organization (admin only). **Endpoint:** `GET /api/invitations/org/{org_name}/list` **Parameters:** | Parameter | Type | Location | Required | Description | |-----------|------|----------|----------|-------------| | `org_name` | string | path | Yes | Organization name | **Authentication:** Required (admin or super-admin) **Response:** ```json { "invitations": [ { "id": 1, "token": "abc123...", "email": "alice@example.com", "role": "member", "created_by": "admin", "created_at": "2025-01-15T10:30:00Z", "expires_at": "2025-01-22T10:30:00Z", "max_usage": null, "usage_count": 0, "is_reusable": false, "is_available": true, "used_at": null, "is_pending": true }, { "id": 2, "token": "xyz789...", "email": null, "role": "visitor", "created_by": "admin", "created_at": "2025-01-10T10:30:00Z", "expires_at": "2026-01-10T10:30:00Z", "max_usage": -1, "usage_count": 42, "is_reusable": true, "is_available": true, "used_at": null, "is_pending": false } ] } ``` **Field Descriptions:** | Field | Description | |-------|-------------| | `is_pending` | True if available and never used (single-use invites only) | | `is_available` | Whether invitation can still be used | | `used_at` | Timestamp when first used (for single-use invites) | **Example:** ```python response = requests.get( f"{BASE_URL}/api/invitations/org/myorg/list", headers={"Authorization": f"Bearer {TOKEN}"} ) invitations = response.json()["invitations"] # Filter by status pending = [i for i in invitations if i["is_pending"]] active_reusable = [i for i in invitations if i["is_reusable"] and i["is_available"]] expired = [i for i in invitations if not i["is_available"]] print(f"Pending: {len(pending)}") print(f"Active reusable: {len(active_reusable)}") print(f"Expired: {len(expired)}") ``` --- ### Delete Invitation Cancel/delete an invitation (admin only). **Endpoint:** `DELETE /api/invitations/{token}` **Parameters:** | Parameter | Type | Location | Required | Description | |-----------|------|----------|----------|-------------| | `token` | string | path | Yes | Invitation token | **Authentication:** Required (admin or super-admin) **Response:** ```json { "success": true, "message": "Invitation deleted successfully" } ``` **Use Cases:** - Cancel pending invitations - Remove expired invitations - Revoke reusable invitation links **Error Responses:** | Status | Description | |--------|-------------| | 403 | Not authorized to delete this invitation | | 404 | Invitation not found | **Example:** ```python response = requests.delete( f"{BASE_URL}/api/invitations/{token}", headers={"Authorization": f"Bearer {TOKEN}"} ) print("Invitation cancelled") ``` --- ## Usage Examples ### Managing Invitations Workflow ```python import requests from datetime import datetime BASE_URL = "http://localhost:28080" TOKEN = "YOUR_TOKEN" headers = {"Authorization": f"Bearer {TOKEN}"} class InvitationManager: def __init__(self, base_url: str, token: str): self.base_url = base_url self.headers = {"Authorization": f"Bearer {token}"} def create_invite(self, org_name: str, email: str = None, role: str = "member", max_usage: int = None, expires_days: int = 7): """Create invitation.""" response = requests.post( f"{self.base_url}/api/invitations/org/{org_name}/create", headers=self.headers, json={ "email": email, "role": role, "max_usage": max_usage, "expires_days": expires_days } ) response.raise_for_status() return response.json() def list_invites(self, org_name: str): """List all organization invitations.""" response = requests.get( f"{self.base_url}/api/invitations/org/{org_name}/list", headers=self.headers ) response.raise_for_status() return response.json()["invitations"] def get_invite_details(self, token: str): """Get invitation details.""" response = requests.get( f"{self.base_url}/api/invitations/{token}" ) response.raise_for_status() return response.json() def cancel_invite(self, token: str): """Cancel/delete invitation.""" response = requests.delete( f"{self.base_url}/api/invitations/{token}", headers=self.headers ) response.raise_for_status() return response.json() def cleanup_expired(self, org_name: str): """Delete all expired invitations.""" invites = self.list_invites(org_name) expired = [i for i in invites if not i["is_available"]] for invite in expired: self.cancel_invite(invite["token"]) print(f"Deleted expired invite: {invite['token'][:8]}...") return len(expired) # Usage mgr = InvitationManager(BASE_URL, TOKEN) # Create different types of invitations email_invite = mgr.create_invite("myorg", email="alice@example.com", role="member") reusable_invite = mgr.create_invite("myorg", role="member", max_usage=10, expires_days=30) public_invite = mgr.create_invite("myorg", role="visitor", max_usage=-1, expires_days=365) print(f"Email invite: {email_invite['invitation_link']}") print(f"Reusable invite (10 uses): {reusable_invite['invitation_link']}") print(f"Public invite (unlimited): {public_invite['invitation_link']}") # List and cleanup invites = mgr.list_invites("myorg") print(f"Total invitations: {len(invites)}") expired_count = mgr.cleanup_expired("myorg") print(f"Cleaned up {expired_count} expired invitations") ``` ### Invitation Statistics ```python def get_invitation_stats(org_name: str, token: str): """Get comprehensive invitation statistics.""" headers = {"Authorization": f"Bearer {token}"} response = requests.get( f"{BASE_URL}/api/invitations/org/{org_name}/list", headers=headers ) invites = response.json()["invitations"] stats = { "total": len(invites), "pending": 0, "used": 0, "expired": 0, "reusable_active": 0, "total_uses": 0, "by_role": {"visitor": 0, "member": 0, "admin": 0} } for invite in invites: # Count by status if invite["is_pending"]: stats["pending"] += 1 elif not invite["is_available"]: stats["expired"] += 1 elif invite["is_reusable"]: stats["reusable_active"] += 1 if invite["usage_count"] > 0: stats["used"] += 1 stats["total_uses"] += invite["usage_count"] # Count by role stats["by_role"][invite["role"]] += 1 return stats # Usage stats = get_invitation_stats("myorg", TOKEN) print(f"Total invitations: {stats['total']}") print(f" Pending: {stats['pending']}") print(f" Used: {stats['used']} ({stats['total_uses']} total uses)") print(f" Expired: {stats['expired']}") print(f" Active reusable: {stats['reusable_active']}") print(f"\nBy role:") for role, count in stats["by_role"].items(): print(f" {role}: {count}") ``` ### User Accepting Invitation ```python def accept_invite_flow(token: str, user_token: str): """Complete invitation acceptance flow.""" # 1. Check invitation details (no auth needed) response = requests.get(f"{BASE_URL}/api/invitations/{token}") invite = response.json() if not invite["is_available"]: print(f"Error: {invite['error_message']}") return False print(f"You are invited to join {invite['org_name']} as {invite['role']}") print(f"Invited by: {invite['inviter_username']}") print(f"Expires: {invite['expires_at']}") if invite["is_reusable"]: print(f"This is a reusable link (used {invite['usage_count']} times)") # 2. Accept invitation (requires authentication) response = requests.post( f"{BASE_URL}/api/invitations/{token}/accept", headers={"Authorization": f"Bearer {user_token}"} ) if response.status_code == 200: result = response.json() print(f"\nSuccess! {result['message']}") return True else: error = response.json() print(f"\nFailed: {error.get('detail', 'Unknown error')}") return False # Usage success = accept_invite_flow("abc123xyz789...", "USER_TOKEN") ``` --- ## JavaScript/TypeScript Example ```javascript class InvitationAPI { constructor(baseURL, token) { this.baseURL = baseURL; this.headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }; } async createInvite(orgName, options = {}) { const { email = null, role = 'member', maxUsage = null, expiresDays = 7 } = options; const response = await fetch( `${this.baseURL}/api/invitations/org/${orgName}/create`, { method: 'POST', headers: this.headers, body: JSON.stringify({ email, role, max_usage: maxUsage, expires_days: expiresDays }) } ); return await response.json(); } async listInvites(orgName) { const response = await fetch( `${this.baseURL}/api/invitations/org/${orgName}/list`, { headers: this.headers } ); const data = await response.json(); return data.invitations; } async getInviteDetails(token) { const response = await fetch( `${this.baseURL}/api/invitations/${token}` ); return await response.json(); } async acceptInvite(token) { const response = await fetch( `${this.baseURL}/api/invitations/${token}/accept`, { method: 'POST', headers: this.headers } ); return await response.json(); } async cancelInvite(token) { const response = await fetch( `${this.baseURL}/api/invitations/${token}`, { method: 'DELETE', headers: this.headers } ); return await response.json(); } } // Usage const inviteAPI = new InvitationAPI('http://localhost:28080', 'YOUR_TOKEN'); // Create invitations const emailInvite = await inviteAPI.createInvite('myorg', { email: 'alice@example.com', role: 'member' }); const reusableInvite = await inviteAPI.createInvite('myorg', { role: 'member', maxUsage: 10, expiresDays: 30 }); console.log('Share this link:', reusableInvite.invitation_link); // List and manage const invites = await inviteAPI.listInvites('myorg'); const pending = invites.filter(i => i.is_pending); console.log(`${pending.length} pending invitations`); ``` --- ## CLI Usage See [CLI Documentation](../CLI.md#invitations) for command-line interface: ```bash # Create email invitation kohub-cli invite create myorg alice@example.com --role member --expires 7 # Create reusable invitation kohub-cli invite create myorg --role member --max-usage 10 --expires 30 # List invitations kohub-cli invite list myorg # Cancel invitation kohub-cli invite cancel TOKEN # Accept invitation (as user) kohub-cli invite accept TOKEN ``` --- ## Next Steps - [Organization Management API](./organizations.md) - Manage organizations and members - [Settings API](./settings.md) - Configure organization settings - [Authentication](../API.md#authentication) - Token-based authentication