Skip to main content
Baobyte AML Suite
Documentation contents

API reference

Platform

18 operations in this area of the contract. Every one declares the permission it requires.

Contract version 0.2.0All areas

Operations

GET/v1/admin/commercial/offers

List immutable commercial offer versions

Required permission: platform.commercial.managelistCommercialOffers

Platform catalogue; tenant users can see only the version linked to their own agreement.

Show parameters and responses

Parameters

NameInTypeDescription
limitqueryintegerPage size, capped at 200.

Responses

  • 200Offer versionsCommercialOfferPage
POST/v1/admin/commercial/offers

Create an immutable offer version

Required permission: platform.commercial.managecreateCommercialOffer

Records supplied terms without inventing a price. Money is optional integer minor units plus a supported ISO currency. SaaS supports monthly/annual subscriptions and optional allowances/rates. On-prem supports one-time/annual licence terms and cannot commit without a separately typed annual support/maintenance component. Add-ons and professional services remain separate components.

Show parameters and responses

Responses

  • 201Immutable offer versionCommercialOffer
  • 400—
  • 409The offer key/version already exists
POST/v1/admin/key-rotations

Propose a tenant subject-DEK root-key rewrap ceremony

Required permission: platform.key_rotation.manageproposeSubjectDEKRotation

The source must be the active configured root and the target must already be configured but inactive. A distinct system administrator approves the resulting maker-checker request. Approval installs an irreversible write barrier for the source while it is still active; it does not switch keys or rewrap data. New subject and audit-anchor writes may therefore refuse during the deliberately short operator cutover. There is no cancel/unblock operation and this endpoint never authorizes physical root removal.

Show parameters and responses

Responses

  • 201Pending four-eyes approval; no barrier or wrapper has changed yetApprovalRequest
  • 409A proposal is already pending or the source/target state is not valid
  • 503The provider cannot enumerate every configured root, so rotation refuses fail closed
GET/v1/admin/key-rotations/{rotationID}

Read rotation progress, readiness and retirement dependencies

Required permission: platform.key_rotation.managegetSubjectDEKRotation

Distinguishes the expected approved-awaiting-key-switch phase from a runnable or blocked configuration. Dependency counts are tenant-scoped. Immutable historical identifier blind indexes and audit anchors normally keep `tenant_dependencies_clear` false, and every tenant would have to be assessed before a process-wide software root could even be considered for a separate retirement ceremony. This API always reports `physical_retirement_supported: false`.

Show parameters and responses

Parameters

NameInTypeDescription
rotationIDrequiredpathstring—

Responses

  • 200Durable rotation cursor and fail-closed readinessSubjectDEKRotationReadiness
  • 404—
POST/v1/admin/key-rotations/{rotationID}/run

Start or resume bounded subject-DEK wrapper batches

Required permission: platform.key_rotation.managerunSubjectDEKRotation

Refuses until the approved target is active, the source remains configured, provider enumeration is complete and both key fingerprints still match approval. The worker authenticates and rewraps at most 100 subject-DEK envelopes per transaction, records immutable per-subject evidence, and transactionally enqueues the next page. It never decrypts or rewrites subject fields. Repeating this call while work is active is idempotent. It does not remove the source root.

Show parameters and responses

Parameters

NameInTypeDescription
rotationIDrequiredpathstring—

Responses

  • 202Bounded resumable work acceptedSubjectDEKRotationReadiness
  • 404—
  • 409Awaiting target activation, blocked, or already complete
GET/v1/admin/tenants/{tenantID}/commercial

One tenant's commercial summary

Required permission: platform.commercial.managegetAdminCommercialSummary

Vendor control-plane read. It does not grant access to customer/AML records.

Show parameters and responses

Parameters

NameInTypeDescription
tenantIDrequiredpathstring—

Responses

  • 200Recorded terms and usage coverageCommercialSummary
POST/v1/admin/tenants/{tenantID}/commercial/agreements

Record a tenant commercial agreement

Required permission: platform.commercial.managecreateCommercialAgreement

Links an immutable offer version to a tenant, with an explicit half-open term and IANA billing timezone. Selected components must belong to that exact offer; on-prem terms require selected annual support. This operation records commercial state only—it does not take payment, issue an invoice, enable a vendor feed or change tenant lifecycle.

Show parameters and responses

Parameters

NameInTypeDescription
tenantIDrequiredpathstring—

Responses

  • 201Agreement and first immutable history revisionCommercialAgreement
  • 400—
  • 404—
  • 409A live agreement or agreement reference already exists
GET/v1/admin/tenants/{tenantID}/commercial/agreements/{agreementID}/history

Immutable history for a managed tenant agreement

Required permission: platform.commercial.managelistAdminCommercialAgreementHistory
Show parameters and responses

Parameters

NameInTypeDescription
tenantIDrequiredpathstring—
agreementIDrequiredpathstring—
limitqueryintegerPage size, capped at 200.

Responses

  • 200Typed agreement revisions, newest firstCommercialHistoryPage
POST/v1/admin/tenants/{tenantID}/commercial/agreements/{agreementID}/status

Transition commercial agreement status

Required permission: platform.commercial.manageupdateCommercialAgreementStatus

Follows the closed lifecycle graph and appends an immutable revision. Past due and disputed are visible commercial states, never automatic AML suspension or quota gates.

Show parameters and responses

Parameters

NameInTypeDescription
tenantIDrequiredpathstring—
agreementIDrequiredpathstring—

Responses

  • 200Agreement in its new commercial stateCommercialAgreement
  • 400—
  • 404—
  • 409Concurrent revision conflict
GET/v1/commercial/agreements/{agreementID}/history

Immutable history for this tenant's agreement

Required permission: tenancy.commercial.readlistCommercialAgreementHistory
Show parameters and responses

Parameters

NameInTypeDescription
agreementIDrequiredpathstring—
limitqueryintegerPage size, capped at 200.

Responses

  • 200Typed agreement revisions, newest firstCommercialHistoryPage
GET/v1/commercial/summary

This tenant's commercial summary

Required permission: tenancy.commercial.readgetCommercialSummary

Own-tenant, read-only view of the recorded agreement, immutable offer version, entitlement-only components, commercial status and usage evidence. Missing agreement or measurement is labelled unavailable rather than rendered as zero. A sanctions or adverse-media add-on is never proof that a provider feed is licensed or connected.

This is not an invoice or payment statement, and a past-due/disputed status does not suspend AML, screening or ingestion. Suspension remains the separate explicit tenant lifecycle operation.

Show parameters and responses

Responses

  • 200Recorded commercial terms and coverageCommercialSummary
GET/v1/tenants

Tenants visible to the caller

Required permission: tenancy.tenant.readlistTenants

**Your own tenant, unless you are a platform operator.** `tenancy.tenant.read` is a read of your own institution's record and returns exactly one row. Seeing the estate requires `tenancy.tenant.write`.

That distinction is a fix, not a nicety. This operation previously ran an unfiltered list inside a platform transaction and returned it to anybody holding `tenancy.tenant.read` — which the ng-core pack grants to `system_admin`, `executive` and `auditor`. Three of the seven BRD §4.1 personas could enumerate every other institution on the installation: slug, legal name, segment, data residency and status.

The estate view is resumable with `cursor`, keyed on (`created_at`, `id`). The single-tenant view returns exactly one row and carries no cursor, because a cursor on your own record would claim there is more of it.

Show parameters and responses

Parameters

NameInTypeDescription
statusqueryprovisioning | active | suspended | closed—
limitqueryintegerPage size, capped at 200.
cursorquerystringOpaque keyset cursor from a previous response. Offset pagination is deliberately not offered: `OFFSET 500000` is a table scan, and if it were offered it would be used (ADR-0012). Only the collections that document this parameter can be resumed. The rest report `truncated: true` and refuse a cursor rather than silently returning the first page again — see the `truncated` field on the page schemas.

Responses

  • 200TenantsTenantPage
POST/v1/tenants

Provision an institution

Required permission: tenancy.tenant.writecreateTenant

The platform control plane. Creates the tenant in **`provisioning`**, never `active`: the authenticator refuses every request from a tenant that is not active, so a tenant left provisioning cannot serve traffic until a content pack has been installed and an administrator provisioned. Creating it active would open a door into an installation with no roles in it (Q11).

`slug` is the subdomain and cannot be changed afterwards — every IdP redirect URI and webhook callback binds to it, so renaming is a migration with a cutover rather than an edit.

Show parameters and responses

Responses

  • 201Provisioned, awaiting activationTenant
  • 400The slug is malformed or reserved
  • 409That slug is already in use
GET/v1/tenants/{tenantID}

One tenant

Required permission: tenancy.tenant.readgetTenant

Without `tenancy.tenant.write` a caller may read only their own tenant, and asking for another id returns the same `404` a non-existent id returns. A `403` would confirm the tenant exists, which is the fact being withheld.

Show parameters and responses

Parameters

NameInTypeDescription
tenantIDrequiredpathstring—

Responses

  • 200The tenantTenant
  • 404—
PATCH/v1/tenants/{tenantID}

Correct a tenant's metadata

Required permission: tenancy.tenant.writeupdateTenant

Replaces display name, segment and residency together rather than patching single fields. A partial update over three columns needs three "was this key present" checks and the one that gets forgotten silently blanks a column — `data_residency` defaulting itself back to empty would put a tenant outside the BRD §9 residency check while looking like a rename.

Status is **not** settable here. See `POST /v1/tenants/{tenantID}/status`.

Show parameters and responses

Parameters

NameInTypeDescription
tenantIDrequiredpathstring—

Responses

  • 200The updated tenantTenant
  • 422—
POST/v1/tenants/{tenantID}/status

Suspend, resume or close a tenant

Required permission: tenancy.tenant.writesetTenantStatus

Its own operation rather than a field on the PATCH, because the blast radius is different in kind: a suspended tenant has **every** request refused by the authenticator with `tenant-inactive` from the moment this returns. An act with that reach should not be reachable by a form that also fixes a typo in the display name.

A `reason` is required for anything but a resume. The audit row is the only place the "why" survives, and "who suspended this bank, and why" is asked after the fact.

Show parameters and responses

Parameters

NameInTypeDescription
tenantIDrequiredpathstring—

Responses

  • 200The tenant in its new stateTenant
  • 422—
GET/v1/usage

Recorded usage for this tenant

Required permission: platform.usage.readlistUsage

Returns measurements that have already been **recorded**, newest period first. A period nobody measured comes back absent, not zero: those are different facts, and conflating them reports a quiet month for a tenant whose measurement simply never ran.

These rows are usage evidence, not invoices or payments. Open rows may be corrected; closed rows name the IANA billing timezone and cannot be changed.

Show parameters and responses

Parameters

NameInTypeDescription
period_startquerystringInclusive lower bound, RFC 3339. A timestamp rather than a date, because `2026-08-01` means two different instants in Lagos and in UTC.
period_endquerystringExclusive upper bound, RFC 3339. The window is half-open.
limitqueryintegerPage size, capped at 200.

Responses

  • 200Recorded measurementsUsagePage
POST/v1/usage/measurements

Measure and record a period

Required permission: platform.usage.readmeasureUsage

`POST`, not `GET`, because it **writes**. The snapshot is the fact; the query that produced it is not repeatable once retention has purged the source rows, so a live count would let a closed period's usage change retroactively or vanish.

The window is **half-open**: `[period_start, period_end)`. A closed range double-counts a transaction booked exactly at a boundary midnight into two months. Timestamps rather than dates, because `2026-08-01` is two different instants in Lagos and in UTC — an hour of a bank's transactions either side of an invoice line.

Re-measuring an open period corrects the recorded row in place rather than appending a second one and is refused after closure. Maximum window: one year. A daily run-on-start worker closes the previous local calendar month from the agreement's explicit IANA timezone; its half-open period key makes retries idempotent.

Show parameters and responses

Parameters

NameInTypeDescription
period_startrequiredquerystringInclusive lower bound, RFC 3339.
period_endrequiredquerystringExclusive upper bound, RFC 3339. A transaction booked exactly here belongs to the next period.

Responses

  • 201The measurements recorded for the periodUsagePage
  • 400The window is empty, inverted or longer than a year
  • 422—