Skip to content

Organizations & Sites

Breeze organises managed infrastructure into a strict multi-tenant hierarchy. Every device belongs to a Site, every site belongs to an Organisation, and every organisation belongs to a Partner. All API queries automatically scope data to the caller’s position in this hierarchy, ensuring tenant isolation by default.


Overview

The hierarchy serves two purposes:

  1. Logical grouping — partners (MSPs) manage multiple customer organisations, each of which can have several physical or logical sites.
  2. Access control — the authenticated user’s scope (system, partner, or organization) determines which records are visible and mutable. No cross-tenant data leakage is possible without explicit system-level access.
Partner (MSP)
└── Organisation (Customer)
└── Site (Location)
└── Device Group
└── Device

Every entity in the hierarchy uses a UUID primary key and carries createdAt / updatedAt timestamps. Partners and organisations support soft deletes via a deletedAt column; sites are hard-deleted.


Partners

A Partner represents a top-level tenant — typically a Managed Service Provider (MSP), an enterprise IT department, or an internal team.

Partner Types

TypeDescription
mspManaged Service Provider managing multiple customer organisations. Default.
enterpriseA single enterprise managing its own infrastructure.
internalInternal IT team or development environment.

Plan Tiers

PlanDescription
freeFree tier. Default.
proProfessional tier with expanded limits.
enterpriseEnterprise tier with advanced features.
unlimitedNo enforced limits.

Partner Fields

FieldTypeDescription
iduuidAuto-generated primary key.
namevarchar(255)Display name. Required.
slugvarchar(100)URL-safe unique identifier. Required.
typeenumOne of msp, enterprise, internal. Defaults to msp.
planenumOne of free, pro, enterprise, unlimited. Defaults to free.
maxOrganizationsintegerOptional cap on the number of organisations.
maxDevicesintegerOptional cap on total devices across all organisations.
settingsjsonbArbitrary partner-level settings (see below).
ssoConfigjsonbSSO configuration blob.
billingEmailvarchar(255)Billing contact email address.
createdAttimestampRecord creation time.
updatedAttimestampLast modification time.
deletedAttimestampSoft-delete marker. Null when active.

Partner Settings

Partner-scoped users can update their own partner’s settings via the /partners/me endpoint. The settings JSONB column supports the following structure:

SettingTypeDescription
timezonestringDefault timezone for the partner (e.g. America/New_York).
dateFormatenumMM/DD/YYYY, DD/MM/YYYY, or YYYY-MM-DD.
timeFormatenum12h or 24h.
languagestringLanguage code. Currently only en is supported.
businessHours.presetenum24/7, business, extended, or custom.
businessHours.customobjectPer-day schedule when preset is custom. Each day has start, end (time strings), and optional closed boolean.
contact.namestringPrimary contact name.
contact.emailstringPrimary contact email.
contact.phonestringPrimary contact phone.
contact.websitestringPartner website URL.

Settings are merged on update — you do not need to send the entire object. Only the keys you include are overwritten.

Creating a Partner

Only system-scoped users can create partners.

POST /api/v1/orgs/partners
{
"name": "Acme MSP",
"slug": "acme-msp",
"type": "msp",
"plan": "pro",
"maxOrganizations": 50,
"maxDevices": 5000,
"billingEmail": "billing@acme-msp.com"
}

Returns the created partner object with a 201 status.

Soft Deletes for Partners

When a partner is deleted via DELETE /partners/:id, the record is not removed from the database. Instead, the deletedAt timestamp is set. All list and detail queries filter out records where deletedAt is not null. This preserves referential integrity and allows potential restoration.


Organisations

An Organisation represents a customer or internal team managed by a partner. Every organisation belongs to exactly one partner.

Organisation Types

TypeDescription
customerAn external customer managed by the partner. Default.
internalAn internal team or testing environment within the partner.

Organisation Status

StatusDescription
activeFully operational. Default.
suspendedAccess is restricted. Devices may still report but management actions are limited.
trialEvaluation period.
churnedCustomer has left. Retained for historical data.

Organisation Fields

FieldTypeDescription
iduuidAuto-generated primary key.
partnerIduuidFK to the parent partner. Required.
namevarchar(255)Display name. Required.
slugvarchar(100)URL-safe identifier. Required.
typeenumOne of customer, internal. Defaults to customer.
statusenumOne of active, suspended, trial, churned. Defaults to active.
maxDevicesintegerOptional device cap for this organisation.
settingsjsonbOrganisation-level settings.
ssoConfigjsonbOrganisation-specific SSO configuration.
contractStarttimestampContract start date.
contractEndtimestampContract end date.
billingContactjsonbBilling contact information.
createdAttimestampRecord creation time.
updatedAttimestampLast modification time.
deletedAttimestampSoft-delete marker. Null when active.

Creating an Organisation

Partner-scoped and system-scoped users can create organisations.

When creating from a partner context, the partnerId is automatically set to the caller’s partner. You can omit it or provide it explicitly (it must match your own partner ID).

POST /api/v1/orgs/organizations
{
"name": "Contoso Ltd",
"slug": "contoso",
"type": "customer",
"status": "trial",
"maxDevices": 500,
"contractStart": "2026-03-01T00:00:00Z",
"contractEnd": "2027-03-01T00:00:00Z"
}

Returns the created organisation object with a 201 status.

Soft Deletes for Organisations

Like partners, organisations use soft deletes. Deleting an organisation sets the deletedAt timestamp. All queries exclude soft-deleted records.


Sites

A Site represents a physical or logical location within an organisation — for example, a branch office, data centre, or remote office.

Site Fields

FieldTypeDescription
iduuidAuto-generated primary key.
orgIduuidFK to the parent organisation. Required.
namevarchar(255)Display name. Required.
addressjsonbStructured address information (free-form JSON).
timezonevarchar(50)IANA timezone identifier. Defaults to UTC.
contactjsonbSite contact information (free-form JSON).
settingsjsonbSite-level settings.
createdAttimestampRecord creation time.
updatedAttimestampLast modification time.

Creating a Site

Organisation-scoped, partner-scoped, and system-scoped users can create sites. The caller must have access to the target organisation.

POST /api/v1/orgs/sites
{
"orgId": "<organization-uuid>",
"name": "Denver HQ",
"timezone": "America/Denver",
"address": {
"street": "123 Main St",
"city": "Denver",
"state": "CO",
"zip": "80202"
},
"contact": {
"name": "Site Manager",
"phone": "+13035551234"
}
}

Returns the created site object with a 201 status. If timezone is omitted, it defaults to UTC.


Multi-Tenant Access Control

All routes under /api/v1/orgs require JWT authentication. Access is governed by the caller’s scope, which is one of three levels:

ScopeDescription
systemFull access to all partners, organisations, and sites. No filtering.
partnerAccess is restricted to organisations the user has been granted access to (via partner_users.org_access and partner_users.org_ids).
organizationAccess is restricted to the user’s own organisation and its sites.

How Scoping Works

Every request passes through authMiddleware, which extracts the JWT payload and pre-computes:

  • accessibleOrgIds — a list of organisation UUIDs the caller can access (null for system scope, meaning no restriction).
  • canAccessOrg(orgId) — a helper function that checks whether a given org ID is in the accessible set.

Route handlers use these values to filter database queries. For example, listing organisations as a partner-scoped user returns only organisations whose IDs are in accessibleOrgIds. If the list is empty, an empty result set is returned immediately.

Scope Requirements by Resource

ResourceListCreateReadUpdateDelete
Partnerssystemsystemsystemsystemsystem
Partner Self-Service (/me)partnerpartner
Organisationspartner, systempartner, systempartner, systempartner, systempartner, system
Sitesorganization, partner, systemorganization, partner, systemorganization, partner, systemorganization, partner, systemorganization, partner, system

Organisation Access Enforcement

For sites, the API performs an explicit access check via ensureOrgAccess() before any read or write operation:

  • Organisation scope — the site’s orgId must match the caller’s orgId.
  • Partner scope — the site’s orgId must be in the caller’s canAccessOrg set.
  • System scope — always allowed.

If access is denied, the API returns 403 Access to this organization denied or 403 Access to this site denied.


Enrollment Keys

Enrollment keys are used to register new devices (agents) into an organisation and optionally a specific site.

FieldTypeDescription
iduuidAuto-generated primary key.
orgIduuidFK to the parent organisation. Required.
siteIduuidFK to a site. Optional — if set, enrolled devices are assigned to this site.
namevarchar(255)Human-readable label for the key.
keyvarchar(64)The unique enrollment key value.
usageCountintegerNumber of times this key has been used. Defaults to 0.
maxUsageintegerOptional limit on the number of times the key can be used.
expiresAttimestampOptional expiry date for the key.
createdByuuidThe user who created the key.
createdAttimestampRecord creation time.

Enrollment keys tie device registration to the organisational hierarchy. A key scoped to a site ensures that any device enrolling with that key is automatically placed in the correct site.


API Reference

All endpoints are mounted under /api/v1/orgs and require JWT authentication.

Partner Endpoints (System Scope)

These endpoints are restricted to system-scoped users.

MethodPathDescription
GET/orgs/partnersList all partners. Supports page and limit query params (default 50, max 100). Returns paginated results.
POST/orgs/partnersCreate a new partner.
GET/orgs/partners/:idGet a partner by ID. Returns 404 if not found or soft-deleted.
PATCH/orgs/partners/:idUpdate a partner. All fields are optional. Returns 400 if no fields provided.
DELETE/orgs/partners/:idSoft-delete a partner. Sets deletedAt. Returns { success: true }.

Partner Self-Service Endpoints (Partner Scope)

These endpoints allow partner-scoped users to view and update their own partner record.

MethodPathDescription
GET/orgs/partners/meGet the current user’s partner details. Requires partner scope and valid partnerId.
PATCH/orgs/partners/meUpdate the current user’s partner. Supports name, billingEmail, and settings (merged, not replaced).

Organisation Endpoints

MethodPathScopeDescription
GET/orgs/organization, partner, systemList organisations accessible to the caller. No pagination; returns all matching records ordered by name.
GET/orgs/organizationspartner, systemList organisations with pagination. Supports partnerId, page, and limit query params.
POST/orgs/organizationspartner, systemCreate a new organisation. Partner-scoped users do not need to specify partnerId.
GET/orgs/organizations/:idpartner, systemGet an organisation by ID. Partner-scoped users can only access their own organisations.
PATCH/orgs/organizations/:idpartner, systemUpdate an organisation. The partnerId cannot be changed. Returns 400 if no fields provided.
DELETE/orgs/organizations/:idpartner, systemSoft-delete an organisation. Sets deletedAt. Returns { success: true }.

Site Endpoints

MethodPathScopeDescription
GET/orgs/sitesorganization, partner, systemList sites. Supports orgId (or organizationId) filter plus page and limit query params. Without a filter, returns sites from all accessible organisations.
POST/orgs/sitesorganization, partner, systemCreate a new site. orgId is required. timezone defaults to UTC if omitted.
GET/orgs/sites/:idorganization, partner, systemGet a site by ID. Access is checked against the site’s parent organisation.
PATCH/orgs/sites/:idorganization, partner, systemUpdate a site. orgId cannot be changed. Returns 400 if no fields provided.
DELETE/orgs/sites/:idorganization, partner, systemHard-delete a site. The record is permanently removed. Returns { success: true }.

Pagination

Paginated endpoints (GET /partners, GET /organizations, GET /sites) accept these query parameters:

ParameterTypeDefaultMaxDescription
pagestring1Page number (1-based). Values below 1 are clamped to 1.
limitstring50100Number of records per page. Values outside 1—100 are clamped.

Paginated responses include a pagination object:

{
"data": [ ... ],
"pagination": {
"page": 1,
"limit": 50,
"total": 127
}
}

Request and Response Examples

Create a Partner

Terminal window
curl -X POST https://breeze.example.com/api/v1/orgs/partners \
-H "Authorization: Bearer <system-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme MSP",
"slug": "acme-msp",
"type": "msp",
"plan": "pro",
"billingEmail": "billing@acme-msp.com"
}'

Update Partner Settings (Self-Service)

Terminal window
curl -X PATCH https://breeze.example.com/api/v1/orgs/partners/me \
-H "Authorization: Bearer <partner-token>" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"timezone": "America/Chicago",
"businessHours": {
"preset": "business"
}
}
}'

Create an Organisation

Terminal window
curl -X POST https://breeze.example.com/api/v1/orgs/organizations \
-H "Authorization: Bearer <partner-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Contoso Ltd",
"slug": "contoso",
"type": "customer",
"status": "active",
"maxDevices": 500,
"contractStart": "2026-03-01T00:00:00Z",
"contractEnd": "2027-03-01T00:00:00Z"
}'

Create a Site

Terminal window
curl -X POST https://breeze.example.com/api/v1/orgs/sites \
-H "Authorization: Bearer <org-token>" \
-H "Content-Type: application/json" \
-d '{
"orgId": "<organization-uuid>",
"name": "Denver HQ",
"timezone": "America/Denver",
"address": {
"street": "123 Main St",
"city": "Denver",
"state": "CO",
"zip": "80202"
}
}'

List Sites with Organisation Filter

Terminal window
curl "https://breeze.example.com/api/v1/orgs/sites?orgId=<org-uuid>&page=1&limit=25" \
-H "Authorization: Bearer <token>"

Audit Logging

All create, update, and delete operations on partners, organisations, and sites are recorded in the audit log. The following actions are tracked:

ActionTrigger
partner.createA new partner is created.
partner.updateA partner’s fields are modified. Logged with changedFields.
partner.deleteA partner is soft-deleted.
partner.settings.updateA partner updates its own settings via /partners/me. Logged with changedFields.
organization.createA new organisation is created. Includes partnerId, status, and type in details.
organization.updateAn organisation’s fields are modified. Logged with changedFields.
organization.deleteAn organisation is soft-deleted.
site.createA new site is created.
site.updateA site’s fields are modified. Logged with changedFields.
site.deleteA site is deleted.

Troubleshooting

”Partner not found” (404)

The partner ID does not exist or the partner has been soft-deleted. Verify the UUID is correct. Soft-deleted partners are excluded from all queries and cannot be updated or deleted again.

”Organization not found” (404)

The organisation ID does not exist, has been soft-deleted, or the caller does not have access to it. Partner-scoped users receive a 404 (rather than 403) when attempting to access an organisation outside their scope, to avoid leaking information about the existence of other organisations.

”Access to this organization denied” (403)

The caller attempted to create or access a site in an organisation they do not have access to. Ensure the target orgId is in the user’s accessible organisation list. For partner-scoped users, check the orgAccess and orgIds fields on the partner_users record.

”Access to this site denied” (403)

The caller attempted to read, update, or delete a site that belongs to an organisation they cannot access. The access check is performed against the site’s parent orgId.

”Access denied to this partner” (403)

A partner-scoped user attempted to create an organisation under a different partner by providing a partnerId that does not match their own. Partner-scoped users can only create organisations under their own partner.

”Partner context required to create organizations” (400)

A partner-scoped user is missing their partnerId in the authentication context. This typically indicates a misconfigured user association. Verify the user has a valid partner_users record.

”partnerId is required for system scope” (400)

System-scoped users must explicitly provide partnerId when creating an organisation because they are not implicitly associated with any partner.

”No updates provided” (400)

A PATCH request was sent with an empty body or no recognised fields. Include at least one field to update.

Empty results when listing organisations or sites

  • Partner scope — the user’s accessibleOrgIds may be empty. Check the partner_users record for org_access set to none or selected with an empty org_ids array.
  • Organisation scope — the user may not have an orgId in their token. Verify the organization_users record exists.
  • Filtering — if a partnerId or orgId query parameter is specified, ensure it matches an existing, non-deleted record.

Site timezone not applied

The timezone field on a site is stored as a string (e.g. America/Denver) and must be a valid IANA timezone identifier. If omitted during creation, it defaults to UTC. To update the timezone, send a PATCH request with the timezone field.

Sites not deleted after organisation deletion

Soft-deleting an organisation does not cascade to its sites. Sites remain in the database. To clean up, list sites for the organisation and delete them individually before or after deleting the organisation. Note that sites are hard-deleted (permanently removed).