diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..1f9d27d --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,1830 @@ +openapi: 3.0.3 +info: + title: php-tenant API + description: | + REST API for the Core PHP Tenant module. Provides workspace management, + entitlement provisioning, cross-app entitlement checks, and webhook management. + + ## Authentication + + Endpoints use one of two authentication methods: + + - **Session auth** (`auth` middleware) — cookie-based session authentication + for browser clients. + - **API key auth** (`api.auth` middleware) — pass an API key via + `Authorization: Bearer hk_xxx` header. Used for server-to-server + communication and external integrations. + + Provisioning endpoints (Blesta) require API key auth with + `entitlements.write` scope. + + ## Rate Limits + + Provisioning endpoints are rate-limited to 60 requests per minute per API key. + version: 1.0.0 + contact: + name: Core Team + license: + name: Proprietary + +servers: + - url: /api + description: API base path (session-authenticated workspace routes) + - url: /api/v1 + description: API v1 base path (cross-app entitlement routes) + - url: /api/provisioning + description: Provisioning base path (Blesta entitlement routes) + +tags: + - name: Workspaces + description: Workspace CRUD and switching (session auth) + - name: Workspaces (API Key) + description: Read-only workspace access via API key + - name: Entitlement Provisioning + description: >- + Blesta provisioning API for creating, suspending, cancelling, and + renewing entitlements. Requires API key with entitlements.write scope. + - name: Cross-App Entitlements + description: >- + Cross-application entitlement checks and usage recording. + Used by external services (e.g. BioHost) to verify feature access. + - name: Entitlement Webhooks + description: >- + Webhook management for entitlement events. Supports CRUD, testing, + secret rotation, circuit-breaker reset, and delivery history. + +paths: + # ========================================================================== + # Workspaces API (Session Auth) + # ========================================================================== + + /workspaces: + get: + operationId: listWorkspaces + summary: List workspaces + description: List all workspaces the authenticated user has access to. + tags: [Workspaces] + security: + - sessionAuth: [] + parameters: + - name: type + in: query + description: Filter by workspace type. + schema: + type: string + enum: [personal, team, agency, custom] + - name: is_active + in: query + description: Filter by active status. + schema: + type: boolean + - name: search + in: query + description: Search workspaces by name. + schema: + type: string + - name: per_page + in: query + description: Results per page (max 100). + schema: + type: integer + default: 25 + maximum: 100 + responses: + '200': + description: Paginated list of workspaces. + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedWorkspaces' + '401': + $ref: '#/components/responses/Unauthenticated' + + post: + operationId: createWorkspace + summary: Create a workspace + description: Create a new workspace. The authenticated user becomes the owner. + tags: [Workspaces] + security: + - sessionAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWorkspaceRequest' + responses: + '200': + description: Workspace created. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceResource' + '401': + $ref: '#/components/responses/Unauthenticated' + '422': + $ref: '#/components/responses/ValidationError' + + /workspaces/current: + get: + operationId: getCurrentWorkspace + summary: Get current workspace + description: Get the user's currently active (default) workspace. + tags: [Workspaces] + security: + - sessionAuth: [] + responses: + '200': + description: Current workspace. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceResource' + '401': + $ref: '#/components/responses/Unauthenticated' + '404': + description: No workspace found. + + /workspaces/{workspace}: + get: + operationId: getWorkspace + summary: Get a workspace + description: Get a single workspace by ID. User must have access. + tags: [Workspaces] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WorkspaceId' + responses: + '200': + description: Workspace details. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceResource' + '401': + $ref: '#/components/responses/Unauthenticated' + '404': + $ref: '#/components/responses/NotFound' + + put: + operationId: updateWorkspace + summary: Update a workspace + description: Update workspace details. Requires owner or admin role. + tags: [Workspaces] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WorkspaceId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateWorkspaceRequest' + responses: + '200': + description: Workspace updated. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceResource' + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + '422': + $ref: '#/components/responses/ValidationError' + + delete: + operationId: deleteWorkspace + summary: Delete a workspace + description: >- + Delete a workspace. Only the owner can delete. Cannot delete the + user's only workspace. + tags: [Workspaces] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WorkspaceId' + responses: + '204': + description: Workspace deleted. + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + '422': + description: Cannot delete the user's only workspace. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: cannot_delete + message: + type: string + example: You cannot delete your only workspace. + + /workspaces/{workspace}/switch: + post: + operationId: switchWorkspace + summary: Switch to a workspace + description: Set a workspace as the user's default (active) workspace. + tags: [Workspaces] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WorkspaceId' + responses: + '200': + description: Switched to workspace. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceResource' + '401': + $ref: '#/components/responses/Unauthenticated' + '404': + $ref: '#/components/responses/NotFound' + + # ========================================================================== + # Workspaces API (API Key Auth — read-only) + # ========================================================================== + + /workspaces#apikey: + get: + operationId: listWorkspacesApiKey + summary: List workspaces (API key) + description: List workspaces accessible to the API key holder. + tags: [Workspaces (API Key)] + security: + - apiKeyAuth: [] + parameters: + - name: type + in: query + schema: + type: string + enum: [personal, team, agency, custom] + - name: is_active + in: query + schema: + type: boolean + - name: search + in: query + schema: + type: string + - name: per_page + in: query + schema: + type: integer + default: 25 + maximum: 100 + responses: + '200': + description: Paginated list of workspaces. + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedWorkspaces' + '401': + $ref: '#/components/responses/Unauthenticated' + + /workspaces/current#apikey: + get: + operationId: getCurrentWorkspaceApiKey + summary: Get current workspace (API key) + description: Get the current workspace for the API key holder. + tags: [Workspaces (API Key)] + security: + - apiKeyAuth: [] + responses: + '200': + description: Current workspace. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceResource' + '401': + $ref: '#/components/responses/Unauthenticated' + + /workspaces/{workspace}#apikey: + get: + operationId: getWorkspaceApiKey + summary: Get a workspace (API key) + description: Get a single workspace by ID via API key. + tags: [Workspaces (API Key)] + security: + - apiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/WorkspaceId' + responses: + '200': + description: Workspace details. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceResource' + '401': + $ref: '#/components/responses/Unauthenticated' + '404': + $ref: '#/components/responses/NotFound' + + # ========================================================================== + # Entitlement Provisioning API (Blesta) + # ========================================================================== + + /entitlements: + post: + operationId: createEntitlement + summary: Create an entitlement + description: >- + Provision a new entitlement for a workspace. Finds or creates the user + by email, creates a workspace if needed, and provisions the package. + New users receive an email verification notification. + tags: [Entitlement Provisioning] + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEntitlementRequest' + responses: + '201': + description: Entitlement created. + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEntitlementResponse' + '404': + description: Package not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '422': + $ref: '#/components/responses/ValidationError' + + /entitlements/{id}: + get: + operationId: getEntitlement + summary: Get entitlement details + description: Retrieve full details for an entitlement by ID. + tags: [Entitlement Provisioning] + security: + - apiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/EntitlementId' + responses: + '200': + description: Entitlement details. + content: + application/json: + schema: + $ref: '#/components/schemas/EntitlementDetailResponse' + '404': + description: Entitlement not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /entitlements/{id}/suspend: + post: + operationId: suspendEntitlement + summary: Suspend an entitlement + description: Suspend an active entitlement. Optionally provide a reason. + tags: [Entitlement Provisioning] + security: + - apiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/EntitlementId' + requestBody: + content: + application/json: + schema: + type: object + properties: + reason: + type: string + description: Reason for suspension. + example: Non-payment + responses: + '200': + description: Entitlement suspended. + content: + application/json: + schema: + $ref: '#/components/schemas/EntitlementActionResponse' + '404': + description: Entitlement not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /entitlements/{id}/unsuspend: + post: + operationId: unsuspendEntitlement + summary: Unsuspend an entitlement + description: Reactivate a suspended entitlement. + tags: [Entitlement Provisioning] + security: + - apiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/EntitlementId' + responses: + '200': + description: Entitlement reactivated. + content: + application/json: + schema: + $ref: '#/components/schemas/EntitlementActionResponse' + '404': + description: Entitlement not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /entitlements/{id}/cancel: + post: + operationId: cancelEntitlement + summary: Cancel an entitlement + description: Cancel an entitlement. Optionally provide a reason. + tags: [Entitlement Provisioning] + security: + - apiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/EntitlementId' + requestBody: + content: + application/json: + schema: + type: object + properties: + reason: + type: string + description: Reason for cancellation. + example: Customer request + responses: + '200': + description: Entitlement cancelled. + content: + application/json: + schema: + $ref: '#/components/schemas/EntitlementActionResponse' + '404': + description: Entitlement not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /entitlements/{id}/renew: + post: + operationId: renewEntitlement + summary: Renew an entitlement + description: >- + Renew an entitlement by extending the expiry date and/or updating the + billing cycle anchor. Cycle-bound boosts from the previous cycle are + expired. + tags: [Entitlement Provisioning] + security: + - apiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/EntitlementId' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RenewEntitlementRequest' + responses: + '200': + description: Entitlement renewed. + content: + application/json: + schema: + $ref: '#/components/schemas/RenewEntitlementResponse' + '404': + description: Entitlement not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + # ========================================================================== + # Cross-App Entitlement API + # ========================================================================== + + /entitlements/check: + get: + operationId: checkEntitlement + summary: Check feature access + description: >- + Check whether a user/workspace is entitled to use a specific feature. + Used by external applications (e.g. BioHost) to verify access before + performing an action. + tags: [Cross-App Entitlements] + security: + - apiKeyAuth: [] + parameters: + - name: email + in: query + required: true + description: User email to look up the workspace. + schema: + type: string + format: email + - name: feature + in: query + required: true + description: Feature code to check (e.g. `social.accounts`). + schema: + type: string + - name: quantity + in: query + description: Quantity to check against the limit (default 1). + schema: + type: integer + minimum: 1 + default: 1 + responses: + '200': + description: Entitlement check result. + content: + application/json: + schema: + $ref: '#/components/schemas/EntitlementCheckResponse' + '404': + description: User or workspace not found. + content: + application/json: + schema: + type: object + properties: + allowed: + type: boolean + example: false + reason: + type: string + example: User not found + feature_code: + type: string + example: social.accounts + '422': + $ref: '#/components/responses/ValidationError' + + /entitlements/usage: + post: + operationId: recordUsage + summary: Record feature usage + description: >- + Record usage for a feature after an action is performed. Used by + external applications to track consumption against entitlement limits. + tags: [Cross-App Entitlements] + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RecordUsageRequest' + responses: + '201': + description: Usage recorded. + content: + application/json: + schema: + $ref: '#/components/schemas/RecordUsageResponse' + '404': + description: User or workspace not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '422': + $ref: '#/components/responses/ValidationError' + + /entitlements/summary: + get: + operationId: myEntitlementSummary + summary: Get my entitlement summary + description: >- + Get the usage summary for the authenticated user's default workspace. + Returns active packages, feature usage by category, and active boosts. + tags: [Cross-App Entitlements] + security: + - sessionAuth: [] + responses: + '200': + description: Entitlement summary. + content: + application/json: + schema: + $ref: '#/components/schemas/EntitlementSummaryResponse' + '401': + $ref: '#/components/responses/Unauthenticated' + '404': + description: No workspace found for user. + + /entitlements/summary/{workspace}: + get: + operationId: workspaceEntitlementSummary + summary: Get workspace entitlement summary + description: >- + Get the usage summary for a specific workspace. Returns active + packages, feature usage grouped by category, and active boosts. + tags: [Cross-App Entitlements] + security: + - sessionAuth: [] + parameters: + - name: workspace + in: path + required: true + description: Workspace ID. + schema: + type: integer + responses: + '200': + description: Entitlement summary. + content: + application/json: + schema: + $ref: '#/components/schemas/EntitlementSummaryResponse' + '401': + $ref: '#/components/responses/Unauthenticated' + '404': + $ref: '#/components/responses/NotFound' + + # ========================================================================== + # Entitlement Webhooks API + # ========================================================================== + + /entitlement-webhooks: + get: + operationId: listWebhooks + summary: List webhooks + description: >- + List all entitlement webhooks for the current workspace. Supports + pagination. + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + parameters: + - name: workspace_id + in: query + description: >- + Explicit workspace ID. Falls back to the user's default workspace + if omitted. + schema: + type: integer + - name: per_page + in: query + description: Results per page. + schema: + type: integer + default: 25 + responses: + '200': + description: Paginated list of webhooks. + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedWebhooks' + '401': + $ref: '#/components/responses/Unauthenticated' + + post: + operationId: createWebhook + summary: Create a webhook + description: >- + Register a new entitlement webhook. The webhook URL is validated + against SSRF attacks. Returns the webhook secret on creation (the + only time it is exposed). + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWebhookRequest' + responses: + '201': + description: Webhook created. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Webhook created successfully + webhook: + $ref: '#/components/schemas/Webhook' + secret: + type: string + description: >- + The webhook signing secret. Only returned on creation. + example: whsec_a1b2c3d4e5f6... + '401': + $ref: '#/components/responses/Unauthenticated' + '422': + description: Validation error or invalid webhook URL. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ValidationErrorBody' + - type: object + properties: + message: + type: string + error: + type: string + example: invalid_webhook_url + + /entitlement-webhooks/events: + get: + operationId: listWebhookEvents + summary: List available event types + description: Get the list of event types that webhooks can subscribe to. + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + responses: + '200': + description: Available event types. + content: + application/json: + schema: + type: object + properties: + events: + type: object + description: >- + Map of event codes to human-readable descriptions. + additionalProperties: + type: string + example: + limit_warning: Limit warning threshold reached + limit_reached: Feature limit reached + package_changed: Package added or changed + boost_activated: Boost activated + boost_expired: Boost expired + + /entitlement-webhooks/{webhook}: + get: + operationId: getWebhook + summary: Get a webhook + description: >- + Get webhook details including delivery count and the 10 most recent + deliveries. + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WebhookId' + responses: + '200': + description: Webhook details with recent deliveries. + content: + application/json: + schema: + type: object + properties: + webhook: + $ref: '#/components/schemas/Webhook' + available_events: + type: object + additionalProperties: + type: string + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + + put: + operationId: updateWebhook + summary: Update a webhook + description: Update webhook settings. All fields are optional. + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WebhookId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateWebhookRequest' + responses: + '200': + description: Webhook updated. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Webhook updated successfully + webhook: + $ref: '#/components/schemas/Webhook' + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + '422': + description: Validation error or invalid webhook URL. + + delete: + operationId: deleteWebhook + summary: Delete a webhook + description: Permanently delete a webhook and its delivery history. + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WebhookId' + responses: + '200': + description: Webhook deleted. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Webhook deleted successfully + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + + /entitlement-webhooks/{webhook}/test: + post: + operationId: testWebhook + summary: Send a test webhook + description: >- + Send a test event to the webhook URL. The URL is re-validated against + SSRF before the request is made. + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WebhookId' + responses: + '200': + description: Test delivery result. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Test webhook sent successfully + delivery: + $ref: '#/components/schemas/WebhookDelivery' + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + '422': + description: Invalid webhook URL (SSRF blocked). + content: + application/json: + schema: + type: object + properties: + message: + type: string + error: + type: string + example: invalid_webhook_url + reason: + type: string + + /entitlement-webhooks/{webhook}/regenerate-secret: + post: + operationId: regenerateWebhookSecret + summary: Regenerate webhook secret + description: Generate a new signing secret for the webhook. + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WebhookId' + responses: + '200': + description: Secret regenerated. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Secret regenerated successfully + secret: + type: string + description: The new webhook signing secret. + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + + /entitlement-webhooks/{webhook}/reset-circuit-breaker: + post: + operationId: resetCircuitBreaker + summary: Reset circuit breaker + description: >- + Re-enable a webhook that was automatically disabled after consecutive + failures (circuit breaker tripped after 5 failures). + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WebhookId' + responses: + '200': + description: Circuit breaker reset, webhook re-enabled. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Webhook re-enabled successfully + webhook: + $ref: '#/components/schemas/Webhook' + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + + /entitlement-webhooks/{webhook}/deliveries: + get: + operationId: listWebhookDeliveries + summary: List webhook deliveries + description: Get the delivery history for a webhook, newest first. + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + parameters: + - $ref: '#/components/parameters/WebhookId' + - name: per_page + in: query + description: Results per page. + schema: + type: integer + default: 50 + responses: + '200': + description: Paginated delivery history. + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedDeliveries' + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + + /entitlement-webhooks/deliveries/{delivery}/retry: + post: + operationId: retryDelivery + summary: Retry a failed delivery + description: >- + Retry a previously failed webhook delivery. Cannot retry a delivery + that already succeeded. + tags: [Entitlement Webhooks] + security: + - sessionAuth: [] + parameters: + - name: delivery + in: path + required: true + description: Delivery ID. + schema: + type: integer + responses: + '200': + description: Retry result. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Delivery retried successfully + delivery: + $ref: '#/components/schemas/WebhookDelivery' + '401': + $ref: '#/components/responses/Unauthenticated' + '403': + $ref: '#/components/responses/Forbidden' + '422': + description: Cannot retry a successful delivery. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Cannot retry a successful delivery + +# ============================================================================== +# Components +# ============================================================================== + +components: + securitySchemes: + sessionAuth: + type: apiKey + in: cookie + name: session + description: Cookie-based session authentication. + + apiKeyAuth: + type: http + scheme: bearer + description: >- + API key authentication. Pass your key as a Bearer token + (`Authorization: Bearer hk_xxx`). + + parameters: + WorkspaceId: + name: workspace + in: path + required: true + description: Workspace ID. + schema: + type: integer + + EntitlementId: + name: id + in: path + required: true + description: Entitlement (workspace package) ID. + schema: + type: integer + + WebhookId: + name: webhook + in: path + required: true + description: Webhook ID. + schema: + type: integer + + responses: + Unauthenticated: + description: Authentication required. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: Unauthenticated + + Forbidden: + description: Insufficient permissions. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: You do not have permission to perform this action. + + NotFound: + description: Resource not found. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Not found + + ValidationError: + description: Validation failed. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationErrorBody' + + schemas: + # ------------------------------------------------------------------ + # Workspace schemas + # ------------------------------------------------------------------ + + WorkspaceResource: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: "Acme Corp" + slug: + type: string + example: acme-corp-a1b2c3 + icon: + type: string + nullable: true + example: fa-building + color: + type: string + nullable: true + example: "#3b82f6" + description: + type: string + nullable: true + example: Main workspace for Acme Corp + type: + type: string + enum: [personal, team, agency, custom] + example: custom + is_active: + type: boolean + example: true + users_count: + type: integer + example: 5 + bio_pages_count: + type: integer + example: 12 + + CreateWorkspaceRequest: + type: object + required: [name] + properties: + name: + type: string + maxLength: 255 + example: My Workspace + slug: + type: string + maxLength: 100 + description: >- + URL-friendly identifier. Auto-generated from name if omitted. + example: my-workspace + icon: + type: string + maxLength: 50 + nullable: true + color: + type: string + maxLength: 20 + nullable: true + description: + type: string + maxLength: 500 + nullable: true + type: + type: string + enum: [personal, team, agency, custom] + default: custom + + UpdateWorkspaceRequest: + type: object + properties: + name: + type: string + maxLength: 255 + slug: + type: string + maxLength: 100 + icon: + type: string + maxLength: 50 + nullable: true + color: + type: string + maxLength: 20 + nullable: true + description: + type: string + maxLength: 500 + nullable: true + is_active: + type: boolean + + PaginatedWorkspaces: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/WorkspaceResource' + links: + $ref: '#/components/schemas/PaginationLinks' + meta: + $ref: '#/components/schemas/PaginationMeta' + + # ------------------------------------------------------------------ + # Entitlement Provisioning schemas + # ------------------------------------------------------------------ + + CreateEntitlementRequest: + type: object + required: [email, name, product_code] + properties: + email: + type: string + format: email + description: Client email. User is found or created by this address. + example: client@example.com + name: + type: string + maxLength: 255 + description: Client name. Used when creating a new user. + example: Jane Doe + product_code: + type: string + description: Package code to provision. + example: starter-plan + billing_cycle_anchor: + type: string + format: date-time + nullable: true + description: Billing cycle anchor date (ISO 8601). + expires_at: + type: string + format: date-time + nullable: true + description: Entitlement expiry date (ISO 8601). + blesta_service_id: + type: string + nullable: true + description: External Blesta service ID for cross-referencing. + + CreateEntitlementResponse: + type: object + properties: + success: + type: boolean + example: true + entitlement_id: + type: integer + example: 42 + workspace_id: + type: integer + example: 7 + workspace_slug: + type: string + example: jane-doe-a1b2c3 + package: + type: string + example: starter-plan + status: + type: string + enum: [active, suspended, cancelled, expired] + example: active + + EntitlementDetailResponse: + type: object + properties: + success: + type: boolean + example: true + entitlement: + type: object + properties: + id: + type: integer + example: 42 + workspace_id: + type: integer + example: 7 + workspace_slug: + type: string + example: jane-doe-a1b2c3 + package_code: + type: string + example: starter-plan + package_name: + type: string + example: Starter Plan + status: + type: string + enum: [active, suspended, cancelled, expired] + example: active + starts_at: + type: string + format: date-time + nullable: true + expires_at: + type: string + format: date-time + nullable: true + billing_cycle_anchor: + type: string + format: date-time + nullable: true + blesta_service_id: + type: string + nullable: true + + EntitlementActionResponse: + type: object + properties: + success: + type: boolean + example: true + entitlement_id: + type: integer + example: 42 + status: + type: string + enum: [active, suspended, cancelled, expired] + + RenewEntitlementRequest: + type: object + properties: + expires_at: + type: string + format: date-time + nullable: true + description: New expiry date (ISO 8601). + billing_cycle_anchor: + type: string + format: date-time + nullable: true + description: New billing cycle anchor (ISO 8601). + + RenewEntitlementResponse: + type: object + properties: + success: + type: boolean + example: true + entitlement_id: + type: integer + example: 42 + status: + type: string + enum: [active, suspended, cancelled, expired] + expires_at: + type: string + format: date-time + nullable: true + + # ------------------------------------------------------------------ + # Cross-App Entitlement schemas + # ------------------------------------------------------------------ + + EntitlementCheckResponse: + type: object + properties: + allowed: + type: boolean + description: Whether the requested action is permitted. + example: true + limit: + type: integer + nullable: true + description: Total limit for the feature (null if unlimited). + example: 10 + used: + type: integer + description: Current usage count. + example: 3 + remaining: + type: integer + nullable: true + description: Remaining quota (null if unlimited). + example: 7 + unlimited: + type: boolean + description: Whether the feature has no limit. + example: false + usage_percentage: + type: number + format: float + description: Usage as a percentage of the limit (0-100). + example: 30.0 + feature_code: + type: string + example: social.accounts + workspace_id: + type: integer + example: 7 + + RecordUsageRequest: + type: object + required: [email, feature] + properties: + email: + type: string + format: email + description: User email to look up the workspace. + example: client@example.com + feature: + type: string + description: Feature code to record usage for. + example: social.posts.scheduled + quantity: + type: integer + minimum: 1 + default: 1 + description: Number of units consumed. + metadata: + type: object + nullable: true + description: Arbitrary metadata to attach to the usage record. + additionalProperties: true + example: + post_id: "abc123" + platform: twitter + + RecordUsageResponse: + type: object + properties: + success: + type: boolean + example: true + usage_record_id: + type: integer + example: 891 + feature_code: + type: string + example: social.posts.scheduled + quantity: + type: integer + example: 1 + + EntitlementSummaryResponse: + type: object + properties: + workspace_id: + type: integer + example: 7 + packages: + type: array + items: + type: object + properties: + code: + type: string + example: starter-plan + name: + type: string + example: Starter Plan + status: + type: string + enum: [active, suspended, cancelled, expired] + expires_at: + type: string + format: date-time + nullable: true + features: + type: object + description: >- + Feature usage grouped by category. Each category key maps to an + array of feature objects. + additionalProperties: + type: array + items: + type: object + properties: + code: + type: string + example: social.accounts + name: + type: string + example: Social Accounts + limit: + type: integer + nullable: true + used: + type: integer + remaining: + type: integer + nullable: true + unlimited: + type: boolean + percentage: + type: number + format: float + example: + social: + - code: social.accounts + name: Social Accounts + limit: 10 + used: 3 + remaining: 7 + unlimited: false + percentage: 30.0 + boosts: + type: array + items: + type: object + properties: + feature: + type: string + example: social.accounts + value: + type: integer + example: 5 + type: + type: string + example: add + expires_at: + type: string + format: date-time + nullable: true + + # ------------------------------------------------------------------ + # Webhook schemas + # ------------------------------------------------------------------ + + Webhook: + type: object + properties: + id: + type: integer + example: 1 + uuid: + type: string + format: uuid + workspace_id: + type: integer + name: + type: string + example: My Webhook + url: + type: string + format: uri + example: https://example.com/webhooks/entitlements + events: + type: array + items: + type: string + enum: + - limit_warning + - limit_reached + - package_changed + - boost_activated + - boost_expired + is_active: + type: boolean + example: true + max_attempts: + type: integer + example: 3 + last_delivery_status: + type: string + enum: [pending, success, failed] + nullable: true + last_triggered_at: + type: string + format: date-time + nullable: true + failure_count: + type: integer + example: 0 + metadata: + type: object + nullable: true + additionalProperties: true + deliveries_count: + type: integer + description: Total number of deliveries (when loaded). + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + CreateWebhookRequest: + type: object + required: [name, url, events] + properties: + name: + type: string + maxLength: 255 + example: Production Webhook + url: + type: string + format: uri + maxLength: 2048 + description: >- + HTTPS endpoint URL. Validated against SSRF (internal network + addresses are rejected). + example: https://example.com/webhooks/entitlements + events: + type: array + minItems: 1 + items: + type: string + enum: + - limit_warning + - limit_reached + - package_changed + - boost_activated + - boost_expired + example: [limit_warning, limit_reached] + secret: + type: string + minLength: 32 + nullable: true + description: >- + Custom signing secret. If omitted, one is auto-generated. + metadata: + type: object + nullable: true + additionalProperties: true + + UpdateWebhookRequest: + type: object + properties: + name: + type: string + maxLength: 255 + url: + type: string + format: uri + maxLength: 2048 + events: + type: array + minItems: 1 + items: + type: string + enum: + - limit_warning + - limit_reached + - package_changed + - boost_activated + - boost_expired + is_active: + type: boolean + max_attempts: + type: integer + minimum: 1 + maximum: 10 + metadata: + type: object + nullable: true + additionalProperties: true + + WebhookDelivery: + type: object + properties: + id: + type: integer + uuid: + type: string + format: uuid + webhook_id: + type: integer + event: + type: string + example: limit_warning + attempts: + type: integer + example: 1 + status: + type: string + enum: [pending, success, failed] + http_status: + type: integer + nullable: true + example: 200 + resend_at: + type: string + format: date-time + nullable: true + resent_manually: + type: boolean + example: false + payload: + type: object + description: The JSON payload that was sent. + response: + type: object + nullable: true + description: The response body received from the endpoint. + created_at: + type: string + format: date-time + + PaginatedWebhooks: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Webhook' + links: + $ref: '#/components/schemas/PaginationLinks' + meta: + $ref: '#/components/schemas/PaginationMeta' + + PaginatedDeliveries: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/WebhookDelivery' + links: + $ref: '#/components/schemas/PaginationLinks' + meta: + $ref: '#/components/schemas/PaginationMeta' + + # ------------------------------------------------------------------ + # Shared schemas + # ------------------------------------------------------------------ + + ErrorResponse: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: Resource not found + + ValidationErrorBody: + type: object + properties: + message: + type: string + example: The given data was invalid. + errors: + type: object + description: >- + Field-level validation errors. Keys are field names, values are + arrays of error messages. + additionalProperties: + type: array + items: + type: string + example: + email: + - The email field is required. + name: + - The name field is required. + + PaginationLinks: + type: object + properties: + first: + type: string + format: uri + nullable: true + last: + type: string + format: uri + nullable: true + prev: + type: string + format: uri + nullable: true + next: + type: string + format: uri + nullable: true + + PaginationMeta: + type: object + properties: + current_page: + type: integer + from: + type: integer + nullable: true + last_page: + type: integer + per_page: + type: integer + to: + type: integer + nullable: true + total: + type: integer