Skip to content

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:

json
{
  "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-fields

Request Body:

json
{
  "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:

  • key must be unique within (organization_id, entity_type)
  • options required if field_type is select, radio, or checkbox
  • Cannot create field with system_key set (system fields are auto-seeded)

Response:

json
{
  "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:

json
{
  "label": "City of Residence",
  "description": "Updated description",
  "sort_order": 15
}

Updatable Fields:

  • label
  • description
  • is_private
  • sort_order
  • options (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:

json
{
  "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_values for 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 Content

Entity Profiles

Get Patient Profile

GET /v1/patients/{id}/profile

Returns all custom field values for the patient.

Response:

json
{
  "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}/profile

Bulk update patient profile values.

Request Body:

json
{
  "city": "Rotterdam",
  "blood_type": "A+",
  "birthdate": "1990-05-15"
}

Behavior:

  • Creates or updates custom_field_values for 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:

json
{
  "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}/profile

Returns all custom field values for the specialist.

Response: Same structure as patient profile.


Update Specialist Profile

PUT /v1/specialists/{id}/profile

Same 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,birthdate

Returns specific custom field values for form auto-fill.

Query Parameters:

  • keys (required) - Comma-separated list of field keys to retrieve

Response:

json
{
  "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

json
{
  "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

json
{
  "status": 403,
  "name": "ForbiddenError",
  "message": "Cannot modify system field",
  "details": {
    "system_key": "patient_birthdate",
    "reason": "System fields are immutable"
  }
}

404 Not Found

json
{
  "status": 404,
  "name": "NotFoundError",
  "message": "Custom field not found"
}

Integration with Forms

When a form field has custom_field_id set, the form service:

  1. 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}
  2. On form save: For each field with custom_field_id, calls internal profile sync to upsert custom_field_values

Example form template field:

json
{
  "custom_field_id": 10,
  "label": "City",
  "type": "text",
  "required": false
}

Resulting form values:

json
{
  "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

go
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"`
}