openapi: 3.1.0 info: title: Tenant Registry version: 0.1.0 description: | Internal platform API. Owns the multi-tenant glue: tenants, projects, product entitlements, API keys, audit log. See `PLATFORM_ARCHITECTURE.md §5c` for the schema, and `PRODUCT_INTEGRATION_SPEC.md §8.4` for the audit shape. This API is not yet authenticated — M4.3 adds Keycloak JWT validation. contact: email: oncall@breakpilot.com license: name: Proprietary servers: - url: http://localhost:8090 description: Local dev - url: https://tenant-registry.stage.breakpilot.com description: Stage - url: https://tenant-registry.breakpilot.com description: Production paths: /healthz: get: summary: Liveness probe responses: "200": description: Always returns ok. content: application/json: schema: type: object properties: { status: { type: string, example: ok } } /readyz: get: summary: Readiness probe — pings the store. responses: "200": description: Store reachable. "503": description: Store unreachable. content: application/json: { schema: { $ref: "#/components/schemas/Error" } } /v1/tenants: post: summary: Create a tenant. requestBody: required: true content: application/json: { schema: { $ref: "#/components/schemas/TenantCreate" } } responses: "201": description: Created. content: application/json: { schema: { $ref: "#/components/schemas/Tenant" } } "400": { $ref: "#/components/responses/BadRequest" } "409": { $ref: "#/components/responses/Conflict" } /v1/tenants/{id}: get: summary: Get tenant by id. parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: "200": description: Found. content: application/json: { schema: { $ref: "#/components/schemas/Tenant" } } "404": { $ref: "#/components/responses/NotFound" } /v1/tenants/by-slug/{slug}: get: summary: Get tenant by slug. parameters: - in: path name: slug required: true schema: { type: string, pattern: '^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$' } responses: "200": description: Found. content: application/json: { schema: { $ref: "#/components/schemas/Tenant" } } "404": { $ref: "#/components/responses/NotFound" } /v1/tenants/{id}/activate: post: summary: Move tenant to status=active. parameters: - in: path name: id required: true schema: { type: string, format: uuid } requestBody: required: false content: application/json: { schema: { $ref: "#/components/schemas/TenantActivate" } } responses: "200": description: Updated. content: application/json: { schema: { $ref: "#/components/schemas/Tenant" } } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } /v1/tenants/{id}/cancel: post: summary: Move tenant to status=frozen. parameters: - in: path name: id required: true schema: { type: string, format: uuid } requestBody: required: false content: application/json: { schema: { $ref: "#/components/schemas/TenantCancel" } } responses: "200": description: Updated. content: application/json: { schema: { $ref: "#/components/schemas/Tenant" } } "404": { $ref: "#/components/responses/NotFound" } /v1/entitlements: get: summary: List product entitlements for a tenant. parameters: - in: query name: tenant_id required: true schema: { type: string, format: uuid } responses: "200": description: List response. content: application/json: schema: type: object properties: items: type: array items: { $ref: "#/components/schemas/TenantProduct" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } /v1/catalog: get: summary: List the products available for any tenant to request. responses: "200": description: Catalog. content: application/json: schema: type: object properties: items: type: array items: { $ref: "#/components/schemas/CatalogEntry" } /v1/catalog/request: post: summary: Request a product (creates an audit event; sales follows up). requestBody: required: true content: application/json: { schema: { $ref: "#/components/schemas/CatalogRequest" } } responses: "202": description: Accepted. "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } /v1/catalog/trial-request: post: summary: Self-serve a 14-day trial. requestBody: required: true content: application/json: { schema: { $ref: "#/components/schemas/CatalogRequest" } } responses: "201": description: Trial entitlement created. content: application/json: { schema: { $ref: "#/components/schemas/TenantProduct" } } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } /v1/api-keys: get: summary: List API keys for a tenant. parameters: - in: query name: tenant_id required: true schema: { type: string, format: uuid } responses: "200": description: List response. The plaintext key is NOT returned; use the create endpoint and store the value immediately. content: application/json: schema: type: object properties: items: type: array items: { $ref: "#/components/schemas/APIKey" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } post: summary: Create an API key. The plaintext is returned ONCE. requestBody: required: true content: application/json: { schema: { $ref: "#/components/schemas/APIKeyCreate" } } responses: "201": description: Created. The plaintext field is shown only here. content: application/json: { schema: { $ref: "#/components/schemas/APIKeyCreated" } } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } /v1/api-keys/{id}: delete: summary: Revoke an API key (sets revoked_at). parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: "204": description: Revoked. "404": { $ref: "#/components/responses/NotFound" } /v1/internal/api-keys/verify: post: summary: Verify an API key. Used by headless products. Returns 200 with valid=false on any failure (never 401). requestBody: required: true content: application/json: schema: type: object required: [key] properties: key: { type: string } responses: "200": description: Verification result. content: application/json: { schema: { $ref: "#/components/schemas/APIKeyVerify" } } /v1/audit: post: summary: Append an audit event. requestBody: required: true content: application/json: { schema: { $ref: "#/components/schemas/AuditAppend" } } responses: "201": description: Created. content: application/json: { schema: { $ref: "#/components/schemas/AuditEvent" } } "400": { $ref: "#/components/responses/BadRequest" } get: summary: List audit events (paginated, cursor-based). parameters: - in: query name: tenant_id schema: { type: string, format: uuid } - in: query name: product schema: { type: string } - in: query name: actor_id schema: { type: string } - in: query name: action schema: { type: string } - in: query name: since schema: { type: string, format: date-time } - in: query name: until schema: { type: string, format: date-time } - in: query name: limit schema: { type: integer, minimum: 1, maximum: 500 } - in: query name: cursor schema: { type: integer, format: int64 } responses: "200": description: Paginated response. next_cursor is omitted when there are no more pages. content: application/json: schema: type: object properties: items: type: array items: { $ref: "#/components/schemas/AuditEvent" } next_cursor: type: integer format: int64 "400": { $ref: "#/components/responses/BadRequest" } components: responses: BadRequest: description: Input failed validation. content: application/json: { schema: { $ref: "#/components/schemas/Error" } } NotFound: description: Resource does not exist. content: application/json: { schema: { $ref: "#/components/schemas/Error" } } Conflict: description: Resource already exists / unique violation. content: application/json: { schema: { $ref: "#/components/schemas/Error" } } schemas: Error: type: object required: [error] properties: error: { type: string, example: invalid_input } message: { type: string } Tenant: type: object required: [id, slug, name, status, kind, plan, created_at, updated_at] properties: id: { type: string, format: uuid } slug: { type: string } name: { type: string } status: { type: string, enum: [demo, trial, active, frozen, archived] } kind: { type: string, enum: [customer, demo] } plan: { type: string } erp_customer_id: { type: string } stripe_cust_id: { type: string } trial_ends_at: { type: string, format: date-time, nullable: true } contract_start: { type: string, format: date, nullable: true } contract_end: { type: string, format: date, nullable: true } sales_owner: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } TenantCreate: type: object required: [slug, name] properties: slug: { type: string, pattern: '^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$' } name: { type: string, minLength: 1, maxLength: 255 } plan: { type: string, default: starter } kind: { type: string, enum: [customer, demo], default: customer } sales_owner: { type: string } TenantActivate: type: object properties: plan: { type: string } contract_start: { type: string, format: date } contract_end: { type: string, format: date } erp_customer_id: { type: string } TenantCancel: type: object properties: reason: { type: string } at_period_end: { type: boolean } TenantProduct: type: object required: [tenant_id, product, enabled, config, created_at, updated_at] properties: tenant_id: { type: string, format: uuid } product: { type: string } enabled: { type: boolean } config: { type: object, additionalProperties: true } expires_at: { type: string, format: date-time, nullable: true } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } CatalogEntry: type: object required: [key, name, description, plans_required, supports_trial] properties: key: { type: string } name: { type: string } description: { type: string } plans_required: { type: array, items: { type: string } } demo_url: { type: string } supports_trial: { type: boolean } CatalogRequest: type: object required: [tenant_id, product] properties: tenant_id: { type: string, format: uuid } product: { type: string } APIKey: type: object required: [id, tenant_id, name, scopes, prefix, created_at] properties: id: { type: string, format: uuid } tenant_id: { type: string, format: uuid } product: { type: string } name: { type: string } scopes: { type: array, items: { type: string } } prefix: { type: string } created_by: { type: string } last_used_at: { type: string, format: date-time, nullable: true } revoked_at: { type: string, format: date-time, nullable: true } created_at: { type: string, format: date-time } APIKeyCreate: type: object required: [tenant_id, name] properties: tenant_id: { type: string, format: uuid } name: { type: string, maxLength: 100 } product: { type: string } scopes: { type: array, items: { type: string } } created_by: { type: string } APIKeyCreated: type: object required: [api_key, plaintext, warning] properties: api_key: { $ref: "#/components/schemas/APIKey" } plaintext: { type: string } warning: { type: string } APIKeyVerify: type: object required: [valid] properties: valid: { type: boolean } tenant_id: { type: string, format: uuid } product: { type: string } scopes: { type: array, items: { type: string } } AuditAppend: type: object required: [action] properties: tenant_id: { type: string, format: uuid } project_id: { type: string, format: uuid } actor_id: { type: string } actor_name: { type: string } actor_type: { type: string } action: { type: string } target_id: { type: string } target_type: { type: string } target_name: { type: string } product: { type: string } metadata: { type: object, additionalProperties: true } AuditEvent: allOf: - $ref: "#/components/schemas/AuditAppend" - type: object required: [id, created_at] properties: id: { type: integer, format: int64 } source_ip: { type: string } user_agent: { type: string } created_at: { type: string, format: date-time }