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.


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.


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

Type Description
msp Managed Service Provider managing multiple customer organisations. Default.
enterprise A single enterprise managing its own infrastructure.
internal Internal IT team or development environment.
Plan Description
free Free tier. Default.
starter Starter tier for initial paid onboarding.
community Community tier with basic paid features.
pro Professional tier with expanded limits.
enterprise Enterprise tier with advanced features.
unlimited No enforced limits.
Field Type Description
id uuid Auto-generated primary key.
name varchar(255) Display name. Required.
slug varchar(100) URL-safe unique identifier. Required.
type enum One of msp, enterprise, internal. Defaults to msp.
plan enum One of free, starter, community, pro, enterprise, unlimited. Defaults to free.
status enum One of pending, active, suspended, churned. Defaults to pending for new registrations, active when provisioned by an admin. Partners with non-active status are blocked by the partner guard middleware.
maxOrganizations integer Optional cap on the number of organisations.
maxDevices integer Optional cap on total devices across all organisations.
settings jsonb Arbitrary partner-level settings (see below).
ssoConfig jsonb SSO configuration blob.
billingEmail varchar(255) Billing contact email address.
createdAt timestamp Record creation time.
updatedAt timestamp Last modification time.
deletedAt timestamp Soft-delete marker. Null when active.

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

Setting Type Description
timezone string Default timezone for the partner (e.g. America/New_York).
dateFormat enum MM/DD/YYYY, DD/MM/YYYY, or YYYY-MM-DD.
timeFormat enum 12h or 24h.
language string Language code. Currently only en is supported.
businessHours.preset enum 24/7, business, extended, or custom.
businessHours.custom object Per-day schedule when preset is custom. Each day has start, end (time strings), and optional closed boolean.
contact.name string Primary contact name.
contact.email string Primary contact email.
contact.phone string Primary contact phone.
contact.website string Partner website URL.

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

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.

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.


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

Type Description
customer An external customer managed by the partner. Default.
internal An internal team or testing environment within the partner.
Status Description
active Fully operational. Default.
suspended Access is restricted. Devices may still report but management actions are limited.
trial Evaluation period.
churned Customer has left. Retained for historical data.
offboarding Being wound down.
merging A merge into a surviving organisation is in progress.
archived Hidden from the active list and read-only. Restorable.
purging Archived data is being permanently deleted.
Field Type Description
id uuid Auto-generated primary key.
partnerId uuid FK to the parent partner. Required.
name varchar(255) Display name. Required.
slug varchar(100) URL-safe identifier. Required.
type enum One of customer, internal. Defaults to customer.
status enum One of active, suspended, trial, churned. Defaults to active.
maxDevices integer Optional device cap for this organisation.
settings jsonb Organisation-level settings.
ssoConfig jsonb Organisation-specific SSO configuration.
contractStart timestamp Contract start date.
contractEnd timestamp Contract end date.
billingContact jsonb Billing contact information.
createdAt timestamp Record creation time.
updatedAt timestamp Last modification time.
deletedAt timestamp Soft-delete marker. Null when active.

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.

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

Archiving retires an organisation without erasing it. It is distinct from a soft delete: the organisation moves to archived status, and the API blocks writes against it until it is restored.

Archiving an organisation:

  • Hides it from the organisation list and search.
  • Leaves existing data intact but read-only — the organisation’s settings page shows a banner and refuses saves.
  • Uninstalls the agents on its devices.
  • Stops billing for that organisation.
  • Optionally schedules a permanent purge after a retention period you choose. You can also keep the data indefinitely, in which case it is never purged automatically.

Restoring is the undo, available any time before the purge runs.

Method Endpoint Description
POST /orgs/organizations/:id/archive Archive the organisation. Accepts the retention choice.
POST /orgs/organizations/:id/restore Restore an archived organisation to active.

Both require partner scope, organizations:write, and a satisfied MFA challenge — an API key or session without MFA is rejected.

Merging moves one organisation’s data into a surviving organisation. Unlike archiving, it is permanent: the merged organisation is retired and cannot be recovered.

Preview first. The preview reports how many rows would move and which would be dropped, so you can see the shape of the merge before committing. A merge above the automatic size limit is refused and needs to be scheduled manually with support.

Method Endpoint Description
POST /orgs/organizations/:id/merge-preview Report what a merge into a chosen survivor would move and drop. Changes nothing.
POST /orgs/organizations/:id/merge Run the merge. Returns a job to poll.
GET /orgs/organizations/merge-runs/:jobId Poll a running merge.

Merging carries the same partner-scope, organizations:write and MFA requirements as archiving. In the dashboard you must also type the organisation’s name to confirm.


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

Field Type Description
id uuid Auto-generated primary key.
orgId uuid FK to the parent organisation. Required.
name varchar(255) Display name. Required.
address jsonb Structured address information (free-form JSON).
timezone varchar(50) IANA timezone identifier. Defaults to UTC.
contact jsonb Site contact information (free-form JSON).
settings jsonb Site-level settings.
createdAt timestamp Record creation time.
updatedAt timestamp Last modification time.

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"
}
}

Only orgId and name are required. timezone, address, contact, and settings are all optional and can be filled in or changed later via PATCH; the minimal valid payload is just { "orgId": "...", "name": "..." }. Returns the created site object with a 201 status. If timezone is omitted, it defaults to UTC.


A Contact is a person attached to an organisation – a billing contact, a technical escalation, an after-hours number. Contacts belong to the organisation, and each one is either organisation-level or pinned to a single site within that organisation via siteId.

Field Type Description
id uuid Auto-generated primary key.
orgId uuid FK to the parent organisation. Required.
siteId uuid Optional FK to a site in the same organisation. Null means an organisation-level contact.
name varchar(255) Display name.
email varchar(320) Stored lower-cased.
phone varchar(64) Primary telephone number.
mobile varchar(64) Mobile number.
title varchar(255) Job title.
roles text[] Validated role vocabulary. Defaults to empty.
isPrimary boolean Marks the headline contact for its scope. Defaults to false.
notes text Free text, up to 5,000 characters.
createdAt timestamp Record creation time.
updatedAt timestamp Last modification time.

Every contact needs an identifier. At least one of name, email, phone, or mobile must be non-blank – a contact known only by an email address is a real contact, but an empty one is not. It is enforced by a database constraint as well as request validation, so nothing can write past it, and violating it returns 400 with code: "no-identifier".

Roles are a fixed vocabulary: billing, technical, escalation, admin, site, after_hours, and portal. A contact can hold any number of them. An unrecognised role returns 400 with code: "invalid-role", and a siteId belonging to a different organisation returns 400 with code: "site-not-in-org".

One primary per scope. isPrimary marks the headline contact for its scope, where a scope is either the organisation (siteId null) or one site. Setting it demotes whichever contact previously held it in that same scope – so an organisation can have one organisation-level primary plus one primary per site, and never two in the same place.

On PATCH, an explicit null clears a field and an omitted key leaves it alone. That distinction is the reason clearing a field lives here rather than in the contacts importer, which merges and never clears.


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

Scope Description
system Full access to all partners, organisations, and sites. No filtering.
partner Access is restricted to organisations the user has been granted access to (via partner_users.org_access and partner_users.org_ids).
organization Access is restricted to the user’s own organisation and its sites.

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.

Resource List Create Read Update Delete
Partners system system system system system
Partner Self-Service (/me) partner partner
Organisations partner, system partner, system partner, system partner, system partner, system
Sites organization, partner, system organization, partner, system organization, partner, system organization, partner, system organization, partner, system

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 are used to register new devices (agents) into an organisation and optionally a specific site.

Field Type Description
id uuid Auto-generated primary key.
orgId uuid FK to the parent organisation. Required.
siteId uuid FK to a site. Optional – if set, enrolled devices are assigned to this site.
name varchar(255) Human-readable label for the key.
key varchar(64) The unique enrollment key value.
usageCount integer Number of times this key has been used. Defaults to 0.
maxUsage integer Optional limit on the number of times the key can be used.
expiresAt timestamp Optional expiry date for the key.
createdBy uuid The user who created the key.
createdAt timestamp Record 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.


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

These endpoints are restricted to system-scoped users.

Method Path Description
GET /orgs/partners List all partners. Supports page and limit query params (default 50, max 100). Returns paginated results.
POST /orgs/partners Create a new partner.
GET /orgs/partners/:id Get a partner by ID. Returns 404 if not found or soft-deleted.
PATCH /orgs/partners/:id Update a partner. All fields are optional. Returns 400 if no fields provided.
DELETE /orgs/partners/:id Soft-delete a partner. Sets deletedAt. Returns { success: true }.

Partner Self-Service Endpoints (Partner Scope)

Section titled “Partner Self-Service Endpoints (Partner Scope)”

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

Method Path Description
GET /orgs/partners/me Get the current user’s partner details. Requires partner scope and valid partnerId.
PATCH /orgs/partners/me Update the current user’s partner. Supports name, billingEmail, and settings (merged, not replaced).
Method Path Scope Description
GET /orgs/ organization, partner, system List organisations accessible to the caller. No pagination; returns all matching records ordered by name.
GET /orgs/organizations partner, system List organisations with pagination. Supports partnerId, page, and limit query params.
POST /orgs/organizations partner, system Create a new organisation. Partner-scoped users do not need to specify partnerId.
GET /orgs/organizations/:id partner, system Get an organisation by ID. Partner-scoped users can only access their own organisations.
PATCH /orgs/organizations/:id partner, system Update an organisation. The partnerId cannot be changed. Returns 400 if no fields provided.
DELETE /orgs/organizations/:id partner, system Soft-delete an organisation. Sets deletedAt. Returns { success: true }.
PATCH /orgs/organizations/order partner Set the partner’s preferred organisation display order. Body { "organizationOrder": ["<org-uuid>", ...] }; unknown or deleted IDs are dropped, omitted orgs are appended in registration order. Persisted to partner settings and reflected by the list endpoints. Returns the sanitised { organizationOrder }.
Method Path Scope Description
GET /orgs/sites organization, partner, system List sites. Supports orgId (or organizationId) filter plus page and limit query params. Without a filter, returns sites from all accessible organisations.
POST /orgs/sites organization, partner, system Create a new site. orgId is required. timezone defaults to UTC if omitted.
GET /orgs/sites/:id organization, partner, system Get a site by ID. Access is checked against the site’s parent organisation.
PATCH /orgs/sites/:id organization, partner, system Update a site. orgId cannot be changed. Returns 400 if no fields provided.
DELETE /orgs/sites/:id organization, partner, system Hard-delete a site. The record is permanently removed. Returns { success: true }.
Method Path Scope Description
GET /orgs/organizations/:id/contacts organization, partner, system List an organisation’s contacts. Supports siteId (a site UUID, or the literal none for organisation-level contacts only), role, page, and limit.
POST /orgs/organizations/:id/contacts organization, partner, system Create a contact. Returns the created record with a 201 status.
PATCH /orgs/contacts/:contactId organization, partner, system Update a contact. No organisation in the path. An explicit null clears a field; an omitted key leaves it alone.
DELETE /orgs/contacts/:contactId organization, partner, system Delete a contact. Returns { success: true }.
POST /orgs/contacts/import/preview organization, partner, system Annotate a batch of contact rows against existing state without writing anything. Up to 1,000 rows.
POST /orgs/contacts/import organization, partner, system Commit a previewed batch. Always answers 200 with { imported, updated, skipped, errors }, including on a partial failure.

Reads require organizations:read. Every write requires organizations:write and a satisfied MFA challenge. sites:write is deliberately not required, even for a contact pinned to a site: contacts are organisation-owned records, and whoever can write the organisation can manage its people. The site axis is still enforced separately – a caller confined to a subset of an organisation’s sites cannot reach a contact pinned outside that subset.

Neither import path is organisation-scoped, because each row names its own organisation. See Recipe 1c in the Migration Toolkit for the row fields, the annotation vocabulary, and the acknowledgement handshake.

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

Parameter Type Default Max Description
page string 1 Page number (1-based). Values below 1 are clamped to 1.
limit string 50 100 Number of records per page. Values outside 1–100 are clamped.

Paginated responses include a pagination object:

{
"data": [ ... ],
"pagination": {
"page": 1,
"limit": 50,
"total": 127
}
}
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"
}'
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"
}
}
}'
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"
}'
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"
}
}'
Terminal window
curl -X POST https://breeze.example.com/api/v1/orgs/organizations/<org-uuid>/contacts \
-H "Authorization: Bearer <partner-token>" \
-H "Content-Type: application/json" \
-d '{
"siteId": "<site-uuid>",
"name": "Dana Reyes",
"email": "dana.reyes@contoso.example",
"phone": "+13035551234",
"title": "Office Manager",
"roles": ["billing", "escalation"],
"isPrimary": true
}'

Omit siteId (or send null) for an organisation-level contact.

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

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

Action Trigger
partner.create A new partner is created.
partner.update A partner’s fields are modified. Logged with changedFields.
partner.delete A partner is soft-deleted.
partner.settings.update A partner updates its own settings via /partners/me. Logged with changedFields.
organization.create A new organisation is created. Includes partnerId, status, and type in details.
organization.update An organisation’s fields are modified. Logged with changedFields.
organization.delete An organisation is soft-deleted.
site.create A new site is created.
site.update A site’s fields are modified. Logged with changedFields.
site.delete A site is deleted.

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.

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)

Section titled ““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.

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.

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)

Section titled ““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)

Section titled ““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.

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

Section titled “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.

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

Section titled “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).