Organizations API
Complete reference for managing organizations, team members, site access, and invitations in SealMetrics multi-tenancy system.
Overview
The Organizations API allows you to:
- Create and manage organizations (workspaces)
- Invite team members and manage their roles
- Control site-level access for team members
- List and manage organization sites
Base path: /organizations
Understanding Multi-Tenancy
SealMetrics uses a three-level access model:
Users <--M:N--> Organizations <--1:N--> Sites
- Users can belong to multiple organizations
- Organizations contain multiple sites (analytics accounts)
- Members have roles that determine their permissions
Organization Roles
| Role | Sites Access | Member Management | Billing | Org Settings |
|---|---|---|---|---|
owner | All org sites | Full (add/remove/role change) | Yes | Full (rename, delete) |
admin | All org sites | Add/remove members (except owners) | No | View only |
member | Only assigned sites | View only | No | No |
Organization Endpoints
List Organizations
GET /organizations
Returns all organizations the current user belongs to.
Response:
{
"success": true,
"data": {
"organizations": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "ACME Corporation",
"slug": "acme-corporation",
"is_active": true,
"member_count": 5,
"site_count": 3,
"user_role": "owner",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2025-01-08T09:15:00Z"
}
],
"total": 1,
"can_create_org": true
}
}
| Field | Description |
|---|---|
can_create_org | true if user can create a new organization |
user_role | Current user's role in this organization |
Create Organization
POST /organizations
Creates a new organization. The creator becomes the owner.
Required scope: write
Request Body:
{
"name": "My Company"
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Organization name (1-255 chars) |
Response (201 Created):
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Company",
"slug": "my-company",
"is_active": true,
"member_count": 1,
"site_count": 0,
"user_role": "owner",
"created_at": "2025-01-10T14:30:00Z",
"updated_at": "2025-01-10T14:30:00Z"
}
}
Note: The slug is auto-generated from the name and used in URLs.
Get Organization
GET /organizations/{slug}
Returns detailed information about a specific organization.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
slug | string | Organization slug (from URL) |
Response:
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "ACME Corporation",
"slug": "acme-corporation",
"is_active": true,
"member_count": 5,
"site_count": 3,
"user_role": "admin",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2025-01-08T09:15:00Z"
}
}
Update Organization
PATCH /organizations/{slug}
Updates organization settings. Only owners can update.
Required role: owner
Request Body:
{
"name": "ACME Corp (Renamed)"
}
Response:
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "ACME Corp (Renamed)",
"slug": "acme-corporation",
"is_active": true,
"member_count": 5,
"site_count": 3,
"user_role": "owner",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2025-01-10T15:00:00Z"
}
}
Delete Organization
DELETE /organizations/{slug}
Soft deletes an organization and all its sites. Requires name confirmation.
Required role: owner
Request Body:
{
"name": "ACME Corporation"
}
| Field | Type | Description |
|---|---|---|
name | string | Type the exact organization name to confirm deletion |
Response:
{
"success": true,
"data": {
"message": "Organization deleted successfully"
}
}
Warning: This action deactivates the organization and all associated sites. Data is retained but inaccessible.
Member Endpoints
List Members
GET /organizations/{slug}/members
Returns all members of the organization.
Response:
{
"success": true,
"data": {
"members": [
{
"user_id": 1,
"email": "owner@acme.com",
"name": "John Owner",
"role": "owner",
"site_count": 3,
"created_at": "2024-01-15T10:00:00Z"
},
{
"user_id": 2,
"email": "analyst@acme.com",
"name": "Jane Analyst",
"role": "member",
"site_count": 1,
"created_at": "2024-03-20T14:00:00Z"
}
],
"total": 2
}
}
| Field | Description |
|---|---|
site_count | Number of sites this member has access to |
Add Member
POST /organizations/{slug}/members
Adds an existing user as a member of the organization.
Required role: admin or owner
Request Body:
{
"user_id": 5,
"role": "member"
}
| Field | Type | Required | Description |
|---|---|---|---|
user_id | integer | Yes | User ID to add |
role | enum | No | owner, admin, or member (default: member) |
Response (201 Created):
{
"success": true,
"data": {
"message": "Member added successfully"
}
}
Note: To invite users who don't have an account yet, use the Invitations API.
Update Member Role
PATCH /organizations/{slug}/members/{user_id}
Changes a member's role in the organization.
Required role: admin or owner
Request Body:
{
"role": "admin"
}
Response:
{
"success": true,
"data": {
"message": "Role updated successfully"
}
}
Restrictions:
- Admins cannot change owner roles
- Cannot demote the last owner
Remove Member
DELETE /organizations/{slug}/members/{user_id}
Removes a member from the organization.
Required role: admin or owner
Response:
{
"success": true,
"data": {
"message": "Member removed successfully"
}
}
Restrictions:
- Cannot remove the last owner
- Admins cannot remove owners
Organization Sites
List Organization Sites
GET /organizations/{slug}/sites
Returns all sites belonging to the organization.
Response:
{
"success": true,
"data": {
"sites": [
{
"id": "acme-main",
"name": "ACME Main Website",
"is_active": true,
"timezone": "Europe/Madrid",
"currency": "EUR",
"created_at": "2024-01-15T10:00:00Z"
},
{
"id": "acme-shop",
"name": "ACME E-commerce",
"is_active": true,
"timezone": "Europe/Madrid",
"currency": "EUR",
"created_at": "2024-02-01T09:00:00Z"
}
],
"total": 2
}
}
Member Site Access
Control which sites a member can access. Only applies to members with the member role (owners and admins have access to all sites automatically).
List Member's Sites
GET /organizations/{slug}/members/{user_id}/sites
Returns sites assigned to a specific member.
Required role: admin or owner
Response:
{
"success": true,
"data": {
"sites": [
{
"id": "acme-main",
"name": "ACME Main Website",
"has_access": true
},
{
"id": "acme-shop",
"name": "ACME E-commerce",
"has_access": false
}
],
"total": 2
}
}
Add Site Access
POST /organizations/{slug}/members/{user_id}/sites
Grants a member access to one or more sites.
Required role: admin or owner
Request Body:
{
"site_ids": ["acme-main", "acme-shop"]
}
Response:
{
"success": true,
"data": {
"added": ["acme-main", "acme-shop"],
"already_assigned": [],
"invalid": []
}
}
| Field | Description |
|---|---|
added | Sites successfully granted access |
already_assigned | Sites the member already had access to |
invalid | Site IDs that don't exist or don't belong to the org |
Remove Site Access
DELETE /organizations/{slug}/members/{user_id}/sites/{site_id}
Removes a member's access to a specific site.
Required role: admin or owner
Response:
{
"success": true,
"data": {
"message": "Site access removed successfully"
}
}
Invitation Endpoints
Invitations allow you to add team members who don't have a SealMetrics account yet.
List Invitations
GET /organizations/{slug}/invitations
Returns pending invitations for the organization.
Required role: admin or owner
Response:
{
"success": true,
"data": {
"invitations": [
{
"id": 1,
"org_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "newuser@example.com",
"role": "member",
"invited_by_name": "John Owner",
"is_active": true,
"expires_at": "2025-01-17T14:30:00Z",
"accepted_at": null,
"created_at": "2025-01-10T14:30:00Z"
}
],
"total": 1
}
}
Send Invitation
POST /organizations/{slug}/invitations
Sends an email invitation to join the organization.
Required role: admin or owner
Request Body:
{
"email": "newuser@example.com",
"role": "member",
"site_ids": ["acme-main"]
}
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address to invite |
role | enum | No | owner, admin, or member (default: member) |
site_ids | string[] | No | Pre-assign sites for member role (ignored for owner/admin) |
Response (201 Created):
{
"success": true,
"data": {
"invitation_id": 1,
"invitation_link": "https://my.sealmetrics.com/accept-invitation?token=abc123...",
"expires_at": "2025-01-17T14:30:00Z"
}
}
Notes:
- Invitation expires in 7 days
- If user already has an account, they can accept while logged in
- If user is new, they'll create an account during acceptance
Resend Invitation
POST /organizations/{slug}/invitations/{invitation_id}/resend
Resends the invitation email with a new token.
Required role: admin or owner
Response:
{
"success": true,
"data": {
"message": "Invitation resent"
}
}
Revoke Invitation
DELETE /organizations/{slug}/invitations/{invitation_id}
Cancels a pending invitation.
Required role: admin or owner
Response:
{
"success": true,
"data": {
"message": "Invitation revoked"
}
}
Accept Invitation Endpoints
These endpoints are called when a user clicks the invitation link.
Accept Invitation (New User)
POST /invitations/accept
Creates a new account and joins the organization.
Request Body:
{
"token": "abc123...",
"name": "Jane Doe",
"password": "<your_password>"
}
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Invitation token from the link |
name | string | Yes | User's full name (1-255 chars) |
password | string | Yes | Password (min 8 chars) |
Response:
{
"success": true,
"data": {
"message": "Invitation accepted",
"organization": {
"name": "ACME Corporation",
"slug": "acme-corporation"
}
}
}
Accept Invitation (Existing User)
POST /invitations/accept-existing
Joins the organization with an existing logged-in account.
Requires: Authentication (JWT or API Key)
Request Body:
{
"token": "abc123..."
}
Response:
{
"success": true,
"data": {
"message": "Invitation accepted",
"organization": {
"name": "ACME Corporation",
"slug": "acme-corporation"
}
}
}
Error Codes
| HTTP Code | Error Code | Description |
|---|---|---|
| 400 | last_owner | Cannot remove or demote the last owner |
| 400 | confirmation_failed | Organization name doesn't match for deletion |
| 403 | org_creation_not_allowed | User cannot create more organizations |
| 403 | insufficient_permissions | User's role doesn't allow this action |
| 404 | org_not_found | Organization doesn't exist or user has no access |
| 404 | member_not_found | User is not a member of this organization |
| 404 | invitation_not_found | Invitation doesn't exist or is expired |
| 409 | member_already_exists | User is already a member of this organization |
| 409 | user_already_member | Invited user is already a member |
Code Examples
Python
import requests
API_KEY = "sm_your_api_key"
BASE_URL = "https://api.sealmetrics.com/api/v1"
headers = {"X-API-Key": API_KEY}
# List my organizations
orgs = requests.get(
f"{BASE_URL}/organizations",
headers=headers
).json()["data"]["organizations"]
print(f"My organizations: {[o['name'] for o in orgs]}")
# Create organization
new_org = requests.post(
f"{BASE_URL}/organizations",
headers=headers,
json={"name": "My New Team"}
).json()["data"]
print(f"Created org: {new_org['slug']}")
# Invite a team member
invitation = requests.post(
f"{BASE_URL}/organizations/{new_org['slug']}/invitations",
headers=headers,
json={
"email": "colleague@example.com",
"role": "member",
"site_ids": ["my-website"]
}
).json()["data"]
print(f"Invitation link: {invitation['invitation_link']}")
# List members
members = requests.get(
f"{BASE_URL}/organizations/{new_org['slug']}/members",
headers=headers
).json()["data"]["members"]
for member in members:
print(f" {member['name']} ({member['role']})")
JavaScript
const API_KEY = 'sm_your_api_key';
const BASE_URL = 'https://api.sealmetrics.com/api/v1';
const headers = {
'X-API-Key': API_KEY,
'Content-Type': 'application/json'
};
// List my organizations
const orgsResponse = await fetch(`${BASE_URL}/organizations`, { headers });
const { data: { organizations } } = await orgsResponse.json();
console.log('My orgs:', organizations.map(o => o.name));
// Create organization
const createResponse = await fetch(`${BASE_URL}/organizations`, {
method: 'POST',
headers,
body: JSON.stringify({ name: 'My New Team' })
});
const { data: newOrg } = await createResponse.json();
console.log(`Created: ${newOrg.slug}`);
// Invite team member with pre-assigned sites
const inviteResponse = await fetch(
`${BASE_URL}/organizations/${newOrg.slug}/invitations`,
{
method: 'POST',
headers,
body: JSON.stringify({
email: 'colleague@example.com',
role: 'member',
site_ids: ['my-website', 'my-blog']
})
}
);
const { data: invitation } = await inviteResponse.json();
console.log(`Invitation sent! Link: ${invitation.invitation_link}`);
// Grant site access to existing member
await fetch(
`${BASE_URL}/organizations/${newOrg.slug}/members/123/sites`,
{
method: 'POST',
headers,
body: JSON.stringify({ site_ids: ['new-site'] })
}
);
PHP
<?php
$apiKey = 'sm_your_api_key';
$baseUrl = 'https://api.sealmetrics.com/api/v1';
$headers = [
'X-API-Key: ' . $apiKey,
'Content-Type: application/json'
];
// Create organization
$ch = curl_init("$baseUrl/organizations");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'name' => 'My Company'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
$org = $response['data'];
curl_close($ch);
echo "Created organization: " . $org['slug'] . "\n";
// Invite member
$ch = curl_init("$baseUrl/organizations/{$org['slug']}/invitations");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'email' => 'team@example.com',
'role' => 'admin'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "Invitation link: " . $response['data']['invitation_link'] . "\n";
?>
Best Practices
Team Structure
- Keep one owner per organization - Designate a primary account owner
- Use admin role sparingly - Admins can manage all members and sites
- Assign specific sites to members - Use
site_idswhen inviting members
Invitation Workflow
- Always pre-assign sites when inviting with
memberrole - Invitations expire in 7 days - resend if needed
- Check
can_create_orgbefore showing "Create Organization" UI
Security
- Revoke access immediately when team members leave
- Periodically audit member list and site access
- Use the audit logs to track changes (see Audit Log)