openapi: 3.1.0 info: title: user-engine portal API version: 0.3.0 servers: - url: /api/v1 security: - verifiedOidc: [] paths: /me: get: operationId: currentUser responses: "200": description: Current user and linked identities "403": $ref: "#/components/responses/Denied" /me/profile: patch: operationId: updateCurrentUserProfile description: Updates display name and versioned consent; verified email cannot be changed here. parameters: [{$ref: "#/components/parameters/IdempotencyKey"}] requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/UpdateSelfProfile"} responses: "200": {description: Durable self-service profile state} "403": {$ref: "#/components/responses/Denied"} /registrations: post: operationId: startRegistration parameters: - $ref: "#/components/parameters/IdempotencyKey" responses: "201": description: Registration started "403": $ref: "#/components/responses/Denied" /registrations/{registrationId}/complete: post: operationId: completeRegistration parameters: - name: registrationId in: path required: true schema: {type: string} - $ref: "#/components/parameters/IdempotencyKey" responses: "200": description: Registration completed "400": description: Missing verified factors or invalid state /tenants/{tenant}/users: get: operationId: listTenantUsers parameters: - $ref: "#/components/parameters/Tenant" - {name: offset, in: query, schema: {type: integer, minimum: 0}} - {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100}} responses: "200": {description: Tenant-scoped memberships} "403": {$ref: "#/components/responses/Denied"} post: operationId: createTenantUser parameters: - $ref: "#/components/parameters/Tenant" - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/CreateUser"} responses: "201": {description: Tenant user and membership created} "403": {$ref: "#/components/responses/Denied"} /tenants/{tenant}/users/{userId}: patch: operationId: updateTenantUserLifecycle parameters: - $ref: "#/components/parameters/Tenant" - name: userId in: path required: true schema: {type: string} - $ref: "#/components/parameters/IdempotencyKey" responses: "200": {description: Tenant account updated} "403": {$ref: "#/components/responses/Denied"} delete: operationId: removeTenantUser parameters: - $ref: "#/components/parameters/Tenant" - $ref: "#/components/parameters/UserId" - $ref: "#/components/parameters/IdempotencyKey" responses: "200": {description: Login deprovisioned and tenant account disabled} "403": {$ref: "#/components/responses/Denied"} /tenants/{tenant}/invitations: get: operationId: listInvitations parameters: [{$ref: "#/components/parameters/Tenant"}] responses: "200": {description: Tenant invitations} "403": {$ref: "#/components/responses/Denied"} post: operationId: createInvitation parameters: - $ref: "#/components/parameters/Tenant" - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/CreateInvitation"} responses: "201": {description: Invitation created with an expiry and version} "409": {$ref: "#/components/responses/Conflict"} /tenants/{tenant}/invitations/{invitationId}/resend: post: operationId: resendInvitation parameters: - $ref: "#/components/parameters/Tenant" - $ref: "#/components/parameters/InvitationId" - $ref: "#/components/parameters/IfMatch" responses: "200": {description: Invitation expiry renewed and version advanced} "409": {$ref: "#/components/responses/Conflict"} /tenants/{tenant}/invitations/{invitationId}/expire: post: operationId: expireInvitation parameters: - $ref: "#/components/parameters/Tenant" - $ref: "#/components/parameters/InvitationId" - $ref: "#/components/parameters/IfMatch" responses: "200": {description: Invitation revoked} "409": {$ref: "#/components/responses/Conflict"} /invitations/{invitationId}/claim: post: operationId: claimInvitation parameters: - $ref: "#/components/parameters/InvitationId" - $ref: "#/components/parameters/IdempotencyKey" responses: "200": {description: Invitation claimed and identity linked} "400": {description: Invitation expired, revoked, or already claimed} /onboarding/{journeyId}/steps/{stepKey}/complete: post: operationId: completeOnboardingStep description: Completes an active user-engine-owned step. Provider-owned steps must use their external handoff and callback. parameters: - name: journeyId in: path required: true schema: {type: string} - name: stepKey in: path required: true schema: {type: string} - $ref: "#/components/parameters/IdempotencyKey" responses: "200": {description: Updated resumable onboarding journey} "403": {$ref: "#/components/responses/Denied"} "404": {description: Journey is absent or belongs to another user} /platform/tenants: post: operationId: createPlatformTenant description: Creates a tenant through the configured tenant authority and optionally prepares its first administrator. parameters: [{$ref: "#/components/parameters/IdempotencyKey"}] requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/CreateTenant"} responses: "201": {description: Tenant created or resumed and first administrator prepared} "403": {$ref: "#/components/responses/Denied"} /platform/tenants/{tenant}: get: operationId: readPlatformTenant description: >- Reads the authoritative tenant record from the tenant authority. user-engine keeps no tenant table; echo the returned version as If-Match on any mutation. parameters: [{$ref: "#/components/parameters/Tenant"}] responses: "200": description: Authoritative tenant record content: application/json: schema: {$ref: "#/components/schemas/TenantRecord"} "403": {$ref: "#/components/responses/Denied"} "404": {$ref: "#/components/responses/NotFound"} patch: operationId: updatePlatformTenant description: >- Changes allow-listed tenant metadata under an atomic compare-and-swap. The identifier is immutable because it is minted into issued tokens. parameters: - $ref: "#/components/parameters/Tenant" - $ref: "#/components/parameters/IdempotencyKey" - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/UpdateTenant"} responses: "200": description: Updated tenant record content: application/json: schema: {$ref: "#/components/schemas/TenantRecord"} "403": {$ref: "#/components/responses/Denied"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} /platform/tenants/{tenant}/retire: post: operationId: retirePlatformTenant description: >- Reversibly retires a tenant through the tenant authority. There is no hard delete: grant and plan history are preserved for audit correlation. parameters: - $ref: "#/components/parameters/Tenant" - $ref: "#/components/parameters/IdempotencyKey" - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/TenantLifecycleChange"} responses: "200": description: Retired tenant record content: application/json: schema: {$ref: "#/components/schemas/TenantRecord"} "403": {$ref: "#/components/responses/Denied"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} /platform/tenants/{tenant}/reactivate: post: operationId: reactivatePlatformTenant description: >- Restores a retired tenant. Revoked grants and plan state are deliberately not resurrected. parameters: - $ref: "#/components/parameters/Tenant" - $ref: "#/components/parameters/IdempotencyKey" - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: {$ref: "#/components/schemas/TenantLifecycleChange"} responses: "200": description: Reactivated tenant record content: application/json: schema: {$ref: "#/components/schemas/TenantRecord"} "403": {$ref: "#/components/responses/Denied"} "404": {$ref: "#/components/responses/NotFound"} "409": {$ref: "#/components/responses/Conflict"} /platform/tenants/{tenant}/users/{userId}/recover: post: operationId: recoverTenantUser description: Reconciles or recreates the provider identity and activates the tenant account without exposing provider credentials. parameters: - $ref: "#/components/parameters/Tenant" - $ref: "#/components/parameters/UserId" - $ref: "#/components/parameters/IdempotencyKey" responses: "200": {description: Redacted recovery outcome} "403": {$ref: "#/components/responses/Denied"} /platform/outbox/deliver: post: operationId: deliverOutbox requestBody: content: application/json: schema: type: object properties: worker_id: {type: string} max_attempts: {type: integer, minimum: 1, maximum: 20} additionalProperties: false responses: "200": {description: Bounded delivery results} "403": {$ref: "#/components/responses/Denied"} /platform/outbox/{eventId}/replay: post: operationId: replayOutboxEvent parameters: - name: eventId in: path required: true schema: {type: string} responses: "200": {description: Event returned to pending delivery} "403": {$ref: "#/components/responses/Denied"} components: securitySchemes: verifiedOidc: type: openIdConnect openIdConnectUrl: https://kc.coulomb.social/.well-known/openid-configuration parameters: Tenant: name: tenant in: path required: true schema: {type: string} IdempotencyKey: name: Idempotency-Key in: header required: true schema: {type: string, minLength: 16, maxLength: 200} UserId: name: userId in: path required: true schema: {type: string} InvitationId: name: invitationId in: path required: true schema: {type: string} IfMatch: name: If-Match in: header required: true schema: {type: string, pattern: '^"?[0-9]+"?$'} schemas: CreateUser: type: object required: [primary_email, role] properties: primary_email: {type: string, format: email} display_name: {type: string, maxLength: 200} role: {type: string, enum: [user, tenant-admin]} additionalProperties: false CreateInvitation: type: object required: [primary_email] properties: primary_email: {type: string, format: email} display_name: {type: string, maxLength: 200} role: {type: string, enum: [user, tenant-admin]} application_id: {type: string} scope_id: {type: string} additionalProperties: false CreateTenant: type: object required: [tenant, display_name] properties: tenant: {type: string, pattern: '^tenant:'} display_name: {type: string, minLength: 1, maxLength: 200} first_admin: type: object required: [primary_email] properties: primary_email: {type: string, format: email} display_name: {type: string, maxLength: 200} additionalProperties: false additionalProperties: false TenantLifecycleChange: type: object required: [reason] properties: reason: {type: string, minLength: 1, maxLength: 200} additionalProperties: false UpdateTenant: type: object required: [metadata, reason] properties: reason: {type: string, minLength: 1, maxLength: 200} metadata: type: object description: >- Mutable tenant metadata. tenant_id and identifier are immutable at the authority and are rejected here. grouping is mutable but only through the authority's own reclassification route, never as metadata, because it resolves a tenant's spend ceiling. minProperties: 1 properties: display_name: {type: string, minLength: 1, maxLength: 200} contact_email: {type: string, format: email} additionalProperties: false additionalProperties: false TenantRecord: type: object description: Authoritative record owned by the tenant authority, not by user-engine. required: [tenant, external_ref, lifecycle, version] properties: tenant: {type: string, pattern: '^tenant:'} external_ref: {type: string} lifecycle: {type: string, enum: [active, retired, unknown]} version: {type: integer, minimum: 0} grouping: type: string nullable: true description: >- Authoritative classification, reported by the tenant authority. Read it from here; never parse it from the identifier, whose own grouping segment is historical once a tenant is reclassified. display_name: {type: string, nullable: true} contact_email: {type: string, nullable: true} retired_at: {type: string, nullable: true} reactivated_at: {type: string, nullable: true} replayed: type: boolean description: True when the authority replayed a durable idempotency receipt. additionalProperties: false UpdateSelfProfile: type: object required: [display_name, consent_accepted, consent_version] properties: display_name: {type: string, minLength: 1, maxLength: 200} consent_accepted: {type: boolean} consent_version: {type: string, minLength: 1, maxLength: 100} additionalProperties: false Error: type: object required: [error, message, correlation_id] properties: error: {type: string} message: {type: string} correlation_id: {type: string} responses: Denied: description: Caller is unauthenticated or unauthorized content: application/json: schema: {$ref: "#/components/schemas/Error"} Conflict: description: Optimistic concurrency or uniqueness conflict content: application/json: schema: {$ref: "#/components/schemas/Error"} NotFound: description: Resource not found content: application/json: schema: {$ref: "#/components/schemas/Error"}