Tenants & Teams API
This section covers tenant management, team members, invitations, and role management. Endpoints are organized into four groups:
- Tenant endpoints — View and update the current tenant
- Team member endpoints — List, change roles, and remove members
- Invitation endpoints — Full invitation lifecycle (create, accept, revoke, resend)
- Admin endpoints — Platform administration of tenants and their teams
Tenant Endpoints
GET /api/v1/tenant
Returns the current tenant for the authenticated user. The tenant is resolved from the authenticated user's context — no tenant ID is needed in the URL.
Auth: Bearer token + tenant member + onboarding complete
Middleware: auth:sanctum → tenant.resolve → tenant.member → onboarding.complete
Response (200):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corp",
"slug": "acme-corp",
"settings": null,
"owner": {
"id": 42,
"name": "John Doe",
"email": "john@acme.com"
},
"users": [
{ "id": 42, "name": "John Doe", "email": "john@acme.com" },
{ "id": 43, "name": "Jane Smith", "email": "jane@acme.com" }
],
"user_joined_at": "2026-01-15T10:00:00.000000Z",
"user_role": "owner",
"user_permissions": ["billing.manage", "team.manage", "roles.manage"],
"has_active_subscription": true,
"has_billing_details": true,
"preferred_currency": "EUR",
"legal_name": "Acme Corporation GmbH",
"address": "123 Main St",
"city": "Berlin",
"postal_code": "10115",
"country": "DE",
"vat_number": "DE123456789",
"billing_email": "billing@acme.com",
"onboarding_completed_at": "2026-01-15T12:00:00.000000Z",
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-03-20T14:30:00.000000Z"
}
}
Tenant resource fields:
| Field | Type | Description |
|---|---|---|
id | UUID | Tenant identifier |
name | string | Display name |
slug | string | URL-safe identifier |
settings | object|null | Custom settings key-value pairs |
owner | object|null | Tenant owner (id, name, email) |
users | array | All tenant members |
user_joined_at | datetime|null | When the current user joined this tenant |
user_role | string|null | Current user's role: owner, admin, or member |
user_permissions | array | Current user's resolved permission names |
has_active_subscription | boolean | Whether the tenant has an active subscription |
has_billing_details | boolean | Whether billing information is complete |
preferred_currency | string | ISO 4217 currency code |
legal_name | string|null | Legal entity name |
address | string|null | Street address |
city | string|null | City |
postal_code | string|null | Postal/ZIP code |
country | string|null | ISO 3166-1 alpha-2 country code |
vat_number | string|null | VAT/tax number |
billing_email | string|null | Billing contact email |
onboarding_completed_at | datetime|null | When onboarding was completed |
PATCH /api/v1/tenant
Update the current tenant. Like the GET endpoint, the tenant is resolved from the authenticated user's context.
Auth: Bearer token + tenant member + update permission on tenant
Middleware: auth:sanctum → tenant.resolve → tenant.member → onboarding.complete
Request Body:
{
"name": "Acme Corp International",
"slug": "acme-intl",
"legal_name": "Acme Corporation International GmbH",
"address": "456 New St",
"city": "Munich",
"postal_code": "80331",
"country": "DE",
"vat_number": "DE987654321",
"billing_email": "finance@acme.com"
}
| Field | Type | Required | Validation |
|---|---|---|---|
name | string | No | Max 255 |
slug | string | No | Unique, alpha_dash, max 255 |
settings | object | No | Arbitrary key-value pairs |
legal_name | string|null | No | Max 255 |
address | string|null | No | Max 500 |
city | string|null | No | Max 255 |
postal_code | string|null | No | Max 20 |
country | string|null | No | 2-char uppercase ISO code (e.g., DE, US) |
vat_number | string|null | No | Max 50 |
billing_email | string|null | No | Valid email, max 255 |
All fields are optional — only include the fields you want to change.
Response (200):
{
"tenant": { ... },
"message": "tenants.updated"
}
Team Member Endpoints
All team member endpoints use the base path /api/v1/tenant/{tenantId}/team/... and require the standard tenant middleware chain.
GET /api/v1/tenant/{tenantId}/team/members
Returns all members of the tenant, sorted by role hierarchy (owner first) then alphabetically.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"data": [
{
"id": 42,
"name": "John Doe",
"email": "john@acme.com",
"avatar": "https://app.example.com/storage/avatars/42.jpg",
"role": {
"id": 1,
"name": "owner"
},
"joined_at": "2026-01-15T10:00:00.000000Z"
},
{
"id": 43,
"name": "Jane Smith",
"email": "jane@acme.com",
"avatar": null,
"role": {
"id": 2,
"name": "admin"
},
"joined_at": "2026-02-01T09:00:00.000000Z"
}
]
}
Team member fields:
| Field | Type | Description |
|---|---|---|
id | int | User ID |
name | string | User's name |
email | string | User's email |
avatar | string|null | Full URL to avatar image (null if no avatar set) |
role | object | Role with id (int) and name (string) |
joined_at | datetime|null | When the user joined the tenant |
GET /api/v1/tenant/{tenantId}/team/roles
Returns all assignable roles for the tenant. Includes both built-in roles (admin, member) and any custom tenant-specific roles.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"data": [
{ "id": 2, "name": "admin" },
{ "id": 3, "name": "member" },
{ "id": 15, "name": "billing-manager" }
]
}
owner role is excluded from this list because it cannot be assigned to other users.GET /api/v1/tenant/{tenantId}/team/stats
Returns team size statistics including seat quota information from the plan.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"data": {
"members": 8,
"pending_invitations": 2,
"total": 10,
"limit": 25,
"available": 15
}
}
| Field | Type | Description |
|---|---|---|
members | int | Current number of team members |
pending_invitations | int | Number of pending (valid) invitations |
total | int | members + pending_invitations |
limit | int|null | Seat limit from plan entitlements (null = unlimited) |
available | int|null | Remaining seats (limit - total, null if unlimited) |
This endpoint is useful for showing seat usage on billing or team management pages. The limit is resolved from the team-members feature entitlement on the tenant's active plan.
PATCH /api/v1/tenant/{tenantId}/team/members/{member}/role
Change a team member's role.
Auth: Bearer token + tenant member + onboarding complete
Request Body:
{
"role_id": 3
}
| Field | Type | Required | Validation |
|---|---|---|---|
role_id | int | Yes | Must reference an existing role (not owner) |
Response (200):
{
"message": "team.role_changed"
}
Error Responses:
| Status | Scenario |
|---|---|
| 403 | Cannot change your own role |
| 403 | Cannot change the owner's role |
| 403 | Insufficient permissions |
| 404 | Member not found in tenant |
DELETE /api/v1/tenant/{tenantId}/team/members/{member}
Remove a member from the tenant.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"message": "team.member_removed"
}
Error Responses:
| Status | Scenario |
|---|---|
| 403 | Cannot remove yourself |
| 403 | Cannot remove the team owner |
| 403 | Insufficient permissions |
| 404 | Member not found in tenant |
Invitation Endpoints
Invitations allow tenant owners and admins to invite new users to join the team. The invitation lifecycle:
- Create — An invitation is sent to an email address with a unique token
- Accept — The invitee uses the token to join the tenant (existing user or new registration)
- Revoke — The inviter cancels the invitation before it's accepted
- Resend — A new token is generated and sent
Invitation Statuses
| Status | Description |
|---|---|
pending | Invitation is valid and waiting to be accepted |
accepted | Invitation has been accepted by the invitee |
revoked | Invitation was cancelled by the inviter |
expired | Invitation has passed its expires_at date |
These statuses are defined in backend/app/Domain/Team/Enums/InvitationStatus.php.
Invitation Resource
All invitation endpoints return the same resource shape:
{
"id": "880e8400-e29b-41d4-a716-446655440020",
"email": "new-member@example.com",
"role": "member",
"status": "pending",
"expires_at": "2026-04-10T10:00:00.000000Z",
"is_expired": false,
"is_valid": true,
"tenant": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corp"
},
"inviter": {
"id": 42,
"name": "John Doe"
},
"created_at": "2026-03-27T10:00:00.000000Z",
"updated_at": "2026-03-27T10:00:00.000000Z"
}
| Field | Type | Description |
|---|---|---|
id | UUID | Invitation identifier |
email | string | Invitee's email (normalized to lowercase) |
role | string | Role to be assigned on acceptance |
status | enum | pending, accepted, revoked, expired |
expires_at | datetime | When the invitation expires |
is_expired | boolean | Whether the expiry date has passed |
is_valid | boolean | true if status is pending and not expired |
tenant | object | Tenant name and ID |
inviter | object | Who created the invitation |
GET /api/v1/invitations/{token}
View invitation details by token. This is a public endpoint — no authentication required.
Auth: None Rate limit: 30/minute
Response (200): InvitationResource
Response (404): If the token is invalid or does not match any invitation.
POST /api/v1/invitations/{token}/accept
Accept an invitation as an existing authenticated user.
Auth: Bearer token (no tenant context required) Rate limit: 30/minute
Request Body: None — the authenticated user's identity is used.
Response (200):
{
"data": {
"invitation": { ... },
"tenant": { ... }
},
"message": "team.invitation_accepted"
}
The user is added to the tenant with the role specified in the invitation.
Error Responses:
| Status | Code | Description |
|---|---|---|
| 403 | EMAIL_MISMATCH | User's email does not match the invitation email |
| 404 | INVITATION_NOT_FOUND | Token is invalid |
| 409 | USER_BELONGS_TO_ANOTHER_TENANT | User already has a tenant (V1 restriction) |
| 409 | ALREADY_MEMBER | User is already a member of this tenant |
| 410 | INVITATION_EXPIRED | Invitation has expired |
| 410 | INVITATION_ALREADY_ACCEPTED | Invitation was already accepted |
| 410 | INVITATION_REVOKED | Invitation was revoked |
POST /api/v1/invitations/{token}/accept-with-registration
Accept an invitation and create a new user account in one step. This is a public endpoint.
Auth: None Rate limit: 30/minute
Request Body:
{
"name": "New Member",
"email": "new-member@example.com",
"password": "securepassword",
"password_confirmation": "securepassword"
}
| Field | Type | Required | Validation |
|---|---|---|---|
name | string | Yes | Max 255 |
email | string | Yes | Valid email, max 255, must match invitation email |
password | string | Yes | Min 8 chars, must match password_confirmation |
Response (201):
{
"data": {
"user": {
"id": 44,
"name": "New Member",
"email": "new-member@example.com"
},
"invitation": { ... },
"tenant": { ... }
},
"meta": {
"access_token": "1|abc123...",
"refresh_token": "2|def456...",
"token_type": "Bearer"
}
}
In SPA mode (cookie-based auth), the meta tokens are omitted and a session cookie is set instead.
Error Responses: Same as the accept endpoint, plus:
| Status | Code | Description |
|---|---|---|
| 409 | ACCOUNT_ALREADY_EXISTS | An account with this email already exists |
| 422 | SEAT_LIMIT_REACHED | The tenant's plan seat quota has been reached |
GET /api/v1/tenant/{tenantId}/team/invitations
List all invitations for the tenant.
Auth: Bearer token + tenant member + onboarding complete
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
pending_only | boolean | false | If true, only return valid pending invitations |
Response (200): Array of InvitationResource objects, sorted by created_at descending.
POST /api/v1/tenant/{tenantId}/team/invitations
Create a new invitation.
Auth: Bearer token + tenant member (owner or admin role) + onboarding complete
Request Body:
{
"email": "new-member@example.com",
"role": "member",
"expires_in_days": 7
}
| Field | Type | Required | Validation |
|---|---|---|---|
email | string | Yes | Valid email, max 255 |
role | string | Yes | Must be a valid assignable role for the tenant |
expires_in_days | int | No | 1–30 days (defaults to system setting) |
Response (201):
{
"data": { ... },
"message": "team.invitation_sent"
}
Error Responses:
| Status | Code | Description |
|---|---|---|
| 403 | INSUFFICIENT_PERMISSIONS | User is not an owner or admin |
| 409 | ALREADY_MEMBER | Email belongs to an existing tenant member |
| 409 | ALREADY_INVITED | A pending invitation already exists for this email |
| 409 | USER_BELONGS_TO_ANOTHER_TENANT | Email belongs to a user in another tenant (V1) |
| 422 | SEAT_LIMIT_REACHED | Plan seat quota would be exceeded |
DELETE /api/v1/tenant/{tenantId}/team/invitations/{invitation}
Revoke a pending invitation.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"message": "team.invitation_revoked"
}
POST /api/v1/tenant/{tenantId}/team/invitations/{invitation}/resend
Resend an invitation with a new token.
Auth: Bearer token + tenant member + onboarding complete
Response (200):
{
"data": { ... },
"message": "team.invitation_resent"
}
A new token is generated, replacing the previous one. The old token becomes invalid.
Role Management Endpoints
Custom roles allow tenants to define fine-grained access beyond the built-in owner, admin, and member roles.
All role management endpoints require the roles.manage permission and use the base path /api/v1/tenant/{tenantId}/roles.
GET /api/v1/tenant/{tenantId}/roles
List all roles available in the tenant, including both built-in and custom roles.
Auth: Bearer token + tenant member + roles.manage permission
Response (200):
{
"data": [
{
"id": 1,
"name": "owner",
"guard_name": "web",
"tenant_id": null,
"is_builtin": true,
"permissions": ["billing.manage", "team.manage", "roles.manage"],
"users_count": 1,
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-01-15T10:00:00.000000Z"
},
{
"id": 15,
"name": "billing-manager",
"guard_name": "web",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"is_builtin": false,
"permissions": ["billing.manage"],
"users_count": 2,
"created_at": "2026-03-20T14:30:00.000000Z",
"updated_at": "2026-03-20T14:30:00.000000Z"
}
]
}
| Field | Type | Description |
|---|---|---|
id | int | Role identifier |
name | string | Role name (e.g., admin, billing-manager) |
guard_name | string | Laravel auth guard (web) |
tenant_id | UUID|null | null for built-in roles, tenant UUID for custom roles |
is_builtin | boolean | Whether this is a system-defined role |
permissions | array | Sorted list of permission names |
users_count | int | Number of users assigned this role |
GET /api/v1/tenant/{tenantId}/roles/permissions
Returns all available permissions that can be assigned to roles.
Auth: Bearer token + tenant member + roles.manage permission
Response (200):
{
"data": ["billing.manage", "roles.manage", "team.manage"]
}
POST /api/v1/tenant/{tenantId}/roles
Create a custom role for the tenant.
Auth: Bearer token + tenant member + roles.manage permission
Response (201): GlobalRoleResource
PATCH /api/v1/tenant/{tenantId}/roles/{role}
Update a custom role.
Auth: Bearer token + tenant member + roles.manage permission
Response (200): GlobalRoleResource
owner, admin, member) cannot be updated or deleted. Attempting to do so returns a 403 error.DELETE /api/v1/tenant/{tenantId}/roles/{role}
Delete a custom role. Users currently assigned this role are reassigned to the default member role.
Auth: Bearer token + tenant member + roles.manage permission
Response (200):
{
"message": "team.role_deleted"
}
Admin Tenant Endpoints
Platform administrators can manage all tenants. Admin endpoints require the platform.admin middleware and are blocked during impersonation sessions.
Base path: /api/v1/admin/...Middleware: auth:sanctum → impersonation.prevent → platform.admin
GET /api/v1/admin/tenants
List all tenants with search and pagination.
Auth: Bearer token + platform admin
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
search | string | — | Search by tenant name |
per_page | int | 25 | Items per page (1–100) |
Response (200):
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corp",
"slug": "acme-corp",
"legal_name": "Acme Corporation GmbH",
"address": "123 Main St",
"city": "Berlin",
"postal_code": "10115",
"country": "DE",
"vat_number": "DE123456789",
"billing_email": "billing@acme.com",
"owner": {
"id": 42,
"name": "John Doe",
"email": "john@acme.com"
},
"member_count": 8,
"subscription_status": "active",
"plan_name": "Pro",
"onboarding_completed_at": "2026-01-15T12:00:00.000000Z",
"created_at": "2026-01-15T10:00:00.000000Z",
"roles": [
{ "id": 1, "name": "owner" },
{ "id": 2, "name": "admin" },
{ "id": 3, "name": "member" }
]
}
],
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 25,
"total": 72
}
}
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/tenants/{tenant} | Get tenant details (AdminTenantResource with roles) |
| PATCH | /api/v1/admin/tenants/{tenant} | Update tenant (same fields as tenant update, except slug) |
| GET | /api/v1/admin/tenants/search-with-roles | Search tenants with roles (requires users.create permission). Query: search (required). Returns {id, name, roles[]}. |
Admin Team Member Endpoints
All require platform.admin middleware. Responses use the same TeamMemberResource shape as tenant-scoped endpoints.
| Method | Path | Description |
|---|---|---|
| GET | .../tenants/{tenant}/members | List all members of a tenant |
| PATCH | .../tenants/{tenant}/members/{user} | Update member profile (name, password) |
| PATCH | .../tenants/{tenant}/members/{user}/role | Change member role. Body: {"role_id": 3} |
| DELETE | .../tenants/{tenant}/members/{user} | Remove member from tenant |
Admin Invitation Endpoints
All require platform.admin middleware. Responses use InvitationResource.
| Method | Path | Description |
|---|---|---|
| GET | .../tenants/{tenant}/invitations | List invitations (supports pending_only query param) |
| POST | .../tenants/{tenant}/invitations | Create invitation. Body: {email, role, expires_in_days?} |
| DELETE | .../tenants/{tenant}/invitations/{invitation} | Revoke invitation |
| POST | .../tenants/{tenant}/invitations/{invitation}/resend | Resend with new token |
What's Next
- Multi-Tenancy — How tenant isolation works
- Teams — Team management features in depth
- Seat-Based Billing Integration — How team size affects billing
- Subscriptions API — Subscription and entitlement endpoints
Usage & Meters API
Usage tracking endpoints: recording events with idempotency, viewing usage summaries and meter details, quota enforcement, and cost estimation.
AI-Assisted Development Overview
How SaaS4Builders is designed for AI coding agents: configuration files, convention documents, and structured execution patterns that keep AI output consistent and correct.