openapi: 3.0.3 info: title: 'VetLlama API Documentation' description: 'API documentation for the VetLlama white-label SaaS backend.' version: 1.0.0 servers: - url: 'https://api.vetllama.com' tags: - name: Admin description: '' - name: Tenant description: '' - name: User description: '' components: securitySchemes: default: type: http scheme: bearer description: 'Use the JWT returned by the relevant login flow: Admin, TenantOwner, or EndUser magic-link verification. Tokens are scoped by guard; an Admin token cannot access TenantOwner or EndUser routes.' security: - default: [] paths: /api/admin/auth/login: post: summary: Login operationId: login description: 'Authenticates a platform admin and returns an Admin-scoped JWT. Development seed example only: `admin@vetllama.test` / `LocalDevPassword123!`.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Logged in successfully.' data: access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: bearer guard: admin expires_in: 3600 user: id: 1 name: 'VetLlama Dev Admin' email: admin@vetllama.test is_active: true email_verified_at: '2026-05-17T00:00:00.000000Z' properties: success: type: boolean example: true message: type: string example: 'Logged in successfully.' data: type: object properties: access_token: type: string example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: type: string example: bearer guard: type: string example: admin expires_in: type: integer example: 3600 user: type: object properties: id: type: integer example: 1 name: type: string example: 'VetLlama Dev Admin' email: type: string example: admin@vetllama.test is_active: type: boolean example: true email_verified_at: type: string example: '2026-05-17T00:00:00.000000Z' 422: description: '' content: application/json: schema: type: object example: success: false message: 'The given data was invalid.' errors: email: - 'Invalid email or password.' properties: success: type: boolean example: false message: type: string example: 'The given data was invalid.' errors: type: object properties: email: type: array example: - 'Invalid email or password.' items: type: string tags: - Admin requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Admin email address.' example: admin@vetllama.test password: type: string description: 'Admin password.' example: LocalDevPassword123! device_token: type: string description: 'Optional device token for future push/session tracking.' example: web-admin-device nullable: true required: - email - password security: [] /api/admin/auth/logout: post: summary: Logout operationId: logout description: 'Invalidates the current Admin JWT and closes the latest open auth activity session.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Logged out successfully.' data: null properties: success: type: boolean example: true message: type: string example: 'Logged out successfully.' data: type: string example: null nullable: true 401: description: '' content: application/json: schema: type: object example: success: false message: Unauthenticated. properties: success: type: boolean example: false message: type: string example: Unauthenticated. tags: - Admin /api/admin/auth/refresh: post: summary: 'Refresh Token' operationId: refreshToken description: 'Exchanges a valid Admin JWT for a fresh Admin JWT.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Token refreshed successfully.' data: access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: bearer guard: admin expires_in: 3600 user: id: 1 name: 'VetLlama Dev Admin' email: admin@vetllama.test is_active: true properties: success: type: boolean example: true message: type: string example: 'Token refreshed successfully.' data: type: object properties: access_token: type: string example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: type: string example: bearer guard: type: string example: admin expires_in: type: integer example: 3600 user: type: object properties: id: type: integer example: 1 name: type: string example: 'VetLlama Dev Admin' email: type: string example: admin@vetllama.test is_active: type: boolean example: true tags: - Admin /api/admin/auth/me: get: summary: 'Current Profile' operationId: currentProfile description: 'Returns the profile for the authenticated platform admin.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Profile fetched successfully.' data: id: 1 name: 'VetLlama Dev Admin' email: admin@vetllama.test is_active: true email_verified_at: '2026-05-17T00:00:00.000000Z' properties: success: type: boolean example: true message: type: string example: 'Profile fetched successfully.' data: type: object properties: id: type: integer example: 1 name: type: string example: 'VetLlama Dev Admin' email: type: string example: admin@vetllama.test is_active: type: boolean example: true email_verified_at: type: string example: '2026-05-17T00:00:00.000000Z' tags: - Admin /api/health: get: summary: 'Health Check' operationId: healthCheck description: 'Confirms that the VetLlama API process is reachable.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'VetLlama API is available.' properties: success: type: boolean example: true message: type: string example: 'VetLlama API is available.' tags: - Admin security: [] /api/public/tenant/auth/register: post: summary: 'Register Tenant Owner' operationId: registerTenantOwner description: 'Creates a draft tenant together with its primary tenant owner, initializes onboarding defaults, creates a primary subdomain host mapping, and returns a JWT so onboarding can start immediately. If `desired_subdomain` is omitted, the backend generates a unique subdomain from `display_name`.' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: success: true message: 'Account created successfully. Onboarding started.' data: access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: bearer guard: tenant_owner expires_in: 3600 user: id: 1 tenant_id: 1 name: 'Dr. Sarah Khan' email: owner@paws-care.test phone: '+10000000000' is_primary: true is_active: true tenant: id: 1 name: 'Paws & Care' slug: paws-care status: draft onboarding_status: in_progress is_active: true properties: success: type: boolean example: true message: type: string example: 'Account created successfully. Onboarding started.' data: type: object properties: access_token: type: string example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: type: string example: bearer guard: type: string example: tenant_owner expires_in: type: integer example: 3600 user: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 name: type: string example: 'Dr. Sarah Khan' email: type: string example: owner@paws-care.test phone: type: string example: '+10000000000' is_primary: type: boolean example: true is_active: type: boolean example: true tenant: type: object properties: id: type: integer example: 1 name: type: string example: 'Paws & Care' slug: type: string example: paws-care status: type: string example: draft onboarding_status: type: string example: in_progress is_active: type: boolean example: true 422: description: '' content: application/json: schema: type: object example: success: false message: 'The given data was invalid.' errors: email: - 'The email has already been taken.' properties: success: type: boolean example: false message: type: string example: 'The given data was invalid.' errors: type: object properties: email: type: array example: - 'The email has already been taken.' items: type: string tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: owner_name: type: string description: 'Primary owner name.' example: 'Dr. Sarah Khan' display_name: type: string description: 'Public brand or business display name.' example: 'Paws & Care' business_name: type: string description: 'Optional legal business name.' example: 'Paws & Care LLC' nullable: true email: type: string description: 'Unique owner email address.' example: owner@demo.vetllama.test password: type: string description: 'Account password.' example: LocalDevPassword123! accepted_terms: type: boolean description: 'Must be true to accept the platform terms.' example: true phone: type: string description: 'Optional owner or clinic phone number.' example: '+10000000000' nullable: true desired_subdomain: type: string description: 'Optional subdomain label. If omitted, one is generated from display_name.' example: paws-care nullable: true password_confirmation: type: string description: 'Password confirmation.' example: LocalDevPassword123! required: - owner_name - display_name - email - password - accepted_terms - password_confirmation security: [] /api/public/tenant/resolve: get: summary: 'Resolve Tenant By Host' operationId: resolveTenantByHost description: "Resolves the active tenant from a mapped website host and returns the public configuration needed by the Angular frontend. By default, resolution uses the request Host header. For testing or frontend bootstrapping, you may also pass one explicit query input: `host`, `domain`, or `subdomain`.\nCustom domains must be active and verified before they resolve publicly. Managed subdomains resolve immediately once active.\n\nSubdomain examples resolve against `VETLLAMA_TENANT_BASE_DOMAIN`, such as `paws-care.vetllama.test` locally. Custom domain examples use the full mapped host, such as `paws-care.com`." parameters: - in: query name: host description: 'Optional full host to resolve.' example: paws-care.vetllama.test required: false schema: type: string description: 'Optional full host to resolve.' example: paws-care.vetllama.test - in: query name: domain description: 'Optional custom domain/full domain to resolve.' example: paws-care.com required: false schema: type: string description: 'Optional custom domain/full domain to resolve.' example: paws-care.com - in: query name: subdomain description: 'Optional subdomain label to resolve against the configured tenant base domain.' example: paws-care required: false schema: type: string description: 'Optional subdomain label to resolve against the configured tenant base domain.' example: paws-care - in: header name: Host description: '' example: demo.vetllama.test schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Tenant configuration resolved.' data: tenant: id: 1 name: 'Demo Clinic' slug: demo-clinic status: active is_active: true template: id: 1 name: 'Classic Practice' slug: classic-practice description: 'Local development template for Classic Practice.' preview_url: 'https://example.com/templates/classic-practice' is_active: true schema: sections: - hero - about - banners - services - faq - contact required_fields: - homepage_content.hero_title - homepage_content.hero_subtitle - contact_details.email branding: primary_color: '#2563eb' secondary_color: '#14b8a6' logo_url: 'https://example.com/demo/logo.png' favicon_url: 'https://example.com/demo/favicon.ico' social_links: facebook: 'https://facebook.com/demo' instagram: 'https://instagram.com/demo' public_config: contact_details: email: hello@demo.vetllama.test phone: '+10000000000' address: '123 Demo Street' homepage_content: hero_title: 'Care, scheduling, and client access in one place' hero_subtitle: 'A local-development tenant used to test public config.' banners: [] faq: [] services: [] settings: booking_enabled: false telehealth_enabled: false is_published: true properties: success: type: boolean example: true message: type: string example: 'Tenant configuration resolved.' data: type: object properties: tenant: type: object properties: id: type: integer example: 1 name: type: string example: 'Demo Clinic' slug: type: string example: demo-clinic status: type: string example: active is_active: type: boolean example: true template: type: object properties: id: type: integer example: 1 name: type: string example: 'Classic Practice' slug: type: string example: classic-practice description: type: string example: 'Local development template for Classic Practice.' preview_url: type: string example: 'https://example.com/templates/classic-practice' is_active: type: boolean example: true schema: type: object properties: sections: type: array example: - hero - about - banners - services - faq - contact items: type: string required_fields: type: array example: - homepage_content.hero_title - homepage_content.hero_subtitle - contact_details.email items: type: string branding: type: object properties: primary_color: type: string example: '#2563eb' secondary_color: type: string example: '#14b8a6' logo_url: type: string example: 'https://example.com/demo/logo.png' favicon_url: type: string example: 'https://example.com/demo/favicon.ico' social_links: type: object properties: facebook: type: string example: 'https://facebook.com/demo' instagram: type: string example: 'https://instagram.com/demo' public_config: type: object properties: contact_details: type: object properties: email: type: string example: hello@demo.vetllama.test phone: type: string example: '+10000000000' address: type: string example: '123 Demo Street' homepage_content: type: object properties: hero_title: type: string example: 'Care, scheduling, and client access in one place' hero_subtitle: type: string example: 'A local-development tenant used to test public config.' banners: type: array example: [] faq: type: array example: [] services: type: array example: [] settings: type: object properties: booking_enabled: type: boolean example: false telehealth_enabled: type: boolean example: false is_published: type: boolean example: true 404: description: '' content: application/json: schema: type: object example: success: false message: 'Tenant could not be resolved for this host.' properties: success: type: boolean example: false message: type: string example: 'Tenant could not be resolved for this host.' tags: - Tenant security: [] /api/public/tenant/services: get: summary: 'List Public Services' operationId: listPublicServices description: 'Returns all enabled tenant service offerings with their active duration and pricing options. Tenant is resolved from the Host header or explicit resolver query parameters.' parameters: - in: query name: host description: 'Optional full host to resolve.' example: pawscare.vetllama.test required: false schema: type: string description: 'Optional full host to resolve.' example: pawscare.vetllama.test - in: query name: domain description: 'Optional custom domain/full domain to resolve.' example: pawscare.test required: false schema: type: string description: 'Optional custom domain/full domain to resolve.' example: pawscare.test - in: query name: subdomain description: 'Optional subdomain label to resolve.' example: pawscare required: false schema: type: string description: 'Optional subdomain label to resolve.' example: pawscare - in: header name: Host description: '' example: pawscare.vetllama.test schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Services fetched successfully.' data: - id: 1 type: telehealth name: 'Telehealth Follow-up' description: 'Video consultation for follow-ups.' delivery_mode: video duration_options: - id: 1 service_offering_id: 1 duration_minutes: 30 price: '69.00' currency: USD is_default: true is_active: true properties: success: type: boolean example: true message: type: string example: 'Services fetched successfully.' data: type: array example: - id: 1 type: telehealth name: 'Telehealth Follow-up' description: 'Video consultation for follow-ups.' delivery_mode: video duration_options: - id: 1 service_offering_id: 1 duration_minutes: 30 price: '69.00' currency: USD is_default: true is_active: true items: type: object properties: id: type: integer example: 1 type: type: string example: telehealth name: type: string example: 'Telehealth Follow-up' description: type: string example: 'Video consultation for follow-ups.' delivery_mode: type: string example: video duration_options: type: array example: - id: 1 service_offering_id: 1 duration_minutes: 30 price: '69.00' currency: USD is_default: true is_active: true items: type: object properties: id: type: integer example: 1 service_offering_id: type: integer example: 1 duration_minutes: type: integer example: 30 price: type: string example: '69.00' currency: type: string example: USD is_default: type: boolean example: true is_active: type: boolean example: true tags: - Tenant security: [] '/api/public/tenant/services/{service_id}': get: summary: 'Get Public Service Detail' operationId: getPublicServiceDetail description: 'Returns one enabled public service with active duration and pricing options.' parameters: [] responses: { } tags: - Tenant security: [] parameters: - in: path name: service_id description: 'The ID of the service.' example: 16 required: true schema: type: integer /api/public/tenant/locations: get: summary: 'List Public Locations' operationId: listPublicLocations description: 'Returns active public locations for physical visit preparation.' parameters: [] responses: { } tags: - Tenant security: [] /api/public/tenant/policies: get: summary: 'Get Public Policies' operationId: getPublicPolicies description: 'Returns public booking policy, FAQ, terms, privacy, and contact blocks.' parameters: [] responses: { } tags: - Tenant security: [] /api/public/tenant/slots: get: summary: 'Preview Bookable Slots' operationId: previewBookableSlots description: 'Generates available bookable slot previews for an active published tenant service. This does not create or reserve a booking.' parameters: - in: query name: service_id description: 'Active service ID.' example: 1 required: true schema: type: integer description: 'Active service ID.' example: 1 - in: query name: duration_id description: 'Active duration ID.' example: 1 required: true schema: type: integer description: 'Active duration ID.' example: 1 - in: query name: date description: 'Date in YYYY-MM-DD format.' example: '2026-05-21' required: true schema: type: string description: 'Date in YYYY-MM-DD format.' example: '2026-05-21' - in: query name: location_id description: 'Optional active location ID for physical visits.' example: 1 required: false schema: type: integer description: 'Optional active location ID for physical visits.' example: 1 nullable: true responses: { } tags: - Tenant security: [] /api/public/tenant/details: get: summary: 'Get Public Tenant Details' operationId: getPublicTenantDetails description: 'Returns the full public tenant website/bootstrap payload, including tenant profile, template, branding, public content, active host mappings, active locations, enabled services, and active duration/pricing options.' parameters: - in: query name: host description: 'Optional full host to resolve.' example: pawscare.vetllama.test required: false schema: type: string description: 'Optional full host to resolve.' example: pawscare.vetllama.test - in: query name: domain description: 'Optional custom domain/full domain to resolve.' example: pawscare.test required: false schema: type: string description: 'Optional custom domain/full domain to resolve.' example: pawscare.test - in: query name: subdomain description: 'Optional subdomain label to resolve.' example: pawscare required: false schema: type: string description: 'Optional subdomain label to resolve.' example: pawscare - in: header name: Host description: '' example: pawscare.vetllama.test schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Tenant details fetched successfully.' data: tenant: id: 1 name: 'Paws & Care Veterinary Clinic' slug: paws-care-vet status: active is_active: true template: { } branding: { } public_config: { } domains: [] locations: [] services: [] properties: success: type: boolean example: true message: type: string example: 'Tenant details fetched successfully.' data: type: object properties: tenant: type: object properties: id: type: integer example: 1 name: type: string example: 'Paws & Care Veterinary Clinic' slug: type: string example: paws-care-vet status: type: string example: active is_active: type: boolean example: true template: type: object properties: { } branding: type: object properties: { } public_config: type: object properties: { } domains: type: array example: [] locations: type: array example: [] services: type: array example: [] tags: - Tenant security: [] /api/tenant/auth/login: post: summary: Login operationId: login description: 'Authenticates a tenant owner/operator and returns a TenantOwner-scoped JWT. Development seed example only: `owner@demo.vetllama.test` / `LocalDevPassword123!`.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Logged in successfully.' data: access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: bearer guard: tenant_owner expires_in: 3600 user: id: 1 tenant_id: 1 name: 'Demo Tenant Owner' email: owner@demo.vetllama.test phone: '+10000000000' is_primary: true is_active: true tenant: id: 1 name: 'Demo Clinic' slug: demo-clinic status: active is_active: true properties: success: type: boolean example: true message: type: string example: 'Logged in successfully.' data: type: object properties: access_token: type: string example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: type: string example: bearer guard: type: string example: tenant_owner expires_in: type: integer example: 3600 user: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 name: type: string example: 'Demo Tenant Owner' email: type: string example: owner@demo.vetllama.test phone: type: string example: '+10000000000' is_primary: type: boolean example: true is_active: type: boolean example: true tenant: type: object properties: id: type: integer example: 1 name: type: string example: 'Demo Clinic' slug: type: string example: demo-clinic status: type: string example: active is_active: type: boolean example: true 422: description: '' content: application/json: schema: type: object example: success: false message: 'The given data was invalid.' errors: email: - 'Invalid email or password.' properties: success: type: boolean example: false message: type: string example: 'The given data was invalid.' errors: type: object properties: email: type: array example: - 'Invalid email or password.' items: type: string tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Tenant owner email address.' example: owner@demo.vetllama.test password: type: string description: 'Tenant owner password.' example: LocalDevPassword123! device_token: type: string description: 'Optional device token for future push/session tracking.' example: web-owner-device nullable: true required: - email - password security: [] /api/tenant/auth/logout: post: summary: Logout operationId: logout description: 'Invalidates the current TenantOwner JWT and closes the latest open auth activity session.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Logged out successfully.' data: null properties: success: type: boolean example: true message: type: string example: 'Logged out successfully.' data: type: string example: null nullable: true tags: - Tenant /api/tenant/auth/refresh: post: summary: 'Refresh Token' operationId: refreshToken description: 'Exchanges a valid TenantOwner JWT for a fresh TenantOwner JWT.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Token refreshed successfully.' data: access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: bearer guard: tenant_owner expires_in: 3600 user: id: 1 tenant_id: 1 email: owner@demo.vetllama.test properties: success: type: boolean example: true message: type: string example: 'Token refreshed successfully.' data: type: object properties: access_token: type: string example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: type: string example: bearer guard: type: string example: tenant_owner expires_in: type: integer example: 3600 user: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 email: type: string example: owner@demo.vetllama.test tags: - Tenant /api/tenant/auth/me: get: summary: 'Current Profile' operationId: currentProfile description: 'Returns the authenticated tenant owner and their tenant summary.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Profile fetched successfully.' data: id: 1 tenant_id: 1 name: 'Demo Tenant Owner' email: owner@demo.vetllama.test phone: '+10000000000' is_primary: true is_active: true tenant: id: 1 name: 'Demo Clinic' slug: demo-clinic status: active is_active: true properties: success: type: boolean example: true message: type: string example: 'Profile fetched successfully.' data: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 name: type: string example: 'Demo Tenant Owner' email: type: string example: owner@demo.vetllama.test phone: type: string example: '+10000000000' is_primary: type: boolean example: true is_active: type: boolean example: true tenant: type: object properties: id: type: integer example: 1 name: type: string example: 'Demo Clinic' slug: type: string example: demo-clinic status: type: string example: active is_active: type: boolean example: true tags: - Tenant /api/tenant/templates: get: summary: 'List Templates' operationId: listTemplates description: 'Lists active website templates available for the tenant owner to select.' parameters: [] responses: { } tags: - Tenant '/api/tenant/templates/{template_id}': get: summary: 'Get Template Details' operationId: getTemplateDetails description: 'Returns the selected template definition, including the schema used by the owner setup UI.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: template_id description: 'The ID of the template.' example: 16 required: true schema: type: integer /api/tenant/templates/selection: put: summary: 'Select Template' operationId: selectTemplate description: 'Assigns one active template to the authenticated tenant.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: template_id: type: string description: 'The id of an existing record in the templates table.' example: architecto required: - template_id /api/tenant/profile: get: summary: 'Get Tenant Profile' operationId: getTenantProfile description: "Returns the authenticated tenant's basic profile and onboarding status fields." parameters: [] responses: { } tags: - Tenant put: summary: 'Update Tenant Profile' operationId: updateTenantProfile description: 'Updates owner-facing business profile fields such as display name, slug, contact details, and onboarding status.' parameters: [] responses: { } tags: - Tenant requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 255 characters.' example: b business_name: type: string description: 'Must not be greater than 255 characters.' example: 'n' nullable: true slug: type: string description: 'Must not be greater than 255 characters.' example: g email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: rowan.gulgowski@example.com nullable: true primary_phone: type: string description: 'Must not be greater than 50 characters.' example: d nullable: true secondary_phone: type: string description: 'Must not be greater than 50 characters.' example: l nullable: true short_bio: type: string description: '' example: architecto nullable: true support_email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: zbailey@example.net nullable: true support_phone: type: string description: 'Must not be greater than 50 characters.' example: i nullable: true status: type: string description: '' example: pending enum: - pending - active - paused onboarding_status: type: string description: '' example: in_progress enum: - not_started - in_progress - completed /api/tenant/branding: get: summary: 'Get Branding' operationId: getBranding description: 'Returns tenant branding assets and colors used by the public website.' parameters: [] responses: { } tags: - Tenant put: summary: 'Update Branding' operationId: updateBranding description: "Updates tenant colors, social links, and branding media. Send multipart/form-data\nwith logo, favicon, profile_photo, or banner_files to store assets on the configured\nfilesystem disk, typically S3." parameters: [] responses: { } tags: - Tenant requestBody: required: false content: multipart/form-data: schema: type: object properties: primary_color: type: string description: 'Primary brand color. Must not be greater than 20 characters.' example: '#2563eb' nullable: true secondary_color: type: string description: 'Secondary brand color. Must not be greater than 20 characters.' example: '#14b8a6' nullable: true logo_url: type: string description: 'Must not be greater than 2048 characters.' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' nullable: true favicon_url: type: string description: 'Must not be greater than 2048 characters.' example: 'https://www.runte.com/ab-provident-perspiciatis-quo-omnis-nostrum-aut-adipisci' nullable: true profile_photo_url: type: string description: 'Must not be greater than 2048 characters.' example: 'https://cronin.com/incidunt-iure-odit-et-et-modi-ipsum.html' nullable: true logo: type: string format: binary description: 'Logo image uploaded to the configured filesystem disk, usually S3. Must be a file. Must not be greater than 4096 kilobytes.' nullable: true favicon: type: string format: binary description: 'Favicon image uploaded to the configured filesystem disk, usually S3. Must be a file. Must not be greater than 1024 kilobytes.' nullable: true profile_photo: type: string format: binary description: 'Owner or business profile image uploaded to the configured filesystem disk. Must be an image. Must not be greater than 4096 kilobytes.' nullable: true banner_images: type: object description: '' example: null properties: { } nullable: true banner_files: type: array description: 'Must be a file. Must not be greater than 8192 kilobytes.' items: type: string format: binary social_links: type: object description: 'Public social profile links.' example: instagram: 'https://instagram.com/pawscare' properties: { } nullable: true post: summary: 'Update Branding' operationId: updateBranding description: "Updates tenant colors, social links, and branding media. Send multipart/form-data\nwith logo, favicon, profile_photo, or banner_files to store assets on the configured\nfilesystem disk, typically S3." parameters: [] responses: { } tags: - Tenant requestBody: required: false content: multipart/form-data: schema: type: object properties: primary_color: type: string description: 'Primary brand color. Must not be greater than 20 characters.' example: '#2563eb' nullable: true secondary_color: type: string description: 'Secondary brand color. Must not be greater than 20 characters.' example: '#14b8a6' nullable: true logo_url: type: string description: 'Must not be greater than 2048 characters.' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' nullable: true favicon_url: type: string description: 'Must not be greater than 2048 characters.' example: 'https://www.runte.com/ab-provident-perspiciatis-quo-omnis-nostrum-aut-adipisci' nullable: true profile_photo_url: type: string description: 'Must not be greater than 2048 characters.' example: 'https://cronin.com/incidunt-iure-odit-et-et-modi-ipsum.html' nullable: true logo: type: string format: binary description: 'Logo image uploaded to the configured filesystem disk, usually S3. Must be a file. Must not be greater than 4096 kilobytes.' nullable: true favicon: type: string format: binary description: 'Favicon image uploaded to the configured filesystem disk, usually S3. Must be a file. Must not be greater than 1024 kilobytes.' nullable: true profile_photo: type: string format: binary description: 'Owner or business profile image uploaded to the configured filesystem disk. Must be an image. Must not be greater than 4096 kilobytes.' nullable: true banner_images: type: object description: '' example: null properties: { } nullable: true banner_files: type: array description: 'Must be a file. Must not be greater than 8192 kilobytes.' items: type: string format: binary social_links: type: object description: 'Public social profile links.' example: instagram: 'https://instagram.com/pawscare' properties: { } nullable: true /api/tenant/public-config: get: summary: 'Get Public Config' operationId: getPublicConfig description: 'Returns template-driven public website content such as homepage sections, FAQs, policies, SEO metadata, and public toggles.' parameters: [] responses: { } tags: - Tenant put: summary: 'Update Public Config' operationId: updatePublicConfig description: 'Updates flexible public website content for the authenticated tenant.' parameters: [] responses: { } tags: - Tenant requestBody: required: false content: application/json: schema: type: object properties: contact_details: type: object description: '' example: null properties: email: type: string description: 'Must be a valid email address.' example: gbailey@example.net nullable: true phone: type: string description: 'Must not be greater than 30 characters.' example: m nullable: true nullable: true homepage_content: type: object description: '' example: null properties: { } nullable: true banners: type: array description: '' example: null items: type: object nullable: true properties: url: type: string description: 'Must be a valid URL.' example: 'https://www.gulgowski.com/nihil-accusantium-harum-mollitia-modi-deserunt' nullable: true faq: type: array description: '' example: null items: type: object nullable: true properties: question: type: string description: '' example: architecto nullable: true answer: type: string description: '' example: architecto nullable: true services: type: object description: '' example: null properties: { } nullable: true privacy_policy: type: object description: '' example: null properties: { } nullable: true terms_conditions: type: object description: '' example: null properties: { } nullable: true seo_meta: type: object description: '' example: null properties: { } nullable: true public_toggles: type: object description: '' example: null properties: { } nullable: true settings: type: object description: '' example: null properties: { } nullable: true is_published: type: boolean description: '' example: true /api/tenant/public-config/media: post: summary: 'Upload Public Config Media' operationId: uploadPublicConfigMedia description: 'Uploads template-driven public website media such as hero, banner, and section images to the configured filesystem disk.' parameters: [] responses: { } tags: - Tenant requestBody: required: false content: multipart/form-data: schema: type: object properties: hero_image: type: string format: binary description: 'Hero image stored under tenant public-config media. Must be an image. Must not be greater than 8192 kilobytes.' nullable: true banner_images: type: array description: 'Must be a file. Must not be greater than 8192 kilobytes.' items: type: string format: binary section_images: type: array description: 'Must be a file. Must not be greater than 8192 kilobytes.' items: type: string format: binary section_key: type: string description: 'Optional section key for uploaded section images. Must not be greater than 100 characters.' example: about nullable: true /api/tenant/public-config/validate-required-fields: get: summary: 'Validate Required Fields' operationId: validateRequiredFields description: "Checks whether the current public content satisfies the selected template's required fields and returns any missing field keys." parameters: [] responses: { } tags: - Tenant /api/tenant/domains: get: summary: 'List Host Mappings' operationId: listHostMappings description: 'Lists all website hosts mapped to the authenticated tenant, including multiple subdomains and multiple custom domains.' parameters: [] responses: { } tags: - Tenant post: summary: 'Add Host Mapping' operationId: addHostMapping description: 'Adds a tenant website host mapping. Use `type=subdomain` with `subdomain`, or `type=custom_domain` with `host`.' parameters: [] responses: { } tags: - Tenant requestBody: required: false content: application/json: schema: type: object properties: host: type: string description: 'Full custom domain host. Use for custom_domain mappings. Must not be greater than 255 characters.' example: paws-care.com domain: type: string description: 'Must not be greater than 255 characters.' example: b subdomain: type: string description: 'Subdomain label. The API stores it as a full host using the configured tenant base domain. Must match the regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/. Must be at least 3 characters. Must not be greater than 63 characters.' example: paws-care label: type: string description: 'Must match the regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/. Must be at least 3 characters. Must not be greater than 63 characters.' example: 'n' type: type: string description: 'Host mapping type: subdomain or custom_domain.' example: subdomain enum: - subdomain - custom_domain is_primary: type: boolean description: 'Whether this mapped host should be the tenant primary public host.' example: true is_active: type: boolean description: 'Whether this mapped host can resolve public tenant configuration.' example: true /api/tenant/domains/sync: put: summary: 'Sync Host Mappings' operationId: syncHostMappings description: "Replaces the tenant's editable website host mappings from frontend-provided arrays. Existing mappings omitted from `subdomains` and `domains` are deactivated. Existing submitted mappings are reactivated." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Host mappings synced.' data: - id: 1 host: paws-care.vetllama.test domain: paws-care.vetllama.test label: paws-care subdomain: paws-care type: subdomain is_primary: true is_active: true is_verified: true - id: 2 host: pawscare.com domain: pawscare.com label: null subdomain: null type: custom_domain is_primary: false is_active: true is_verified: false properties: success: type: boolean example: true message: type: string example: 'Host mappings synced.' data: type: array example: - id: 1 host: paws-care.vetllama.test domain: paws-care.vetllama.test label: paws-care subdomain: paws-care type: subdomain is_primary: true is_active: true is_verified: true - id: 2 host: pawscare.com domain: pawscare.com label: null subdomain: null type: custom_domain is_primary: false is_active: true is_verified: false items: type: object properties: id: type: integer example: 1 host: type: string example: paws-care.vetllama.test domain: type: string example: paws-care.vetllama.test label: type: string example: paws-care subdomain: type: string example: paws-care type: type: string example: subdomain is_primary: type: boolean example: true is_active: type: boolean example: true is_verified: type: boolean example: true tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: subdomains: type: array description: 'Complete list of subdomain labels to keep mapped.' example: - paws-care - londonpets items: type: string domains: type: array description: 'Complete list of custom domains to keep mapped.' example: - pawscare.com - telepaws.co.uk items: type: string primary_host: type: string description: 'Optional primary host. May be a full host or one of the submitted subdomain labels.' example: paws-care.vetllama.test nullable: true required: - subdomains - domains '/api/tenant/domains/{domain_id}': put: summary: 'Update Host Mapping' operationId: updateHostMapping description: 'Updates host mapping status, verification metadata, or primary flag for a tenant-owned website host.' parameters: [] responses: { } tags: - Tenant requestBody: required: false content: application/json: schema: type: object properties: host: type: string description: 'Full custom domain host. Use for custom_domain mappings. Must not be greater than 255 characters.' example: paws-care.com domain: type: string description: 'Must not be greater than 255 characters.' example: b subdomain: type: string description: 'Subdomain label. The API stores it as a full host using the configured tenant base domain. Must match the regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/. Must be at least 3 characters. Must not be greater than 63 characters.' example: paws-care label: type: string description: 'Must match the regex /^[a-z0-9]+(?:-[a-z0-9]+)*$/. Must be at least 3 characters. Must not be greater than 63 characters.' example: 'n' type: type: string description: 'Host mapping type: subdomain or custom_domain.' example: subdomain enum: - subdomain - custom_domain is_primary: type: boolean description: 'Whether this mapped host should be the tenant primary public host.' example: true is_active: type: boolean description: 'Whether this mapped host can resolve public tenant configuration.' example: true delete: summary: 'Deactivate Host Mapping' operationId: deactivateHostMapping description: 'Deactivates a tenant website host mapping without removing historical configuration.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: domain_id description: 'The ID of the domain.' example: 16 required: true schema: type: integer '/api/tenant/domains/{domain_id}/verification': get: summary: 'Get Verification Details' operationId: getVerificationDetails description: 'Returns DNS TXT verification instructions and current status for a tenant custom domain.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: domain_id description: 'The ID of the domain.' example: 16 required: true schema: type: integer '/api/tenant/domains/{domain_id}/verification/check': post: summary: 'Check Domain Verification' operationId: checkDomainVerification description: 'Triggers a DNS TXT verification check for a tenant custom domain and updates its verification state.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: domain_id description: 'The ID of the domain.' example: 16 required: true schema: type: integer '/api/tenant/domains/{domain_id}/primary': post: summary: 'Set Primary Host Mapping' operationId: setPrimaryHostMapping description: "Marks this mapped host as the tenant's primary public host and clears other primary flags." parameters: [] responses: { } tags: - Tenant parameters: - in: path name: domain_id description: 'The ID of the domain.' example: 16 required: true schema: type: integer /api/tenant/locations: get: summary: 'List Locations' operationId: listLocations description: 'Lists physical visit locations for the authenticated tenant.' parameters: [] responses: { } tags: - Tenant post: summary: 'Create Location' operationId: createLocation description: 'Creates a physical location that can be linked to physical visit services.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 255 characters.' example: b address_line_1: type: string description: 'Must not be greater than 255 characters.' example: 'n' address_line_2: type: string description: 'Must not be greater than 255 characters.' example: g nullable: true city: type: string description: 'Must not be greater than 255 characters.' example: z state: type: string description: 'Must not be greater than 255 characters.' example: m nullable: true country: type: string description: 'Must not be greater than 255 characters.' example: i postal_code: type: string description: 'Must not be greater than 30 characters.' example: 'y' nullable: true latitude: type: number description: 'Must be between -90 and 90.' example: -89 nullable: true longitude: type: number description: 'Must be between -180 and 180.' example: -179 nullable: true contact_phone: type: string description: 'Must not be greater than 50 characters.' example: l nullable: true instructions: type: string description: '' example: architecto nullable: true is_primary: type: boolean description: '' example: false is_active: type: boolean description: '' example: false required: - name - address_line_1 - city - country '/api/tenant/locations/{id}': get: summary: 'Show Location' operationId: showLocation description: 'Returns one tenant-owned location.' parameters: [] responses: { } tags: - Tenant put: summary: 'Update Location' operationId: updateLocation description: 'Updates a tenant-owned physical location.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 255 characters.' example: b address_line_1: type: string description: 'Must not be greater than 255 characters.' example: 'n' address_line_2: type: string description: 'Must not be greater than 255 characters.' example: g nullable: true city: type: string description: 'Must not be greater than 255 characters.' example: z state: type: string description: 'Must not be greater than 255 characters.' example: m nullable: true country: type: string description: 'Must not be greater than 255 characters.' example: i postal_code: type: string description: 'Must not be greater than 30 characters.' example: 'y' nullable: true latitude: type: number description: 'Must be between -90 and 90.' example: -89 nullable: true longitude: type: number description: 'Must be between -180 and 180.' example: -179 nullable: true contact_phone: type: string description: 'Must not be greater than 50 characters.' example: l nullable: true instructions: type: string description: '' example: architecto nullable: true is_primary: type: boolean description: '' example: true is_active: type: boolean description: '' example: false required: - name - address_line_1 - city - country delete: summary: 'Delete Location' operationId: deleteLocation description: 'Deactivates a tenant-owned location.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: id description: 'The ID of the location.' example: 16 required: true schema: type: integer /api/tenant/services: get: summary: 'List Services' operationId: listServices description: 'Lists tenant service offerings, including their duration and pricing options.' parameters: [] responses: { } tags: - Tenant post: summary: 'Create Service' operationId: createService description: 'Creates a service offering for physical visits, telehealth, or instant consult configuration.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: type: type: string description: '' example: physical_visit enum: - physical_visit - telehealth - instant_consult name: type: string description: 'Must not be greater than 255 characters.' example: b description: type: string description: '' example: 'Eius et animi quos velit et.' nullable: true is_enabled: type: boolean description: '' example: false location_id: type: string description: 'This field is required when type is physical_visit. The id of an existing record in the tenant_locations table.' example: null nullable: true delivery_mode: type: string description: 'Must not be greater than 255 characters.' example: v nullable: true metadata: type: object description: '' example: null properties: { } nullable: true sort_order: type: integer description: 'Must be at least 0.' example: 42 nullable: true status: type: string description: '' example: active enum: - active - draft - archived required: - type - name '/api/tenant/services/{id}': get: summary: 'Show Service' operationId: showService description: 'Returns one tenant-owned service offering with duration and pricing options.' parameters: [] responses: { } tags: - Tenant put: summary: 'Update Service' operationId: updateService description: 'Updates a tenant-owned service offering.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: type: type: string description: '' example: instant_consult enum: - physical_visit - telehealth - instant_consult name: type: string description: 'Must not be greater than 255 characters.' example: b description: type: string description: '' example: 'Eius et animi quos velit et.' nullable: true is_enabled: type: boolean description: '' example: true location_id: type: string description: 'This field is required when type is physical_visit. The id of an existing record in the tenant_locations table.' example: null nullable: true delivery_mode: type: string description: 'Must not be greater than 255 characters.' example: v nullable: true metadata: type: object description: '' example: null properties: { } nullable: true sort_order: type: integer description: 'Must be at least 0.' example: 42 nullable: true status: type: string description: '' example: active enum: - active - draft - archived required: - type - name delete: summary: 'Delete Service' operationId: deleteService description: 'Archives a tenant-owned service offering.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: id description: 'The ID of the service.' example: 16 required: true schema: type: integer '/api/tenant/services/{service_id}/durations': get: summary: 'List Service Durations' operationId: listServiceDurations description: 'Lists pricing and duration options for a tenant-owned service.' parameters: [] responses: { } tags: - Tenant post: summary: 'Create Service Duration' operationId: createServiceDuration description: 'Creates a pricing and duration option for a tenant-owned service.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: duration_minutes: type: integer description: 'Must be at least 5. Must not be greater than 480.' example: 1 price: type: number description: 'Must be at least 0.' example: 39 currency: type: string description: 'Must be 3 characters.' example: gzm is_default: type: boolean description: '' example: false is_active: type: boolean description: '' example: true required: - duration_minutes - price - currency parameters: - in: path name: service_id description: 'The ID of the service.' example: 16 required: true schema: type: integer '/api/tenant/services/{service_id}/durations/{duration_id}': put: summary: 'Update Service Duration' operationId: updateServiceDuration description: 'Updates a pricing and duration option for a tenant-owned service.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: duration_minutes: type: integer description: 'Must be at least 5. Must not be greater than 480.' example: 1 price: type: number description: 'Must be at least 0.' example: 39 currency: type: string description: 'Must be 3 characters.' example: gzm is_default: type: boolean description: '' example: true is_active: type: boolean description: '' example: true required: - duration_minutes - price - currency delete: summary: 'Delete Service Duration' operationId: deleteServiceDuration description: 'Deactivates a pricing and duration option.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: service_id description: 'The ID of the service.' example: 16 required: true schema: type: integer - in: path name: duration_id description: 'The ID of the duration.' example: 16 required: true schema: type: integer /api/tenant/availability: get: summary: 'List Availability' operationId: listAvailability description: 'Lists weekly recurring availability windows for the authenticated tenant.' parameters: [] responses: { } tags: - Tenant post: summary: 'Create Availability' operationId: createAvailability description: 'Creates a weekly recurring availability window for a tenant, service, or location.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: service_offering_id: type: string description: 'The id of an existing record in the service_offerings table.' example: null nullable: true location_id: type: string description: 'The id of an existing record in the tenant_locations table.' example: null nullable: true day_of_week: type: integer description: 'Must be between 0 and 6.' example: 1 start_time: type: string description: 'Must be a valid date in the format H:i.' example: '11:23' end_time: type: string description: 'Must be a valid date in the format H:i. Must be a date after start_time.' example: '2052-06-26' timezone: type: string description: 'Must be a valid time zone, such as Africa/Accra.' example: Asia/Ulaanbaatar is_active: type: boolean description: '' example: false required: - day_of_week - start_time - end_time - timezone '/api/tenant/availability/{id}': put: summary: 'Update Availability' operationId: updateAvailability description: 'Updates a weekly recurring availability window.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: service_offering_id: type: string description: 'The id of an existing record in the service_offerings table.' example: null nullable: true location_id: type: string description: 'The id of an existing record in the tenant_locations table.' example: null nullable: true day_of_week: type: integer description: 'Must be between 0 and 6.' example: 1 start_time: type: string description: 'Must be a valid date in the format H:i.' example: '11:23' end_time: type: string description: 'Must be a valid date in the format H:i. Must be a date after start_time.' example: '2052-06-26' timezone: type: string description: 'Must be a valid time zone, such as Africa/Accra.' example: Asia/Ulaanbaatar is_active: type: boolean description: '' example: true required: - day_of_week - start_time - end_time - timezone delete: summary: 'Delete Availability' operationId: deleteAvailability description: 'Deactivates a weekly recurring availability window.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: id description: 'The ID of the availability.' example: 16 required: true schema: type: integer /api/tenant/availability-exceptions: get: summary: 'List Availability Exceptions' operationId: listAvailabilityExceptions description: 'Lists blocked dates or one-off availability exceptions for the authenticated tenant.' parameters: [] responses: { } tags: - Tenant post: summary: 'Create Availability Exception' operationId: createAvailabilityException description: 'Creates a full-day or partial-day blocked date for a tenant, service, or location.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: service_offering_id: type: string description: 'The id of an existing record in the service_offerings table.' example: null nullable: true location_id: type: string description: 'The id of an existing record in the tenant_locations table.' example: null nullable: true specific_date: type: string description: 'Must be a valid date.' example: '2026-06-03T11:23:05' start_time: type: string description: 'This field is required when full_day is false. Must be a valid date in the format H:i.' example: '11:23' nullable: true end_time: type: string description: 'This field is required when full_day is false. Must be a valid date in the format H:i. Must be a date after start_time.' example: '2052-06-26' nullable: true reason: type: string description: 'Must not be greater than 255 characters.' example: 'n' nullable: true full_day: type: boolean description: '' example: false required: - specific_date '/api/tenant/availability-exceptions/{id}': put: summary: 'Update Availability Exception' operationId: updateAvailabilityException description: 'Updates a blocked date or availability exception.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: service_offering_id: type: string description: 'The id of an existing record in the service_offerings table.' example: null nullable: true location_id: type: string description: 'The id of an existing record in the tenant_locations table.' example: null nullable: true specific_date: type: string description: 'Must be a valid date.' example: '2026-06-03T11:23:05' start_time: type: string description: 'This field is required when full_day is false. Must be a valid date in the format H:i.' example: '11:23' nullable: true end_time: type: string description: 'This field is required when full_day is false. Must be a valid date in the format H:i. Must be a date after start_time.' example: '2052-06-26' nullable: true reason: type: string description: 'Must not be greater than 255 characters.' example: 'n' nullable: true full_day: type: boolean description: '' example: false required: - specific_date delete: summary: 'Delete Availability Exception' operationId: deleteAvailabilityException description: 'Deletes a blocked date or availability exception.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: id description: 'The ID of the availability exception.' example: 16 required: true schema: type: integer /api/tenant/booking-policies: get: summary: 'Get Booking Policies' operationId: getBookingPolicies description: 'Returns booking rule configuration for the authenticated tenant.' parameters: [] responses: { } tags: - Tenant put: summary: 'Update Booking Policies' operationId: updateBookingPolicies description: 'Updates booking rule configuration such as advance notice, buffers, cancellation, and reschedule settings.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: timezone: type: string description: 'Must be a valid time zone, such as Africa/Accra.' example: Asia/Yekaterinburg min_advance_notice_minutes: type: integer description: 'Must be at least 0.' example: 39 max_advance_booking_days: type: integer description: 'Must be at least 1. Must not be greater than 730.' example: 7 buffer_before_minutes: type: integer description: 'Must be at least 0.' example: 12 buffer_after_minutes: type: integer description: 'Must be at least 0.' example: 77 cancellation_cutoff_hours: type: integer description: 'Must be at least 0.' example: 8 nullable: true reschedule_cutoff_hours: type: integer description: 'Must be at least 0.' example: 76 nullable: true slot_interval_minutes: type: integer description: 'Must be at least 5.' example: 14 nullable: true default_booking_status: type: string description: '' example: pending enum: - pending - confirmed allow_cancellation: type: boolean description: '' example: false allow_reschedule: type: boolean description: '' example: false required: - timezone - min_advance_notice_minutes - max_advance_booking_days - buffer_before_minutes - buffer_after_minutes - default_booking_status - allow_cancellation - allow_reschedule /api/tenant/licenses: get: summary: 'List Licenses' operationId: listLicenses description: 'Lists tenant licenses, certifications, and prescribing eligibility metadata.' parameters: [] responses: { } tags: - Tenant post: summary: 'Create License' operationId: createLicense description: "Creates a tenant license or certification record. Send multipart/form-data\nwith file to upload a PDF or image document to the configured filesystem disk." parameters: [] responses: { } tags: - Tenant requestBody: required: true content: multipart/form-data: schema: type: object properties: type: type: string description: 'License or certification type. Must not be greater than 100 characters.' example: professional title: type: string description: 'Display title for the license or certification. Must not be greater than 255 characters.' example: 'Veterinary Practice License' license_number: type: string description: 'Optional license or certificate number. Must not be greater than 255 characters.' example: VET-2026-001 nullable: true issuing_authority: type: string description: 'Must not be greater than 255 characters.' example: b nullable: true issue_date: type: string description: 'Must be a valid date.' example: '2026-06-03T11:23:05' nullable: true expiry_date: type: string description: 'Must be a valid date. Must be a date after or equal to issue_date.' example: '2052-06-26' nullable: true can_prescribe: type: boolean description: '' example: false file: type: string format: binary description: 'Optional PDF or image document uploaded to the configured filesystem disk, usually S3. Must be a file. Must not be greater than 10240 kilobytes.' nullable: true notes: type: string description: '' example: architecto nullable: true is_active: type: boolean description: '' example: false verification_status: type: string description: 'Internal verification status.' example: pending enum: - pending - verified - rejected nullable: true required: - type - title '/api/tenant/licenses/{id}': put: summary: 'Update License' operationId: updateLicense description: "Updates a tenant license or certification record. Send multipart/form-data\nwith file to replace the stored document." parameters: [] responses: { } tags: - Tenant requestBody: required: true content: multipart/form-data: schema: type: object properties: type: type: string description: 'License or certification type. Must not be greater than 100 characters.' example: professional title: type: string description: 'Display title for the license or certification. Must not be greater than 255 characters.' example: 'Veterinary Practice License' license_number: type: string description: 'Optional license or certificate number. Must not be greater than 255 characters.' example: VET-2026-001 nullable: true issuing_authority: type: string description: 'Must not be greater than 255 characters.' example: b nullable: true issue_date: type: string description: 'Must be a valid date.' example: '2026-06-03T11:23:05' nullable: true expiry_date: type: string description: 'Must be a valid date. Must be a date after or equal to issue_date.' example: '2052-06-26' nullable: true can_prescribe: type: boolean description: '' example: true file: type: string format: binary description: 'Optional PDF or image document uploaded to the configured filesystem disk, usually S3. Must be a file. Must not be greater than 10240 kilobytes.' nullable: true notes: type: string description: '' example: architecto nullable: true is_active: type: boolean description: '' example: false verification_status: type: string description: 'Internal verification status.' example: pending enum: - pending - verified - rejected nullable: true required: - type - title delete: summary: 'Delete License' operationId: deleteLicense description: 'Deactivates a tenant license or certification record.' parameters: [] responses: { } tags: - Tenant parameters: - in: path name: id description: 'The ID of the license.' example: 16 required: true schema: type: integer '/api/tenant/licenses/{license_id}': post: summary: 'Update License' operationId: updateLicense description: "Updates a tenant license or certification record. Send multipart/form-data\nwith file to replace the stored document." parameters: [] responses: { } tags: - Tenant requestBody: required: true content: multipart/form-data: schema: type: object properties: type: type: string description: 'License or certification type. Must not be greater than 100 characters.' example: professional title: type: string description: 'Display title for the license or certification. Must not be greater than 255 characters.' example: 'Veterinary Practice License' license_number: type: string description: 'Optional license or certificate number. Must not be greater than 255 characters.' example: VET-2026-001 nullable: true issuing_authority: type: string description: 'Must not be greater than 255 characters.' example: b nullable: true issue_date: type: string description: 'Must be a valid date.' example: '2026-06-03T11:23:05' nullable: true expiry_date: type: string description: 'Must be a valid date. Must be a date after or equal to issue_date.' example: '2052-06-26' nullable: true can_prescribe: type: boolean description: '' example: true file: type: string format: binary description: 'Optional PDF or image document uploaded to the configured filesystem disk, usually S3. Must be a file. Must not be greater than 10240 kilobytes.' nullable: true notes: type: string description: '' example: architecto nullable: true is_active: type: boolean description: '' example: true verification_status: type: string description: 'Internal verification status.' example: pending enum: - pending - verified - rejected nullable: true required: - type - title parameters: - in: path name: license_id description: 'The ID of the license.' example: 16 required: true schema: type: integer /api/tenant/payment-config: get: summary: 'Get Payment Configuration' operationId: getPaymentConfiguration description: 'Returns tenant payment provider configuration and connection status.' parameters: [] responses: { } tags: - Tenant put: summary: 'Update Payment Configuration' operationId: updatePaymentConfiguration description: 'Updates payment provider configuration. Secrets are encrypted at rest and omitted from responses.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: provider: type: string description: 'Must not be greater than 50 characters.' example: b mode: type: string description: '' example: live enum: - test - live connected_account_id: type: string description: 'Must not be greater than 255 characters.' example: 'n' nullable: true publishable_key: type: string description: 'Must not be greater than 255 characters.' example: g nullable: true secret_key: type: string description: '' example: architecto nullable: true webhook_secret: type: string description: '' example: architecto nullable: true charges_enabled: type: boolean description: '' example: false payouts_enabled: type: boolean description: '' example: true details_submitted: type: boolean description: '' example: true onboarding_completed_at: type: string description: 'Must be a valid date.' example: '2026-06-03T11:23:05' nullable: true status: type: string description: '' example: not_connected enum: - not_connected - pending - connected - disabled metadata: type: object description: '' example: null properties: { } nullable: true provider_metadata: type: object description: '' example: null properties: { } nullable: true required: - provider - mode - status /api/tenant/payment-config/stripe/connect: post: summary: 'Start Stripe Connect Onboarding' operationId: startStripeConnectOnboarding description: 'Creates or reuses a Stripe connected account and returns an onboarding URL.' parameters: [] responses: { } tags: - Tenant requestBody: required: true content: application/json: schema: type: object properties: return_url: type: string description: 'Frontend URL Stripe should return to after onboarding. Must be a valid URL. Must not be greater than 2048 characters.' example: 'https://owner.vetllama.test/settings/payments/return' refresh_url: type: string description: 'Frontend URL Stripe should use if onboarding link expires. Must be a valid URL. Must not be greater than 2048 characters.' example: 'https://owner.vetllama.test/settings/payments/refresh' country: type: string description: 'Optional ISO-3166 alpha-2 country for the connected account. Must be 2 characters.' example: US nullable: true required: - return_url - refresh_url /api/tenant/payment-config/stripe/status: get: summary: 'Get Stripe Connection Status' operationId: getStripeConnectionStatus description: 'Returns the locally stored Stripe connection status for this tenant.' parameters: [] responses: { } tags: - Tenant /api/tenant/payment-config/stripe/refresh: post: summary: 'Refresh Stripe Connection Status' operationId: refreshStripeConnectionStatus description: 'Pulls the latest connected-account status from Stripe and stores it locally.' parameters: [] responses: { } tags: - Tenant /api/tenant/payment-config/stripe/disconnect: post: summary: 'Disconnect Stripe' operationId: disconnectStripe description: 'Clears the local Stripe connected-account state. This does not delete the Stripe account.' parameters: [] responses: { } tags: - Tenant /api/tenant/onboarding/status: get: summary: 'Get Onboarding Status' operationId: getOnboardingStatus description: 'Returns step-by-step onboarding completion status for the authenticated tenant.' parameters: [] responses: { } tags: - Tenant /api/tenant/onboarding/publish-readiness: get: summary: 'Get Publish Readiness' operationId: getPublishReadiness description: 'Returns whether the authenticated tenant has completed the required setup to publish.' parameters: [] responses: { } tags: - Tenant /api/tenant/publish/status: get: summary: 'Get Publish Status' operationId: getPublishStatus description: 'Returns current tenant publication state and publish readiness details.' parameters: [] responses: { } tags: - Tenant /api/tenant/publish: post: summary: 'Publish Tenant' operationId: publishTenant description: 'Publishes the tenant website if all readiness requirements are satisfied.' parameters: [] responses: { } tags: - Tenant /api/tenant/unpublish: post: summary: 'Unpublish Tenant' operationId: unpublishTenant description: 'Removes the tenant website from public published status without deleting configuration.' parameters: [] responses: { } tags: - Tenant /api/tenant/pause: post: summary: 'Pause Tenant' operationId: pauseTenant description: 'Pauses public tenant availability while keeping setup data intact.' parameters: [] responses: { } tags: - Tenant /api/user/auth/magic-link/request: post: summary: 'Request Magic Link' operationId: requestMagicLink description: "Requests a one-time magic sign-in link for an existing EndUser under the resolved tenant. This endpoint never creates users and always returns a generic success response so email existence is not exposed.\n\nUse `Host: demo.vetllama.test` in local development when testing against seeded tenant data." parameters: - in: header name: Host description: '' example: demo.vetllama.test schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'If an account exists for that email, a sign-in link has been sent.' data: null properties: success: type: boolean example: true message: type: string example: 'If an account exists for that email, a sign-in link has been sent.' data: type: string example: null nullable: true 404: description: '' content: application/json: schema: type: object example: success: false message: 'Tenant could not be resolved for this host.' properties: success: type: boolean example: false message: type: string example: 'Tenant could not be resolved for this host.' 429: description: '' content: application/json: schema: type: object example: message: 'Too Many Attempts.' properties: message: type: string example: 'Too Many Attempts.' tags: - User requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Existing end-user email for the resolved tenant.' example: jane@example.com required: - email security: [] /api/user/auth/magic-link/verify: post: summary: 'Verify Magic Link' operationId: verifyMagicLink description: "Consumes a valid, single-use, tenant-bound magic-link token and returns an EndUser-scoped JWT. Tokens are stored hashed, expire after 15 minutes, and cannot be reused.\n\nUse `Host: demo.vetllama.test` in local development when testing against seeded tenant data." parameters: - in: header name: Host description: '' example: demo.vetllama.test schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Logged in successfully.' data: access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: bearer guard: end_user expires_in: 3600 user: id: 1 tenant_id: 1 name: 'Jane Customer' email: jane@example.com phone: null is_active: true properties: success: type: boolean example: true message: type: string example: 'Logged in successfully.' data: type: object properties: access_token: type: string example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: type: string example: bearer guard: type: string example: end_user expires_in: type: integer example: 3600 user: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 name: type: string example: 'Jane Customer' email: type: string example: jane@example.com phone: type: string example: null nullable: true is_active: type: boolean example: true 422: description: '' content: application/json: schema: type: object example: success: false message: 'The given data was invalid.' errors: token: - 'This magic link is invalid or has expired.' properties: success: type: boolean example: false message: type: string example: 'The given data was invalid.' errors: type: object properties: token: type: array example: - 'This magic link is invalid or has expired.' items: type: string tags: - User requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'End-user email used when requesting the magic link.' example: jane@example.com token: type: string description: '64-character magic-link token from the email.' example: 2dc9LW5MsHGpJmw9v72Uw0HppFWlNqHNrHuKxnjIuRtb2UZ2DFbpURkiN0Lr7qkg device_token: type: string description: 'Optional device token for future push/session tracking.' example: web-user-device nullable: true required: - email - token security: [] /api/user/auth/logout: post: summary: Logout operationId: logout description: 'Invalidates the current EndUser JWT and closes the latest open auth activity session. Tenant host resolution is still required.' parameters: - in: header name: Host description: '' example: demo.vetllama.test schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Logged out successfully.' data: null properties: success: type: boolean example: true message: type: string example: 'Logged out successfully.' data: type: string example: null nullable: true 403: description: '' content: application/json: schema: type: object example: success: false message: 'Unauthorized for this tenant.' properties: success: type: boolean example: false message: type: string example: 'Unauthorized for this tenant.' tags: - User /api/user/auth/refresh: post: summary: 'Refresh Token' operationId: refreshToken description: 'Exchanges a valid EndUser JWT for a fresh EndUser JWT. The token must belong to the tenant resolved from the Host header.' parameters: - in: header name: Host description: '' example: demo.vetllama.test schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Token refreshed successfully.' data: access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: bearer guard: end_user expires_in: 3600 user: id: 1 tenant_id: 1 email: jane@example.com properties: success: type: boolean example: true message: type: string example: 'Token refreshed successfully.' data: type: object properties: access_token: type: string example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... token_type: type: string example: bearer guard: type: string example: end_user expires_in: type: integer example: 3600 user: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 email: type: string example: jane@example.com tags: - User /api/user/auth/me: get: summary: 'Current Profile' operationId: currentProfile description: 'Returns the authenticated EndUser profile. The token must belong to the tenant resolved from the Host header.' parameters: - in: header name: Host description: '' example: demo.vetllama.test schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: success: true message: 'Profile fetched successfully.' data: id: 1 tenant_id: 1 name: 'Jane Customer' email: jane@example.com phone: null is_active: true properties: success: type: boolean example: true message: type: string example: 'Profile fetched successfully.' data: type: object properties: id: type: integer example: 1 tenant_id: type: integer example: 1 name: type: string example: 'Jane Customer' email: type: string example: jane@example.com phone: type: string example: null nullable: true is_active: type: boolean example: true tags: - User