Organizations API¶
Base path: /api/v1/organizations
Manages organization profiles, subscription plans, resource limits, BYOK LLM configuration, SSO, and physical locations. Every authenticated user belongs to exactly one organization; their JWT carries an org_id claim that identifies it.
See API Reference for auth, errors, and pagination.
Core¶
GET /organizations/plans¶
Return the full plan catalog with pricing and resource limits.
Auth: None — public endpoint.
Response — 200 OK
[
{
"name": "trial",
"price_monthly_usd": 0,
"max_users": 30,
"max_integrations": 3,
"max_locations": 1,
"description": "Free 30-day trial — same limits as Starter."
},
{
"name": "starter",
"price_monthly_usd": 49,
"max_users": 30,
"max_integrations": 3,
"max_locations": 1,
"description": "For small teams getting started with institutional memory."
},
{
"name": "growth",
"price_monthly_usd": 149,
"max_users": 150,
"max_integrations": -1,
"max_locations": -1,
"description": "For growing organisations across multiple locations."
},
{
"name": "enterprise",
"price_monthly_usd": 399,
"max_users": -1,
"max_integrations": -1,
"max_locations": -1,
"description": "Unlimited scale with dedicated support."
}
]
A value of -1 for any limit means unlimited. Plan changes go through POST /api/v1/billing/change-plan.
curl
GET /organizations/me¶
Return the authenticated caller's organization.
Auth: JWT — any role. The JWT must carry a valid org_id claim.
Response — 200 OK
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Acme Corp",
"slug": "acme-corp",
"plan": "growth",
"plan_started_at": "2025-03-01T00:00:00Z",
"trial_ends_at": null,
"is_active": true,
"max_users": 150,
"max_integrations": -1,
"max_locations": -1,
"settings_json": {
"timezone": "Asia/Riyadh",
"locale": "ar"
},
"created_at": "2025-03-01T00:00:00Z",
"updated_at": "2025-11-20T14:32:10Z"
}
stripe_customer_id and stripe_subscription_id are not included in this response.
Errors
| Status | Code | Cause |
|---|---|---|
| 401 | — | Missing or invalid JWT |
| 403 | MISSING_ORG_CLAIM |
JWT contains no org_id claim |
| 403 | INVALID_ORG_CLAIM |
org_id claim is not a valid UUID |
| 404 | — | Organization deleted after token was issued |
curl
GET /organizations/me/limits¶
Return the caller's organization's current resource usage compared to plan limits.
Auth: JWT — any role.
Response — 200 OK
{
"org_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"plan": "growth",
"user_count": 42,
"max_users": 150,
"integration_count": 5,
"max_integrations": -1,
"location_count": 3,
"max_locations": -1,
"can_add_user": true,
"can_add_integration": true,
"can_add_location": true
}
can_add_* is false when the current count has reached the plan ceiling (not applicable when limit is -1).
curl
PATCH /organizations/me¶
Update mutable organization fields.
Auth: JWT — role admin or member with is_org_owner: true.
Request body (all fields optional)
| Field | Type | Constraints | Description |
|---|---|---|---|
name |
string | null | 2–255 chars | Display name of the organization |
settings_json |
object | null | Any valid JSON object | Arbitrary org-level configuration |
Example request
curl -X PATCH \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Corporation", "settings_json": {"timezone": "Asia/Riyadh", "locale": "ar"}}' \
https://api.knora.io/api/v1/organizations/me
Response — 200 OK — full updated organization object (same shape as GET /me).
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | — | name fails length validation |
| 401 | — | Missing or invalid JWT |
| 403 | — | Caller is a member without is_org_owner |
| 404 | — | Organization not found |
BYOK (Enterprise)¶
Bring Your Own Key lets an organization supply its own LLM API credentials. All endpoints require an Enterprise plan and admin or owner role.
GET /organizations/byok¶
Return the current BYOK configuration. The stored API key is never returned verbatim — only the last 4 characters are shown as a hint (e.g. ****...ab12).
Auth: JWT — admin/owner. Plan: Enterprise.
Response — 200 OK
{
"byok_enabled": true,
"provider": "anthropic",
"api_key_hint": "****...ab12",
"model_override": "anthropic/claude-sonnet-4-6"
}
Errors
| Status | Code | Cause |
|---|---|---|
| 403 | ENTERPRISE_REQUIRED |
Org is not on the Enterprise plan |
| 403 | — | Caller is not an admin or owner |
curl
PUT /organizations/byok¶
Set or update the BYOK configuration. The plaintext api_key is encrypted with Fernet before storage. byok_enabled is set to true automatically once both provider and api_key are present.
Auth: JWT — admin/owner. Plan: Enterprise.
Request body (at least one field required)
| Field | Type | Description |
|---|---|---|
provider |
"openai" | "anthropic" | "custom" |
LLM provider |
api_key |
string | Plaintext key to encrypt and store. Omit to keep existing. |
model_override |
string | LiteLLM model string, e.g. "gpt-4o". Omit to keep existing. |
Example request
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider": "anthropic", "api_key": "sk-ant-...", "model_override": "anthropic/claude-sonnet-4-6"}' \
https://api.knora.io/api/v1/organizations/byok
Response — 200 OK — same shape as GET /organizations/byok.
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR |
No fields provided, or field fails validation |
| 400 | BYOK_ERROR |
Key storage or encryption failure |
| 403 | ENTERPRISE_REQUIRED |
Org is not on the Enterprise plan |
DELETE /organizations/byok¶
Disable BYOK and clear all stored credentials. The org immediately reverts to the platform's default LLM. The encrypted key is deleted from the database.
Auth: JWT — admin/owner. Plan: Enterprise.
Response — 200 OK — BYOK config with byok_enabled: false and all fields cleared.
curl
POST /organizations/byok/test¶
Validate a BYOK API key by making a minimal LiteLLM completion call. If api_key is omitted, the currently stored (encrypted) key is used.
Auth: JWT — admin/owner. Plan: Enterprise.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
provider |
"openai" | "anthropic" | "custom" |
Yes | LLM provider to test against |
api_key |
string | No | Key to test. Omit to test the stored key. |
model_override |
string | No | Model to use for the test call. Defaults to provider's canonical model. |
Example request
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider": "openai", "api_key": "sk-..."}' \
https://api.knora.io/api/v1/organizations/byok/test
Response — 200 OK
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | BYOK_TEST_FAILED |
LLM call failed — bad key, wrong provider, or model unavailable |
| 400 | VALIDATION_ERROR |
Request body fails schema validation |
| 403 | ENTERPRISE_REQUIRED |
Org is not on the Enterprise plan |
SSO (Enterprise)¶
Manage Single Sign-On configuration. All endpoints require an Enterprise plan and admin or owner role. The actual SAML/OIDC handshake (SP-initiated login, ACS callback) is a separate implementation — these routes manage configuration storage only.
GET /organizations/sso¶
Return the current SSO configuration. Sensitive fields (certificate, client_secret, private_key) are replaced with "[REDACTED]".
Auth: JWT — admin/owner. Plan: Enterprise.
Response — 200 OK
{
"sso_enabled": true,
"sso_provider": "oidc",
"sso_enforce": false,
"config": {
"client_id": "your-client-id",
"client_secret": "[REDACTED]",
"discovery_url": "https://accounts.google.com/.well-known/openid-configuration",
"scopes": ["openid", "email", "profile"]
},
"updated_at": "2026-01-10T09:00:00Z"
}
When no SSO has been configured, sso_enabled is false and config is null.
curl
PUT /organizations/sso¶
Create or update the SSO configuration. Config fields are merged with existing values — only supplied keys are overwritten.
Auth: JWT — admin/owner. Plan: Enterprise.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
provider |
"saml" | "oidc" | "google_workspace" | "microsoft_entra" |
Required on first setup | SSO provider identifier |
config |
object | No | Provider-specific config fields (see below). Partial update — unset keys are preserved. |
sso_enforce |
boolean | No | When true, disables password login for all org members. |
config fields by provider
| Provider | Key fields |
|---|---|
saml |
entity_id, sso_url, certificate, metadata_url |
oidc |
client_id, client_secret, discovery_url, scopes |
google_workspace |
customer_id, admin_email |
microsoft_entra |
tenant_id, client_id, client_secret |
All config fields are optional at the schema level. Provider-specific validation is deferred to the SAML/OIDC handshake. attribute_mapping (object) is supported across all providers for mapping IdP claims to Knora user fields.
Example request
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"provider": "oidc",
"config": {
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"discovery_url": "https://accounts.google.com/.well-known/openid-configuration"
}
}' \
https://api.knora.io/api/v1/organizations/sso
Response — 200 OK — same shape as GET /organizations/sso (secrets redacted).
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR |
Request body fails schema validation |
| 400 | SSO_CONFIG_ERROR |
Config storage failure |
| 403 | ENTERPRISE_REQUIRED / PLAN_UPGRADE_REQUIRED |
Org is not on the Enterprise plan |
POST /organizations/sso/test¶
Probe the stored SSO metadata/discovery URL for reachability. Performs a lightweight HTTP GET to confirm the IdP endpoint is accessible — does not validate SAML XML schema or negotiate any OIDC token flow. Always returns 200; check the reachable field.
Auth: JWT — admin/owner. Plan: Enterprise.
Response — 200 OK
{
"reachable": true,
"url_tested": "https://accounts.google.com/.well-known/openid-configuration",
"message": "IdP endpoint is reachable."
}
curl
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
https://api.knora.io/api/v1/organizations/sso/test
DELETE /organizations/sso¶
Disable SSO and clear all stored config. sso_enforce is also reset to false, which immediately re-enables password-based login for all org members.
Auth: JWT — admin/owner. Plan: Enterprise.
Response — 200 OK — SSO config with sso_enabled: false and all fields cleared.
curl
Locations¶
Physical locations within an organization. List is available to all members; create/update/delete require manager or above.
GET /organizations/locations¶
List active locations for the caller's org.
Auth: JWT — any role.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
include_inactive |
boolean | false |
Include deactivated locations. Admin only. |
Response — 200 OK
[
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"org_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Riyadh HQ",
"description": "Main headquarters",
"address": "King Fahd Road, Riyadh",
"is_active": true,
"created_at": "2025-06-01T10:00:00Z",
"updated_at": "2025-06-01T10:00:00Z"
}
]
curl
POST /organizations/locations¶
Create a new location.
Auth: JWT — manager or admin. Plan: Growth+.
Request body
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
name |
string | Yes | 1–255 chars | Location display name |
description |
string | No | max 2000 chars | Optional description |
address |
string | No | max 500 chars | Physical address |
Example request
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Jeddah Office", "address": "Corniche Road, Jeddah"}' \
https://api.knora.io/api/v1/organizations/locations
Response — 201 Created — the created location object (same shape as list items).
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR |
Missing name or field exceeds length limit |
| 400 | LOCATION_LIMIT_EXCEEDED |
Org has reached its plan's location ceiling |
| 403 | — | Caller is a member (not manager/admin) |
| 403 | PLAN_UPGRADE_REQUIRED |
Plan is below Growth |
| 409 | LOCATION_ALREADY_EXISTS |
A location with that name already exists in this org |
PUT /organizations/locations/:id¶
Update name, description, or address of an existing location.
Auth: JWT — manager or admin.
Path parameter: id — UUID of the location.
Request body (all fields optional)
| Field | Type | Constraints | Description |
|---|---|---|---|
name |
string | 1–255 chars | New display name |
description |
string | max 2000 chars | New description |
address |
string | max 500 chars | New address |
Example request
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Jeddah Branch", "address": "Prince Sultan Road, Jeddah"}' \
https://api.knora.io/api/v1/organizations/locations/b2c3d4e5-f6a7-8901-bcde-f12345678901
Response — 200 OK — the updated location object.
Errors
| Status | Code | Cause |
|---|---|---|
| 403 | — | Caller is a member (not manager/admin) |
| 404 | LOCATION_NOT_FOUND |
No location with that UUID in this org |
| 409 | LOCATION_ALREADY_EXISTS |
Another location already uses the new name |
DELETE /organizations/locations/:id¶
Soft-deactivate a location. Sets is_active = false; the row is retained for historical referential integrity.
Auth: JWT — manager or admin.
Path parameter: id — UUID of the location.
Response — 200 OK
Errors
| Status | Code | Cause |
|---|---|---|
| 403 | — | Caller is a member (not manager/admin) |
| 404 | LOCATION_NOT_FOUND |
No location with that UUID in this org |
curl
curl -X DELETE \
-H "Authorization: Bearer $TOKEN" \
https://api.knora.io/api/v1/organizations/locations/b2c3d4e5-f6a7-8901-bcde-f12345678901
Plan Catalog¶
| Plan | Price/mo | Max users | Max integrations | Max locations |
|---|---|---|---|---|
trial |
$0 | 30 | 3 | 1 |
starter |
$49 | 30 | 3 | 1 |
growth |
$149 | 150 | Unlimited | Unlimited |
enterprise |
$399 | Unlimited | Unlimited | Unlimited |
Plan changes go through POST /api/v1/billing/change-plan. Downgrading is only permitted when current usage is within the target plan's limits; otherwise 400 LIMIT_EXCEEDED is returned.
Error Codes¶
| HTTP | Code | Description |
|---|---|---|
| 400 | LIMIT_EXCEEDED |
Downgrade blocked — current usage exceeds the target plan's ceiling |
| 400 | BYOK_ERROR |
BYOK key storage or encryption failure |
| 400 | BYOK_TEST_FAILED |
BYOK LLM test call failed |
| 400 | SSO_CONFIG_ERROR |
SSO configuration storage failure |
| 400 | LOCATION_LIMIT_EXCEEDED |
Org has reached its location limit |
| 401 | — | JWT absent, expired, or malformed |
| 402 | SUBSCRIPTION_REQUIRED |
Org is inactive or trial has expired |
| 403 | MISSING_ORG_CLAIM |
JWT does not contain an org_id claim |
| 403 | INVALID_ORG_CLAIM |
org_id claim in JWT is not a valid UUID |
| 403 | ENTERPRISE_REQUIRED |
Feature requires Enterprise plan |
| 403 | PLAN_UPGRADE_REQUIRED |
Route requires a higher plan; details includes required_plan and feature |
| 403 | INSUFFICIENT_ROLE |
Caller's role is too low for this operation |
| 404 | — | Requested resource does not exist |
| 409 | LOCATION_ALREADY_EXISTS |
A location with that name already exists in this org |