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