Specialties API
NOT BUILT — reconciled 2026-08-02
None of these endpoints exist. See index.md for the build status. Three conventions in the examples below predate the platform's own and are wrong:
- PKs are UUIDs, never integers (P26 — UUIDv7). Every
"id": 1below should read as a UUID. - Authorization is per-org permission codes, never a role check. The "Roles Allowed" table has been corrected; if you find yourself comparing role codes in handler code, add a permission instead.
- Pagination params are
page+limit(apiquery.ParsePage), default ≤ 50, hard-capped atapiquery.MaxLimit(500) — notpage_sizewith a max of 100.
All specialty endpoints are organization-scoped via Row-Level Security. Callers can only access specialties within their current organization; the org comes from the resolved hostname → X-Organization-ID header, and every per-org route group additionally mounts RequireURLOrgMatchesScope("id") (P47).
Authentication
All endpoints require an authenticated principal:
Authorization: Bearer <session token>RBAC Summary
Authorization is by per-org permission code, checked by RequirePermission at the route and by current_app_has_permission(resource, action) inside the RLS policy. Superadmin (platform_memberships) bypasses.
| Operation | Permission |
|---|---|
| List/Read | membership-read (any principal with an active org membership) |
| Create | specialties.manage |
| Update | specialties.manage |
| Delete | specialties.manage — pre-checked, 409 with in-use counts when specialists or offerings still reference the row |
None of these permission rows are seeded today. This feature's migration (000040) seeds them and grants them to the system role templates.
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.