Skip to content

Form API Endpoints

API endpoints for managing form instances (runtime forms filled by patients/specialists).


GET /v1/forms

List forms. Org-scoped. Patients see own forms only.

Query params:

  • ?status=pending,in_progress
  • ?type=survey
  • ?appointment_id=102
  • ?include=appointment

Response: 200

json
{
  "data": [
    {
      "id": 201,
      "title": "Patient Intake Survey",
      "type": "survey",
      "status": "in_progress",
      "appointment_id": 102,
      "created_at": "2025-01-15T10:30:00Z"
    }
  ],
  "meta": { "page": 1, "page_size": 25, "total": 3, "total_pages": 1 }
}

POST /v1/forms

Create form instance from template. Snapshots template fields, auto-fills from patient profile.

Request:

json
{
  "form_template_id": 10,
  "user_id": 42,
  "appointment_id": 102
}

Response: 201

json
{
  "data": {
    "id": 201,
    "title": "Patient Intake Survey",
    "type": "survey",
    "status": "pending",
    "template_version": 3,
    "fields": [...],
    "values": {"field_10": "Amsterdam"},
    "files": {}
  }
}

Business logic:

  1. Read form_template (fields, title, description, version)
  2. Snapshot fields into form.fields (immutable from this point)
  3. Record template_version
  4. Auto-fill: read custom_field_values for fields with custom_field_id set
  5. Pre-populate form.values with format field_{custom_field_id}: value
  6. Set status = "pending"

GET /v1/forms/{id}

Get form with fields, values, and files. Single query, single row.

Response: 200

json
{
  "data": {
    "id": 201,
    "title": "Patient Intake Survey",
    "type": "survey",
    "status": "in_progress",
    "template_version": 3,
    "form_template_id": 10,
    "appointment_id": 102,
    "user_id": 42,
    "fields": [
      {
        "custom_field_id": 10,
        "label": "City",
        "type": "text",
        "required": false,
        "private": false
      },
      {
        "custom_field_id": 11,
        "label": "Pain Level",
        "type": "select",
        "required": true,
        "options": ["No pain", "Less pain", "Big pain"]
      }
    ],
    "values": {
      "field_10": "Amsterdam",
      "field_11": "Big pain"
    },
    "files": {
      "field_20": {
        "url": "s3://...",
        "name": "sig.png",
        "size": 45000,
        "mime": "image/png"
      }
    },
    "completed_at": null,
    "signed_at": null,
    "created_at": "2025-01-15T10:30:00Z"
  }
}

PUT /v1/forms/{id}

Save form values. Validates against field definitions. Syncs custom_field_id values to custom_field_values. Evaluates segments.

Request:

json
{
  "values": {
    "field_10": "Amsterdam",
    "field_11": "Big pain"
  }
}

Response: 200

json
{
  "data": {
    "id": 201,
    "status": "completed",
    "completed_at": "2025-01-15T11:00:00Z"
  }
}

Business logic:

  1. Validate form.status is NOT "signed" (returns 409 Conflict if signed)
  2. Validate values against form.fields (required, types, options)
  3. Merge new values with existing
  4. For fields with custom_field_id set → upsert custom_field_values for the patient
  5. Evaluate segments referencing this form template
  6. Update status:
    • Partially filled → "in_progress"
    • All required fields filled → "completed"

Errors:

  • 409 form_already_signed - Cannot update a signed form
  • 400 validation_error - Values don't match field definitions

POST /v1/forms/{id}/sign

Sign a completed form. Makes it immutable.

Request: (empty body)

Response: 200

json
{
  "data": {
    "id": 201,
    "status": "signed",
    "signed_at": "2025-01-15T12:00:00Z"
  }
}

Business logic:

  1. Validate form.status is "completed"
  2. Set status = "signed", signed_at = NOW()
  3. From this point: values and fields are IMMUTABLE
  4. Audit log records signing event

Errors:

  • 400 form_not_completed - Form must be completed before signing
  • 409 form_already_signed - Form is already signed

DELETE /v1/forms/{id}

Delete form. Admin only. Cannot delete signed forms.

Response: 200

json
{
  "data": {
    "id": 201,
    "message": "Form deleted successfully"
  }
}

Errors:

  • 403 forbidden - Only admins can delete forms
  • 409 form_already_signed - Cannot delete signed forms

File Upload Endpoints

POST /v1/forms/{id}/files/{fieldKey}

Upload a file for a file field in the form.

Request:

  • Content-Type: multipart/form-data
  • Body: file upload

Response: 201

json
{
  "data": {
    "field_key": "field_20",
    "file": {
      "s3_key": "org-1/forms/201/field_20/abc123.png",
      "name": "signature.png",
      "size": 45000,
      "mime": "image/png",
      "uploaded_at": "2025-01-15T10:00:00Z"
    },
    "signed_url": "https://s3.amazonaws.com/...?signature=..."
  }
}

Business logic:

  1. Validate form exists and is not signed (409 if signed)
  2. Validate field with key exists and has type "file"
  3. Validate file size ≤ field.max_file_size (default: 10MB)
  4. Validate file MIME type in field.allowed_file_types
  5. Upload to S3: {orgID}/forms/{formID}/{fieldKey}/{uuid}.{ext}
  6. Store reference in form.files JSONB
  7. Return signed URL for immediate display

Errors:

  • 409 form_already_signed - Cannot upload files to signed forms
  • 400 field_not_found - Field key doesn't exist
  • 400 field_not_file_type - Field is not a file field
  • 400 file_too_large - File exceeds max_file_size
  • 400 invalid_file_type - File MIME type not allowed

GET /v1/forms/{id}/files/{fieldKey}

Get signed URL for a file field.

Response: 302 Redirects to time-limited signed S3 URL (15-minute expiry)

Access control:

  • Patient: own forms only (user_id match)
  • Specialist/admin: all forms in org
  • Signed URL is short-lived — no persistent URL leaks

DELETE /v1/forms/{id}/files/{fieldKey}

Delete a file from a form.

Response: 200

json
{
  "data": {
    "field_key": "field_20",
    "message": "File deleted successfully"
  }
}

Business logic:

  1. Validate form is not signed (409 if signed)
  2. Remove S3 object
  3. Clear files JSONB entry

Errors:

  • 409 form_already_signed - Cannot delete files from signed forms

Error Codes

CodeHTTPDescription
form_not_found404Form doesn't exist or not accessible
form_already_signed409Cannot modify a signed form
form_not_completed400Form must be completed before signing
validation_error400Values don't match field definitions
field_not_found400Field key doesn't exist in form
field_not_file_type400Field is not a file field
file_too_large400File exceeds max_file_size
invalid_file_type400File MIME type not allowed
forbidden403Insufficient permissions