Organization Admin Endpoints
POST /api/organizations/:org_slug/invitations
Create a new invitation to join an organization. The invitation is sent via email (if SMTP is configured) and can be accepted by the invited user.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Email address of the user to invite |
role |
string | Yes | Role to assign: owner, admin, or member |
Request Headers:
Authorization: Bearer {jwt_token}
Content-Type: application/json
Example Request:
curl -X POST https://sso.example.com/api/organizations/acme-corp/invitations \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com",
"role": "member"
}'
Response (200 OK):
{
"invitation": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"org_id": "org-123",
"email": "newuser@example.com",
"role": "member",
"invited_by": "user-456",
"status": "pending",
"token": "hashed-token-value",
"expires_at": "2025-01-22T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z"
},
"inviter": {
"id": "user-456",
"email": "admin@acme.com",
"created_at": "2024-01-01T00:00:00Z"
},
"token": "550e8400-e29b-41d4-a716-446655440000"
}
Response Fields:
Invitation Object:
id(string): Invitation unique identifierorg_id(string): Organization IDemail(string): Invited user’s emailrole(string): Assigned role (owner, admin, or member)invited_by(string): User ID of inviterstatus(string): Invitation status (pending, accepted, rejected, cancelled)token(string): Hashed token (for internal use)expires_at(string): ISO 8601 expiration timestampcreated_at(string): ISO 8601 creation timestamp
Inviter Object:
id(string): Inviter’s user IDemail(string): Inviter’s emailcreated_at(string): Inviter’s account creation timestamp
Token Field:
token(string): Plaintext token for email links (only returned once on creation)
Error Responses:
400 Bad Request: Invalid role, user already a member, or invitation already sent{ "error": "User is already a member of this organization" }{ "error": "Invitation already sent" }{ "error": "Invalid role. Must be one of: owner, admin, member" }401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Organization not found
Important Notes:
- The plaintext token is only returned once upon creation for use in email links
- Valid roles are:
owner,admin,member - Cannot invite users who are already members
- Cannot create duplicate pending invitations for the same email
- Invitations expire after 7 days by default
- Member limit enforcement happens during invitation acceptance, not creation
- A webhook event
user.invitedis published to configured webhooks
Email Integration: If SMTP is configured for the organization, an invitation email is automatically sent with:
- Invitation details (organization name, inviter name, role)
- Direct acceptance link:
{BASE_URL}/invitations/accept?token={token} - Expiration information
GET /api/organizations/:org_slug/invitations
List all invitations for an organization with pagination support. Includes invitation details and inviter information.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page |
integer | No | 1 | Page number (minimum: 1) |
limit |
integer | No | 50 | Results per page (1-100) |
status |
string | No | - | Filter by status (pending, accepted, rejected, cancelled) |
Request Headers:
Authorization: Bearer {jwt_token}
Example Request:
curl -X GET "https://sso.example.com/api/organizations/acme-corp/invitations?page=1&limit=20" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response (200 OK):
[
{
"invitation": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "newuser@example.com",
"role": "member",
"status": "pending",
"token": "hashed-token",
"expires_at": "2025-01-22T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z"
},
"inviter": {
"id": "user-456",
"email": "admin@acme.com",
"created_at": "2024-01-01T00:00:00Z"
}
},
{
"invitation": {
"id": "660e8400-e29b-41d4-a716-446655440111",
"email": "another@example.com",
"role": "admin",
"status": "accepted",
"token": "hashed-token",
"expires_at": "2025-01-20T08:15:00Z",
"created_at": "2025-01-13T08:15:00Z"
},
"inviter": {
"id": "user-789",
"email": "owner@acme.com",
"created_at": "2023-12-01T00:00:00Z"
}
}
]
Response: Array of invitation objects with inviter details
Error Responses:
401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Organization not found
Pagination:
- Default limit: 50 invitations per page
- Maximum limit: 100 invitations per page
- Page numbers start at 1
- Results are ordered by creation date (newest first)
POST /api/organizations/:org_slug/invitations/:invitation_id
Cancel a pending invitation. Only pending invitations can be cancelled.
Authentication: Required (Organization Management JWT)
Permissions: Organization owner or admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
invitation_id |
string | Invitation ID |
Query Parameters: None
Request Body: None
Request Headers:
Authorization: Bearer {jwt_token}
Example Request:
curl -X POST https://sso.example.com/api/organizations/acme-corp/invitations/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response: 200 OK with empty JSON object {}
Error Responses:
401 Unauthorized: Missing or invalid JWT token403 Forbidden: User is not an admin or owner404 Not Found: Invitation not found or already processed{ "error": "Invitation not found or already processed" }
Important Notes:
- Only pending invitations can be cancelled
- Accepted, rejected, or expired invitations cannot be cancelled
- Cancelled invitations cannot be reactivated (create a new invitation instead)
- A webhook event
invitation.revokedis published to configured webhooks
User Endpoints
GET /api/invitations
List all pending invitations for the authenticated user across all organizations.
Authentication: Required (any valid JWT)
Permissions: Any authenticated user
Query Parameters: None
Request Headers:
Authorization: Bearer {jwt_token}
Example Request:
curl -X GET https://sso.example.com/api/invitations \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Response (200 OK):
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"role": "member",
"token": "hashed-token",
"expires_at": "2025-01-22T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z",
"organization": {
"slug": "acme-corp",
"name": "Acme Corporation"
}
},
{
"id": "660e8400-e29b-41d4-a716-446655440111",
"email": "user@example.com",
"role": "admin",
"token": "hashed-token",
"expires_at": "2025-01-23T14:20:00Z",
"created_at": "2025-01-16T14:20:00Z",
"organization": {
"slug": "techstart",
"name": "TechStart Inc"
}
}
]
Response Fields:
- Array of invitation objects with organization details
- Only pending (not yet accepted/rejected/cancelled) invitations are returned
- Only invitations for the authenticated user’s email address are returned
- Expired invitations are excluded from results
Error Responses:
401 Unauthorized: Missing or invalid JWT token
Use Cases:
- Display pending invitations in user dashboard
- Show organization join requests
- Allow users to accept/decline multiple invitations
POST /api/invitations/accept
Accept a pending invitation using the invitation token. Creates a membership and optionally creates a user account if it doesn’t exist.
Authentication: Public (no authentication required)
Permissions: None (token-based)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | Invitation token (UUID format) |
Request Headers:
Content-Type: application/json
Example Request:
curl -X POST https://sso.example.com/api/invitations/accept \
-H "Content-Type: application/json" \
-d '{
"token": "550e8400-e29b-41d4-a716-446655440000"
}'
Response: 200 OK with empty JSON object {}
Error Responses:
400 Bad Request: Invalid, expired, or already processed token{ "error": "Invitation has expired" }{ "error": "Invitation not found or already processed" }{ "error": "Team limit reached" }500 Internal Server Error: Database error
Acceptance Flow:
- Token is hashed using SHA256 and looked up in database
- Invitation validation:
- Must be in
pendingstatus - Must not be expired
- Must pass team member limit check
- Must be in
- User account is found or created using the invitation email
- Membership is created with the specified role
- Invitation status is updated to
accepted - A webhook event
invitation.acceptedis published
Team Limit Enforcement:
- Member count is checked against organization tier limits
- Default free tier limit: 5 members
- Organization-specific overrides:
max_usersfield - Tier-specific limits:
OrganizationTier.default_max_users - Invitation is rejected if limit is reached
User Creation:
- If user with invitation email doesn’t exist, a new user account is created
- User can then log in using OAuth or password (if configured)
- User inherits the role specified in the invitation
Important Notes:
- Token can only be used once
- Expired invitations cannot be accepted
- Team member limits are enforced at acceptance time
- Transaction ensures atomic membership creation
POST /api/invitations/decline
Decline a pending invitation using the invitation token.
Authentication: Public (no authentication required)
Permissions: None (token-based)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | Invitation token (UUID format) |
Request Headers:
Content-Type: application/json
Example Request:
curl -X POST https://sso.example.com/api/invitations/decline \
-H "Content-Type: application/json" \
-d '{
"token": "550e8400-e29b-41d4-a716-446655440000"
}'
Response: 200 OK with empty JSON object {}
Error Responses:
400 Bad Request: Invalid or already processed token{ "error": "Invitation not found or already processed" }{ "error": "Invitation has expired" }500 Internal Server Error: Database error
Decline Flow:
- Token is hashed using SHA256 and looked up in database
- Invitation validation:
- Must be in
pendingstatus - Expiration is checked but allows declining expired invitations
- Must be in
- Invitation status is updated to
rejected - A webhook event
invitation.declinedis published
Important Notes:
- Token can only be used once
- Declining an invitation is permanent (cannot be undone)
- Expired invitations can still be declined
- No user account or membership is created
GET /invitations/accept/:token
Email link redirect endpoint for one-click invitation acceptance. This endpoint handles invitation acceptance via email links.
Authentication: Public (no authentication required)
Permissions: None (token-based)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
token |
string | Invitation token from email |
Example Request:
# User clicks link in email:
https://sso.example.com/invitations/accept/550e8400-e29b-41d4-a716-446655440000
Response: 301 Permanent Redirect to /invitations/accept?token={token}
Purpose:
This is a convenience endpoint for email links. It redirects to the frontend application’s invitation acceptance page, which then calls the POST /api/invitations/accept endpoint with the token.
Flow:
- User receives invitation email with link
- User clicks link:
GET /invitations/accept/ABC123 - Backend redirects to:
/invitations/accept?token=ABC123(frontend route) - Frontend displays invitation details and acceptance UI
- User confirms acceptance
- Frontend calls
POST /api/invitations/acceptwith token - Backend processes acceptance and creates membership
Integration:
- Configure your frontend router to handle the
/invitations/acceptroute - Extract token from query parameters
- Display invitation details (organization name, role, etc.)
- Provide “Accept” and “Decline” buttons
- Call the appropriate API endpoint based on user action