# Surfaces

Surfaces are agent-generated data collection interfaces delivered to patients through existing communication channels. Agents analyze entity state and data gaps in the [world model](https://docs.amigo.ai/developer-guide/platform-api/platform-api/data-world-model), generate a `SurfaceSpec`, and the platform renders and delivers it. Submissions flow back as world model events through standard confidence gates and review.

{% hint style="info" %}
**Conceptual overview** - For the architecture, healthcare examples, and design rationale, see [Surfaces](https://docs.amigo.ai/docs/agent/surfaces) in the conceptual docs.
{% endhint %}

## Voice Agent Surface Tools

Four context graph tools allow voice agents to create and manage surfaces during live calls:

| Tool                   | Parameters                                                                                                                                      | Returns                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `create_surface`       | `entity_id` (required), `title` (required), `fields` (required, list), `description`, `channel`, `expires_in_hours`, `resource_type`, `context` | `surface_id`, `url`, `status`, `expires_at`                                                                 |
| `deliver_surface`      | `surface_id` (required), `channel_address` (required - phone number or email)                                                                   | `surface_id`, `url`, `status`                                                                               |
| `check_surface_status` | `surface_id` (required)                                                                                                                         | `surface_id`, `status`, `title`, `fields_count`, `entity_id`, `created_at`                                  |
| `list_entity_surfaces` | `entity_id` (required), `status` (optional filter)                                                                                              | `surfaces[]` (id, title, status, channel, created\_at), `count`                                             |
| `get_surface_insights` | `entity_id` (required)                                                                                                                          | `entity_id`, `completion_rate`, `preferred_channel`, `pending_count`, `total_surfaces`, `recent_surfaces[]` |

`get_surface_insights` queries the entity's surface analytics before creating new surfaces. Returns completion rate, preferred channel, and count of pending (unfinished) surfaces. Call this before `create_surface` to avoid surface fatigue - if a patient already has multiple pending surfaces or a low completion rate, the agent may choose to collect data verbally instead.

These tools proxy through the Platform API surface endpoints. Authentication uses the call's `agent_session` JWT, which is workspace-scoped, so surface operations are automatically isolated to the correct workspace.

All five tools handle errors gracefully - if the Platform API is unreachable, the agent receives an error message rather than an exception, allowing it to continue the call.

## Permissions

Two scopes control surface access:

| Scope            | Grants                                                |
| ---------------- | ----------------------------------------------------- |
| `surfaces:read`  | View surface specs, submissions, and lifecycle status |
| `surfaces:write` | Generate new surface specs                            |

Both scopes are included in the `member` role by default. RBAC enforcement is applied on all surface CRUD routes (view, create, update, delete).

## Management API

| Endpoint                         | Method | Description                                                                                  |
| -------------------------------- | ------ | -------------------------------------------------------------------------------------------- |
| `/v1/{ws}/surfaces`              | `POST` | Create a surface. Returns spec + signed `token` + absolute `url`.                            |
| `/v1/{ws}/surfaces`              | `GET`  | List surfaces. Filterable by `entity_id`, `status`. Paginated.                               |
| `/v1/{ws}/surfaces/{id}`         | `GET`  | Get a specific surface with current status.                                                  |
| `/v1/{ws}/surfaces/{id}/deliver` | `POST` | Deliver via SMS (or other channel). Sends URL to `channel_address`, records lifecycle event. |

The delivery endpoint sends the surface URL to the patient, marks the surface as `delivered`, writes an audit event, and publishes a `surface.delivered` SSE event for real-time dashboards. For SMS delivery, the platform checks patient consent before sending - if the patient has opted out of SMS (via STOP or similar keywords), delivery returns `403 Forbidden`.

**SMS delivery** uses a two-step process to maximize link clickability:

1. **vCard contact card** - A contact card attachment is sent first so the patient can save the sender's number. This is required for iOS, which blocks clickable links in SMS from unknown senders. The vCard includes the workspace's business name and phone number. If vCard delivery fails, the form link is still sent (best-effort).
2. **Short form link** - The surface URL is sent as a short redirect URL (`/s/f/{surface_id}`) rather than the full signed token URL. The short URL resolves server-side via 302 redirect to the full surface page. This keeps SMS messages compact and prevents carrier truncation.

The response includes delivery metadata:

| Field               | Type   | Description                                    |
| ------------------- | ------ | ---------------------------------------------- |
| `surface_id`        | string | The surface that was delivered                 |
| `status`            | string | Updated status (`delivered`)                   |
| `channel_address`   | string | The delivery target (phone number or email)    |
| `message_id`        | string | Provider message ID for tracking               |
| `from_number`       | string | The phone number the SMS was sent from         |
| `delivery_provider` | string | The delivery provider used                     |
| `short_url`         | string | The short redirect URL sent in the SMS         |
| `vcard_url`         | string | The vCard contact card URL (SMS delivery only) |
| `delivered_at`      | string | ISO 8601 timestamp of delivery                 |

**Lifecycle SSE events**: `surface.delivered`, `surface.opened`, `surface.submitted` are published in real time as they happen.

**Surface expiry**: Non-terminal surfaces (not yet completed) automatically expire after their configured TTL. Expired surfaces reject renders, submissions, and auto-saves.

## Channels

| Channel  | Value      | Delivery Method                              |
| -------- | ---------- | -------------------------------------------- |
| SMS      | `sms`      | Text message with secure link                |
| WhatsApp | `whatsapp` | WhatsApp message with secure link            |
| Email    | `email`    | Email with branded link                      |
| Voice    | `voice`    | IVR-style data collection during a live call |
| Web      | `web`      | Direct link for portals or dashboards        |

## Field Types

| Type          | Value         | Use Case                                                 |
| ------------- | ------------- | -------------------------------------------------------- |
| Short text    | `text`        | Name, address, pharmacy                                  |
| Long text     | `textarea`    | Symptoms, special instructions                           |
| Date          | `date`        | Date of birth, appointment date                          |
| Phone         | `phone`       | Contact number, emergency contact                        |
| Email         | `email`       | Patient email for follow-up                              |
| Number        | `number`      | Age, weight, dosage                                      |
| Single select | `select`      | Preferred provider, insurance type                       |
| Multi select  | `multiselect` | Symptom checklist, available days                        |
| Checkbox      | `checkbox`    | Consent confirmation, HIPAA acknowledgment               |
| Photo         | `photo`       | Insurance card, wound photo, ID                          |
| Signature     | `signature`   | Digital consent, authorization                           |
| File          | `file`        | Referral letter, lab results, prior records              |
| Heading       | `heading`     | Section headers within longer forms (display only)       |
| Info          | `info`        | Read-only explanatory text between fields (display only) |

## SurfaceSpec

The specification an agent generates to define a data collection surface.

| Field              | Type              | Required | Default      | Description                                                  |
| ------------------ | ----------------- | -------- | ------------ | ------------------------------------------------------------ |
| `entity_id`        | UUID              | Yes      | -            | Entity this surface is for                                   |
| `title`            | string            | Yes      | -            | Surface title shown to patient                               |
| `description`      | string            | No       | -            | Explanatory text below the title                             |
| `fields`           | SurfaceField\[]   | Yes      | -            | 1-100 data collection fields                                 |
| `channel`          | ChannelType       | No       | `web`        | Delivery channel                                             |
| `expires_in_hours` | integer           | No       | 168 (7 days) | TTL in hours (range: 1-8760)                                 |
| `context`          | object            | No       | `{}`         | Agent-provided context metadata                              |
| `resource_type`    | string            | No       | -            | Optional resource type tag (max 128 chars)                   |
| `branding`         | BrandingConfig    | No       | -            | Custom branding overrides (falls back to workspace branding) |
| `sections`         | SurfaceSection\[] | No       | -            | Multi-page form sections (each groups fields into a step)    |

## SurfaceField

A single data collection field within a surface.

| Field           | Type      | Required | Default | Description                                                 |
| --------------- | --------- | -------- | ------- | ----------------------------------------------------------- |
| `key`           | string    | Yes      | -       | Unique field identifier (1-128 chars)                       |
| `label`         | string    | Yes      | -       | Display label shown to patient                              |
| `field_type`    | FieldType | Yes      | -       | One of the 12 field types above                             |
| `required`      | boolean   | No       | `true`  | Whether the field must be filled                            |
| `options`       | string\[] | No       | -       | Choices for `select`, `multiselect` fields                  |
| `description`   | string    | No       | -       | Help text below the field                                   |
| `placeholder`   | string    | No       | -       | Placeholder text (max 256 chars)                            |
| `prefill_value` | any       | No       | -       | Pre-populated value from known data                         |
| `validation`    | object    | No       | -       | Field-specific validation rules                             |
| `sensitive`     | boolean   | No       | `false` | PHI flag for additional handling                            |
| `condition`     | object    | No       | -       | Conditional display rules (show/hide based on other fields) |

## BrandingConfig

Visual branding applied to the patient-facing form. Can be set at workspace level (default for all surfaces) or per-surface (overrides workspace defaults).

| Field              | Type   | Required | Description                                         |
| ------------------ | ------ | -------- | --------------------------------------------------- |
| `logo_url`         | string | No       | Logo image URL (max 2048 chars)                     |
| `primary_color`    | string | No       | Primary brand color, e.g., `#1A73E8` (max 32 chars) |
| `background_color` | string | No       | Page background color (max 32 chars)                |
| `font_family`      | string | No       | Font family name (max 256 chars)                    |

Workspace-level branding is configured through the branding settings endpoints:

| Endpoint                     | Method | Description                                     |
| ---------------------------- | ------ | ----------------------------------------------- |
| `/v1/{ws}/settings/branding` | `GET`  | Get workspace default branding (any role)       |
| `/v1/{ws}/settings/branding` | `PUT`  | Update workspace default branding (admin/owner) |

When rendering a surface, the platform merges workspace branding with surface-level branding. Surface-level values take precedence. Changes to workspace branding are audit-logged.

## SurfaceSection

Groups fields into pages for multi-step forms. When sections are defined, the patient sees one section at a time with navigation between steps.

| Field         | Type      | Required | Description                                                                   |
| ------------- | --------- | -------- | ----------------------------------------------------------------------------- |
| `title`       | string    | Yes      | Section heading shown to patient                                              |
| `description` | string    | No       | Explanatory text for the section                                              |
| `field_keys`  | string\[] | Yes      | 1-100 field keys from the surface's `fields` array to include in this section |

## SurfaceSubmission

Data submitted by a patient when completing a surface.

| Field              | Type          | Description                                      |
| ------------------ | ------------- | ------------------------------------------------ |
| `surface_id`       | UUID          | Which surface was submitted                      |
| `entity_id`        | UUID          | Entity the surface belongs to                    |
| `data`             | object        | Submitted field values (keyed by field `key`)    |
| `submitted_at`     | ISO timestamp | When the submission occurred                     |
| `channel`          | ChannelType   | Channel the submission came through              |
| `fields_submitted` | string\[]     | List of field keys that were filled              |
| `partial`          | boolean       | Whether this is a partial (auto-save) submission |

## Lifecycle

{% @mermaid/diagram content="%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#D4E2E7", "primaryTextColor": "#100F0F", "primaryBorderColor": "#083241", "lineColor": "#575452", "textColor": "#100F0F", "clusterBkg": "#F1EAE7", "clusterBorder": "#D7D2D0"}}}%%
stateDiagram-v2
\[*] --> Created : Agent generates SurfaceSpec
Created --> Delivered : Sent via channel (SMS, email, etc.)
Delivered --> Opened : Patient opens link
Opened --> Partial : Auto-save (some fields)
Partial --> Partial : Additional fields saved
Opened --> Completed : All required fields submitted
Partial --> Completed : Remaining fields submitted
Created --> Expired : TTL exceeded
Delivered --> Expired : TTL exceeded
Opened --> Expired : TTL exceeded
Partial --> Expired : TTL exceeded
Completed --> \[*]
Expired --> \[\*]" %}

| Status    | Value       | Description                     |
| --------- | ----------- | ------------------------------- |
| Created   | `created`   | Spec generated and stored       |
| Delivered | `delivered` | Sent to patient via channel     |
| Opened    | `opened`    | Patient opened the link         |
| Partial   | `partial`   | Some fields auto-saved          |
| Completed | `completed` | All required fields submitted   |
| Expired   | `expired`   | TTL exceeded without completion |

## Data Flow

Submissions are written to the world model as events:

* **Source**: `surface` - distinguishable from EHR, voice, or manual imports
* **Confidence**: Initial confidence level for patient-reported data
* **Entity**: All surface events (creation, delivery, submission) are linked to the `entity_id` on the event row, so they appear in entity timeline queries and trigger state recomputation

When a patient submits a surface, entity state recomputation runs within the same transaction. Submitted form data flows directly into entity demographic and clinical projections with no delay.

Surface data flows through the same confidence gates, review queues, and entity resolution pipelines as data from any other source. No special handling is required.

## Patient-Facing Endpoints

Patients access surfaces through HMAC-signed URL tokens - no login or account required. Each token grants access to one specific surface.

| Endpoint                            | Auth               | Description                                                                                                                                                                   |
| ----------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /s/f/{surface_id}`             | None               | Short URL redirect. Generates a fresh HMAC token and issues a 302 redirect to `/s/{token}`. Used in SMS delivery for compact links. Returns 404 if not found, 410 if expired. |
| `GET /s/vcard/{workspace_slug}.vcf` | None               | Serve a vCard 3.0 contact card for the workspace's SMS sender number. Used to establish iOS sender trust before delivering form links.                                        |
| `GET /s/{token}`                    | Token (no API key) | Render surface as mobile-first HTML. Updates status to `opened`.                                                                                                              |
| `POST /s/{token}/submit`            | Token (no API key) | Accept form submission. Writes events at confidence 0.5. Rejects if already completed or expired. Triggers entity state recomputation within the same transaction.            |
| `PUT /s/{token}/fields/{key}`       | Token (no API key) | Incremental auto-save for a single field. Updates status to `partial`.                                                                                                        |

**Token format**: HMAC-SHA256 signed with a configured secret. Contains surface ID and expiration. Validated on every request.

**Rate limiting** (per IP):

* Render: 30 requests/minute
* Submit: 10 requests/minute
* Auto-save: 30 requests/minute

**Field value validation**: Individual field values are capped at 10KB to prevent payload abuse.

**Rendering**: Server-side HTML generation for all 12 field types. Mobile-first, XSS-safe. Photo/file uploads use native device capabilities. Signature fields use touch input.

**Atomic lifecycle**: Status transitions (opened, partial, completed) are written atomically - check and insert happen in a single transaction to prevent race conditions like double submissions.

When creating a surface via the management API, the response includes the signed `token` and full `url` if the token secret is configured.

## Gap Scanner Settings

The gap scanner is a background loop on the connector runner that proactively detects missing entity data and creates surfaces. Configuration is per-workspace via settings endpoints.

**`GET /settings/gap-scanner`** - Get current gap scanner settings (any role).

**`PUT /settings/gap-scanner`** - Update gap scanner settings (admin/owner). Partial updates supported.

| Setting                       | Type              | Default      | Description                                                                                         |
| ----------------------------- | ----------------- | ------------ | --------------------------------------------------------------------------------------------------- |
| `enabled`                     | boolean           | `false`      | Enable/disable the scanner                                                                          |
| `scan_interval_seconds`       | integer           | 300 (5 min)  | How often to scan                                                                                   |
| `appointment_lookahead_hours` | integer           | 72 (3 days)  | How far ahead to check for upcoming appointments                                                    |
| `cooldown_hours`              | integer           | 168 (7 days) | How long before re-scanning the same entity                                                         |
| `max_surfaces_per_tick`       | integer           | 10           | Maximum surfaces created per scan cycle                                                             |
| `max_pending_surfaces`        | integer           | 3            | Skip entities with this many unfinished surfaces (fatigue gating)                                   |
| `min_completion_rate`         | float (0-1)       | 0.0          | Skip entities whose completion rate is below this threshold                                         |
| `channel_optimization`        | boolean           | `false`      | When enabled, use each entity's historically preferred channel instead of the requirement's default |
| `requirements`                | GapRequirement\[] | `[]`         | Gap detection rules                                                                                 |

### GapRequirement

| Field                 | Type                | Required | Default                | Description                                                   |
| --------------------- | ------------------- | -------- | ---------------------- | ------------------------------------------------------------- |
| `name`                | string              | Yes      | -                      | Rule name                                                     |
| `entity_type`         | string              | No       | `person`               | Entity type to scan                                           |
| `trigger`             | string              | No       | `upcoming_appointment` | When to check: `upcoming_appointment` or `recent_interaction` |
| `required_fields`     | GapRequiredField\[] | Yes      | -                      | 1-50 fields that must be present                              |
| `channel`             | ChannelType         | No       | `sms`                  | Delivery channel for generated surfaces                       |
| `priority`            | string              | No       | `normal`               | `low`, `normal`, or `high`                                    |
| `surface_title`       | string              | No       | Rule name              | Title for generated surfaces                                  |
| `surface_description` | string              | No       | -                      | Description shown on generated surfaces                       |

### GapRequiredField

| Field        | Type      | Required | Description                                                     |
| ------------ | --------- | -------- | --------------------------------------------------------------- |
| `path`       | string    | Yes      | Dot-notation path into entity state (e.g., `insurance.carrier`) |
| `label`      | string    | Yes      | Display label for the surface field                             |
| `field_type` | FieldType | No       | Field type (default: `text`)                                    |
| `min_items`  | integer   | No       | For list fields, minimum items required                         |

The scanner integrates with the connector runner's metrics and `/internal/status` endpoint. Changes to settings are audit-logged.

## Surface Analytics

Four endpoints under `/v1/{workspace_id}/analytics/surfaces/` for closed-loop optimization:

| Endpoint                                        | Description                                                                                                                |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `GET /analytics/surfaces/completion-rates`      | Overall completion rate, time-bucketed trend, by-source breakdown (agent, gap scanner, manual). Filterable by `entity_id`. |
| `GET /analytics/surfaces/channel-effectiveness` | Per-channel completion rate and average time-to-complete                                                                   |
| `GET /analytics/surfaces/field-abandonment`     | Per-field drop-off rate and save rate for abandoned surfaces                                                               |
| `GET /analytics/surfaces/entity/{entity_id}`    | Per-entity surface history: completion stats, preferred channel, recent surfaces                                           |

**Common parameters** (completion-rates, channel-effectiveness, field-abandonment):

| Parameter               | Type               | Default | Description                                        |
| ----------------------- | ------------------ | ------- | -------------------------------------------------- |
| `days`                  | integer (1-90)     | 30      | Lookback window                                    |
| `date_from` / `date_to` | date               | -       | Explicit date range                                |
| `interval`              | `1h` / `1d` / `1w` | `1d`    | Time bucket for trend data (completion-rates only) |

Entity history accepts `limit` (1-100, default 20).

All endpoints require `viewer+` permissions and are workspace-scoped.

## Entity Type

Surfaces introduce the `interaction` entity type to the world model ontology. Surface lifecycle events (created, delivered, opened, submitted) are tracked as first-class entities with full event sourcing.

## API Reference

* [Surfaces](https://docs.amigo.ai/api-reference/readme/platform/surfaces)
* [Gap Scanner Settings](https://docs.amigo.ai/api-reference/readme/platform/gap-scanner-settings)
