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