Specialties API
All specialty endpoints are organization-scoped via Row-Level Security. Users can only access specialties within their current organization (set via /v1/me/switch-organization).
Authentication
All endpoints require authentication via Clerk session token:
Authorization: Bearer <clerk_session_token>RBAC Summary
| Operation | Roles Allowed |
|---|---|
| List/Read | All authenticated users in the organization |
| Create | Admin, Superadmin |
| Update | Admin, Superadmin |
| Delete | Admin, Superadmin |
GET /v1/specialties
List all specialties in the current organization.
Request
Query Parameters:
page(integer, optional) - Page number (default: 1)page_size(integer, optional) - Items per page (default: 25, max: 100)sort(string, optional) - Sort field (e.g.,title,-created_atfor descending)
Response: 200 OK
{
"data": [
{
"id": 1,
"organization_id": 1,
"title": "Cardiology",
"slug": "cardiology",
"created_at": "2025-01-10T08:00:00Z",
"updated_at": "2025-01-10T08:00:00Z"
},
{
"id": 2,
"organization_id": 1,
"title": "Dermatology",
"slug": "dermatology",
"created_at": "2025-01-12T10:30:00Z",
"updated_at": "2025-01-12T10:30:00Z"
}
],
"meta": {
"page": 1,
"page_size": 25,
"total": 2,
"total_pages": 1
}
}Errors
401 unauthorized- Missing or invalid authentication token403 forbidden- User not a member of any organization
POST /v1/specialties
Create a new specialty. Admin-only.
Request
Headers:
Content-Type: application/jsonBody:
{
"title": "Cardiology",
"slug": "cardiology"
}Fields:
title(string, required) - Display name for the specialtyslug(string, required) - URL-safe identifier (must be unique within organization)
Response: 201 Created
{
"data": {
"id": 1,
"organization_id": 1,
"title": "Cardiology",
"slug": "cardiology",
"created_at": "2025-01-10T08:00:00Z",
"updated_at": "2025-01-10T08:00:00Z"
}
}Errors
400 validation_error- Missing or invalid fieldsjson{ "error": { "code": "validation_error", "message": "Validation failed", "details": { "fields": { "title": "is required", "slug": "must be lowercase alphanumeric with hyphens only" } } } }403 forbidden- Non-admin attempting to create specialty409 conflict- Slug already exists in this organizationjson{ "error": { "code": "conflict", "message": "Specialty with slug 'cardiology' already exists in this organization" } }
GET /v1/specialties/{id}
Get a single specialty with related specialists.
Request
Path Parameters:
id(integer, required) - Specialty ID
Query Parameters:
include(string, optional) - Comma-separated list of relations to include (e.g.,specialists)
Response: 200 OK
{
"data": {
"id": 1,
"organization_id": 1,
"title": "Cardiology",
"slug": "cardiology",
"created_at": "2025-01-10T08:00:00Z",
"updated_at": "2025-01-10T08:00:00Z",
"specialists": [
{
"id": 5,
"name": "Dr. Jane Smith",
"title": "Cardiologist",
"avatar_url": "https://s3.../avatar.jpg"
}
]
}
}Response: 200 OK (without include parameter)
{
"data": {
"id": 1,
"organization_id": 1,
"title": "Cardiology",
"slug": "cardiology",
"created_at": "2025-01-10T08:00:00Z",
"updated_at": "2025-01-10T08:00:00Z"
}
}Errors
404 not_found- Specialty doesn't exist or not accessible in current organizationjson{ "error": { "code": "not_found", "message": "Specialty not found" } }
PUT /v1/specialties/{id}
Update a specialty. Admin-only.
Request
Path Parameters:
id(integer, required) - Specialty ID
Body:
{
"title": "Cardiovascular Medicine",
"slug": "cardiovascular-medicine"
}Fields:
title(string, optional) - Updated display nameslug(string, optional) - Updated slug (must be unique within organization)
Response: 200 OK
{
"data": {
"id": 1,
"organization_id": 1,
"title": "Cardiovascular Medicine",
"slug": "cardiovascular-medicine",
"created_at": "2025-01-10T08:00:00Z",
"updated_at": "2025-01-15T14:22:00Z"
}
}Errors
400 validation_error- Invalid field values403 forbidden- Non-admin attempting to update specialty404 not_found- Specialty doesn't exist or not accessible409 conflict- New slug already exists in this organization
DELETE /v1/specialties/{id}
Delete a specialty. Admin-only.
Request
Path Parameters:
id(integer, required) - Specialty ID
Response: 204 No Content
No response body.
Errors
403 forbidden- Non-admin attempting to delete specialty404 not_found- Specialty doesn't exist or not accessible409 conflict- Cannot delete specialty with active referencesjson{ "error": { "code": "conflict", "message": "Cannot delete specialty: 5 specialists are linked to this specialty" } }
Notes on Deletion
Foreign key constraints prevent deletion if:
- Specialists are linked via
specialist_specialtiesjunction table - Appointment templates reference this specialty via
specialty_id
The API returns a 409 conflict error with details about blocking references. Admins must first:
- Remove specialist associations, or
- Reassign appointment templates to a different specialty
Related Endpoints
Specialist-Specialty Association
Specialists link to specialties via the specialist_specialties junction table. Use the Specialists API to manage these associations:
POST /v1/specialists- Includespecialty_idsarray in request bodyPUT /v1/specialists/{id}- Updatespecialty_idsarray
Example:
{
"name": "Dr. Jane Smith",
"title": "Cardiologist",
"specialty_ids": [1, 3]
}See /docs/features/specialists/api.md for complete documentation.