Custom Fields API
REST API endpoints for managing custom field definitions and entity profile values.
Overview
The custom fields API provides:
- Field definition management (admin-only)
- Entity profile CRUD (per entity type)
- Auto-fill helpers for form pre-population
Endpoints
Field Definitions (Admin Only)
List Custom Fields
GET /v1/custom-fields?entity_type={type}Query Parameters:
entity_type(optional) - Filter by entity type:patient,specialist,appointment,organization
Response:
{
"fields": [
{
"id": 1,
"organization_id": 5,
"entity_type": "patient",
"key": "city",
"label": "City",
"field_type": "text",
"options": null,
"description": null,
"is_private": false,
"sort_order": 10,
"system_key": null,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
},
{
"id": 2,
"organization_id": 5,
"entity_type": "patient",
"key": "blood_type",
"label": "Blood Type",
"field_type": "select",
"options": ["A+", "A-", "B+", "B-", "O+", "O-", "AB+", "AB-"],
"description": "Patient blood type",
"is_private": true,
"sort_order": 20,
"system_key": null,
"created_at": "2025-01-15T10:05:00Z",
"updated_at": "2025-01-15T10:05:00Z"
},
{
"id": 3,
"organization_id": 5,
"entity_type": "patient",
"key": "birthdate",
"label": "Date of Birth",
"field_type": "date",
"options": null,
"description": null,
"is_private": false,
"sort_order": 1,
"system_key": "patient_birthdate",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}Create Custom Field
POST /v1/custom-fieldsRequest Body:
{
"entity_type": "patient",
"key": "city",
"label": "City",
"field_type": "text",
"description": "Patient's city of residence",
"is_private": false,
"sort_order": 10
}Field Types: text, textarea, select, date, checkbox, radio, number, email, phone
Validation:
keymust be unique within(organization_id, entity_type)optionsrequired iffield_typeisselect,radio, orcheckbox- Cannot create field with
system_keyset (system fields are auto-seeded)
Response:
{
"id": 1,
"organization_id": 5,
"entity_type": "patient",
"key": "city",
"label": "City",
"field_type": "text",
"options": null,
"description": "Patient's city of residence",
"is_private": false,
"sort_order": 10,
"system_key": null,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}Update Custom Field
PUT /v1/custom-fields/{id}Request Body:
{
"label": "City of Residence",
"description": "Updated description",
"sort_order": 15
}Updatable Fields:
labeldescriptionis_privatesort_orderoptions(for select/radio/checkbox)
Not Updatable:
key(would break form references)entity_type(would break entity references)field_type(would break data validation)system_key(system fields are immutable)
Protection: Cannot update or delete fields where system_key IS NOT NULL
Response:
{
"id": 1,
"organization_id": 5,
"entity_type": "patient",
"key": "city",
"label": "City of Residence",
"field_type": "text",
"options": null,
"description": "Updated description",
"is_private": false,
"sort_order": 15,
"system_key": null,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T11:30:00Z"
}Delete Custom Field
DELETE /v1/custom-fields/{id}Cascade Behavior:
- All
custom_field_valuesfor this field are deleted (FK CASCADE) - Forms referencing this custom_field_id are unaffected (they have snapshotted field definitions)
- Future form saves referencing this custom_field_id will skip profile sync (field doesn't exist)
Protection: Cannot delete fields where system_key IS NOT NULL
Response:
204 No ContentEntity Profiles
Get Patient Profile
GET /v1/patients/{id}/profileReturns all custom field values for the patient.
Response:
{
"patient_id": 123,
"profile": {
"city": "Amsterdam",
"blood_type": "A+",
"birthdate": "1990-05-15",
"residence": "Amsterdam, Netherlands",
"occupation": "Engineer",
"sex": "Male"
},
"fields": [
{
"key": "city",
"label": "City",
"field_type": "text",
"is_private": false,
"system_key": null
},
{
"key": "blood_type",
"label": "Blood Type",
"field_type": "select",
"is_private": true,
"system_key": null
},
{
"key": "birthdate",
"label": "Date of Birth",
"field_type": "date",
"is_private": false,
"system_key": "patient_birthdate"
}
]
}Access Control:
- Patient: own profile only
- Specialist/Admin: all patients in org
Update Patient Profile
PUT /v1/patients/{id}/profileBulk update patient profile values.
Request Body:
{
"city": "Rotterdam",
"blood_type": "A+",
"birthdate": "1990-05-15"
}Behavior:
- Creates or updates
custom_field_valuesfor each provided key - Validates that custom fields with matching
key+entity_type='patient'exist - Returns 400 if unknown key provided
- Omitted keys are not modified (partial update)
Response:
{
"patient_id": 123,
"profile": {
"city": "Rotterdam",
"blood_type": "A+",
"birthdate": "1990-05-15",
"residence": "Amsterdam, Netherlands",
"occupation": "Engineer",
"sex": "Male"
}
}Get Specialist Profile
GET /v1/specialists/{id}/profileReturns all custom field values for the specialist.
Response: Same structure as patient profile.
Update Specialist Profile
PUT /v1/specialists/{id}/profileSame structure and behavior as patient profile update.
Auto-Fill Helpers
Get Pre-fill Values for Form
GET /v1/patients/{id}/prefill?keys=city,blood_type,birthdateReturns specific custom field values for form auto-fill.
Query Parameters:
keys(required) - Comma-separated list of field keys to retrieve
Response:
{
"patient_id": 123,
"values": {
"city": "Amsterdam",
"blood_type": "A+",
"birthdate": "1990-05-15"
}
}Behavior:
- Returns only the requested keys
- Missing keys are omitted from response (not included as null)
- Used by form service when creating forms that reference custom_field_id
Access Control:
- Same as GET profile
Error Responses
400 Bad Request - Validation Error
{
"status": 400,
"name": "ValidationError",
"message": "Validation failed",
"details": {
"errors": [
{"field": "key", "message": "already exists for this entity type"},
{"field": "options", "message": "required for select field type"}
]
}
}403 Forbidden - System Field Modification
{
"status": 403,
"name": "ForbiddenError",
"message": "Cannot modify system field",
"details": {
"system_key": "patient_birthdate",
"reason": "System fields are immutable"
}
}404 Not Found
{
"status": 404,
"name": "NotFoundError",
"message": "Custom field not found"
}Integration with Forms
When a form field has custom_field_id set, the form service:
- On form creation: Reads custom_field_values for the patient's custom fields and auto-fills form.values with keys like
field_{custom_field_id} - On form save: For each field with
custom_field_id, calls internal profile sync to upsertcustom_field_values
Example form template field:
{
"custom_field_id": 10,
"label": "City",
"type": "text",
"required": false
}Resulting form values:
{
"field_10": "Amsterdam"
}Validation on form template publish:
For each field where custom_field_id is set:
→ Lookup custom_fields WHERE id = custom_field_id
→ If not found, return 400: "custom_field_id 10 does not exist"
→ Validate entity_type = 'patient' (for patient forms)Go Service Interface
type CustomFieldService interface {
// Field definitions (admin)
ListFields(ctx context.Context, entityType string) ([]CustomField, error)
CreateField(ctx context.Context, input CreateCustomFieldInput) (*CustomField, error)
UpdateField(ctx context.Context, fieldID int64, input UpdateCustomFieldInput) (*CustomField, error)
DeleteField(ctx context.Context, fieldID int64) error
// Field values (per entity)
GetEntityProfile(ctx context.Context, entityType string, entityID int64) (EntityProfile, error)
SetEntityProfile(ctx context.Context, entityType string, entityID int64, values map[string]string) error
// Auto-fill helper
GetPrefill(ctx context.Context, patientID int64, keys []string) (map[string]string, error)
// Internal: used by form service for profile sync
SyncProfileValues(ctx context.Context, tx pgx.Tx, entityType string, entityID int64, keyValues map[string]string) error
}
type EntityProfile struct {
EntityID int64 `json:"entity_id"`
Profile map[string]string `json:"profile"`
Fields []CustomFieldSummary `json:"fields"`
}
type CustomFieldSummary struct {
Key string `json:"key"`
Label string `json:"label"`
FieldType string `json:"field_type"`
IsPrivate bool `json:"is_private"`
SystemKey *string `json:"system_key"`
}