# Changelog (/docs/changelog) Notable changes to the developer platform and this documentation, newest first. ## September 2026 [#september-2026] * **Portal redesign.** developer.anybio.io now carries the AnyBio design system - the marketing site's type, color, and dark mode - plus a restructured, task-first navigation and this changelog. * **Coding assistants promoted.** The AI-tool integration path ([Claude Code, Cursor, Lovable, Bolt](/docs/coding-assistants)) is now a top-level section, and the context-file downloads are served directly from this site. * **developer.anybio.io** replaced docs.anybio.io as the canonical docs domain; old links redirect. ## May 2026 [#may-2026] * **Device-agnostic rewrite.** Docs restructured around the normalized biosignal API rather than specific hardware, with new [Device Integration](/docs/device-integration) coverage. * **DSP guides.** New guides for building [custom signal processing algorithms](/docs/dsp). ## April 2026 [#april-2026] * **New site.** Docs migrated to a faster static stack with full-text search and [`/llms.txt`](/llms.txt) indexes for AI tools. * **bio-journal reference** added; [`bio-mood`, `bio-journal`, and `bio-health-metrics`](/docs/bioui-web) surfaced in the component index. * **Bolt integration guide** added. ## March 2026 [#march-2026] * **Lovable integration guide** added, with the Supabase Edge Function token-minting pattern. * **Token-mode authentication** documented as the recommended production auth for BioUI Web. * **Component references** added for `bio-progress-ring`, `bio-task-list`, `bio-mood`, and `bio-health-metrics`. # Coding assistants (/docs/coding-assistants) If an AI tool writes your code, start here. AnyBio ships context files and paste-ready prompts so your assistant produces working integrations - correct CDN URLs, correct auth flow, correct widget markup - instead of guessing. ## Three steps [#three-steps] ### Give your tool context [#give-your-tool-context] Download the context file for your tool and drop it in your project root: * **Claude Code**: [`ANYBIO.md`](/templates/ANYBIO.md) - reference it from your `CLAUDE.md` * **Cursor**: [`.cursorrules`](/templates/anybio.cursorrules) * **Lovable / Bolt**: no file needed - the paste prompts below carry the context inline This site also serves [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt), so any tool that reads docs URLs can pull the full reference directly. ### Add your keys [#add-your-keys] Have your project key (`proj_...`) and API key ready from the [AnyBio dashboard](https://secure.anybio.io/login) - your first sign-in creates an organization with a free sandbox. Keep the API key server-side - the guides below show the token-mode pattern for each platform. ### Prompt in plain language [#prompt-in-plain-language] Paste a prompt from the [AI prompt templates](/docs/bioui-web/guides/ai-prompt-templates) or write your own. With the context file in place, "add a mood check-in widget to my dashboard" produces correct `` markup and a working token endpoint. ## Per-tool guides [#per-tool-guides] Each guide covers setup, working prompt patterns, and the failure modes we've seen that tool hit: * [Claude Code](/docs/bioui-web/guides/claude-code-integration) - `ANYBIO.md` setup and terminal workflow * [Cursor](/docs/bioui-web/guides/cursor-integration) - `.cursorrules` setup and `@docs` usage * [Lovable](/docs/bioui-web/guides/lovable-integration) - Supabase Edge Function for token minting * [Bolt](/docs/bioui-web/guides/bolt-integration) - Next.js API route for token minting * [AI prompt templates](/docs/bioui-web/guides/ai-prompt-templates) - copy-paste prompts for common builds ## What your assistant should know [#what-your-assistant-should-know] The short version of what the context files teach: * Widgets are standard web components from `https://cdn.anybio.io/biosdk-web/v1/bioui.js`, wrapped in one ``. * Production auth is token mode: the backend calls `POST /api/v1/sdk/token`, the browser only holds a short-lived JWT. * Every widget needs a `project-key` and a user identity (`xuser-id` or `xuser-token`). * The full component and API reference lives on this site, indexed in [`/llms.txt`](/llms.txt). # AnyBio Developer Docs (/docs) Build applications that capture, stream, and process biosignals from BLE wearables. # Sandbox & limits (/docs/sandbox-and-limits) Every AnyBio organization starts with a **free sandbox environment**. It is a real environment on real infrastructure - the same APIs, SDKs, and widgets as production - scoped for building and testing. [Sign in](https://secure.anybio.io/login) to create yours; the first sign-in sets up your organization automatically. ## What the sandbox is for [#what-the-sandbox-is-for] Build your integration end to end without a contract or a card: render widgets, mint tokens, stream data, run scenario data through the pipeline. The sandbox is where you develop; production is where real users and real health data live. ## How the sandbox is limited [#how-the-sandbox-is-limited] The sandbox is capped, not metered for billing: * **Rate limits** - API throughput is capped per organization. Hitting a cap throttles or rejects requests; it never generates a charge. * **Retention** - sandbox data is for development and is retained on a shorter window than production. * **No real patient data** - the sandbox is for synthetic and test data only. Use [Synthetics scenarios](/docs/biosignals) to generate realistic streams without hardware. If you hit a sandbox cap while building something real, that is the signal to talk to us about production access. ## When billing starts [#when-billing-starts] You are never invoiced for sandbox usage. Billing begins when your organization moves to a production environment and live transactions flow - production is metered on usage, not seats. For current production terms, [contact us](https://anybio.io/contact). ## Environment model [#environment-model] | Environment | Purpose | Billed? | | ----------- | -------------------------------------------- | ---------------------- | | Sandbox | Build, test, integrate - synthetic data only | Never - capped instead | | Production | Live users, real health data | Usage-based | Your API keys and project keys are scoped per environment, so a sandbox key can never write into production. # Trust and security posture (/docs/trust) This page is for the person who has to sign off on the integration rather than write it. It states posture. It does not grade our own safety, and you will not find an accuracy percentage anywhere on this site. ## Certifications and agreements [#certifications-and-agreements] * **SOC 2 Type II**, across all five trust services criteria. * **HIPAA.** We sign Business Associate Agreements. If you are a covered entity or a business associate, you need one with us before production data flows. * **Model providers are under BAA with zero data retention.** The models that see patient-facing content do not retain it, and the agreement covering that is ours, not something you have to negotiate separately. ## Where data lives [#where-data-lives] **Environments are separate, and the separation is enforced rather than conventional.** Sandbox and production are distinct environments with their own keys. A sandbox key cannot write into production. Sandbox is for synthetic and test data; real patient data belongs in production. **Your data is yours.** Algorithms, protocols and care models belong to the people who built them. Running a model inside our environment does not give us rights to it, and it does not move a copy of patient data into yours. ## What your reviewers receive [#what-your-reviewers-receive] The record is the part most security reviews actually care about, because it is what turns a claim into evidence: * **Every patient-facing message is evaluated before it is sent.** The decision is recorded whether the message went out or not. * **A withheld message is recorded too.** What was proposed, what the gate decided, the policy and version that decided it, and whether a person released it. * **Holds route to a person.** Anything the gate will not clear waits for someone on your team, with the reason attached. * **Decisions cite a version.** Each one names the program document version that made it, so a reviewer can reconstruct what the rules were at the time rather than what they are now. For how that record is produced, see [the gate and its concepts](/docs/govern/concepts). For a two-week observation before anything is enforced, see [the observe engagement](/docs/govern/observe-engagement). ## Reaching us [#reaching-us] Security questions, BAAs and questionnaires: [contact us](https://anybio.io/contact). # Algorithm Marketplace (/docs/algorithm-marketplace) **Private preview.** The pages here document the concept and the customer-facing verification surface. [Contact us](https://anybio.io/contact) if you'd like to participate. ## What it is [#what-it-is] The Algorithm Marketplace lets specialized algorithm vendors publish DSP modules to the AnyBio platform; entitled customer organizations then run those algorithms against their own biosignal streams without ever taking a copy of the algorithm code. This solves a recurring problem in biosignal infrastructure: the best signal-processing algorithms (clinical-grade arrhythmia detection, sleep staging, advanced HRV analytics, novel biomarkers) are owned by specialized vendors who don't want to ship the source. Their customers need to run those algorithms inside compliant, validated infrastructure. The Marketplace bridges the two: * Algorithm vendors publish a signed binary to AnyBio. The platform stores it and the signature. * Customer organizations get entitled to specific algorithms by the vendor. * Entitled customers' streams are processed by the algorithm alongside the platform's built-in DSP, with outputs landing on the same `feature` and `observation` APIs as everything else. * The algorithm code itself is never exposed in API responses to the customer; it runs inside the platform's sandboxed runtime. ## Why it matters for your application [#why-it-matters-for-your-application] If you're a customer: * You can opt into specialized algorithms that fit your clinical or research use case without negotiating IP terms with the vendor on your own. * The same APIs you already use for built-in DSP outputs cover marketplace algorithm outputs - your application doesn't need new code paths. * Provenance is captured automatically: every observation tagged with a `method_code` identifies the algorithm and version that produced it. If you're an algorithm vendor: * Reach customers who need your algorithm without giving up source code or binaries. * Cryptographic signing means your customers and their auditors can verify the algorithm they're executing matches what you signed. * Per-customer telemetry is built in. ## What's documented publicly [#whats-documented-publicly] The public API documented here is the [signature verification endpoint](/docs/algorithm-marketplace/verification), which lets entitled customers and external auditors verify the algorithm bytes haven't been tampered with. Entitlement management, telemetry and vendor onboarding are handled with you directly - [contact us](https://anybio.io/contact) for access. ## Where to go next [#where-to-go-next] # Signature Verification (/docs/algorithm-marketplace/verification) **Private preview.** The endpoint documented here is the public-facing surface for signature verification. [Contact us](https://anybio.io/contact) for access. Every algorithm published to the AnyBio Marketplace is cryptographically signed by its publishing organization. Entitled customers - and external auditors acting on their behalf - can verify that the algorithm running against their streams matches what the publisher signed. ## How signing works [#how-signing-works] When an algorithm is published, the platform: 1. Computes a SHA-256 hash of the algorithm binary. 2. Signs the hash with the publisher organization's signing key. 3. Stores the signature alongside the algorithm. At execution time, the platform recomputes the hash and verifies the signature before running the algorithm. Verification is fail-closed: a mismatch causes execution to abort with an audit log entry, not a silent fallback. The signing key is per-publisher-organization, not per-algorithm. The same public key verifies every algorithm a given publisher has signed. ## Public-key endpoint [#public-key-endpoint] To verify externally - for example, in an audit pipeline that re-checks algorithm integrity outside the platform - fetch the publisher's public key: ```http GET /api/v1/algorithms/{algorithmId}/public-key ``` Returns the public key in PEM-encoded SubjectPublicKeyInfo form: ``` -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEA... -----END PUBLIC KEY----- ``` Authentication: any caller that can list the algorithm (the publisher organization itself, or an entitled customer organization) can fetch the public key. ## What the key verifies [#what-the-key-verifies] A successful verification confirms: * The algorithm binary hasn't been altered since the publisher signed it. * The signing key belongs to the publisher organization recorded on the algorithm. * Therefore: the algorithm running against your streams is the same code the publisher submitted. The public key endpoint is the trust artifact for external verification. Entitled customers don't need to do anything to trigger verification - the platform performs it on every execution - but having the public key available means an auditor can independently confirm what's running. ## Algorithm identity [#algorithm-identity] Each algorithm carries: * A stable `algorithmId` (UUID). * A version tag in its `method_code` (e.g., `algorithm:partner_hrv_v2`). * A reference to its publisher organization. Versions are immutable. When an algorithm is updated, the new version gets a new method code and a new (or rotated) signature. Historical observations keep their original method code so prior analyses remain reproducible. ## Why this matters [#why-this-matters] For clinical and regulated applications, signed algorithms with verifiable provenance are table stakes. The combination of cryptographic signing, fail-closed verification, and a public verification surface gives you a defensible chain of custody from algorithm vendor to observation: the auditor can confirm the algorithm bytes, the platform confirms the algorithm binding, and every output observation carries the algorithm version that produced it. For non-clinical applications the same machinery is still useful as a tamper-evident integrity check across the algorithm-vendor / platform / customer boundary. # BioSDK for iOS (/docs/biosdk) BioSDK is an iOS SDK for streaming biosignals from BLE wearables to the AnyBio platform. It handles device discovery, connection management, real-time data streaming, and reliable data delivery — so you can focus on your application logic. ## What You Can Build [#what-you-can-build] * **Real-time biosignal monitoring** - Stream ECG, PPG, EDA, temperature, heart rate, and other biosignals from supported wearables * **Multi-device sessions** - Connect a primary biosignal device alongside one or more secondary peripherals at the same time * **Reliable data capture** - Built-in buffering ensures no data is lost during network interruptions * **Session-based workflows** - Automatic session lifecycle with conflict detection and recovery ## Requirements [#requirements] * iOS 16.0+ * Swift 5.9+ * Xcode 15+ ## Next Steps [#next-steps] * [**Quickstart**](/docs/biosdk/quickstart) — Stream your first biosignal in 5 minutes * [**Installation**](/docs/biosdk/guides/installation) — Add BioSDK to your project * [**BioUI Widgets**](/docs/bioui/overview) — Drop-in SwiftUI chart components # Quickstart (/docs/biosdk/quickstart) Stream your first biosignal from a BLE wearable in under 5 minutes. ## 1. Install the SDK [#1-install-the-sdk] Add the binary Swift package in Xcode: 1. **File > Add Package Dependencies...** 2. Enter: `https://github.com/anybio/biosdk-ios-binary.git` 3. Add the `BioSDK` product to your app target See the full [Installation guide](/docs/biosdk/guides/installation) for details. ## 2. Configure the SDK [#2-configure-the-sdk] ```swift import BioSDK // Initialize with your project credentials let config = BioSDKClient.Configuration( organizationKey: "org_your_key", projectKey: "proj_your_key" ) let sdk = try await BioSDKClient.initialize(configuration: config) ``` ## 3. Subscribe to Events [#3-subscribe-to-events] ```swift import Combine var cancellables = Set() sdk.events .sink { event in switch event { case .lifecycle(let lifecycle): switch lifecycle { case .discovered(let device): print("Found: \(device.name)") case .connected(let device): print("Connected: \(device.name)") case .disconnected(let device, _, _, _): print("Disconnected: \(device.name)") } case .sample(let deviceId, let sample): print("[\(deviceId)] \(sample.signalName): \(sample.data)") case .deviceState(let device, let connection, let stream): print("\(device.name): \(connection) / \(stream)") case .session(let sessionEvent): print("Session: \(sessionEvent)") case .notification: break } } .store(in: &cancellables) ``` ## 4. Scan and Connect [#4-scan-and-connect] ```swift // Start scanning for BLE devices sdk.startScan() // When you find your device (e.g., from a UI list): let device = sdk.discoveredDevices.first! sdk.connect(device) ``` ## 5. Start Streaming [#5-start-streaming] Once the device reaches `.ready` connection state: ```swift sdk.startStreaming(deviceId: device.id) ``` Biosignal samples will flow through the `events` publisher as `.sample` events. ## 6. Stop and Disconnect [#6-stop-and-disconnect] ```swift sdk.stopStreaming(deviceId: device.id) sdk.disconnect(device) ``` ## What's Next [#whats-next] * [Configuration](/docs/biosdk/guides/configuration) — Environment setup and SDK options * [Scanning & Connecting](/docs/biosdk/guides/scanning-and-connecting) — Device discovery and connection lifecycle * [Session Management](/docs/biosdk/guides/session-management) — Session lifecycle, conflict handling, and recovery * [BioUI Widgets](/docs/bioui/overview) — Add real-time charts to your SwiftUI views # Catalog (/docs/biosignals/catalog) This is the canonical list of biosignals AnyBio supports today. Each row is a stable contract: the abbreviation, unit, and LOINC code are guaranteed to remain consistent across releases. ## Vitals [#vitals] | Signal | Abbr | Unit | Sample Rate | LOINC | Validation | | ----------------- | ------ | ---- | ----------- | ------- | ---------- | | Heart Rate | `HR` | bpm | 1 Hz | 8867-4 | 20-300 | | Respiratory Rate | `RR` | /min | 1 Hz | 9279-1 | 4-60 | | Blood Pressure | `BP` | mmHg | 1 Hz | - | - | | Body Temperature | `TEMP` | Cel | 1 Hz | 8310-5 | 30-45 | | Oxygen Saturation | `SpO2` | % | 1 Hz | 59408-5 | 0-100 | ## Waveforms [#waveforms] | Signal | Abbr | Unit | Sample Rate | LOINC | Validation | | ------------------ | ----- | ---- | ----------- | ----- | ---------- | | Electrocardiogram | `ECG` | mV | 250-500 Hz | - | - | | Photoplethysmogram | `PPG` | AU | 100-250 Hz | - | - | ## Autonomic & derived [#autonomic--derived] | Signal | Abbr | Unit | Sample Rate | LOINC | Validation | | ---------------------- | ----- | ---- | ----------- | ------- | ---------- | | Electrodermal Activity | `EDA` | uS | 1 Hz | - | 0-100 | | Heart Rate Variability | `HRV` | ms | 0.2 Hz | 80404-7 | 0-500 | ## Metabolic [#metabolic] | Signal | Abbr | Unit | Sample Rate | LOINC | Validation | | ------------- | ----- | ----- | ----------- | ------- | ---------- | | Glucose (CGM) | `GLU` | mg/dL | 1 Hz | 41653-7 | 20-600 | ## Activity & sleep [#activity--sleep] | Signal | Abbr | Unit | Sample Rate | LOINC | Validation | | ------------------- | ------- | --------- | ----------- | ------- | ---------- | | Steps | `STEPS` | steps/day | 1 Hz | 55423-8 | 0-100,000 | | Sleep Duration | `SLEEP` | min | 1 Hz | 93832-4 | 0-1,440 | | Calories Burned | `CAL` | kcal/day | 1 Hz | 41981-2 | 0-10,000 | | Active Zone Minutes | `AZM` | min/day | 1 Hz | - | 0-1,440 | ## Field reference [#field-reference] Every biosignal entry in the catalog carries the following fields: | Field | Type | Description | | ------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------- | | `id` | integer | Stable numeric identifier. Used in observation queries and stream metadata. | | `name` | string | Human-readable name (e.g., `"Heart Rate"`). | | `abbreviation` | string | Short identifier used in UIs and SDK code (e.g., `"HR"`). | | `unit` | string | UCUM-aligned unit string (e.g., `"bpm"`, `"mV"`, `"Cel"`). | | `loinc_code` | string \| null | LOINC code for FHIR interoperability, where one exists. See [FHIR Mapping](/docs/biosignals/fhir-mapping). | | `signal_type` | string | Category: `"Vital Signs"`, `"Waveform"`, `"Autonomic"`, `"Activity"`, `"Sleep"`, `"Metabolic"`, `"Derived"`. | | `throttle_seconds` | integer | Minimum interval between persisted observations from the same device (per-device throttling). `0` for waveforms. | | `validation_range` | `[min, max]` \| null | Range used to flag suspect values. Out-of-range values are still ingested but tagged. | | `emission_enabled` | boolean | Whether observations for this biosignal are persisted. Set to `false` to ingest without storing. | ## Adding new biosignals [#adding-new-biosignals] The catalog is data-driven. New biosignals can be added by extending the catalog without code changes - your existing application picks them up the next time it queries the API. Reach out if you have a biosignal type that isn't covered. Need a biosignal that isn't listed? The catalog is intentionally extensible. We add biosignals as we onboard new device categories - if your use case needs something specific, [contact us](https://anybio.io/contact). # FHIR Mapping (/docs/biosignals/fhir-mapping) AnyBio biosignals are designed for clinical interoperability. Every observation the platform persists carries enough metadata to render as a FHIR `Observation` resource without further translation. ## LOINC codes [#loinc-codes] Most vitals and derived rhythms in the [catalog](/docs/biosignals/catalog) carry a LOINC code. The code is on the biosignal definition, so the same observation can be exported to FHIR without per-record lookup. | Biosignal | LOINC | Display | | ----------------- | ------- | ------------------------------------------------------- | | Heart Rate | 8867-4 | Heart rate | | Respiratory Rate | 9279-1 | Respiratory rate | | Body Temperature | 8310-5 | Body temperature | | Oxygen Saturation | 59408-5 | Oxygen saturation in Arterial blood by Pulse oximetry | | HRV (RMSSD) | 80404-7 | R-R interval.standard deviation | | Glucose (CGM) | 41653-7 | Glucose \[Mass/volume] in Capillary blood by Glucometer | | Steps | 55423-8 | Number of steps in unspecified time Pedometer | | Sleep Duration | 93832-4 | Sleep duration | | Calories Burned | 41981-2 | Calories burned | ECG and PPG waveforms don't have direct LOINC scalar codes (LOINC isn't designed for high-rate continuous signals). They're exported as FHIR `Observation` resources with the waveform attached as a `valueSampledData` component. ## Observation export [#observation-export] Use the export API to retrieve a FHIR Bundle for a subject and time range: ```http GET /api/v1/fhir/observations ?subject_id={id} &since={ISO 8601} &until={ISO 8601} ``` The response is a FHIR R4 `Bundle` of `Observation` resources, ready to POST to a downstream FHIR server. Bundle entries include the `request` element so the bundle can be submitted as a transaction. ```json { "resourceType": "Bundle", "type": "transaction", "entry": [ { "resource": { "resourceType": "Observation", "status": "final", "code": { "coding": [{ "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" }] }, "subject": { "reference": "Patient/..." }, "effectiveDateTime": "2026-05-08T14:32:11Z", "valueQuantity": { "value": 72, "unit": "bpm", "system": "http://unitsofmeasure.org", "code": "/min" } }, "request": { "method": "POST", "url": "Observation" } } ] } ``` ## Provenance and method coding [#provenance-and-method-coding] Every observation carries a `method_code` describing how it was derived: * `device-reported` - the device emitted the value directly (e.g., wrist-worn HR from a smartwatch). * `algorithm:ecg_v1`, `algorithm:ppg_v1`, ... - derived by an AnyBio DSP algorithm, with a version tag for traceability. The corresponding FHIR `Provenance` resource is generated alongside each `Observation` in the bundle, linking back to the source device session. ## Units [#units] AnyBio uses UCUM-aligned units throughout. The `unit` field on an observation matches the `valueQuantity.code` in the FHIR export: * `bpm` (heart rate) maps to UCUM `/min` * `Cel` (temperature) maps to UCUM `Cel` * `mg/dL` (glucose) maps to UCUM `mg/dL` * `uS` (EDA) maps to UCUM `uS` (microsiemens) * `mV` (ECG) maps to UCUM `mV` ## Downstream integration [#downstream-integration] The FHIR export is suitable for ingestion into any standards-compliant FHIR server. We've validated the bundle format against several common targets, including hosted FHIR services. If you have a specific downstream system in mind, [contact us](https://anybio.io/contact) for integration notes. # Biosignals Overview (/docs/biosignals) AnyBio is biosignal infrastructure. Whatever the source - a chest strap, a ring, a smartwatch, a continuous glucose monitor, an EEG headset, or a custom-built sensor - the platform normalizes the stream into a consistent set of biosignals that your application can query, visualize, and act on. This section documents the **biosignal catalog**: the canonical list of biosignals AnyBio understands, the metadata each one carries (units, sampling rate, validation ranges, LOINC codes), and the APIs you use to discover and query them. ## What a biosignal is [#what-a-biosignal-is] In AnyBio, a biosignal is a typed measurement series with stable identifiers and units. Whether the underlying device emits a 250 Hz ECG waveform or a once-per-minute heart rate value, the platform represents both as biosignals - the difference shows up in the sample rate and the way you consume the stream, not in two different APIs. Each biosignal has: * A **stable ID and abbreviation** (`HR`, `ECG`, `PPG`, `SpO2`, ...). * A **unit** (`bpm`, `mV`, `Cel`, `mg/dL`, ...). * A **sample rate range** (1 Hz for derived vitals; 100-500 Hz for waveforms). * An optional **LOINC code** for FHIR interoperability. * A **validation range** the platform uses to flag suspect values. * An **emission policy** (per-device throttling, quality-aware filtering). ## What's in the catalog [#whats-in-the-catalog] AnyBio's biosignal catalog covers vitals, waveforms, autonomic signals, derived rhythms, metabolic data, activity, and sleep. See [Catalog](/docs/biosignals/catalog) for the full reference. The catalog is data-driven. New biosignals can be added without code changes, and your application discovers them through the API rather than hard-coding identifiers. ## Where to go next [#where-to-go-next] # Lookup & Query API (/docs/biosignals/lookup) The biosignal lookup API lets your application discover what's available at runtime - which biosignals a project is subscribed to, which are currently flowing, and what their metadata looks like - rather than hard-coding identifiers in your client. All endpoints are authenticated and scoped to the caller's organization. ## List biosignals available to a project [#list-biosignals-available-to-a-project] ```http GET /api/v1/projects/{projectId}/biosignals ``` Returns every biosignal configured for the project, including which are currently flowing from connected devices. ```typescript import { fetchProjectBiosignals } from "@anybio/sdk"; const biosignals = await fetchProjectBiosignals(projectId); for (const bs of biosignals) { console.log(`${bs.abbreviation} (${bs.name}) - ${bs.unit}`); if (bs.is_flowing) { console.log(` ...streaming live from ${bs.source_type}`); } } ``` Response shape: ```json [ { "id": 1, "name": "Heart Rate", "abbreviation": "HR", "unit": "bpm", "loinc_code": "8867-4", "signal_type": "Derived", "role": "primary", "source_type": "device", "is_flowing": true } ] ``` ## Query observations [#query-observations] Once you know which biosignals are available, query historical observations by ID: ```http GET /api/v1/observations ?biosignal_id={id} &subject_id={id} &since={ISO 8601 timestamp} &until={ISO 8601 timestamp} ``` ```typescript const observations = await fetchObservations({ biosignalId: 1, // HR subjectId, since: "2026-05-01T00:00:00Z", }); ``` Returns FHIR-aligned observation records: ```json [ { "id": "...", "biosignal_id": 1, "loinc_code": "8867-4", "value": 72, "unit": "bpm", "effective_time": "2026-05-08T14:32:11Z", "device_id": "...", "method_code": "algorithm:ecg_v1" } ] ``` ## Query feature streams [#query-feature-streams] For derived features (HR from ECG, HRV-RMSSD, PRV from PPG, etc.), query the feature series produced by the DSP pipeline: ```http GET /api/v1/streams/{streamId}/features ``` ```typescript const features = await fetchStreamFeatures(streamId); // features[i].name e.g. "hr_ecg", "hrv_rmssd", "prv_sdnn_5min" // features[i].ts_ns array of nanosecond timestamps // features[i].values array of feature values // features[i].cadence_hz output sample rate // features[i].qmask array of quality flags (0=good, 1=suspect, 2=bad) ``` See [DSP](/docs/dsp) for what features each biosignal produces. ## Real-time subscription [#real-time-subscription] For live data, subscribe to the WebSocket feed instead of polling: ```typescript const ws = new WebSocket( `wss://api.anybio.io/ws?subject=${subjectId}` ); ws.onmessage = (event) => { const frame = parseFrame(event.data); // 100ms protocol-buffer frame // frame.ecg, frame.ppg, frame.eda, frame.temp arrays }; ``` The wire format is a 100ms binary frame containing every active waveform channel. See the BioSDK [streaming guide](/docs/biosdk/guides/streaming) for the iOS-side equivalent. ## Discovery patterns [#discovery-patterns] A few patterns we recommend over hard-coding biosignal IDs in your application: 1. **Look up by abbreviation at startup.** Cache the `id` for the abbreviations your app cares about (`HR`, `ECG`, `SpO2`, ...) once per session, then query by ID afterward. 2. **Drive UI from the catalog.** A signal picker that renders the list returned by `GET /projects/{id}/biosignals` automatically gains support for new biosignals as they're added. 3. **Filter on `is_flowing`** to show only what's currently streaming from a connected device, instead of every biosignal the project is subscribed to. # BioUI Overview (/docs/bioui) # BioUI [#bioui] BioUI provides drop-in SwiftUI components for visualizing real-time biosignal data from BioSDK. All charts are designed to work directly with `BioLiveStore` and `BioSample` data. ## Available Components [#available-components] ### Charts [#charts] | Component | Signal | Description | | ----------------------------------------------- | ----------- | ------------------------------------ | | [`BioECGChart`](/docs/bioui/charts/ecg-chart) | ECG | Real-time electrocardiogram waveform | | [`BioPPGChart`](/docs/bioui/charts/ppg-chart) | PPG | Photoplethysmography waveform | | [`BioEDAChart`](/docs/bioui/charts/eda-chart) | EDA | Electrodermal activity waveform | | [`BioTempChart`](/docs/bioui/charts/temp-chart) | Temperature | Skin temperature over time | | [`BioHRChart`](/docs/bioui/charts/hr-chart) | Heart Rate | Heart rate trend line | ### Widgets [#widgets] | Component | Description | | -------------------------------------------------------------- | ---------------------------------------------------- | | [`BioHeartRateGauge`](/docs/bioui/components/heart-rate-gauge) | Real-time heart rate gauge with animated BPM display | | [`BioDeviceListView`](/docs/bioui/components/device-list-view) | Scan, discover, and connect to BLE devices | ## Installation [#installation] BioUI is included as an optional product in the BioSDK package. Add it to your target: ```swift .product(name: "BioUI", package: "biosdk-ios-binary") ``` ## Quick Example [#quick-example] ```swift import BioSDK import BioUI struct MonitorView: View { @ObservedObject var store: BioLiveStore var body: some View { VStack { BioHeartRateGauge(store: store) if let ecgSamples = store.signals["ecg"] { BioECGChart(samples: ecgSamples) } if let ppgSamples = store.signals["ppg"] { BioPPGChart(samples: ppgSamples) } } } } ``` ## Theming [#theming] All BioUI components support customization through `BioChartTheme` and `BioColorMap`. See the [Theming guide](/docs/bioui/theming). # Theming (/docs/bioui/theming) All BioUI chart components support visual customization through `BioChartTheme` and `BioColorMap`. ## BioChartTheme [#biocharttheme] ```swift let theme = BioChartTheme( lineColor: .green, backgroundColor: .black, lineWidth: 2.0, gridColor: .gray.opacity(0.3) ) BioECGChart(samples: samples) .bioChartTheme(theme) ``` ## BioColorMap [#biocolormap] Map signal types to specific colors across all charts: ```swift let colorMap = BioColorMap( ecg: .green, ppg: .red, eda: .blue, temp: .orange, hr: .pink ) ``` ## Dark Mode [#dark-mode] All BioUI components respect the system color scheme automatically. Use `BioChartTheme` to customize colors for each mode if needed. # BioUI Web Overview (/docs/bioui-web) # BioUI Web [#bioui-web] BioUI Web is a framework-agnostic widget library for digital health applications. It provides drop-in Web Components that handle check-ins, progress tracking, and task management — so you can embed patient-reported outcome (PRO) workflows into any web app with a few lines of HTML. ## Architecture [#architecture] BioUI Web is built on modern web standards: * **Web Components** — Custom elements that work in any framework (React, Vue, Angular, plain HTML) * **Lit** — Lightweight rendering library for fast, reactive components * **Light DOM** — Widgets render into the light DOM so your stylesheets apply naturally * **Lazy loading** — Only the widgets you use are fetched and registered * **CSS custom properties** — Theming via standard `--bio-*` custom properties ## Widget Catalog [#widget-catalog] ### Components [#components] | Component | Description | | ----------------------------------------------------------------------- | --------------------------------------------------------------- | | [``](/docs/bioui-web/components/bio-provider) | Auth context provider — wraps all other widgets | | [``](/docs/bioui-web/components/bio-checkin) | PRO check-in questionnaire (FHIR QuestionnaireResponse) | | `` | Displays outstanding tasks for the current user | | `` | Animated circular progress indicator | | `` | Multi-day progress visualization as a mountain chart | | [``](/docs/bioui-web/components/bio-mood) | Emoji-scale mood tracker (one observation per day) | | [``](/docs/bioui-web/components/bio-journal) | Free-text journal entry with optional prompt and history | | [``](/docs/bioui-web/components/bio-health-metrics) | OAuth wearable connection (Fitbit, etc.) and synced health data | ## Auth Model [#auth-model] BioUI Web uses a service-to-service auth model. Your application provides three identifiers: | Attribute | Purpose | | ------------- | --------------------------------------------------------------------- | | `api-key` | Organization developer key (sent as a Bearer token) | | `project-key` | Identifies the project within your organization | | `xuser-id` | Your app's external user ID, resolved to an AnyBio UUID on the server | There is no end-user login. Your application owns the identity and passes it through ``. See the [Authentication guide](/docs/bioui-web/guides/authentication) for details. ## Distribution [#distribution] BioUI Web is distributed via CDN. Add a single script tag to get started: ```html ``` ## Next Steps [#next-steps] * [**Quickstart**](/docs/bioui-web/quickstart) — Display a widget in under 10 lines of code * [**Authentication**](/docs/bioui-web/guides/authentication) — How credentials and user identity work * [**Check-in Integration**](/docs/bioui-web/guides/checkin-integration) — Build a PRO check-in workflow # Quickstart (/docs/bioui-web/quickstart) Display a BioUI Web widget in under 10 lines of code. No build step, no framework requirement - the widgets are standard web components served from our CDN. ## Prerequisites [#prerequisites] No account yet? [Sign in or create one](https://secure.anybio.io/login) - your first sign-in creates an organization with a free sandbox. You need two values from the AnyBio dashboard: * **Project key** (`proj_...`) - identifies which project the widget writes to. * **API key** (`org_...`) or **SDK key** (`sdk_...`) - see [Authentication](/docs/bioui-web/guides/authentication) for which one fits your setup. For this quickstart's local HTML file, direct mode with an `org_` key is fine; switch to token mode before anything user-facing. Both live in your project settings in the dashboard. Two modes, one decision: | Mode | Best for | API key in browser? | | ---------------------------- | ---------------------------- | -------------------------- | | **Token mode** (recommended) | Production, third-party apps | No - key stays server-side | | **Direct mode** | Prototyping, internal tools | Yes - visible in HTML | This quickstart uses direct mode to get you rendering fastest. The [Authentication guide](/docs/bioui-web/guides/authentication) covers switching to token mode - it is a two-attribute change. ## Your first widget [#your-first-widget] ### Add the CDN script [#add-the-cdn-script] Include the BioUI Web script and default theme in your HTML ``: ```html ``` ### Add the provider and a widget [#add-the-provider-and-a-widget] Wrap your widgets in a `` with your credentials, then drop in any widget: ```html ``` The provider authenticates, resolves the user, and every child widget receives the context automatically. ### Open the page [#open-the-page] Open the file in your browser. **Success looks like this: the progress ring renders and animates with the user's data.** A new `xuser-id` starts empty, so a fresh ring at zero is still a working ring - the widget is live and bound to that user. ## If your first render fails [#if-your-first-render-fails] | What you see | Likely cause | Fix | | -------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Nothing renders, unstyled text shows | CDN script or CSS didn't load | Check both URLs from step 1 are in ``; look for 404s in the network tab | | Widget renders but shows an auth error | Wrong or mistyped key | Keys are prefixed: `org_` (API), `proj_` (project), `sdk_` (SDK). Re-copy from the dashboard | | `401` in the browser console | Key doesn't have access to that project, or a stale `xuser-token` | Confirm key and `project-key` belong to the same project; in token mode, mint a fresh token | | Widget stuck in a loading state | `` missing or the widget sits outside it | Every widget must be a child of one `` | | Worked locally, fails deployed | Direct mode with an `org_` key in public HTML | Switch to [token mode](/docs/bioui-web/guides/authentication) - the browser should only ever hold a short-lived token | Still stuck? The [Authentication guide](/docs/bioui-web/guides/authentication) walks through both auth modes end to end. ## Full example [#full-example] ```html BioUI Web Quickstart

Today's Progress

``` ## What's next [#whats-next] * [Authentication](/docs/bioui-web/guides/authentication) - switch to token mode before going user-facing * [Check-in Integration](/docs/bioui-web/guides/checkin-integration) - build a full PRO check-in workflow * [``](/docs/bioui-web/components/bio-provider) - provider configuration reference * [``](/docs/bioui-web/components/bio-checkin) - check-in widget reference # Adding a Device (/docs/device-integration/adding-a-device) Adding a new wearable to AnyBio is a managed process - we handle the per-device protocol work so your application code stays at the biosignal layer. ## When a custom integration makes sense [#when-a-custom-integration-makes-sense] If your project depends on a wearable that isn't yet on the [supported list](/docs/device-integration/supported-devices), consider whether one of these paths fits first: * **Standard profiles.** If the device implements [Bluetooth SIG Heart Rate](/docs/device-integration/bluetooth-sig) or another public profile, the platform may already work without any custom work. * **Substituting hardware.** Many studies and applications can use any device of a given category (e.g., any ECG chest patch, any CGM). If your use case isn't tied to one specific model, a supported alternative may be a faster path. * **Cloud-to-cloud.** For devices that don't stream over BLE but expose a vendor API (most CGMs, activity aggregators), the integration is cloud-to-cloud rather than via the BioSDK. When none of those fit - the device is essential, has proprietary protocols, and isn't currently supported - reach out about a custom integration. ## What we need from you [#what-we-need-from-you] To scope a new integration, the following information helps us move quickly: * **Device specification.** Manufacturer, model, firmware version range. The vendor's BLE protocol document if you have one. * **Signals.** Which biosignals the device emits, expected sample rates, units, and any known quality artifacts. * **Use case.** Whether this is a one-off study, a long-term partnership, or a deployment target. Affects integration depth and timeline. * **Sample data, if available.** A few minutes of representative output (BLE captures, exported recordings) accelerates validation. [Contact us](https://anybio.io/contact) and we'll scope the work. ## What we deliver [#what-we-deliver] A supported integration includes: * A device-specific decoder running inside the platform that translates the wearable's native format into the AnyBio biosignal catalog. * Validation against the platform's standard quality framework (range checks, quality mask propagation). * DSP wiring for any waveform signals the device produces (HR/HRV from ECG, HR/PRV from PPG, etc.). * BioSDK support so applications can discover and connect to the device through the same scan-and-connect flow as any other supported wearable. * A consistent biosignal API surface for your application: no new endpoints, no new code paths, just additional sources for the biosignals you already consume. ## Versioning and maintenance [#versioning-and-maintenance] Once an integration is live, the platform tracks the device's firmware versions and applies decoder updates as the vendor releases changes. Your application doesn't need to coordinate with firmware updates - the integration layer handles compatibility. For partnerships involving multiple device models or a long-running roadmap, we offer dedicated support and faster integration timelines for new models in the same family. Reach out for details. # Bluetooth SIG Profiles (/docs/device-integration/bluetooth-sig) Where a wearable implements a Bluetooth SIG standard profile, AnyBio integrates it without per-vendor work. This is the cleanest integration path: any device that complies with the standard works the same way. ## Heart Rate profile [#heart-rate-profile] The [Bluetooth SIG Heart Rate profile](https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/) is supported end-to-end. Any chest strap, wristband, or watch that exposes the standard `Heart Rate Service` (UUID `0x180D`) and `Heart Rate Measurement` characteristic (UUID `0x2A37`) will: * Be discoverable through the BioSDK scan flow. * Connect through the standard scan-and-connect path described in [Scanning & Connecting](/docs/biosdk/guides/scanning-and-connecting). * Stream heart rate to the platform as a `HR` biosignal (1 Hz, validated 20-300 bpm). * Persist as observations queryable via the [biosignal lookup API](/docs/biosignals/lookup). * Export to FHIR with LOINC code `8867-4`. You don't need to know whether the underlying device is a Polar strap, a Wahoo monitor, a Garmin watch, or any other compliant peripheral - the integration is the same. ## Auto-connect [#auto-connect] For applications that want to connect to nearby standard heart rate peripherals automatically: ```swift BioBLEManager.shared.enableAutoConnectHRM(true, maxConnections: 3) ``` This scans for devices advertising the Heart Rate Service and connects to up to `maxConnections` of them. Useful in multi-device sessions where a primary biosignal device (e.g., a ring or chest patch) is paired with one or more standard heart rate peripherals to round out signal coverage. ## Other profiles [#other-profiles] Additional Bluetooth SIG profiles are added as customer demand justifies the work. If your project depends on a specific standard profile, [contact us](https://anybio.io/contact) - the platform's standards path is intentionally kept up to date. ## Why standards matter [#why-standards-matter] Standardized profiles let your application portfolio scale across hardware without per-device integration cost. A clinic deploying AnyBio for a remote-monitoring program can let patients use whichever compatible heart rate peripheral they already own. A consumer app can target "any Bluetooth SIG heart rate device" without negotiating a partnership for every wearable on the market. For waveform-class signals (ECG, PPG raw streams), no public Bluetooth SIG standard exists at the level of fidelity AnyBio operates on, so vendor-specific integrations remain the path for those biosignals. See [Supported Devices](/docs/device-integration/supported-devices) for the categories we cover today. # Device Integration Overview (/docs/device-integration) The AnyBio platform is device-agnostic by design. Whether the source is a chest strap, a ring, a smartwatch, a continuous glucose monitor, or a custom-built sensor, the same biosignal catalog and the same APIs apply. Device-specific protocols are handled inside the platform, so your application code stays the same as you add or swap hardware. ## How it works [#how-it-works] When a wearable connects, AnyBio normalizes its raw output into the platform's canonical wire format. From there: * **Waveforms** (ECG, PPG, EDA, temperature traces) flow into the [DSP pipeline](/docs/dsp), which produces feature series like heart rate and HRV. * **Discrete signals** (device-reported HR, glucose, steps, sleep, calories, etc.) become observations directly. * **Both** are queryable through the same endpoints documented in [Biosignals](/docs/biosignals). Your application sees biosignals - not protocols, not characteristic UUIDs, not vendor binary formats. ## Two integration paths [#two-integration-paths] AnyBio supports two paths for getting a device onto the platform: 1. **Standardized protocols.** For devices that implement the Bluetooth SIG Heart Rate or other standard profiles, the platform integrates without per-vendor work. See [Bluetooth SIG profiles](/docs/device-integration/bluetooth-sig). 2. **Vendor-specific integrations.** For devices with proprietary protocols, the platform ships per-vendor decoders that translate the device's binary format into the canonical wire format. We maintain integrations for several major wearable categories - see [Supported Devices](/docs/device-integration/supported-devices). If you have a device that isn't yet supported, see [Adding a Device](/docs/device-integration/adding-a-device) for how integrations are added. ## Where to go next [#where-to-go-next] # Supported Devices (/docs/device-integration/supported-devices) AnyBio supports a range of wearables across categories. New integrations are added regularly; if you don't see your device, [contact us](https://anybio.io/contact). ## By signal type [#by-signal-type] The same device often emits multiple biosignals; the table below groups by what the platform receives, not by hardware category. | Signal | Sources | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | ECG (waveform) | Single-lead chest straps and patches; rings and watches with ECG capability | | PPG (waveform) | Optical sensors in rings, wristbands, and finger pulse oximeters | | EDA | Wristbands and patches with electrodermal sensing | | Body Temperature | Skin-temperature sensors in rings, watches, and patches | | Heart Rate (device-reported) | Any device that exposes the [Bluetooth SIG Heart Rate profile](/docs/device-integration/bluetooth-sig), plus vendor-specific HR streams | | SpO2 | Pulse oximeters and watches with SpO2 capability | | Glucose | Continuous glucose monitors via vendor APIs | | Steps, Calories, Active Zone Minutes | Activity-tracking wearables and watches | | Sleep | Sleep-staging wearables and rings | ## Categories of integration [#categories-of-integration] Three broad categories of wearable integrate cleanly today: * **Direct BLE wearables** that stream waveform or HR data over Bluetooth. The [BioSDK](/docs/biosdk) handles discovery, connection, and the protocol translation; your application code stays at the biosignal layer. * **Cloud-to-cloud sources** like continuous glucose monitors and aggregated activity data, which arrive via vendor APIs rather than direct BLE. * **Standards-based devices** implementing public profiles like Bluetooth SIG Heart Rate. These work without any per-vendor code. ## Active integrations [#active-integrations] We currently maintain integrations across several major wearable categories. For specific device names, model numbers, or to confirm whether a particular wearable is supported in your project, [contact us](https://anybio.io/contact) - we keep the per-device list current with our partners directly rather than in public docs. ## What "supported" means [#what-supported-means] A supported device gets: * Real-time streaming through the BioSDK or platform APIs. * Automatic translation from the device's native data format to the AnyBio biosignal catalog. * Per-biosignal validation (range checks, quality mask propagation). * DSP processing for waveforms (HR/HRV from ECG; HR/PRV from PPG). * FHIR observation export for clinically relevant biosignals. It does not require any device-specific code in your application. Swap hardware, keep the integration. # DSP Overview (/docs/dsp) AnyBio's DSP layer turns raw biosignal streams into clean, time-aligned features your application can consume. It runs server-side, so your client doesn't need to bundle filter coefficients or peak detectors - you query feature series the same way you query observations. The DSP layer has two parts: * **Generic primitives** - Butterworth filters, peak detection, FFT, spectral analysis. Building blocks, not application logic. * **Modality extractors** - Per-biosignal pipelines that compose primitives into validated, production-grade features (e.g., ECG to HR via Pan-Tompkins; PPG to HR via pulse-foot detection). ## What you get out of the box [#what-you-get-out-of-the-box] For each waveform biosignal, the platform produces a set of derived feature series at lower cadences. Your application consumes the feature series; you don't need to run DSP on the device or in the browser. | Source | Features | Cadence | Algorithm | | ------ | ----------------------------------- | ------- | ------------------------------------- | | ECG | `hr_ecg` (heart rate) | 1 Hz | Pan-Tompkins R-peak detection | | ECG | `hrv_rmssd` (HRV, 5s rolling) | 0.2 Hz | Successive R-R differences | | ECG | `hrv_sdnn_5min` (HRV, 5min rolling) | 0.2 Hz | Standard deviation of R-R intervals | | PPG | `hr_ppg` (heart rate) | 1 Hz | Pulse-foot detection | | PPG | `prv_rmssd` (PRV, 5s rolling) | 0.2 Hz | Successive pulse-to-pulse differences | | PPG | `prv_sdnn_5min` (PRV, 5min rolling) | 0.2 Hz | Standard deviation of pulse intervals | Discrete biosignals (HR from a smartwatch, glucose from a CGM, steps from an activity monitor, etc.) are emitted as observations directly without further DSP. The same query API covers both derived features and device-reported values. ## Where to go next [#where-to-go-next] # Concepts (/docs/govern/concepts) The gate, its policies, its decision records and the review queue serve two callers: AnyBio's own agents, and a third party's application calling `/api/v1/govern` with its own key. Both are evaluated by the same gate against the same policy model and recorded the same way. ## Application [#application] An application is one integration of your AI: a name, a policy scope (a project, or the organization-level active policy), a default mode, and per-channel overrides for `sms`, `app_message`, `web_chat`, `voice` and `email`. Applications are managed under `/api/v1/govern/applications` by an organization admin or an `Admin` developer key. Each application has its own **application key** (`gk_...`), a server-side secret shown once at mint and hashed at rest. It authenticates only under `/api/v1/govern/`, is read from the `Authorization` header only, and is never shipped to a browser or an app binary. Rotation mints a new key and gives the previous keys an overlap window (one day by default, at most seven) before they are refused. ## Policy [#policy] A communication policy is an authored document: program positioning (`wellness` or `clinical`), a review mode (`none`, `on_violation`, `always`), what happens on a violation (`allow`, `rewrite`, `block_and_review`), lists of allowed and prohibited claims, and the claims that require a disclaimer. It is versioned; a new active version supersedes the last for the same scope. Policies are managed under `/api/v1/compliance/policies` and described in full in [the policy model](/docs/govern/policy-model). The policy authoring surface in the dashboard is the Compliance Governance page under Settings. ## Decision [#decision] A decision is the record of one evaluation, written whether or not the content was delivered. The caller receives `allow`, `hold` or `block`, with the decision id, the mode, the policy and version, reason codes, the rationale, the detected claims and the review state. One decision is recorded per application and `turn_id`, so a replay of a turn returns the stored decision. The decision id is also the id of the compliance check a reviewer releases, so the id a caller polls (`GET /api/v1/govern/decisions/{decision_id}`) is the id a reviewer acts on. The compliance record behind it carries the original content, any substituted content, the rationale, the detected claims, the policy, the mode, the would-have decision in observe, and the review state; it is read at `GET /api/v1/compliance/checks/{check_id}` (Admin scope). ## Mode [#mode] Per application and per channel: **off** (not evaluated, nothing recorded), **observe** (the caller is told allow at once and the gate runs after the response; the record carries `would_have`, the decision the gate would have made), **enforce** (the decision is returned inline and binds). A new application defaults to observe. Every mode change is an audited event carrying the actor, the scope, the previous mode and the new mode. The observe report (`GET /api/v1/govern/applications/{id}/observe-report`, also as CSV and PDF) summarizes a window of decisions by policy, channel and model. Would-have and enforced counts are kept apart and never combined into a single number. ## Hold [#hold] A hold is a decision the gate will not deliver until a person acts. It is produced by a `block_and_review` violation, by a draft the policy would rewrite when the audience is a patient (a patient never receives substituted text), by a policy that reviews every send, by a missing AI-identity disclosure the policy requires, and by a missing policy (the gate fails closed). If the gate does not answer within the inline budget in enforce, the answer is `hold` with `gate_unavailable`. A held record sits in the dashboard's Review Queue. A signed-in person releases it with `POST /api/v1/compliance/checks/{check_id}/release` or rejects it with `.../reject`; the record moves to `released_after_approval`, `release_delivery_failed`, or `rejected_after_review`, so the trail never claims a delivery that did not happen. A third party's application learns the outcome by polling the decision or by the signed webhook to its `webhook_url`; once released, the decision carries `released_content`. Per-patient state is checked before the gate, keyed on a hash of the caller's `patient_ref`: in enforce, a turn to a patient who opted out, withdrew consent, or has no consent on a channel that requires it is `block`, and a turn over the per-patient frequency cap is `hold`. Consent and opt-out are recorded under `/api/v1/govern/patients/`. A stopped draft is one of two classes, named by `stop_class`: `never_sendable` (a prohibited claim from the policy, or a draft telling a patient to start, stop or change a medication or dose), which a reviewer cannot release, and `uncertain` (held because nobody has read it yet), which a reviewer can release. When a patient-audience turn is stopped, the decision carries `send_holding_line: true`: send your holding line now, the fixed text your clinical owner wrote in advance saying that a person will reply, when, and what to do if it is urgent. The text is yours and is never model-written; Govern never sends it for you. Send at most one holding line per decision id. ## Route to a person [#route-to-a-person] `evaluate` accepts the patient message the turn answers, as `inbound`. It is screened by the deterministic red-flag floor the SMS interview runs, restricted to the categories the application configured (`cardiac`, `respiratory`, `neuro_stroke`, `self_harm`). A match sets `route_to_human: true`, independent of the decision: a clean draft is `allow` with a person brought in, a violating draft is `hold` as well. In enforce, a match or a hold opens a hand-off, or joins the one already open for the same application and patient reference, routed by the application's `handoff_routing`. The customer's team works it under `/api/v1/govern/handoffs` or in the console review queue. In observe, a match is recorded as `would_have_routed` and nothing is opened. Inside AnyBio's own programs, the same red-flag screen and the care hand-off are documented under [Run a program](/docs/programs/symptom-agent-and-gate). ## What the gate is, and is not [#what-the-gate-is-and-is-not] The gate is deterministic term and claim matching against the policy plus a disclosure check; it catches what the policy names, and only that. It is stateless and fails closed. Nothing on this portal describes it as semantic. ## The words [#the-words] Evaluate, allow, hold, block, observe, enforce, record. A person reviews, releases, rejects, claims, resolves. We do not say a message is compliant or certified, we do not quote a percentage, and nothing here is autonomous. # Gate your AI (/docs/govern) Govern is the communication gate, open to AI that is not ours. Your server makes one call before each message goes out, `POST /api/v1/govern/evaluate` with an application key, and gets back `allow`, `hold` or `block` with the record already written. The same gate, policies, decision records and review queue run under AnyBio's own agents: every patient-facing message a program sends is evaluated, and what it withholds waits for a person. For JavaScript and TypeScript, the `@anybio/govern` SDK (0.2.x on npm) wraps the call, with adapters for the Vercel AI SDK and LangChain: ```bash npm install @anybio/govern ``` Each application runs a policy per channel in one of three modes: **off**, **observe** (evaluated and recorded; the caller is always told allow) or **enforce** (the answer binds). A new application starts in observe. ## Pages [#pages] ## Read this first [#read-this-first] * What the gate is: deterministic term and claim matching against an authored policy, plus a disclosure check. It catches what the policy names, and only that. It is not a semantic classifier. * What a decision is: a record with the original content, any rewritten content, the rationale, the detected claims, the policy and version, the audience, and the review state. The record is written whether or not the message goes out. * What a hold is: the gate refusing to deliver until a person releases or rejects. A patient never receives substituted text; a draft the policy would rewrite for a patient is held. * What we do not say: that a message is compliant, that the gate certifies anything, or any percentage. Say evaluate, allow, hold, block, observe, enforce, record. The response loop the gate sits inside - programs, rules, the symptom agent, hand-offs - is documented under [Run a program](/docs/programs). # Govern for n8n (/docs/govern/n8n) `n8n-nodes-anybio` is the AnyBio community package for n8n. The **AnyBio Govern** node evaluates a draft under your Govern policy and routes it to one of three outputs, so a workflow branches without code. The record is written when the call returns. The **AnyBio Govern Trigger** node starts a workflow on a signed Govern webhook: a hold released or rejected, a hand-off opened, claimed, resolved or resumed. The video, three and a half minutes, runs on a sandbox application with synthetic data. Three drafts go through one Govern node: an ordinary check-in is delivered and its outcome reported; a draft that changes a dose is held as never sendable and the patient gets the hold text; a message answering a patient who reports chest pain is delivered and opens a hand-off to a person on the record. Then the same node runs as a tool for an AI Agent, which asks the gate before it answers. ## Install on self-hosted n8n [#install-on-self-hosted-n8n] The package has no runtime dependencies. It needs a self-hosted n8n with community packages enabled. **From the editor.** Settings, Community Nodes, Install, then enter `n8n-nodes-anybio`. **By hand** (queue mode, or an instance without the editor install): ```sh mkdir -p ~/.n8n/nodes && cd ~/.n8n/nodes npm install n8n-nodes-anybio --omit=dev --omit=peer # restart n8n ``` In the official Docker image `~/.n8n` is `/home/node/.n8n`. `--omit=peer` matters: `n8n-workflow` is provided by n8n itself, and installing it again pulls native build steps the image does not have. If your browser reaches n8n by IP address rather than a hostname, start n8n with `WEBHOOK_URL` and `N8N_HOST` set to that address. Otherwise the Chat Trigger and every webhook are built against `localhost` and fail from any other machine. ## Credentials [#credentials] One credential type, **AnyBio Govern API**, used by both nodes. Create it from inside the node, or from Overview, Credentials. | Field | What it is | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Environment | Sandbox (synthetic data only) or Production. A key is bound to one application and one environment; this labels every item the nodes emit. | | Application Key | The application key from the AnyBio console (`gk_...`). Sent only as `Authorization: Bearer`. | | Application ID | Optional. The application's UUID; when set it is sent as `application_id`, and the API refuses a key that is not bound to it. | | Base URL | Default `https://api.anybio.io`. Sandbox and production keys use the same host. | | Webhook Signing Secret | For AnyBio Govern Trigger only: the application's `whsec_...` secret, shown once when the application is created or the secret is rotated. | The application, its key and its signing secret come from the console: Govern, Applications, New application. The key and the secret are shown once, on the create page. **Test** on the credential reads the application's webhook delivery log and writes nothing: testing never creates a decision. ## The demo workflow [#the-demo-workflow] Download [govern-three-drafts-one-gate.json](/downloads/n8n/govern-three-drafts-one-gate.json), create a new workflow, click the canvas and paste the file's contents. It is the first workflow in the video. **Conversation** sets a conversation id, a patient reference, the hold message and the disclosure text. **Record Disclosure** records the AI-identity disclosure for the conversation. Every patient-audience turn needs one on record, or the gate holds it with `disclosure_missing`. **Three drafts** produces the three synthetic items. Draft C carries the patient's inbound message, so the gate screens the patient's words as well as the draft. **AnyBio Govern** evaluates each item under the application's policy and routes it: Deliver, Show Hold Text or Send Nothing. On the Deliver branch, **Report Delivered** reports the outcome, so the record shows what the patient received. On the Show Hold Text branch, **Get Decision** fetches the full record: policy version, reasons, stop class, review state. Execute it once and open the Govern node. Expect the check-in and the chest-pain acknowledgement on Deliver, the latter with `routeToHuman` true and a hand-off id, and the dose change on Show Hold Text with `medication_instruction` in `reasons` and `stopClass` `never_sendable`. ## The node [#the-node] ### Evaluate [#evaluate] | Input | Notes | | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Draft Text | The message your AI step produced. Never put in an error or a log by the node. | | Conversation ID | Your thread id, at most 187 characters. | | Channel | SMS, Voice, Email (outbound) or Web Chat, App Message (in-session). | | Audience | Patient or Clinician. SMS and Voice accept Patient only. | | Patient Reference | Your opaque id; the API stores only its SHA-256. Consent, opt-out, the cap and hand-offs key on it. | | Hold Message | The text Show Hold Text carries: your holding line, fixed text your clinical owner wrote in advance. | | Turn ID (additional) | The idempotency key. Default: derived from the workflow, execution, node and item, so a retry within an execution replays the stored decision. Map your own message id here to make a re-run replay too. | | Inbound Message, Inbound Received At (additional) | The patient's message this turn answers, for the red-flag screen. Requires Patient Reference. | | Model, Model Version (additional) | Recorded as sent and counted in the observe report. | Three outputs: | Output | When | `text` | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | | **Deliver** | The decision is allow, including observe and a channel that is off. | The draft. A patient audience never receives rewritten text. | | **Show Hold Text** | A hold or block where the hold message is to be shown now: every clinician hold; every patient hold on an in-session channel; a patient hold on an outbound channel when a holding line is owed. | The Hold Message. Never the draft. | | **Send Nothing** | A hold or block on an outbound channel with no holding line owed. | Empty. Connect nothing that messages the patient here. | Every item carries `output`, `text`, `decisionId`, `decision`, `mode`, `status`, `reasons` (codes, never text), `stopClass`, `routeToHuman`, `handoffId`, `automationPaused`, `inboundFlags`, `wouldHave`, `disclosureRequired`, `conversationId`, `turnId`, `channel`, `audience` and `environment`. `routeToHuman` is independent of the output: an allowed draft can go to Deliver with `routeToHuman` true, meaning send it and bring a person in. ### Report Outcome, Record Disclosure, Get Decision [#report-outcome-record-disclosure-get-decision] **Report Outcome** takes the decision id, whether the message was delivered, and the delivered text; the node hashes the text with SHA-256 and sends only the hash. One outcome per decision. **Record Disclosure** takes the conversation id, the channel and the text shown, verbatim; once per conversation. **Get Decision** returns the decision with its current review state and, after a release, the released content. ### As a tool for an AI Agent [#as-a-tool-for-an-ai-agent] The node can be attached to an AI Agent as a tool, which is the second workflow in the video: the agent drafts a reply and asks the gate before it answers. As a tool it tells the agent the decision; it does not stand between the agent's reply and the patient. To gate what is actually sent, put AnyBio Govern inline, between the step that drafts and the step that sends, as the demo workflow does. ## When AnyBio cannot be reached [#when-anybio-cannot-be-reached] The node follows the `@anybio/govern` SDK here. A turn that comes back without a decision carries `gate_unavailable` (an outage; heals on its own) or `client_misconfigured` (a refused key; yours to fix). On a channel last seen in observe or off, the draft goes to Deliver with `failOpen` true and no decision id. On a channel last seen in enforce, or never seen, the node fails closed: in session the hold message is shown, on an outbound channel nothing is sent. ## Trigger [#trigger] **AnyBio Govern Trigger** is a webhook trigger. Copy its production URL into the application's webhook URL in the console. Every delivery is verified before anything is parsed, with the credential's signing secret, and an unsigned, stale or mis-signed delivery is answered 401 and starts nothing. Events: hold released, hold rejected, hand-off opened, claimed, resolved, automation resumed. ## Source and support [#source-and-support] The package is on npm as [`n8n-nodes-anybio`](https://www.npmjs.com/package/n8n-nodes-anybio), MIT licensed, with the source at [github.com/anybio/n8n-nodes-anybio](https://github.com/anybio/n8n-nodes-anybio). The gate's behaviour is documented under [Concepts](/docs/govern/concepts) and [The policy model](/docs/govern/policy-model); the TypeScript SDK is the [Govern SDK](/docs/govern/sdk-quickstart) page. # The observe engagement (/docs/govern/observe-engagement) An observe engagement answers the question a health system's AI review asks of a vendor: what can your AI say, what stops it, and who sees the record. It runs on an application in observe mode: every draft is evaluated and recorded, and your AI is never blocked. This page says what happens in that engagement and what you get. ## What it runs on [#what-it-runs-on] * **The gate** and its decision records ([the policy model](/docs/govern/policy-model)). * **A sandbox organization** of your own, with a policy authored to your positioning and claims, and synthetic patients. * **An application in observe mode**, with its own key. Your server calls `POST /api/v1/govern/evaluate` on each draft, or `POST /api/v1/govern/transcripts` with a batch of past turns, and each is recorded with the decision the gate would have made. * **The review queue** in the dashboard, where held decisions wait for a person and are released or rejected with a note. * **The observe report**, `GET /api/v1/govern/applications/{id}/observe-report`, also as CSV and PDF. ## How the two weeks run [#how-the-two-weeks-run] 1. **Policy.** Together we write your communication policy: positioning, what happens on a violation, the prohibited phrases in the words your model actually produces, the phrases that require a disclaimer, and the review mode. It is validated (`POST /api/v1/compliance/policies/validate`) and activated for your sandbox project. 2. **Drafts.** Your application sends representative drafts your AI would send - synthetic or de-identified, never real patient text - through the gate in observe mode. Every draft produces a decision record with the original content, the rationale, the detected claims and the would-have decision, exactly as a live send would. 3. **Review.** Your reviewers read the would-have decisions for the period. To see what the hold-and-release loop asks of a team, switch a sandbox channel to enforce: held items then wait in the review queue, and reviewers release or reject them with notes. 4. **Adjust.** Policy versions are immutable; each change is a new version, and each decision names the version it was evaluated under, so a policy change mid-engagement is visible in the record rather than rewriting it. ## What the reviewer receives [#what-the-reviewer-receives] * **The observe report** for the period, as JSON, CSV or PDF: decisions by would-have outcome, broken down by policy and version, by channel and by the model your application named, with would-have and enforced counts kept apart. * **The decision records** for the period: per draft, the channel and audience, the decision, the rationale, the detected claims, the policy version, and the review state with the reviewer's action and notes. * **The policy versions** in force during the period, as the API returns them. None of these contains a score, a percentage, or a verdict that anything is compliant; they say what the gate did and what a person did after. ## What the engagement is not [#what-the-engagement-is-not] It is not a certification and it is not a claim about your model. It shows, on your drafts and your policy, what the gate allows, holds and blocks, and it produces the records a committee can read. Whether that is sufficient for a given review is the committee's call. # Try the gate (/docs/govern/playground) The default draft is a message any nurse might send. Press **Evaluate the draft** and the gate holds it. The answer names the words it matched and the rule each one broke, because the gate matches what the policy names and only that. ## What to try [#what-to-try] * **Switch the audience to Clinician with Rewrite selected.** The same draft is delivered to a clinician as substituted text and held for a patient, because a patient is never sent machine-substituted text. * **Switch positioning to Clinical.** The wellness screen turns off and `medication` stops counting. The draft is still held, for the missing disclaimer alone. * **Add a disclaimer.** That clears the disclaimer rule only. Under wellness positioning, `medication` still holds the draft. A disclaimer is not a pass for everything else. * **Load the dose change.** It is held as never sendable: a reviewer cannot release it to the patient. Send it to a clinician and it becomes uncertain, which a reviewer can release. ## What the preview leaves out [#what-the-preview-leaves-out] This page evaluates one draft against the policy shown, and stores nothing. Calling the gate from your own application adds what a stateless page cannot have: * **A decision record.** Every evaluate call writes one, with an id your reviewer acts on in the review queue. * **Block.** It comes from per-patient state, a standing opt-out or missing consent, which the preview has none of. * **Modes.** A new application starts in observe, where the caller is always told allow and the record keeps what the gate would have done. To call it from your code, start with the [Govern SDK](/docs/govern/sdk-quickstart). The full rule set is on [the policy model](/docs/govern/policy-model). # The policy model (/docs/govern/policy-model) A communication policy is the authored document the gate applies to every patient-facing message. It is versioned per scope and managed at `/api/v1/compliance/policies`. This page is the model as the code reads it. ## The fields [#the-fields] | Field | Values | What the gate does with it | | ------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `program_positioning` | `wellness`, `clinical` | Under `wellness`, a fixed list of clinical terms is a violation on its own: `diagnose`, `diagnosis`, `treat`, `treatment`, `cure`, `prevent`, `disease`, `medication`, `prescription`, `clinical trial`. Under `clinical` that screen is off and only your claim lists apply. | | `review_mode` | `none`, `on_violation` (default), `always` | When a delivered message also needs a person to look at it. Anything the gate withholds needs one regardless. | | `on_violation` | `allow`, `rewrite`, `block_and_review` | What a violation becomes. `rewrite` is term substitution and is a hold for a patient audience. | | `allowed_claims` | list of phrases | Recorded on the policy; not enforced by the gate. | | `prohibited_claims` | list of phrases | A match is a violation. | | `requires_disclaimer_for` | list of phrases | A match without a disclaimer marker in the same message is a violation. | | `name`, `description` | text | Identity. `name` defaults to "Default Communication Policy". | | `project_id` | integer or null | Scope. A project policy overrides the organization policy for that project. | Claims are normalized on write: trimmed and lower-cased. Values for the three enum fields are accepted in any case and stored lower-case. ### How matching works [#how-matching-works] A phrase matches when it appears in the message as whole words, case-insensitively across Unicode, with a small inflection tolerance on the final word (plurals and simple tenses). It is term matching, not meaning: a policy that prohibits `diagnosis` does not catch "what you have is", and a policy that names nothing catches nothing beyond the wellness screen. State the policy in the words a model would actually produce. A message "has a disclaimer" when it contains one of the fixed markers: "not medical advice", "not intended to diagnose", "consult a healthcare professional", "consult your clinician", "for wellness purposes only". ### What a rewrite does [#what-a-rewrite-does] Each prohibited match is replaced with "wellness indicator", each wellness-screen term with "wellness", and the fixed wellness disclaimer is appended once when a disclaimer was required or a clinical term was replaced. The result is machine term substitution, which is why it is delivered only to a clinician audience and held for a person when the audience is a patient. The rewritten text is stored on the decision either way. ### Scope resolution and the default [#scope-resolution-and-the-default] The gate resolves the active policy for the project first, then falls back to the organization's active policy. Every organization is provisioned with a restrictive default: `wellness` positioning, `on_violation` review, `block_and_review` on violation, empty claim lists. The default's strictness comes from the wellness screen, not from seeded claims. If no active policy resolves for a scope, the gate withholds and records `policy_missing`. ### Versioning [#versioning] `PATCH /api/v1/compliance/policies/active` does not edit in place: it deactivates the current active policy for that exact scope and inserts a new row with `version` incremented. Every decision record names the `policy_id` it was evaluated under, so a reviewer can always read the exact document that produced a decision. ## Routes [#routes] | Method | Path | Scope | Notes | | ------ | -------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------- | | GET | `/api/v1/compliance/policies` | Read | list, filter `project_id`, `limit` | | GET | `/api/v1/compliance/policies/active` | Read | the resolved active policy for `project_id` (or the organization) | | PATCH | `/api/v1/compliance/policies/active` | Write | upsert a new active version for the scope; body below | | POST | `/api/v1/compliance/policies/validate` | Write | returns the normalized policy, a `risk_level` and `warnings`, without saving | | POST | `/api/v1/compliance/policies/draft` | Write | assistant-drafted policy from `intent`, `preferred_positioning`, `strictness`, `channels`, optional `existing_policy`, `use_ai` | | POST | `/api/v1/compliance/policies/{policy_id}/activate` | Write | re-activate a stored version | Body for the upsert: ```json { "project_id": 7, "name": "Between-visits messaging", "description": "Engagement and logistics only; no interpretation of readings.", "program_positioning": "wellness", "review_mode": "on_violation", "on_violation": "block_and_review", "allowed_claims": ["trend awareness", "reminder to take a reading"], "prohibited_claims": ["diagnosis", "you have hypertension", "increase your dose"], "requires_disclaimer_for": ["blood pressure", "heart rate"] } ``` ## The decision records [#the-decision-records] Every evaluation writes one record. `GET /api/v1/compliance/checks` (Admin scope) lists them with filters `communication_channel`, `communication_ref`, `communication_ref_prefix`, `conversation_id`, `decision`, `project_id`, `review_required`, `release_status`, `limit`, `offset`, and returns beside the page a histogram of `release_status` counts that ignores the active filter, so the queue's buckets are always visible. `GET /api/v1/compliance/checks/{check_id}` returns one record. | Field | Meaning | | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `communication_channel`, `audience` | Where the message was bound and who it was for | | `communication_ref` | Your reference for the message | | `decision` | `allow`, `rewrite`, `block_and_review`, `policy_missing` | | `rationale` | The violations, in words | | `detected_claims` | The phrases that matched | | `original_content`, `rewritten_content` | The draft, and the substituted text when any | | `policy_id` | The exact policy version applied | | `review_required`, `release_status` | `auto_handled`, `held_for_review`, `released_after_approval`, `release_delivery_failed`, `rejected_after_review` | | `reviewed_by`, `reviewed_at`, `released_with`, `release_notes` | The person's action, when there was one | `POST /api/v1/compliance/checks` (Admin scope) inserts a record of a decision that was already made elsewhere; it does not evaluate. To have the gate evaluate a third party's draft, call `POST /api/v1/govern/evaluate` ([Concepts](/docs/govern/concepts)). ## Which surfaces are gated [#which-surfaces-are-gated] Every path in the platform that composes patient-facing text calls the gate before sending: rule notifications, adherence nudges, agent nudges and notifications, symptom interview questions, manual patient notifications, and chat replies to a patient. Clinician-facing agent insights are evaluated with a clinician audience. There is no ungated path to a patient; the type that a notification body must be can only be produced by the gate or by a reviewer's release. # Govern SDK (/docs/govern/sdk-quickstart) `@anybio/govern` wraps the evaluate call for JavaScript and TypeScript, with adapters for the Vercel AI SDK and LangChain. It runs server-side only: the constructor throws if it finds a browser `window`, because the application key is a secret. **Pre-1.0.** The package is 0.2.x. The public surface may change in a minor version, so pin it and read the changelog before upgrading. ## Prerequisites [#prerequisites] In the dashboard, open **Govern**, then **Applications**, and choose **New application**. Creating one takes an organization admin; other staff can see applications but not change them. The key is shown once, when the application is created, because it is hashed at rest. Copy it into your server's secret store. It is your `ANYBIO_GOVERN_KEY`: ```bash ANYBIO_GOVERN_KEY=gk_... ``` To create an application from a script instead, send the same request the dashboard does, with an **Admin** scope organization key from Settings, Organization keys. The key is `key.secret` in the response: ```bash curl -X POST https://api.anybio.io/api/v1/govern/applications \ -H "Authorization: Bearer $ANYBIO_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"care-companion","default_mode":"observe"}' ``` A new application starts in **observe**, where the caller is always told allow and the record carries the would-have decision. Choose enforce on the form, or pass `"default_mode": "enforce"`, if you want the answer to bind while you build; [the observe engagement](/docs/govern/observe-engagement) is the path to production. Two optional variables: `ANYBIO_GOVERN_APPLICATION` names the application when you do not pass it in code, and `ANYBIO_GOVERN_BASE_URL` points at something other than production. Node 20 or later, and edge runtimes. The core path uses `fetch` and Web Crypto only. Your floor is the higher of this package's and your framework's. The Vercel AI SDK 7 declares Node 22, so a project using it is a Node 22 project. ## Watch it hold something [#watch-it-hold-something] Before wiring anything into your own code, make the gate stop a message. To see it without installing anything, [try the gate in your browser](/docs/govern/playground) first. The CLI posts a synthetic draft that names a cure and a medication change, and prints what came back: ```bash npm install @anybio/govern npx @anybio/govern test-message ``` **Success looks like a refusal.** In enforce you see the hold inline. In observe the gate runs off the request path, so the CLI hands you a `decision` command to read the would-have a moment later. No patient data is involved, and the throwaway conversation gets a disclosure recorded first, so the turn is complete rather than a fragment. `npx @anybio/govern init` reads your `package.json`, detects the Vercel AI SDK or LangChain, and prints the install line, the environment variable and the file to add. It writes nothing unless you pass `--write`, and it never edits a file you already have. ## Gate your own model [#gate-your-own-model] `guard` is the short path. It runs your draft function, evaluates what it returns, and hands you what to deliver: ```ts import { Govern } from "@anybio/govern"; const govern = new Govern({ apiKey: process.env.ANYBIO_GOVERN_KEY, application: "care-companion", }); const reply = await govern.guard( () => llm.chat(messages), { conversationId, turnId, channel: "sms", audience: "patient" }, ); if (reply.text !== "") await sendSms(patient.phone, reply.text); ``` **That `if` is the whole contract.** `reply.text` is the draft when the gate allows it. When it does not, `text` is either the holding line your clinical owner wrote in advance, or the empty string when nothing should be sent to this patient at all - because they opted out, consent is missing or revoked, or the frequency cap is reached. The empty string is the one "send nothing" value, so a caller that sends whatever it is handed sends nothing. Call `reply.ack()` after you deliver. If you do not, the next turn in the same conversation reports the previous one for you. `reply.decisionId` is the record. Everything else about the decision, including the reasons and the policy version that made it, is on the same object. ## Framework adapters [#framework-adapters] The adapters are the install; the raw client is the escape hatch. Each ships as a subpath export and declares its framework as an optional peer dependency. ```ts import { wrapLanguageModel, streamText } from "ai"; import { withGovern } from "@anybio/govern/vercel-ai"; const model = wrapLanguageModel({ model: openai("gpt-4o"), middleware: withGovern({ govern, channel: "web_chat", audience: "patient" }), }); const result = streamText({ model, messages, providerOptions: { anybioGovern: { conversationId: chatId, turnId: messageId }, }, }); ``` A patient-audience stream is buffered and released whole after the decision, or replaced. A clinician-audience stream passes through and is evaluated once it ends. The decision lands in `providerMetadata.anybioGovern`. ```ts import { governed, GovernCallbackHandler } from "@anybio/govern/langchain"; const gated = governed(model, { govern, channel: "web_chat", audience: "patient" }); const message = await gated.invoke(messages, { configurable: { anybioGovern: { conversationId: threadId, turnId: messageId } }, }); ``` `governed` is the gating path, because a callback cannot withhold text: by the time `handleLLMEnd` fires the output is already on its way. `GovernCallbackHandler` records a decision per generation and is the observe-mode install for chains you would rather not wrap. ## After a hold [#after-a-hold] A hold waits for a person. Three ways to learn it resolved, in ascending order of how much you want to run: * **`waitForRelease(decisionId)`** polls and returns the released content, or null. * **A webhook.** `handleWebhook` verifies the signature and parses the event; `WEBHOOK_EVENTS` names what can arrive. * **`getDecision(decisionId)`** reads the current state whenever you ask. ## If your first call fails [#if-your-first-call-fails] | What you see | Likely cause | Fix | | --------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Constructor throws about `window` | The client was constructed in browser code | It is server-side only. Move it behind an API route | | `GovernConfigError` | A missing key, or a draft function returning something that is not text | The message names the field | | `GovernNotAvailableError` in enforce | The API could not be reached | This is a fail-closed hold, not a crash. `reply.text` carries the holding line when one is owed | | Every decision comes back allow | The application's policy is in observe | Observe records what would have happened. Check `wouldHave` | | A replayed `turnId` returns the same decision | Working as intended | `turnId` is the idempotency key | ## Reference [#reference] The package README is the exhaustive reference and ships with the code, so it never drifts from the version you installed: modes, channel rules, the holding line, consent and the frequency cap, hold resolution, disclosure, outcomes, streaming, and what the SDK does not cover. * [`@anybio/govern` on npm](https://www.npmjs.com/package/@anybio/govern) * [Concepts](/docs/govern/concepts) - application, policy, decision, mode, hold * [The policy model](/docs/govern/policy-model) - what a policy can say * [The observe engagement](/docs/govern/observe-engagement) - two weeks of would-have before anything is enforced # Model Registry Overview (/docs/model-registry) # Model Registry [#model-registry] The AnyBio Model Registry is a versioned catalog for machine learning models that process biosignal data. Register, version, and deploy models that run against data captured by BioSDK. ## Key Concepts [#key-concepts] * **Model** — A trained ML model that accepts biosignal input (ECG, PPG, EDA, etc.) and produces predictions or classifications * **Version** — Each model can have multiple versions, with one active version deployed at a time * **Deployment** — Deploy a model version to run against incoming session data on the AnyBio platform ## Getting Started [#getting-started] See the [Getting Started guide](/docs/model-registry/guides/getting-started) to register your first model. # API reference (/docs/programs/api-reference) Two references, and which to trust for what: * **The route list on this page** is read from the backend's routers and is the complete public surface, with the key scope each route requires. It is the authoritative list. * **The generated OpenAPI document** ([`/openapi/anybio-v1.json`](/openapi/anybio-v1.json)) carries request and response schemas for the documented handlers. It is a subset of the route list. The API also serves the document for the deployed version at `GET /api-docs/openapi.json`, `GET /api-docs/openapi.yaml` and a browsable UI at `/swagger-ui/`. Where the generated document and the mounted routes differ, this page uses the mounted paths: `/api/v1/developer/keys` and `/api/v1/ingest/sessions/start`. ## Conventions [#conventions] * **Base URL** `https://api.anybio.io`. Paths below are absolute. * **Authentication** `Authorization: Bearer `. API keys are created in the dashboard (Write scope keys start with `sk_sandbox_` or `sk_prod_`; older keys start with `org_`). A key is bound to one environment; the request's environment is the key's. Browser sessions (the dashboard) and short-lived SDK tokens (`POST /api/v1/sdk/token`) use the same header. * **Scopes** `Read`, `Write`, `Admin`, `Sdk`. Each group below states what its reads and writes need. A handful of routes additionally require a **signed-in staff member** and refuse API keys outright; they are marked. * **Environment header** A dashboard session may send `X-Environment-Id` on writes; a mismatch with the session's environment is a `409`. API keys do not need it. * **Caching** Responses that carry patient identifiers are sent `Cache-Control: no-store`; every error response is `no-store`. * **Rate limits** Per-session limits on the authenticated API and per-IP limits on the public routes return `429`. Sandbox organizations are capped, never invoiced; a write against an expired sandbox trial returns `402`. * **Path parameters** are written `{name}`. Ids are UUIDs unless the name says otherwise (`project_id`, `key_id`, `biosignal_id` are integers). ## Programs [#programs] Read scope for `GET`, Write for the rest. | Method | Path | Notes | | ------ | ------------------------------------------------- | -------------------------------------------------- | | GET | `/api/v1/episode-profiles` | filters `project_id`, `status` | | GET | `/api/v1/episode-profiles/count` | | | GET | `/api/v1/episode-profiles/{id}` | | | GET | `/api/v1/episode-profiles/{id}/status` | | | GET | `/api/v1/episode-profiles/{id}/protocol-state` | authoring draft state | | GET | `/api/v1/episode-profiles/{id}/symptom-readiness` | program-scoped readiness ladder | | GET | `/api/v1/episode-profiles/{id}/enrollments/count` | | | GET | `/api/v1/episode-profiles/{id}/qr.png` | enrollment QR | | GET | `/api/v1/episode-profiles/{id}/versions` | | | GET | `/api/v1/episode-profiles/sessions` | authoring sessions | | GET | `/api/v1/biosignals/available` | signals a program may require | | POST | `/api/v1/episode-profiles` | create (201) | | POST | `/api/v1/episode-profiles/validate` | validate a spec without saving | | POST | `/api/v1/episode-profiles/sessions` | start an authoring session | | PATCH | `/api/v1/episode-profiles/{id}` | | | DELETE | `/api/v1/episode-profiles/{id}` | soft delete | | POST | `/api/v1/episode-profiles/{id}/activate` | | | POST | `/api/v1/episode-profiles/{id}/deprecate` | | | POST | `/api/v1/episode-profiles/{id}/toggle-active` | | | POST | `/api/v1/episode-profiles/{id}/clone` | body `target_project_id` | | POST | `/api/v1/episode-profiles/{slug}/enroll` | body `xuser_id`, optional `project_id`, `metadata` | Program authoring with the assistant: `GET /api/v1/profile-authoring/tools` (Read), `POST /api/v1/profile-authoring/start`, `POST /api/v1/profile-authoring/conversations/{conversation_id}/message`, `POST /api/v1/profile-authoring/conversations/{conversation_id}/stream` (per-IP rate limited). ## Episodes and attention [#episodes-and-attention] Read for `GET`, Write for the rest. | Method | Path | Notes | | ------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | GET | `/api/v1/episodes` | filters `xuser_id`, `project_id`, `episode_profile_id`, `status`, `attention_gte`, `limit`, `offset` | | GET | `/api/v1/episodes/resolve` | | | GET | `/api/v1/episodes/{id}` | | | GET | `/api/v1/episodes/{id}/attention` | the attention breakdown | | GET | `/api/v1/episodes/{id}/fhir` | the episode as a FHIR bundle | | GET | `/api/v1/episodes/{id}/artifacts` | | | GET | `/api/v1/episodes/{episode_id}/artifacts/{artifact_id}/download` | | | GET | `/api/v1/episodes/{id}/summaries` | | | GET | `/api/v1/episodes/{id}/progress` | | | GET | `/api/v1/episodes/{id}/sdk-config` | | | GET | `/api/v1/episodes/{episode_id}/timeline` | also `/timeline/stats` | | GET | `/api/v1/episodes/{episode_id}/observations` | | | POST | `/api/v1/episodes` | create by hand | | PATCH | `/api/v1/episodes/{id}` | | | DELETE | `/api/v1/episodes/{id}` | | | POST | `/api/v1/episodes/{id}/close` | | | POST | `/api/v1/episodes/{id}/reopen` | | | POST | `/api/v1/episodes/{id}/attention/acknowledge` | lower the floor | | POST | `/api/v1/episodes/{episode_id}/questionnaire-response` | submit a PRO response | Also `GET /api/v1/timeline` (project-wide), `GET /api/v1/xusers/{xuser_id}/timeline` and `/patient-timeline` (Read). ## Rules [#rules] Read for `GET`, Write for the rest. | Method | Path | | ------ | ----------------------------------------- | | GET | `/api/v1/policies` | | GET | `/api/v1/policies/{policy_id}` | | GET | `/api/v1/policies/{policy_id}/executions` | | POST | `/api/v1/policies` | | PUT | `/api/v1/policies/{policy_id}` | | DELETE | `/api/v1/policies/{policy_id}` | | POST | `/api/v1/policies/{policy_id}/test` | ## Patients and contact points [#patients-and-contact-points] Read for `GET`, Write for the rest. | Method | Path | Notes | | ------ | ------------------------------------------ | ----------------------------- | | GET | `/api/v1/xusers` | | | GET | `/api/v1/xusers/{xuser_id}` | | | GET | `/api/v1/xusers/{xuser_id}/vitals-summary` | | | GET | `/api/v1/xusers/{xuser_id}/health-metrics` | | | GET | `/api/v1/xusers/stats/unassociated` | | | POST | `/api/v1/xusers` | create or get by `identifier` | | POST | `/api/v1/xusers/add_to_project` | | | PATCH | `/api/v1/xusers/{xuser_id}` | | | DELETE | `/api/v1/xusers/{xuser_id}` | soft delete | | GET | `/api/v1/contact_points` | also `GET /fhir/ContactPoint` | | POST | `/api/v1/contact_points` | | | PATCH | `/api/v1/contact_points/{id}` | | | DELETE | `/api/v1/contact_points/{id}` | and `POST .../{id}/restore` | | PUT | `/api/v1/xusers/{xuser_id}/phone` | body `value` (E.164 or null) | ## Observations [#observations] Read for `GET`, Write for `POST`. Direct writes link to the `episode_ids` you pass and do not evaluate rules. | Method | Path | | ------ | ------------------------------------------------ | | GET | `/api/v1/observations` | | GET | `/api/v1/observations/latest` | | GET | `/api/v1/observations/trend` | | GET | `/api/v1/observations/{id}` | | POST | `/api/v1/observations` | | POST | `/api/v1/observations/batch` | | POST | `/api/v1/xusers/{xuser_id}/observations/journal` | ## Device ingest and inbound webhooks [#device-ingest-and-inbound-webhooks] | Method | Path | Auth | | ------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | POST | `/webhooks//{project_key}` | cellular device hub; shared secret in `Authorization`; public route | | POST | `/webhooks//sms/{project_key}` | inbound SMS; provider signature; public route | | POST | `/ingest/image` | Write | | POST | `/ingest/captures` | Write; 8 MB | | POST | `/ingest/raw-packet` | Write; SDK streaming | | GET | `/api/v1/images`, `/api/v1/images/{image_id}`, `/api/v1/images/{image_id}/file` | Read | | POST | `/api/v1/images/reprocess` | Write | Wearable account webhooks are mounted per provider under `/webhooks/` and verified per provider. Ingest sessions and signal streams (`/api/v1/ingest/sessions/...`, `/api/v1/sessions/{session_id}/signal-streams`, `/api/v1/signal-streams/{stream_id}/...`) are the SDK's streaming surface; see [Stream on iOS](/docs/biosdk). ## Symptom interviews, agents and conversations [#symptom-interviews-agents-and-conversations] | Method | Path | Scope | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | GET | `/api/v1/agents`, `/api/v1/agents/{id}`, `/api/v1/agents/{agent_id}/runs`, `/api/v1/agents/{agent_id}/runs/{run_id}`, `/api/v1/agents/{agent_id}/preview` | Read | | GET | `/api/v1/episode-profiles/{profile_id}/agents` | Read | | GET | `/api/v1/agent-runs`, `/api/v1/agent-runs/{run_id}` | Read | | GET | `/api/v1/agent-runs/pending-reviews` | Read; the review queue of agent runs | | POST | `/api/v1/agents/{agent_id}/invoke` | Write | | POST | `/api/v1/agent-runs/{run_id}/review` | Write; staff session required | | POST, PATCH, DELETE | `/api/v1/agents`, `/api/v1/agents/{id}`, `/api/v1/agents/{agent_id}/attach`, `/api/v1/agent-profile-links/{link_id}` | Admin | | GET | `/api/v1/conversations`, `/api/v1/conversations/{id}` | Read | | POST | `/api/v1/conversations`, `.../{conversation_id}/messages`, `.../{conversation_id}/tool-results`, `.../{id}/archive` | Write | The symptom interview has no route of its own: it is opened by a rule, driven by inbound SMS, and read through agent runs, observations and the timeline. ## The gate: policies and decision records [#the-gate-policies-and-decision-records] | Method | Path | Scope | | ------ | -------------------------------------------------- | --------------------------------------------- | | GET | `/api/v1/compliance/policies` | Read | | GET | `/api/v1/compliance/policies/active` | Read; `project_id` optional | | PATCH | `/api/v1/compliance/policies/active` | Write; upsert a new active version | | POST | `/api/v1/compliance/policies/draft` | Write; assistant-drafted policy | | POST | `/api/v1/compliance/policies/validate` | Write; normalize and warn | | POST | `/api/v1/compliance/policies/{policy_id}/activate` | Write | | GET | `/api/v1/compliance/checks` | Admin; filters and a release-status histogram | | POST | `/api/v1/compliance/checks` | Admin; records a decision already made | | GET | `/api/v1/compliance/checks/{check_id}` | Admin | | POST | `/api/v1/compliance/checks/{check_id}/release` | Admin; staff session required | | POST | `/api/v1/compliance/checks/{check_id}/reject` | Admin; staff session required | ## Govern: the gate for AI that is not ours [#govern-the-gate-for-ai-that-is-not-ours] Govern is authenticated differently from the rest of this list. The runtime routes take an **application key** (`gk_...`, created with the application), not an organization API key, and they are the only routes in the public surface that do. Management routes take an organization key or a dashboard session. The four hand-off actions take a **dashboard session only** and refuse keys outright: claiming or resolving a hand-off is a person's act and the record names them. The [`@anybio/govern` SDK](/docs/govern/sdk-quickstart) wraps the runtime routes. **Runtime.** Application key. | Method | Path | Notes | | ------ | ------------------------------------------------ | ---------------------------------------------------------------------- | | POST | `/api/v1/govern/evaluate` | Evaluate one turn before it is sent. `turn_id` is the idempotency key | | GET | `/api/v1/govern/decisions/{decision_id}` | The decision with its review state; the first hold-resolution path | | POST | `/api/v1/govern/decisions/{decision_id}/outcome` | What was delivered. One outcome per decision | | POST | `/api/v1/govern/disclosures` | Record the AI-identity disclosure shown at the start of a conversation | | POST | `/api/v1/govern/patients/state` | The patient's state. A POST so the reference travels in the body | | POST | `/api/v1/govern/patients/consent` | Consent granted or revoked | | POST | `/api/v1/govern/patients/opt-out` | A patient opting out, or back in | | GET | `/api/v1/govern/webhook-deliveries` | The webhook delivery log, newest first | | POST | `/api/v1/govern/transcripts` | A batch of past turns, evaluated observe-only | | GET | `/api/v1/govern/transcripts/{job_id}` | A transcript job's status and counts | | GET | `/api/v1/govern/transcripts/{job_id}/turns` | The job's turns in batch order with their decisions | **Applications, keys and reports.** Every route here takes an organization admin's dashboard session or an **Admin** scope organization key; lower key scopes are refused. The same screens are in the dashboard under Govern, Applications. | Method | Path | Notes | | ------ | -------------------------------------------------------------------- | ---------------------------------------------------- | | GET | `/api/v1/govern/applications` | The organization's applications in this environment | | POST | `/api/v1/govern/applications` | Creates the application and mints its first key | | GET | `/api/v1/govern/applications/{application_id}` | One application, with its channel modes | | PATCH | `/api/v1/govern/applications/{application_id}` | A mode change is audited with the actor | | GET | `/api/v1/govern/applications/{application_id}/keys` | Key metadata, never a secret | | POST | `/api/v1/govern/applications/{application_id}/keys/rotate` | Mints a new key and ages out the previous | | POST | `/api/v1/govern/applications/{application_id}/webhook-secret/rotate` | The new secret is in the response only | | GET | `/api/v1/govern/applications/{application_id}/observe-report` | Counts by decision, policy and channel over a window | | GET | `/api/v1/govern/applications/{application_id}/observe-report.csv` | The buckets the counts were summed from | | GET | `/api/v1/govern/applications/{application_id}/observe-report.pdf` | The document an account holder sends to a reviewer | | DELETE | `/api/v1/govern/applications/{application_id}/patients` | Purges patient state and ledger for the application | **Hand-offs.** Reads take a key or a session; every action below requires a signed-in staff member and refuses an API key. | Method | Path | Notes | | ------ | ------------------------------------------------------ | ----------------------------------------------------------- | | GET | `/api/v1/govern/handoffs` | Read | | GET | `/api/v1/govern/handoffs/{handoff_id}` | Read; its events and the decisions that opened or joined it | | POST | `/api/v1/govern/handoffs/{handoff_id}/claim` | `expected_version` must be the current version | | POST | `/api/v1/govern/handoffs/{handoff_id}/contact-attempt` | On a claimed hand-off | | POST | `/api/v1/govern/handoffs/{handoff_id}/resolve` | On a claimed hand-off | | POST | `/api/v1/govern/handoffs/{handoff_id}/resume` | Releases the hold on a resolved hand-off | ## Care hand-offs and the messaging policy [#care-hand-offs-and-the-messaging-policy] Every route here requires a signed-in staff member in addition to the key scope. | Method | Path | Scope | | ------ | -------------------------------------------- | ------------------------- | | GET | `/api/v1/care-handoffs` | Read | | GET | `/api/v1/care-handoffs/{id}` | Read | | POST | `/api/v1/care-handoffs/{id}/claim` | Write | | POST | `/api/v1/care-handoffs/{id}/contact-attempt` | Write | | POST | `/api/v1/care-handoffs/{id}/resolve` | Write | | POST | `/api/v1/care-handoffs/{id}/resume` | Write | | GET | `/api/v1/patient-messaging-policy` | Read | | GET | `/api/v1/patient-messaging-policy/staff` | Read | | PUT | `/api/v1/patient-messaging-policy` | Admin; organization admin | ## Consent and SMS [#consent-and-sms] | Method | Path | Auth | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | POST | `/api/v1/public/sms-optin` | public, captcha, per-IP limit | | POST | `/api/v1/communication_consent/sms/resend` | Write | | GET | `/api/v1/anybio/programs/{handle}/disclosure` | public | | GET, POST | `/api/v1/notification-provider-configs` | Admin | | GET, PATCH, DELETE | `/api/v1/notification-provider-configs/{config_id}` | Admin | | GET, POST | `/api/v1/notifications`, `.../{notification_id}`, `.../{notification_id}/acknowledge`, `/dismiss`, `/channels/{channel_id}/click`, `/device-tokens` | Read for `GET`, Write for `POST` | ## Billing periods and adherence [#billing-periods-and-adherence] Staff session required on the billing-period routes. | Method | Path | Scope | | ------ | --------------------------------------------------------------------- | ----- | | GET | `/api/v1/billing-periods/current-period` | Read | | GET | `/api/v1/billing-periods/patients/{xuser_id}` | Read | | GET | `/api/v1/billing-periods/programs/{episode_profile_id}/patients` | Read | | PATCH | `/api/v1/billing-periods/enrollments/{enrollment_id}/order-reference` | Write | ## Export [#export] Read for `GET`, Write for the rest. | Method | Path | | ------------------ | ------------------------------------------------------------- | | GET, POST | `/api/v1/data-exports` | | GET, PATCH, DELETE | `/api/v1/data-exports/{id}` | | POST | `/api/v1/data-exports/{id}/test` | | GET | `/api/v1/episodes/{episode_id}/data-exports` | | POST | `/api/v1/episodes/{episode_id}/data-exports/{data_export_id}` | | GET | `/api/v1/episode-data-exports/{id}` | | POST | `/api/v1/episode-data-exports/{id}/retry` | | GET, POST | `/api/v1/fhir-medplum-configs` | | GET, PATCH, DELETE | `/api/v1/fhir-medplum-configs/{id}` | | GET, POST | `/api/v1/hl7-endpoints` | | GET, PATCH, DELETE | `/api/v1/hl7-endpoints/{id}` | | GET | `/api/v1/integrations/epic/sandbox-demo` | Public FHIR terminology: `GET /fhir/CodeSystem/biosignal`. Public keys for SMART Backend Services: `GET /.well-known/jwks.json`. ## Synthetics [#synthetics] Read for `GET`, Write for `POST`. | Method | Path | | ------ | --------------------------------------------------------------------------------------------------------------- | | GET | `/api/v1/synthetics/scenarios` | | GET | `/api/v1/synthetics/runs`, `/api/v1/synthetics/runs/{id}`, `/api/v1/synthetics/runs/{id}/impact` | | POST | `/api/v1/synthetics/runs` (`scenario_slug`, `project_id`, `xuser_id`, `developer_key_id`, `episode_profile_id`) | | POST | `/api/v1/synthetics/runs/{id}/cancel` | Scenarios drive ECG, PPG, EDA and temperature streams into a program; a cuff reading is posted through the device webhook instead. ## Keys, projects, organization, environment [#keys-projects-organization-environment] | Method | Path | Scope | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | GET, POST | `/api/v1/developer/keys` | Admin | | GET, DELETE | `/api/v1/developer/keys/{key_id}` | Admin | | POST | `/api/v1/developer/keys/{key_id}/revoke` | Admin | | POST | `/api/v1/sdk/token` | Write; mints a short-lived patient token | | POST | `/api/v1/sdk/session` | public; publishable key flow | | GET, POST | `/api/v1/projects` | Read / Write | | GET, PATCH, DELETE | `/api/v1/projects/{project_id}` | Read / Write | | GET, POST, DELETE | `/api/v1/projects/{project_id}/users` | Read / Write | | GET, PATCH | `/api/v1/organizations`, `/api/v1/organizations/{id}` | Read / Write | | GET | `/api/v1/environments`, `/api/v1/session/environment` | Read | | POST | `/api/v1/session/environment` | Write; switch a session's environment | | GET | `/api/v1/audit`, `/api/v1/audit/entities/{entity_type}/{entity_id}`, `/api/v1/audit/xusers/{xuser_id}`, `/api/v1/audit/episodes/{episode_id}`, `/api/v1/audit/correlation/{correlation_id}` | Admin | | GET | `/api/v1/admin/usage/summary`, `/api/v1/admin/usage/xusers/{xuser_id}/summary`, `/api/v1/admin/usage/events` | Admin | ## Other areas [#other-areas] Documented in their own sections of this portal: the biosignal catalog (`/api/v1/biosignals`, `/api/v1/public/biosignals`, `/api/v1/projects/{project_id}/biosignals`), device catalog and profiles (`/api/v1/devices`, `/api/v1/device-profiles`), wearable connections and sources (`/api/v1/xusers/{xuser_id}/connections`, `/api/v1/sources`, `/api/v1/oauth/...`, `/api/v1/oauth_providers`), feature series and charts (`/api/v1/feature-series/{feature_id}`, `/api/v1/charts/query`), the model registry and routing (`/api/v1/models`, `/api/v1/infer/image`, `/api/v1/infer/biosignal`, `/api/v1/model-routing`), DSP algorithms (`/api/v1/dsp-algorithms`), the content library, and the MCP endpoint (`/mcp`, OAuth bearer). Internal SuperAdmin routes under `/api/v1/admin/` are in the generated document but are not part of the customer API. # Billing readiness (/docs/programs/billing-readiness) The ledger on this page presents evidence a billing team reconciles: which patient-local days carried a device reading, in which fixed period, against which configured bars. No route here records a charge state or an amount, and nothing in this product determines what may be charged. That determination belongs to the organization and its billing counsel. ## The ledger [#the-ledger] For every live program enrollment, the ledger divides time into fixed periods (30 days by default, never rolling), counts the patient-local calendar days in each period with at least one reading from the cellular device source, and labels the period by the bars the program configured. The calendar is evaluated in the patient's IANA timezone, and a reading counts on the day it was taken (device time), not the day it arrived. Period one starts on the enrollment date by default, or on the first device reading on or after enrollment when the program says so; a first-transmission enrollment with no reading yet has no periods and is listed as omitted rather than silently missing. ### Configuration [#configuration] A program sets its bars in `spec.billing_period`; an absent block means the defaults, and the response echoes the configuration it used so a non-default program stays legible. ```json "billing_period": { "period_days": 30, "tier_low_min_days": 2, "tier_high_min_days": 16, "anchor": "enrollment" } ``` `anchor` is `enrollment` or `first_transmission`. A malformed block is reported as `config_fell_back_to_defaults: true` on every row it affects, and the defaults are used. ### Tiers [#tiers] | `tier` | Meaning | | -------------- | ---------------------------------------------------------------- | | `none` | Fewer unique transmission days than the lower bar | | `days_2_to_15` | The lower bar reached (default 2 days) and the upper bar not yet | | `days_16_plus` | The upper bar reached (default 16 days) | The labels are fixed vocabulary; the thresholds that produce them are the program's. `tier_reached_at`, `low_tier_reached_at` and `high_tier_reached_at` carry the device time of the reading that crossed each bar. ### Pace, for the current period only [#pace-for-the-current-period-only] The current period also carries where it stands against the upper bar today: | Field | Meaning | | --------------------------- | ------------------------------------------------------------------------------------------------------- | | `days_elapsed` | Days of the period through today, today included | | `days_remaining` | Days of the period after today | | `days_needed_for_high_tier` | Unique days still needed to reach the upper bar; zero once reached | | `high_tier_reachable` | `days_needed_for_high_tier <= days_remaining` | | `on_pace` | Unique days so far are at or above the pro-rata share of the upper bar for the days elapsed, rounded up | | `pace_state` | One of three, exhaustive and disjoint | | `pace_state` | Meaning | | ------------------ | ----------------------------------------------------------------------- | | `on_pace` | At or ahead of the pro-rata share | | `behind_reachable` | Below the share, and the days remaining can still cover the days needed | | `not_reachable` | The days remaining cannot cover the days needed; this period is decided | On the default configuration: three unique days on day 3 is `on_pace` (13 needed, 27 remaining); five unique days on day 15 is `behind_reachable` (11 needed, 15 remaining); five on day 20 is `not_reachable` (11 needed, 10 remaining); sixteen on day 30 is `on_pace` with nothing needed; fifteen on day 30 is `not_reachable`. A new period starts over. The adherence nudge reads the same derivation: a patient is nudged only while `behind_reachable`; `on_pace` and `not_reachable` both end the evaluation with no message. ## Routes [#routes] All four routes require a signed-in staff session on top of the key scope; developer keys alone and the patient SDK receive `403`. They are environment-scoped, and every `200` carries `Cache-Control: no-store` because the bodies name patients. ### `GET /api/v1/billing-periods/current-period` [#get-apiv1billing-periodscurrent-period] The coordinator's view: the current period per live enrollment, narrowed by `project_id` and/or `episode_profile_id`, paged by `limit` (default 100, max 500) and `offset`. Rows are sorted `behind_reachable` first, then `not_reachable`, then `on_pace`, and within a state by fewest days remaining, so the patients a person can still help come first. Live enrollments with no current period yet are listed under `omitted` with reason `no_current_period`. ### `GET /api/v1/billing-periods/patients/{xuser_id}` [#get-apiv1billing-periodspatientsxuser_id] Every enrollment ledger for one patient: the enrollment context and all of its periods. ### `GET /api/v1/billing-periods/programs/{episode_profile_id}/patients` [#get-apiv1billing-periodsprogramsepisode_profile_idpatients] One row per (patient, period) for a program, selected by `period_index` (1-based), by `from` and `to` (periods overlapping the range; either bound alone selects a day), or, by default, the current period. `selection` echoes which. `omitted` lists enrollments with `no_reading_yet` or `no_period_in_selection`. ### `PATCH /api/v1/billing-periods/enrollments/{enrollment_id}/order-reference` [#patch-apiv1billing-periodsenrollmentsenrollment_idorder-reference] Body: `{ "order_reference": "" }`; `null` or blank clears it. Returns `enrollment_id`, `order_reference`, `order_reference_missing`. The reference is stored beside the enrollment and reported on every ledger row; its absence is reported (`order_reference_missing: true`), not treated as an error. ## What each row carries [#what-each-row-carries] Per enrollment: `enrollment_id`, `xuser_id`, `episode_profile_id`, `project_id`, `identifier` and `name` (returned only to the authorized tenant), `timezone`, `enrolled_at`, `revoked_at`, `setup_completed_at`, `consent_recorded_at` and `consent_missing` (the earliest recorded consent grant for the patient; absent is reported, not an error), `order_reference` and `order_reference_missing`, `config`, `config_fell_back_to_defaults`, `anchor_date`. Per period: `period_index`, `period_start`, `period_end`, `unique_transmission_days`, `tier`, the three reached-at times, `device_identifiers` (distinct devices that reported), `device_classes` (for example `blood_pressure_cuff`), `device_replaced_in_period`, `first_reading_at`, `last_reading_at`, `is_current`, and the pace fields above on the current period only. ## In the dashboard [#in-the-dashboard] The **RPM Adherence** page reads `GET /api/v1/billing-periods/current-period` and nothing else: the current period per enrollment with its pace, sorted as above. What a coordinator sees is what the API returns. # Care hand-offs API (/docs/programs/care-handoffs) A care hand-off is a durable follow-up owned by a person. It is created when the loop cannot continue on its own for a patient, it moves `queued` to `claimed` to `resolved`, and while it stands, automated care SMS to that patient is withheld. The routes are under `/api/v1/care-handoffs` and are staff-session only: no API key, patient SDK token or patient account can claim ownership, record contact, or release a hold. ## When a hand-off is created [#when-a-hand-off-is-created] A hand-off is opened with one of these `reason` values: | `reason` | What happened | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------- | | `message_budget_exhausted` | The patient's automated care SMS allowance for the window is spent | | `message_policy_missing` | No patient messaging policy is configured for the environment, so no send is permitted | | `automation_paused` | A protocol rule matched (a red flag on a reading or on inbound text) and automation is paused for the patient | | `delivery_outcome_unknown` | A send was attempted and the provider's outcome could not be established | | `budget_exhausted`, `policy_missing` | The same two conditions, recorded by an older caller | There is at most one open hand-off per patient per environment. A second cause while one is open appends an `automation_withheld` event to the existing case, bumps its version so a person must refresh, and reopens it if it had been resolved with automation still paused. A new patient reply reopens a resolved case too; the prior disposition stays in the event log. Each hand-off carries: `state`, `reason`, `queue_label`, `routing_snapshot`, `assigned_to`, `claimed_at`, `due_at`, `escalation_due_at`, `escalated_at`, `resolved_at`, `resolved_by`, `disposition`, `resolution_note`, `automation_resumed_at`, `episode_id`, `version`. ## Read routes [#read-routes] Read scope on the key is not enough; the caller must be a signed-in staff member (`admin`, `member` or `physician`) of the organization. Responses carry `Cache-Control: no-store`. ### `GET /api/v1/care-handoffs` [#get-apiv1care-handoffs] Query: `status` (`queued`, `claimed`, `resolved`), `sort` (`created`, the default, oldest first; or `due`, earliest first with unset last), `xuser_id`, `assigned_to`, `project_id` (membership by the project roster), `overdue` (`true` for unresolved cases past `due_at`), `limit` (1 to 100, default 50), `offset`. Response: `environment_id`, `items` (the hand-off rows), `total`, and `patient_names` keyed by patient id. ### `GET /api/v1/care-handoffs/{id}` [#get-apiv1care-handoffsid] Query: `events_before` (a cursor for older events). Response: `environment_id`, `handoff`, `events` (oldest first, up to 100, with `next_event_cursor` when more exist), `patient` (`id`, `display_name`), and `recent_replies` - the patient messages already attached to this case, never the patient's whole conversation. Event types: `automation_withheld`, `claimed`, `contact_attempt`, `resolved`, `automation_resumed`, `patient_reply_received`, `protocol_rule_routed`, `overdue_escalated`. Each carries an `evidence` object of references (episode, session, message ids), never copies of patient text. ## Write routes [#write-routes] Write scope on the key, plus a staff session. Every write takes `expected_version`, and every successful write increments `version`. ### `expected_version` and the 409 [#expected_version-and-the-409] The row's `version` is bumped by every state change, including ones a person did not make: fresh evidence, a new patient reply, an overdue escalation. A write whose `expected_version` does not equal the current `version` is refused: ```json HTTP 409 Conflict { "error": "Follow-up changed; refresh before acting" } ``` Re-read the detail, look at what changed, and resubmit with the new version. The same 409 is returned when the action is not valid in the current state (claiming a resolved case, contacting a case you do not own). ### `POST /api/v1/care-handoffs/{id}/claim` [#post-apiv1care-handoffsidclaim] Body: `{ "expected_version": 3 }`. Moves `queued` to `claimed` and sets `assigned_to` to the caller. Eligibility follows the routing snapshot: before escalation, the primary staff list; after escalation, primary plus backup. If none of the routed staff are still members, anyone on staff may claim, so a queue cannot be orphaned by a membership change. An organization admin may reclaim a claimed case. ### `POST /api/v1/care-handoffs/{id}/contact-attempt` [#post-apiv1care-handoffsidcontact-attempt] Body: `expected_version`, `outcome` (`contacted` or `no_answer`), `note` (required, up to 4000 characters), `next_due_at` (required when `outcome` is `no_answer`; must be in the future and within a year). Only the assignee may record contact. A `next_due_at` moves `due_at` and, before escalation, `escalation_due_at`. Returns `422` with a message when the body is incomplete. ### `POST /api/v1/care-handoffs/{id}/resolve` [#post-apiv1care-handoffsidresolve] Body: `expected_version`, `disposition`, `note` (required), `resume_automation` (boolean). Dispositions: | `disposition` | Meaning | | --------------------- | -------------------------------------------------------- | | `follow_up_completed` | The person reached the patient and the follow-up is done | | `no_outreach_needed` | Reviewed; no contact was required | | `referred` | Routed onward to another team or service | Resolution alone does not release the automation hold. Only `resume_automation: true` here, or a later `resume`, sets `automation_resumed_at`; until then automated care SMS to the patient stays withheld and a new reply reopens the case. Resolving clears the attention contribution the hand-off raised. ### `POST /api/v1/care-handoffs/{id}/resume` [#post-apiv1care-handoffsidresume] Body: `expected_version`, `note` (required: why automation may resume). Valid only on a resolved case whose automation is still paused; the assignee or an admin may call it. ## Routing, due times and escalation [#routing-due-times-and-escalation] Due and escalation times come from the environment's patient messaging policy at the moment the hand-off is created, frozen onto the row as `routing_snapshot`: `due_at` is now plus `response_seconds`; `escalation_due_at` is `due_at` plus `escalation_seconds`. A worker sweep (at most 100 cases per pass) marks unresolved cases past `escalation_due_at` as escalated, returns them to `queued`, clears the owner, and records `overdue_escalated` with `"routing": "backup_queue"`. The sweep changes the in-product queue only; it sends no external notification and makes no claim of after-hours coverage. ## The messaging policy routes [#the-messaging-policy-routes] The hand-off queue and the per-patient message budget read one policy per organization and environment. There are no numeric defaults: a missing policy is itself a hand-off reason. ### `GET /api/v1/patient-messaging-policy` [#get-apiv1patient-messaging-policy] Read scope plus a staff session. Returns `{ "policy": ..., "environment_id": ... }`; `policy` is `null` until one is saved. ### `PUT /api/v1/patient-messaging-policy` [#put-apiv1patient-messaging-policy] Admin scope plus an organization admin session. Body: ```json { "max_messages": 8, "window_seconds": 3600, "routing": { "queue_label": "Care team follow-up", "primary_user_ids": [""], "backup_user_ids": [""], "response_seconds": 600, "escalation_seconds": 120, "coverage_note": "Weekday clinic hours; overnight cases wait for the morning huddle" } } ``` Validation: `max_messages` positive; `window_seconds`, `response_seconds` and `escalation_seconds` between 1 and 31,536,000; both staff lists non-empty, distinct, at most 100 each, and every id a current staff member; `queue_label` 1 to 120 characters; `coverage_note` 1 to 1000 characters. Unknown fields are rejected. Durations are elapsed time, not a staffed-hours calendar; `coverage_note` is where a team writes what its hours are. ### `GET /api/v1/patient-messaging-policy/staff` [#get-apiv1patient-messaging-policystaff] Read scope plus a staff session. Lists the staff members (`id`, `name`) who may appear in the routing lists. ## Where it appears [#where-it-appears] The dashboard's **Review Queue** page carries the hand-off queue beside agent runs and held messages. The settings page for patient messaging edits the policy above. # Concepts (/docs/programs/concepts) AnyBio routes, surfaces, holds, ranks and records. It does not decide for a clinician and it does not diagnose. Each concept below names the object in the API, what it is for, and the routes that touch it. ## Patient [#patient] A patient is an `xuser`: a person in your organization, identified by your own external `identifier` and attached to a project. Patients are created with `POST /api/v1/xusers` and read with `GET /api/v1/xusers/{xuser_id}`. A patient's phone number is a contact point (`PUT /api/v1/xusers/{xuser_id}/phone`), and SMS consent is recorded per address, not per person. Every other object in this section hangs off a patient: episodes, observations, hand-offs, consent evidence. Patient data is scoped to one organization and one environment; a sandbox key never reads a production patient. ## Episode [#episode] An episode is a bounded window of a patient's time that holds what happened in it: the observations linked to it, the timeline events, the attention level. A program opens one at enrollment; detection-based profiles open one when a condition holds. Episodes are read with `GET /api/v1/episodes` (filter by `xuser_id`, `episode_profile_id`, `status`, `attention_gte`) and `GET /api/v1/episodes/{id}`. `GET /api/v1/episodes/{id}/attention` returns the attention breakdown, `GET /api/v1/episodes/{id}/fhir` returns the episode as a FHIR bundle, and `POST /api/v1/episodes/{id}/close` ends it. An observation is routed to an episode only when the episode is active and its program declares the observation's signal in `required_signals`. ## Program [#program] A program is an episode profile: the authored document that says what a cohort's episodes require, watch for, and do. Its `spec` carries `required_signals`, `detection`, `thresholds`, `attention_config`, `agents`, `disclosure`, `exports`, `sdk`, an optional `symptom_protocol` and an optional `billing_period`. Programs are created with `POST /api/v1/episode-profiles`, activated with `POST /api/v1/episode-profiles/{id}/activate`, and patients are enrolled with `POST /api/v1/episode-profiles/{slug}/enroll`. Only an active program accepts enrollment. Saving a program also materializes the rules its spec declares (schedule triggers and the symptom trigger) as policy rows, so the authored document and the rules that run cannot disagree. ## Rule [#rule] A rule is a policy: a trigger, a condition, and a list of actions. Triggers are `observation_create`, `event_create`, `episode_start`, `episode_update`, `schedule`, `inactivity` and `manual`. The condition is an expression over the reading (`systolic >= 160 OR diastolic >= 100`), state, baselines and named thresholds. Actions raise or clear attention, set a floor, emit an event, run an agent, send a gated notification, create an observation, or start a symptom interview. Rules are scoped to the environment, a project, or one program; each has a `cooldown_seconds` per episode and an optional `max_executions_per_episode`. They are managed at `/api/v1/policies`, dry-run with `POST /api/v1/policies/{policy_id}/test`, and every evaluation is recorded at `GET /api/v1/policies/{policy_id}/executions`. The full schema is in the [policy reference](/docs/programs/policy-reference). ## Attention [#attention] Attention is a number from 0 to 100 that ranks episodes for a care team. It is never set directly; it is computed from a ledger of contributions, each with points, a reason, the signals involved and a half-life. Contributions decay, converging signals multiply, and a floor keeps attention from falling below a value until a person acknowledges it. The bands are Routine, Elevated, Concerning, Urgent and Critical, with thresholds the program can configure. Rules add contributions (`contribute_attention`), the symptom agent adds one when an interview surfaces something, and a resolved hand-off clears the one it raised. A clinician lowers the floor with `POST /api/v1/episodes/{id}/attention/acknowledge`. The formula and defaults are in the [policy reference](/docs/programs/policy-reference#attention). ## Hold [#hold] A hold is the loop refusing to proceed on its own. There are two. The **gate hold** is a patient-facing message the communication gate withheld: its decision record has `release_status: held_for_review`, and only a signed-in person can release or reject it (`POST /api/v1/compliance/checks/{check_id}/release`, `.../reject`). The **automation hold** is a care hand-off whose `automation_resumed_at` is empty: while it stands, automated care SMS to that patient is withheld and a person owns the conversation. A hold is not a failure state; it is the record that a person is now required. ## Hand-off [#hand-off] A hand-off is a durable follow-up owned by a person. It is created when automation cannot continue for a patient (a message budget exhausted, a messaging policy missing, automation paused by a protocol rule, a delivery outcome unknown) and moves `queued` to `claimed` to `resolved`. It carries a queue label, routing to primary and backup staff, a due time and an escalation time, contact attempts with outcomes, and a disposition. Every change appends an event and bumps a version; a stale actor gets a 409. The routes are under `/api/v1/care-handoffs` and are staff-session only. See [care hand-offs](/docs/programs/care-handoffs). ## Record [#record] A record is what the loop leaves behind so a reviewer can see what happened without asking anyone. Rules leave `policy_executions` with the context they evaluated and the actions they ran. The gate leaves a `communication_compliance_check` per message with the decision, the rationale, the detected claims, the original content and any rewritten content. Hand-offs leave `care_handoff_events`. Attention leaves contributions and acknowledgments. Symptom answers become observations. Audit queries are under `/api/v1/audit` (Admin scope). Records are read back through the same API and can be exported; see [export](/docs/programs/export). ## The verbs [#the-verbs] We say a rule **routes** a reading, the agent **surfaces** a finding, the gate **holds** a message, attention **ranks** an episode, and the system **records** a decision. A person reviews, releases, claims, resolves and acknowledges. The software does not triage, escalate on its own judgement, decide, or diagnose, and nothing here is autonomous: every path that reaches a patient is gated and every path that needs judgement ends at a person. # Device ingest (/docs/programs/device-ingest) Readings reach a program by four paths: a cellular device pushes each measurement to a per-project webhook; a phone or app posts an image or a waveform capture; the mobile SDK streams raw packets; a wearable account syncs on a schedule. This page covers the first two and the provenance fields all of them write. The SDK paths are documented under [Stream on iOS](/docs/biosdk) and wearable connections under [Connect devices](/docs/device-integration). ## The cellular device webhook [#the-cellular-device-webhook] Cellular cuffs, scales, pulse oximeters, glucose meters and thermometers deliver each reading as one HTTP POST from the device supplier's hub to a webhook addressed to your project. There is no pull step: the value is in the payload, and the receiver resolves the patient and writes the observation directly. ``` POST https://api.anybio.io/webhooks//{project_key} Authorization: Content-Type: application/json ``` The provider segment and the full URL are shown on the dashboard's Integrations page once the source is added; copy it from there. The route is public (no API key) and authenticates by a shared secret. ### Authentication [#authentication] * The hub sends a fixed `Authorization` header on every POST. You configure the expected value per project when you add the source (Integrations, Connected Sources, Add Data Source, field **Authorization**), or with `POST /api/v1/oauth_providers` (`webhook_secret`, `webhook_enabled`). * The stored value must be the **entire header value, scheme included** (for example `Basic ` or `Bearer `). A bare token is rejected at save time because it would silently fail every delivery. * The receiver compares in constant time after digesting both sides to a fixed length, so the comparison leaks neither the content nor the length of the secret. * A project with no configured secret, or with webhook delivery disabled, accepts nothing. ### Payload [#payload] One JSON object per POST, at most 64 KB. Only the fields below are read; extra fields are ignored. | Field | Meaning | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `metric` | The measurement kind. Matched case-insensitively: `blood_pressure` (also `bp`), `pulse` (`heart_rate`, `hr`), `glucose` (`blood_glucose`), `weight` (`body_weight`), `spO2` (`oxygen_saturation`), `temperature` (`body_temperature`). Unknown metrics are acknowledged and skipped. | | `patient_id` | Your external identifier for the patient. It must equal the patient's `identifier`; a patient that does not exist yet is created in the project. | | `value_1`, `value_2` | Numeric strings. For `blood_pressure`, `value_1` is systolic and `value_2` is diastolic. | | `timestamp` | Device measurement time in UTC, RFC 3339. Becomes the observation's `effective_datetime` and the deduplication key, so a redelivered reading is idempotent. Fallbacks: an offset-less `YYYY-MM-DD HH:MM:SS` (read as UTC), epoch seconds, then `created`. | | `device_name`, `hwi_device_id`, `hardware_uuid`, `sensor_code` | Device identity; recorded as the observation's `device_reference` and in metadata. | | `timezone_offset` | Informational; not applied, because `timestamp` is already UTC. | ### What is written [#what-is-written] | `metric` | Biosignal | Code | Unit written | | ---------------- | ----------------------- | --------------------------------------------------------------------- | ------------------------------------------------- | | `pulse` | 1, heart rate | LOINC 8867-4 | beats/minute | | `blood_pressure` | 3, blood pressure panel | components `systolic` and `diastolic`; FHIR derives the 85354-9 panel | mmHg | | `temperature` | 4, body temperature | LOINC 8310-5 | Celsius, converted from the supplier's Fahrenheit | | `spO2` | 5, oxygen saturation | LOINC 59408-5 | percent | | `glucose` | 14, fingerstick glucose | LOINC 41653-7 | mg/dL | | `weight` | 24, body weight | LOINC 29463-7 | kg, converted from the supplier's pounds | Perfusion index and irregular-heartbeat flags are not mapped today. The raw payload is preserved verbatim under a provider-named key in `observation.metadata`, so a converted value can always be traced to what arrived. `source_id` names the cellular source; `method_code` is left empty because the device is the method. After the write, the reading is linked to the patient's active episodes whose program declares the signal in `required_signals`, and every active `observation_create` rule scoped to those episodes is evaluated. That is the whole response loop; see the [quickstart](/docs/programs/quickstart) for it end to end. ### Response semantics [#response-semantics] The supplier's hub waits a short time and does not retry, so the receiver acknowledges aggressively: | Status | When | | ------ | --------------------------------------------------------------------------------------------------- | | `200` | Accepted, including an unmapped metric or a data-level problem, which is logged rather than refused | | `400` | The body is not valid JSON for the payload above | | `401` | The `Authorization` header is missing or does not match | | `404` | Unknown project key, no cellular source configured for the project, or webhook delivery disabled | A `200` does not mean a rule fired. Check `GET /api/v1/policies/{policy_id}/executions` or the episode's attention. ## Image ingest [#image-ingest] `POST /ingest/image` (Write scope; the token must carry an environment) stores a photograph or screenshot against a patient for later classification. ```json { "xuser_id": "", "device_id": "phone-01", "ts_ms": 1789740300000, "image_data": [255, 216, 255, 224, ...], "mime_type": "image/jpeg" } ``` * `image_data` is the raw bytes as a JSON array; `mime_type` must be one of `image/jpeg`, `image/png`, `image/heic`, `image/heif`; `ts_ms` is the capture time in milliseconds and must be positive. * The patient must be active, in your organization and in the token's environment; anything else is `403`. An empty image, an unsupported type or a non-positive timestamp is `400`. * Bytes are hashed; an identical image already stored for the organization returns the existing record instead of a duplicate. * There is no dedicated size limit on this route today (only the service-wide request cap applies); keep uploads to a few megabytes. The iOS SDK's `uploadImage` compresses to JPEG for you; see the [BioSDK image guide](/docs/biosdk/guides/image-ingest). Read back with `GET /api/v1/images` (filters `xuser_id`, `session_id`, `date_range`, `limit`, `offset`), `GET /api/v1/images/{image_id}`, and `GET /api/v1/images/{image_id}/file` for the bytes. Each record carries `captured_at`, `image_url`, `thumbnail_url`, a `classification` (`result`, `confidence`, `model_version`) once an image model from the [Model Registry](/docs/model-registry) has run, and `change_detected`. `POST /api/v1/images/reprocess` (Write scope) re-queues unclassified images. A program maps classification labels to attention points with `attention_config.classification_attention_map`. ## Waveform captures [#waveform-captures] `POST /ingest/captures` (Write scope, at most 8 MB) lands a client-recorded telemetry strip - today an ECG from a phone or watch - as one observation and one signal stream: | Field | Meaning | | --------------------------------------------- | --------------------------------------------------------------------------------- | | `source` | `apple-healthkit` or `oauth-fitbit` | | `external_ref` | The provider's stable per-capture key; a repeat is deduplicated (`deduped: true`) | | `signal_type` | `ecg` (the only type supported today) | | `samples_mv`, `sample_rate_hz`, `lead_count` | The waveform in millivolts | | `started_at`, `ended_at` | Capture window | | `result_classification`, `average_heart_rate` | The device's own reading, if any | | `raw_payload` | Required; the audit record for the capture | | `device_reference` | Optional device identity | The response carries `observation_id`, `signal_stream_id` and `deduped`. ## Provenance fields [#provenance-fields] Every observation, whichever path wrote it, carries the fields a reviewer needs to know when it was measured, when it arrived, where it came from and how: | Field | Meaning | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `effective_datetime` | When the measurement was taken, as the device or provider reports it. The clinical time. | | `created_at` | When AnyBio wrote the row. The receipt time. | | `measured_at`, `received_at` | Set by wearable syncs that carry both; the pair behind `latency_class`. | | `latency_class` | `realtime` (received within 60 seconds of measurement), `near_realtime` (within an hour), `delayed` (within a day), `historical` (older). | | `trigger_eligible` | Whether the reading may fire rules. `realtime` and `near_realtime` are eligible; `delayed` and `historical` are not. Paths that do not classify (the cellular webhook, the observations API) leave the class unset and the reading eligible. | | `source_id` / `source_slug` | Where the data came from: the cellular source, a wearable, the iOS or web SDK, `manual`. | | `method_code` | How it was derived, when the source alone does not say; `synthetic` marks a fabricated reading. | | `device_reference` | The device that measured it. | | `metadata` | The raw payload or provider annotations. | Rules can filter on these: `trigger_config.latency_classes` and `trigger_config.max_data_age_minutes` on an `observation_create` rule. Adherence and billing ledgers count **device time** (`effective_datetime`) and report the server's `last_data_sync_at` separately, so a late-arriving reading is credited to the day it was taken, not the day it arrived. ## Not covered here [#not-covered-here] * `POST /ingest/raw-packet` and the ingest session routes under `/api/v1/ingest/sessions` are the SDK's streaming path; see [Stream on iOS](/docs/biosdk). * `POST /api/v1/observations` and `/batch` write observations directly (Write scope) and link only to the `episode_ids` you pass; they do not evaluate rules. Use them for imports and journals, not to simulate a device. * Wearable webhooks (per-project routes under `/webhooks/`) are signature-verified pull triggers for specific providers; their data arrives through the sync path with `latency_class` set. # Export (/docs/programs/export) An episode can be sent onward as a FHIR R4 transaction bundle or an HL7 v2.5.1 ORU^R01 message. Exports are declared on the program, fire on triggers, run through a coordinator with retries, and leave one status row per attempt. This page documents the targets that exist, how a program binds to them, and how to read the result. ## Targets [#targets] | Target | `kind` | Transport | Status | | ------------------------ | -------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | FHIR R4 server | `fhir_medplum` | Transaction bundle over HTTPS with an OAuth client credential; idempotent Observation writes | Shipped | | Epic | `fhir_epic` | Transaction bundle over HTTPS with SMART Backend Services; Patient.Search before write | Proven against Epic's developer sandbox; production use requires your organization's Epic grant | | HL7 v2 interface engine | `hl7v2_mllp` | ORU^R01 v2.5.1 over MLLP with ACK handling | Shipped | | Read-only episode bundle | none | `GET /api/v1/episodes/{id}/fhir` | Shipped | Exports go to a FHIR server or an HL7 interface; no export target posts JSON to an arbitrary URL you own. The webhooks in this section are inbound (devices to AnyBio); see [device ingest](/docs/programs/device-ingest). ## The FHIR bundle [#the-fhir-bundle] `GET /api/v1/episodes/{id}/fhir` (Read scope) returns the episode as a FHIR R4 `Bundle`. The same builder feeds both FHIR adapters. The bundle contains: * **Patient binding** - by default no `Patient` resource is emitted: each Observation's `subject` is a logical `identifier` reference (system and value captured at enrollment and held per endpoint), and the receiving system resolves it to its own patient. AnyBio never creates or matches patients in a provider's system. An endpoint can require that binding, in which case an export for a patient without one is refused rather than sent unbound. A separate mode emits a literal `Patient` for a demonstration server that owns none. * **Observation** - one per linked observation, with the biosignal's LOINC code and unit, a business `identifier` under `https://api.anybio.io/fhir/sid/observation` so a re-export can be a conditional create rather than a duplicate, and `meta.profile` set from the biosignal catalog's declared profiles (for example US Core Heart Rate). A blood-pressure panel is one Observation with systolic and diastolic components. * **DocumentReference** - a telemetry strip rendered as PDF, when the episode produced one. * **Provenance** - who produced what, with in-bundle targets. * **Organization** - the exporting organization. Per export, a program binding can shape what a FHIR target receives: `shaping.mode` is `raw` (default, everything in the window) or `decimate` with `decimate_seconds` (at most one observation per biosignal per bucket). Shaping thins what one opted-in export sends by time; it never drops a value and never touches what is stored. ### Validation in CI [#validation-in-ci] The backend's CI runs the official HL7 FHIR validator (FHIR 4.0.1, `hl7.fhir.us.core#6.1.0`) on every change. Two gates block the build: a hand-written known-good fixture must validate with zero errors, and a deliberately malformed fixture must be rejected. A third gate validates the bundle AnyBio actually generates against a checked-in list of known findings; a new finding fails the build. ## The HL7 v2 message [#the-hl7-v2-message] The HL7 adapter assembles an ORU^R01 (v2.5.1) from the episode: `PID` and `PV1` from the patient and episode, `OBR-4` naming the program, and `OBX` segments from three sources - the linked biosignal observations as numeric results, `condition_detected` timeline events as text, and `agent_insight` events as text. A telemetry strip is referenced with a placeholder OBX pointing at the document, whose delivery is the FHIR adapter's job. Sending is over MLLP; the adapter parses the ACK and maps `AA` to success, `AE` and `AR` to a permanent failure, and a connect or ACK timeout to a retry. ## Configure a target [#configure-a-target] ### FHIR endpoints [#fhir-endpoints] `POST /api/v1/fhir-medplum-configs` (Write scope), also `GET`, `GET /{id}`, `PATCH /{id}`, `DELETE /{id}`. Body: ```json { "name": "Sandbox FHIR server", "base_url": "https://fhir.example.org/fhir/R4/", "oauth_provider_id": 12, "project_id": 7, "epic_token_url": null, "enabled": true } ``` `oauth_provider_id` names the credential row configured under Integrations, Data Destinations. `epic_token_url` is the SMART Backend Services token endpoint and is set only for an Epic row; it is distinct from `base_url`, the FHIR resource base. Both URLs pass the same guard at write time and again before any network call: HTTPS only, no user info, no IP literals, no `localhost`, no dotted-quad embedded in the host, no base that already ends in a resource type or `metadata`, and the host must be on the deployment's export host allowlist. An empty allowlist fails closed. ### HL7 endpoints [#hl7-endpoints] `POST /api/v1/hl7-endpoints` (Write scope), also `GET`, `GET /{id}`, `PATCH /{id}`, `DELETE /{id}`. Body: ```json { "name": "Interface engine (test)", "host": "interface.example.org", "port": 6661, "sending_application": "ANYBIO", "sending_facility": "ANYBIO.IO", "receiving_application": "ENGINE", "receiving_facility": "CLINIC", "project_id": 7, "hl7_version": "2.5.1", "connect_timeout_ms": 5000, "ack_timeout_ms": 10000, "auto_submit_on_episode_close": false, "enabled": true } ``` Optional: `encoding_characters`, `processing_id`, `metadata`. ### Data exports [#data-exports] A data export names a target and a kind. `POST /api/v1/data-exports` (Write scope), also `GET`, `GET /{id}`, `PATCH /{id}`, `DELETE /{id}` (disable): ```json { "name": "Clinic interface", "kind": "hl7v2_mllp", "project_id": 7, "target_ref": { "kind": "hl7v2_mllp", "hl7_endpoint_id": "" }, "enabled": true } ``` `target_ref` shapes: `{"kind":"hl7v2_mllp","hl7_endpoint_id":""}`; `{"kind":"fhir_medplum","fhir_medplum_config_id":""}`; the Epic adapter reads the same endpoint id key from a row whose `epic_token_url` is set. `POST /api/v1/data-exports/{id}/test` sends a synthetic test message through the configured destination. The HL7 adapter forces `MSH-11 = T` (training) so a downstream reviewer cannot mistake it for clinical traffic. ## Bind an export to a program [#bind-an-export-to-a-program] A program lists its exports in `spec.exports`. Each binding names a data export and the triggers that fire it: ```json "exports": [ { "data_export_id": "", "enabled": true, "triggers": [ { "type": "on_episode_start" }, { "type": "on_schedule", "cron": "0 7 * * *", "timezone": "America/Chicago", "require_new_data": true }, { "type": "on_attention_crossed", "threshold": 61, "debounce_seconds": 3600 }, { "type": "on_timeline_event", "event_type": "condition_detected" }, { "type": "on_episode_end" } ], "shaping": { "mode": "decimate", "decimate_seconds": 300 } } ] ``` | Trigger | Fires | | ---------------------- | ------------------------------------------------------------------------------------------------- | | `on_episode_start` | Once, at enrollment | | `on_episode_end` | Once, if the episode closes (a long-lived program episode may never close) | | `on_timeline_event` | Per matching `episode_events` row; optional `predicate` is a JSON subset match on the payload | | `on_schedule` | On a five-field cron in a timezone (`user_local` allowed); `require_new_data` skips empty windows | | `on_attention_crossed` | When attention rises above `threshold`, rate-limited by `debounce_seconds` (default 3600) | | `on_demand` | Only by `POST /api/v1/episodes/{episode_id}/data-exports/{data_export_id}` | ## Read the result [#read-the-result] Every fire creates an `episode_data_exports` row. `GET /api/v1/episodes/{episode_id}/data-exports` lists them; `GET /api/v1/episode-data-exports/{id}` returns one with `kind`, `trigger_reason`, `export_status`, `started_at`, `completed_at`, `protocol_control_id`, `protocol_ack`, `error_message`, `retry_count`, `max_retries`, `next_retry_at`, `payload_ref`, `payload_bytes`, `window_start`, `window_end`. Statuses: `pending`, `in_progress`, `success`, `failed`, `retrying`. A transient failure (network, 5xx, redirect, token acquisition, MLLP timeout) schedules a retry at 1, 5 and 15 minutes and reuses the original window; a permanent failure (4xx, `AE` or `AR`, a malformed frame, a disabled endpoint, a bundle the builder could not produce) stops. `POST /api/v1/episode-data-exports/{id}/retry` re-queues a failed row by hand. ## The Epic adapter, precisely [#the-epic-adapter-precisely] What is proven on demand: `GET /api/v1/integrations/epic/sandbox-demo` runs a read-only round-trip against Epic's public developer sandbox and its synthetic patients - SMART Backend Services authentication (an RS384 client assertion exchanged for a token), `Patient.Read`, `Patient.Search` by identifier, and a best-effort `Observation.Search`. The sandbox URLs are hard-coded in the service; it never reads a configured endpoint and never accepts a URL from the request, so it cannot touch a real Epic instance. When no signing key is configured for the environment it answers `available: false` with a static reason. How the write adapter behaves (implemented, exercised in the test suite): it resolves the patient with `Patient.Search` on the logical identifier and refuses on zero or more than one result (it never creates a Patient), rewrites every in-bundle subject to the resolved `Patient/`, posts the transaction bundle, and surfaces any non-2xx per-entry response as an error with Epic's diagnostic. The Epic adapter is validated against Epic's developer sandbox; production use requires your organization's Epic grant. # Programs and the response loop (/docs/programs) A program is the thing you run between visits. A patient is enrolled, their readings arrive, rules route what matters, attention ranks who a care team looks at first, and every step is recorded. This section documents that loop in the API, page by page, with the routes each page cites. Every page here describes the platform in the same words as the code. Examples are synthetic. Nothing in these pages is a clinical instruction. ## The loop in one pass [#the-loop-in-one-pass] 1. **Enroll.** A program (an episode profile) opens an episode for a patient. 2. **Ingest.** A reading arrives - from a cellular cuff through the device webhook, from a wearable, from the SDK - and is linked to the patient's active episode. 3. **Route.** Rules on the program evaluate the reading. A matching rule can raise attention, start a symptom interview, emit an event or a notification. 4. **Interview.** The symptom agent asks the clinician-authored questions over SMS, records the answers, and routes to a person when a red flag or the protocol says so. 5. **Gate.** Every patient-facing message passes the communication gate before it is sent. A held message waits for a person. 6. **Rank.** Attention is computed from a ledger of contributions with decay, so the care team's list orders itself. 7. **Hand off.** A held case becomes a follow-up with an owner, a due time and a disposition. 8. **Record and export.** Executions, decisions, hand-off events and observations are recorded, and can be exported as FHIR or HL7 v2. ## Pages [#pages] ## Before you start [#before-you-start] * [Sign in](https://secure.anybio.io/login) - your first sign-in creates an organization with a free sandbox. * Create a sandbox API key with **Write** scope in the dashboard. Keys created today start with `sk_sandbox_`; older keys start with `org_`. Send it as `Authorization: Bearer `. * API keys are bound to one environment. A sandbox key reads and writes sandbox data only. # Messaging and consent (/docs/programs/messaging-and-consent) Nothing patient-facing is sent unless four things are true: a sender is configured for the environment, an active communication policy allowed the content, the address has consent open, and the patient's message allowance for the window has room. Each of those fails closed on its own, and each is recorded. This page covers consent and the budget; the [gate](/docs/programs/symptom-agent-and-gate#the-gate) covers content. ## The sender [#the-sender] An SMS sender is configured per organization and environment under Settings, Notifications, or with `POST /api/v1/notification-provider-configs` (`channel: "sms"`, `provider`, `enabled`, `config`, `credentials`), read with `GET`, changed with `PATCH /{config_id}` and removed with `DELETE /{config_id}`. There is no platform fallback: an environment with no enabled sender cannot send, and the readiness ladder reports it as the missing rung. Your provider account must point its inbound-message webhook at the URL the dashboard shows for your project (shape `/webhooks//sms/{project_key}`); that is how STOP reaches AnyBio. ## Consent is per address, granted only by the patient [#consent-is-per-address-granted-only-by-the-patient] Consent is recorded per channel and address (a phone number), not per person, and its only sendable state is `opted_in`. The ledger has two parts: a gate row per address with its current `status` (`opted_in` or `opted_out`) and any outstanding invitation, and an append-only evidence trail of what was asked, what was shown, and what the patient replied. Two rules are enforced by the types, not by convention: * **Nothing an organization does can grant consent.** A form, an operator, or an API caller can only create a *pending* invitation. The grant is the patient's reply from the handset, verified against the carrier's signature. * **Only a patient reply can reopen a closed door.** Once an address is `opted_out`, the only path back to `opted_in` is the patient texting `START` from that number. There is no override. ### Asking: `POST /api/v1/public/sms-optin` [#asking-post-apiv1publicsms-optin] The public, unauthenticated endpoint an enrollment page posts to. Body: `program_handle` (the program's public handle, the one its QR code encodes), `phone` (any format that normalizes to E.164), `captcha_token` (from the widget on the enrollment page; required in practice). It creates a pending invitation and sends the patient a message that carries the identity disclosure and asks them to reply `START`. It never grants. The invitation expires after 24 hours. The response is deliberately uniform so the endpoint cannot be used to learn about a phone number: `400` for a string that is not a phone number or a destination the form does not text, `401` for a token no captcha issued, and otherwise `202` with the same body every time: ```json { "status": "accepted" } ``` A number that is already opted in, already opted out, never seen, over its rate limit, or attached to no program all receive that same `202`. ### Re-asking: `POST /api/v1/communication_consent/sms/resend` [#re-asking-post-apiv1communication_consentsmsresend] Write scope. Body: `{ "address": "+15550100" }`. Re-sends the invitation to an address the organization already asked, on the same per-handset budget as the public form, so a re-send is never a way around the cap. Returns `invitation_id` and `correlation_id`; it is not a delivery report. ### The patient's reply [#the-patients-reply] Inbound messages arrive at the per-project webhook, are verified against your provider account's signing token, and are classified by keyword: | Reply | Effect | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `STOP`, `STOPALL`, `UNSUBSCRIBE`, `CANCEL`, `END`, `QUIT` | Consent for the address is revoked and any outstanding invitation withdrawn. A `STOP` with nothing to revoke is recorded as evidence and changes no state. | | `START`, `UNSTOP` | Confirms an outstanding invitation, granting consent under the disclosure version that was shown. With no invitation outstanding, restores a consent this address previously gave. Never manufactures consent for an address that was shown nothing. | | `HELP`, `INFO` | An informational reply; no state change. | | anything else, `YES` included | No consent change. The invitation asks for `START`, and only `START` confirms it. | A message that is not a keyword is persisted durably, deduplicated in the database, attributed to the patient by the address the carrier signed, and handed to the symptom interview if one is open for that patient. Nothing is dropped between the webhook and the record, because the provider does not redeliver a message it has already been told was received. The inbound webhook is rate-limited on its own bucket and that limiter fails open when its store is unavailable: a missed `STOP` is worse than an extra request. ## The identity disclosure [#the-identity-disclosure] The consent invitation carries the AI-identity disclosure and is stamped with a fixed version identifier (`sms-v1.2` today), so every consent record names the exact wording the patient was shown. The composer refuses to produce an invitation when a party the document names (the organization, the program) has no usable name; the honest surface for that is a page that offers no form, not a message with a blank in it. Each program with an agent also carries its own disclosure in `spec.disclosure` (`identity`, `human_contact`); the symptom interview's opening message includes it, and an adherence nudge establishes it once per episode before saying anything else. `GET /api/v1/anybio/programs/{handle}/disclosure` returns a program's disclosure for a public enrollment page to render. ## The per-patient message budget [#the-per-patient-message-budget] Automated care SMS is bounded by the environment's patient messaging policy: `max_messages` per `window_seconds`, set by an organization admin with `PUT /api/v1/patient-messaging-policy` (see [care hand-offs](/docs/programs/care-handoffs#the-messaging-policy-routes)). Every automated send reserves a slot before it reaches the wire: * The policy counts logical messages, not carrier segments. * The patient's sends across all their addresses, and the address's sends across all patient records, must both fit the same allowance. * A reservation is committed before transport. An accepted or ambiguous send stays charged; only an established non-delivery releases the slot. A stale job cannot send a second copy after the window rolls over. * Consent invitations and the replies to `HELP` are service messages with their own controls and do not draw on this budget. When the allowance is spent, or when no policy is configured, the send is not made and a **care hand-off** is opened for the patient with reason `message_budget_exhausted` or `message_policy_missing`, so a person owns the conversation from there. There is no numeric default; the numbers are agreed with the receiving team and written into the policy. ## What a program needs before it can text [#what-a-program-needs-before-it-can-text] `GET /api/v1/episode-profiles/{id}/symptom-readiness` answers the program-scoped half of this list; the patient-scoped half is listed but not answered: | Rung | Scope | | -------------------------------------------------------------------- | ------- | | A live project in the environment | program | | A valid symptom protocol on the program, disclosure included | program | | A rule that routes the reading to a symptom session | program | | An enabled SMS sender for the organization and environment | program | | An active communication policy | program | | The patient exists and has an active episode that accepts the signal | patient | | A phone number on file (`PUT /api/v1/xusers/{xuser_id}/phone`) | patient | | SMS consent open for that number | patient | | No symptom session already open for the patient | patient | ## What is recorded [#what-is-recorded] Every step above leaves a row a reviewer can read without asking anyone: the invitation and its disclosure version, each inbound message with its classification, each consent grant and revocation with the address and the reply that caused it, each reservation against the budget and its settlement, and, for anything the gate touched, the decision record with the original and any rewritten content. Consent evidence is what the billing ledger reads to report `consent_recorded_at` per enrollment; see [billing readiness](/docs/programs/billing-readiness). # Policy reference (/docs/programs/policy-reference) A rule is a policy row. This page is the schema of that row, the grammar of its condition, the behaviour of each action, and the attention arithmetic those actions feed. Numbers here are the defaults in code; a program can override the attention ones in its `spec.attention_config`. Routes: `GET /api/v1/policies` (filters `project_id`, `episode_profile_id`, `status`, `trigger_type`), `POST /api/v1/policies`, `GET`, `PUT` and `DELETE /api/v1/policies/{policy_id}`, `POST /api/v1/policies/{policy_id}/test`, `GET /api/v1/policies/{policy_id}/executions`. Reads need the Read scope; writes need Write. ## The rule schema [#the-rule-schema] | Field | Type | Meaning | | ---------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `slug`, `name`, `description`, `version` | string | Identity. `slug` is unique within its scope. | | `scope` | `global`, `project`, `episode_profile` | Which subjects the rule sees. Inferred from `episode_profile_id` or `project_id` when omitted. A global rule sees every project in its environment. | | `project_id`, `episode_profile_id` | int, uuid | The scope anchors. A program-scoped rule only evaluates for episodes of that program. | | `status` | `draft`, `active`, `paused`, `deprecated` | Only `active` rules run. `POST` defaults to `draft`. | | `trigger_type` | see below | When the rule is evaluated. | | `trigger_config` | object | Per-trigger filter; shape depends on `trigger_type`. | | `condition_expression` | string | Must evaluate true for the actions to run. Validated on write. | | `condition_params` | object of number | Named thresholds available to the expression as `{thresholds.name}` or `$name`. | | `actions` | array | Executed in order when the condition matches. Each has a `type`. | | `priority` | small int | Evaluation order within a trigger (materialized rules use 50). | | `cooldown_seconds` | int | Minimum seconds between executions of this rule on the same episode. `0` disables. | | `max_executions_per_episode` | int or null | Hard cap per episode. | Rules are environment-bound: a rule authored in the sandbox never evaluates a production observation. ### Triggers [#triggers] | `trigger_type` | Fires on | `trigger_config` | | --------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `observation_create` | A new observation linked to an active episode | `biosignal_ids: [int]`, `loinc_codes: [string]` (empty means any), `latency_classes: [string]` (`realtime`, `near_realtime`, `delayed`, `historical`; empty means any), `max_data_age_minutes: int or null`, `context_only: bool` | | `event_create` | A new timeline event | `event_types: [string]` | | `episode_start`, `episode_update` | Episode lifecycle | none | | `schedule` | A cron grid, evaluated by the policy worker | `cron: string`, `timezone: string` (default `UTC`) | | `inactivity` | No data for a period, evaluated by the policy worker | `threshold_hours: int` | | `manual` | Never automatically | none | An observation only reaches `observation_create` rules after it is linked to an active episode, and it is linked only when the episode's program lists that signal in `required_signals`. The public `POST /api/v1/observations` write links to the `episode_ids` you pass explicitly and does not evaluate rules; device paths (the cellular webhook, wearable syncs, the DSP pipeline) link and evaluate. ### The condition grammar [#the-condition-grammar] The expression language is deliberately small: * Comparisons: `>=`, `<=`, `>`, `<`, `==`, `!=` * Logic: `AND`, `OR`, `NOT`, parentheses * Arithmetic: `+`, `-`, `*`, `/` * Null checks: `state.start_ts != null` * Variables: signal names (`hr`, `hrv_rmssd`, `spo2`, `systolic`), `state.`, `baseline.` * Thresholds: `{thresholds.systolic_high}` or `$systolic_high`, substituted from `condition_params` before parsing On an `observation_create` trigger the reading is bound as follows: LOINC `8867-4` becomes `hr`, `80404-7` becomes `hrv_rmssd`, `59408-5` becomes `spo2`, any other coded scalar becomes `observation_value`, and each numeric component of a panel observation is bound under its own name (a blood-pressure panel exposes `systolic` and `diastolic`). `POST /api/v1/policies/{policy_id}/test` evaluates the condition against signals, state and baselines you supply, with the rule's own `condition_params` as thresholds, and returns `condition_matched` without executing anything. ## Actions [#actions] Each action is an object with a `type` and the fields below. Attention actions require an episode in context; without one they record `success: false` with `"error": "No episode context"` and the rule moves on. | `type` | Fields | What it does | | ----------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `contribute_attention` | `points` (1-100), `signals: [string]`, `reason`, `half_life_hours` (default `2.0`) | Appends a contribution to the episode's attention ledger, sourced to this rule's `slug`, then recomputes attention. This is the normal way a rule raises attention. | | `set_attention_floor` | `value` (0-100), `reason` | Sets the episode's floor. Attention cannot fall below the floor until a person lowers it by acknowledgment. | | `clear_contribution` | `source_name`, `reason` | Clears every active contribution on the episode whose source name matches (for example the slug of the rule that raised it), then recomputes. Use it for the end condition of the thing that raised attention. | | `set_attention` | `value`, `reason` | Legacy direct set. Still accepted; prefer `contribute_attention`. | | `update_episode_status` | `status`, `reason` | Changes the episode status. | | `create_event` | `event_type`, `payload` | Emits a timeline event sourced to the rule, which can in turn fire `event_create` rules and export triggers. | | `enqueue_agent` | `agent_type` (an agent slug or type), `priority` | Runs one of the program's agents against the episode. | | `send_notification` | `channel`, `template`, `params` | Renders the template and sends it **through the communication gate**. A held or withheld outcome is recorded and nothing is sent. | | `create_observation` | `loinc_code` or `biosignal_id`, `value_expression`, `interpretation` | Writes a derived observation whose value is the expression's result. | | `start_symptom_session` | `protocol_ref` | Opens an SMS symptom interview against the program's `symptom_protocol`, whose `id` must equal `protocol_ref`. Validated before opening; a missing or malformed protocol is a recorded failed action, never a session. | Every evaluation that passes the trigger filter is recorded in the rule's execution history, matched or not: `trigger_type`, `trigger_ref`, `condition_result`, `actions_executed`, `actions_failed`, `evaluation_duration_ms`, `executed_at`. A rule in cooldown, or a reading outside the rule's trigger filter, is skipped before evaluation and leaves no execution row. ### Rules the program materializes [#rules-the-program-materializes] Two kinds of rule are derived from a program's spec on every save, so the authored document and the running rule cannot drift: * Each agent with an `on_schedule` trigger gets a `schedule` rule whose action is `enqueue_agent` for that agent. * A `symptom_protocol.trigger` gets an `observation_create` rule scoped to the program, filtered to the trigger's `biosignal_slugs`, with the trigger's `expression`, `thresholds` as `condition_params`, `cooldown_seconds`, `max_sessions_per_episode` as the execution cap, `max_reading_age_minutes` as the data-age guard, and a single `start_symptom_session` action naming the protocol's `id`. Materialized rules appear in `GET /api/v1/policies` like any other and are re-synced when the program is saved. ## Attention [#attention] Attention is computed, never stored as an opinion. The calculator reads the episode's active contributions, decays each one, sums the ones still above a minimum, multiplies for converging signals, caps at 100, and applies the floor: ``` current(c) = c.points × 0.5 ^ (hours_since(c.created_at) / c.half_life_hours) base_sum = Σ current(c) for active c with current(c) ≥ min_contribution_threshold attention = max(floor, min(100, base_sum × correlation_multiplier)) ``` `correlation_multiplier` is chosen by the largest `signals` list on any single active contribution: one signal `1.0`, two signals `1.25`, three or more `1.5`. ### Defaults, and where a program overrides them [#defaults-and-where-a-program-overrides-them] All of these live in `spec.attention_config`; a program without the block uses the defaults. | Setting | Default | Path | | ------------------------------- | -------------------- | ------------------------------------------------------------------------------ | | Default half-life | `2.0` hours | `decay.default_half_life_hours` | | Minimum active contribution | `1.0` | `decay.min_contribution_threshold` | | Multipliers | `1.0`, `1.25`, `1.5` | `correlation_multipliers.single_signal`, `.two_signals`, `.three_plus_signals` | | Routine | `0` to `20` | `thresholds.routine_max` | | Elevated | `21` to `40` | `thresholds.elevated_max` | | Concerning | `41` to `60` | `thresholds.concerning_max` | | Urgent | `61` to `80` | `thresholds.urgent_max` | | Critical | `81` to `100` | above `urgent_max` | | Acknowledgment required above | `60` | `floor.require_acknowledgment_above` | | Auto-clear floor below | `0` (disabled) | `floor.auto_clear_below` | | Classification labels to points | empty | `classification_attention_map` (image and model classifications) | ### The floor and acknowledgment [#the-floor-and-acknowledgment] A floor is set by a rule (`set_attention_floor`) and is the minimum attention until a person acknowledges. `POST /api/v1/episodes/{id}/attention/acknowledge` takes `new_floor` (must be less than or equal to the current floor), `reason`, and optional `action_taken` and `notes`; it writes an acknowledgment record with the previous floor and the attention at that moment, then lowers the floor and recomputes. A floor cannot be raised through acknowledgment. ### Who else writes the ledger [#who-else-writes-the-ledger] * The symptom agent adds a contribution when a closed interview surfaces something a person should see; reviewing that agent run clears it. * A care hand-off routed by a protocol rule adds a contribution sourced `care_handoff`; resolving the hand-off clears it. * Every recompute that changes the level also emits an `attention_level` observation on the episode, so attention over time is queryable like any other signal. `GET /api/v1/episodes/{id}/attention` returns `level`, `level_label`, `floor`, `base_sum`, `correlation_multiplier`, `active_contributions`, `top_reason`, the per-contribution breakdown, and `config_source` (`profile:` or `system_default`). `GET /api/v1/episodes?attention_gte=61` lists the episodes at or above a level. # Quickstart: from a reading to a routed follow-up (/docs/programs/quickstart) In about fifteen minutes you will create a program with one blood-pressure rule, enroll a synthetic patient, post one cuff reading through the same webhook a cellular device uses, and read back the rule execution and the attention it raised. Everything runs on your sandbox with synthetic data. No hardware, no PHI. ## Prerequisites [#prerequisites] No account yet? [Sign in or create one](https://secure.anybio.io/login) - your first sign-in creates an organization with a free sandbox. You need two values from the dashboard: * A sandbox **API key with Write scope**. Keys created today start with `sk_sandbox_`; older keys start with `org_`. Send it as `Authorization: Bearer ` on every `/api/v1` request. The key is bound to the sandbox environment, so nothing in this guide can touch production. * Your **project key** (`proj_...`). The device webhook is addressed per project, and the patient you create must belong to the same project. Both live in the dashboard under Settings. Cellular cuffs deliver each reading as an HTTP POST to a per-project webhook, authenticated by a shared secret you choose. For this guide, your terminal plays the device. In the dashboard, open **Integrations**, then **Connected Sources**, then **Add Data Source**, and pick the cellular device hub. Two fields matter: * **Authorization** - the exact `Authorization` header value the sender will put on every POST, including the scheme (for example `Bearer `). The receiver compares the full header value in constant time; a bare token without a scheme is rejected at save time. * **Webhook enabled** - must be on. A project whose webhook is disabled answers `404` to every delivery. The same can be done with `POST /api/v1/oauth_providers` (`project_id`, `provider_config_id` from `GET /api/v1/oauth_provider_configs?provider_type=rpm_webhook`, `webhook_secret`, `webhook_enabled: true`, `enabled: true`). Copy the webhook URL the dashboard shows for your project. Its shape is `https://api.anybio.io/webhooks//`; this guide writes it as `$WEBHOOK_URL`. Set the values once so the steps below can be pasted: ```bash export ANYBIO_KEY="sk_sandbox_..." # Write scope export PROJECT_KEY="proj_..." export WEBHOOK_URL="https://api.anybio.io/webhooks//$PROJECT_KEY" export WEBHOOK_SECRET="Bearer " export API="https://api.anybio.io/api/v1" ``` ## Your first routed reading [#your-first-routed-reading] ### Create the program and its rule [#create-the-program-and-its-rule] A program is an episode profile. The spec below requires the `blood_pressure` signal (so cuff readings link to the episode), opens an episode at enrollment, and carries no agents yet. Activate it, then attach one rule: when a linked reading has `systolic >= 160 OR diastolic >= 100`, add 45 attention points with a six-hour half-life. ```bash # 1a. Create the program (201 Created; keep "id" and "slug") curl -s -X POST "$API/episode-profiles" \ -H "Authorization: Bearer $ANYBIO_KEY" -H "Content-Type: application/json" \ -d '{ "slug": "bp-between-visits", "name": "Blood pressure between visits", "category": "PROGRAM", "lifecycle_mode": "AUTO_ENSURE_ACTIVE", "auto_start_trigger": "ENROLLMENT", "spec": { "required_signals": ["blood_pressure"], "detection": { "start_condition": "true" }, "thresholds": { "systolic_high": 160, "diastolic_high": 100 } } }' # 1b. Activate it (a draft program does not accept enrollment) curl -s -X POST "$API/episode-profiles/$PROFILE_ID/activate" \ -H "Authorization: Bearer $ANYBIO_KEY" # 1c. Attach one threshold rule scoped to this program (201 Created; keep "id") curl -s -X POST "$API/policies" \ -H "Authorization: Bearer $ANYBIO_KEY" -H "Content-Type: application/json" \ -d '{ "episode_profile_id": "'"$PROFILE_ID"'", "slug": "bp-high-reading", "name": "High cuff reading", "status": "active", "trigger_type": "observation_create", "trigger_config": { "biosignal_ids": [3] }, "condition_expression": "systolic >= 160 OR diastolic >= 100", "actions": [ { "type": "contribute_attention", "points": 45, "signals": ["blood_pressure"], "reason": "Cuff reading above the program threshold", "half_life_hours": 6 } ], "cooldown_seconds": 3600 }' ``` ```javascript const API = "https://api.anybio.io/api/v1"; const headers = { Authorization: `Bearer ${process.env.ANYBIO_KEY}`, "Content-Type": "application/json", }; const post = (path, body) => fetch(`${API}${path}`, { method: "POST", headers, body: JSON.stringify(body) }) .then(async (r) => ({ status: r.status, json: await r.json().catch(() => null) })); const profile = await post("/episode-profiles", { slug: "bp-between-visits", name: "Blood pressure between visits", category: "PROGRAM", lifecycle_mode: "AUTO_ENSURE_ACTIVE", auto_start_trigger: "ENROLLMENT", spec: { required_signals: ["blood_pressure"], detection: { start_condition: "true" }, thresholds: { systolic_high: 160, diastolic_high: 100 }, }, }); const profileId = profile.json.id; await post(`/episode-profiles/${profileId}/activate`, {}); const policy = await post("/policies", { episode_profile_id: profileId, slug: "bp-high-reading", name: "High cuff reading", status: "active", trigger_type: "observation_create", trigger_config: { biosignal_ids: [3] }, condition_expression: "systolic >= 160 OR diastolic >= 100", actions: [ { type: "contribute_attention", points: 45, signals: ["blood_pressure"], reason: "Cuff reading above the program threshold", half_life_hours: 6, }, ], cooldown_seconds: 3600, }); const policyId = policy.json.id; ``` ```python import os, requests API = "https://api.anybio.io/api/v1" H = {"Authorization": f"Bearer {os.environ['ANYBIO_KEY']}"} profile = requests.post(f"{API}/episode-profiles", headers=H, json={ "slug": "bp-between-visits", "name": "Blood pressure between visits", "category": "PROGRAM", "lifecycle_mode": "AUTO_ENSURE_ACTIVE", "auto_start_trigger": "ENROLLMENT", "spec": { "required_signals": ["blood_pressure"], "detection": {"start_condition": "true"}, "thresholds": {"systolic_high": 160, "diastolic_high": 100}, }, }).json() profile_id = profile["id"] requests.post(f"{API}/episode-profiles/{profile_id}/activate", headers=H) policy = requests.post(f"{API}/policies", headers=H, json={ "episode_profile_id": profile_id, "slug": "bp-high-reading", "name": "High cuff reading", "status": "active", "trigger_type": "observation_create", "trigger_config": {"biosignal_ids": [3]}, "condition_expression": "systolic >= 160 OR diastolic >= 100", "actions": [{ "type": "contribute_attention", "points": 45, "signals": ["blood_pressure"], "reason": "Cuff reading above the program threshold", "half_life_hours": 6, }], "cooldown_seconds": 3600, }).json() policy_id = policy["id"] ``` Biosignal `3` is the blood-pressure panel. A panel reading has no single value; the engine exposes each component as a signal named after it, which is why the condition reads `systolic` and `diastolic`. A rule created without `"status": "active"` is saved as a draft and never fires. ### Enroll a synthetic patient [#enroll-a-synthetic-patient] Create a patient in the same project as the webhook, then enroll them in the program. Enrollment opens the episode that readings will link to. The `identifier` you choose is the value the webhook will carry as `patient_id`. ```bash # 2a. Create the patient (201 Created; keep "id") curl -s -X POST "$API/xusers" \ -H "Authorization: Bearer $ANYBIO_KEY" -H "Content-Type: application/json" \ -d '{ "identifier": "synthetic-0001", "name": "Synthetic Patient 0001", "project_key": "'"$PROJECT_KEY"'" }' # 2b. Enroll (keep "episode_id") curl -s -X POST "$API/episode-profiles/bp-between-visits/enroll" \ -H "Authorization: Bearer $ANYBIO_KEY" -H "Content-Type: application/json" \ -d '{ "xuser_id": "'"$XUSER_ID"'" }' ``` ```javascript const patient = await post("/xusers", { identifier: "synthetic-0001", name: "Synthetic Patient 0001", project_key: process.env.PROJECT_KEY, }); const xuserId = patient.json.id; const enrollment = await post("/episode-profiles/bp-between-visits/enroll", { xuser_id: xuserId, }); const episodeId = enrollment.json.episode_id; ``` ```python patient = requests.post(f"{API}/xusers", headers=H, json={ "identifier": "synthetic-0001", "name": "Synthetic Patient 0001", "project_key": os.environ["PROJECT_KEY"], }).json() xuser_id = patient["id"] enrollment = requests.post( f"{API}/episode-profiles/bp-between-visits/enroll", headers=H, json={"xuser_id": xuser_id}, ).json() episode_id = enrollment["episode_id"] ``` The enroll response carries `episode_id`, `profile_slug`, `status` and `xuser_id`. Enrollment is refused with `400` when the program is not active. ### Post a cuff reading and watch it route [#post-a-cuff-reading-and-watch-it-route] Send one reading to the project's device webhook. This is the exact path a cellular cuff takes: the receiver checks the shared secret, resolves the patient by `patient_id`, writes a blood-pressure panel observation, links it to the active episode that requires `blood_pressure`, and evaluates every active `observation_create` rule scoped to that episode's program. Then read the rule's execution record and the episode's attention. ```bash # 3a. Post the reading (200 OK; the body is empty) curl -s -o /dev/null -w "%{http_code}\n" -X POST "$WEBHOOK_URL" \ -H "Authorization: $WEBHOOK_SECRET" -H "Content-Type: application/json" \ -d '{ "metric": "blood_pressure", "patient_id": "synthetic-0001", "value_1": "172.00", "value_2": "104.00", "timestamp": "2026-09-16T14:05:00Z", "hwi_device_id": "synthetic-cuff-01" }' # 3b. The rule's execution record curl -s "$API/policies/$POLICY_ID/executions?episode_id=$EPISODE_ID" \ -H "Authorization: Bearer $ANYBIO_KEY" # 3c. The episode's attention breakdown curl -s "$API/episodes/$EPISODE_ID/attention" \ -H "Authorization: Bearer $ANYBIO_KEY" ``` ```javascript const delivery = await fetch(process.env.WEBHOOK_URL, { method: "POST", headers: { Authorization: process.env.WEBHOOK_SECRET, "Content-Type": "application/json" }, body: JSON.stringify({ metric: "blood_pressure", patient_id: "synthetic-0001", value_1: "172.00", value_2: "104.00", timestamp: "2026-09-16T14:05:00Z", hwi_device_id: "synthetic-cuff-01", }), }); console.log("webhook", delivery.status); // 200 const executions = await fetch( `${API}/policies/${policyId}/executions?episode_id=${episodeId}`, { headers }, ).then((r) => r.json()); const attention = await fetch(`${API}/episodes/${episodeId}/attention`, { headers }) .then((r) => r.json()); console.log(executions.executions[0].condition_result, attention.level, attention.level_label); ``` ```python delivery = requests.post(os.environ["WEBHOOK_URL"], headers={"Authorization": os.environ["WEBHOOK_SECRET"]}, json={ "metric": "blood_pressure", "patient_id": "synthetic-0001", "value_1": "172.00", "value_2": "104.00", "timestamp": "2026-09-16T14:05:00Z", "hwi_device_id": "synthetic-cuff-01", }) print("webhook", delivery.status_code) # 200 executions = requests.get(f"{API}/policies/{policy_id}/executions", headers=H, params={"episode_id": episode_id}).json() attention = requests.get(f"{API}/episodes/{episode_id}/attention", headers=H).json() print(executions["executions"][0]["condition_result"], attention["level"], attention["level_label"]) ``` **Success looks like this.** The executions list has one entry with `condition_result: true` and a `contribute_attention` result: ```json { "executions": [ { "policy_id": "…", "episode_id": "…", "trigger_type": "observation_create", "condition_result": true, "actions_executed": [ { "action_type": "contribute_attention", "success": true, "result": { "points": 45, "attention_level": 45, "signals": ["blood_pressure"] } } ], "actions_failed": null, "executed_at": "2026-09-16T14:05:02Z" } ] } ``` and the attention breakdown reads: ```json { "level": 45, "level_label": "Concerning", "floor": 0, "base_sum": 45.0, "correlation_multiplier": 1.0, "active_contributions": 1, "top_reason": "Cuff reading above the program threshold", "contributions": [ { "source_name": "bp-high-reading", "original_points": 45, "current_value": 45.0, "signals": ["blood_pressure"], "half_life_hours": 6.0, "reason": "Cuff reading above the program threshold" } ], "config_source": "profile:bp-between-visits" } ``` In the dashboard, open **Episodes**: the synthetic patient's episode now ranks near the top, labelled Concerning. Post a second reading with a later `timestamp` after the cooldown and the contribution stacks; wait, and `current_value` decays with the six-hour half-life. ## If your first request fails [#if-your-first-request-fails] | What you see | Likely cause | Fix | | ----------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `401` from `/api/v1` | Missing or mistyped API key | Re-copy the key from the dashboard; send it as `Authorization: Bearer ` | | `403` from `/api/v1` | Key scope too low, or the patient is not in your organization | Program, rule, patient and enrollment writes need **Write** scope | | `402` from a write | The sandbox trial clock on this organization has expired | Sandbox is capped, never invoiced; contact us to extend or move to production | | `400` creating the program | An enum string or the spec was rejected | `category`, `lifecycle_mode` and `auto_start_trigger` are upper-case; `spec.detection.start_condition` is required | | `409` creating the program | A program with that `slug` already exists | Pick another slug, or reuse the existing one | | `400` enrolling | Program not active, or `xuser_id` missing | Call `POST /api/v1/episode-profiles/{id}/activate` first | | `404` from the webhook | Unknown project key, or webhook delivery disabled for the project | Check the project key in the URL and the **Webhook enabled** switch | | `401` from the webhook | Shared secret mismatch | The header must equal the saved value exactly, scheme included | | `200` from the webhook but no execution | Reading did not link to an episode, or the rule did not match | The episode must be active and its program must list `blood_pressure` in `required_signals`; the rule must be `active` with `biosignal_ids: [3]`; a repeated `timestamp` is deduplicated | | Execution shows `condition_result: false` | The reading is below the threshold | `value_1` is systolic and `value_2` is diastolic | | `429` | Per-session rate limit | Back off and retry | ## From here to the interview and the review queue [#from-here-to-the-interview-and-the-review-queue] The rule you wrote raises attention. The same trigger can open an SMS symptom interview and, when the interview surfaces something, put an item in the care team's queue. Those steps need things this guide did not set up, and each one fails closed rather than sending a message it should not: * **A symptom protocol on the program** (`spec.symptom_protocol`): the clinician-authored questions, red-flag rules, routing, timing, the trigger expression and the AI-identity disclosure. Saving the program materializes the trigger as an `observation_create` rule whose action is `start_symptom_session`. See [symptom agent and the gate](/docs/programs/symptom-agent-and-gate). * **An SMS sender** for your organization and environment - dashboard only, under Settings, Notifications. * **An active communication policy** - dashboard under Settings, Compliance Governance, or `PATCH /api/v1/compliance/policies/active`. Without one, the gate withholds every patient-facing message. * **A phone number on file** (`PUT /api/v1/xusers/{xuser_id}/phone`) and **SMS consent open for that number** - consent is only ever granted by the patient's own reply; see [messaging and consent](/docs/programs/messaging-and-consent). `GET /api/v1/episode-profiles/{id}/symptom-readiness` walks this ladder for a program and names what is missing. When everything is in place, a closed interview becomes an agent run in the dashboard's **Review Queue**, its attention contribution moves the episode up the list, and a case automation cannot continue becomes a [care hand-off](/docs/programs/care-handoffs) with an owner and a due time. ## What's next [#whats-next] * [Policy reference](/docs/programs/policy-reference) - every trigger, action and the attention formula * [Device ingest](/docs/programs/device-ingest) - the webhook payload in full, image ingest, provenance fields * [Export](/docs/programs/export) - send the episode onward as FHIR or HL7 v2 * [API reference](/docs/programs/api-reference) - the verified route list # Symptom agent and the gate (/docs/programs/symptom-agent-and-gate) The symptom agent is a bounded, two-way SMS interview that a rule opens when a reading comes in out of range. It asks the clinician's questions, records the answers as observations, and routes to a person when a red flag or the protocol says so. Every message it sends passes the communication gate first. This page describes both as they run today. ## What the agent may do [#what-the-agent-may-do] The agent runs a tool-calling loop with exactly four tools. Nothing it says to a patient is free text of its own authorship. | Tool | What it does | What the patient receives | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `ask` | Chooses **which** protocol field to ask about next. The question text comes from the protocol's `question_bank` and is sent verbatim; the model cannot rephrase, shorten or add to it. | The clinician-authored question | | `record_symptom` | Records one structured answer (a boolean, a number within the declared scale, an enum option, or a short note) against a required field. The value must cite the patient message it came from; a value with no patient-attributable source is refused. | Nothing | | `escalate` | Routes the interview to the care team urgently and stops asking. | The protocol's static `emergency_guidance`, sent verbatim; the model does not write it | | `handoff` | Routes the interview to a person and stops asking - the patient asked for someone, the conversation went outside what the interview covers, or they ended it with fields still missing. | The protocol's `cannot_continue_message` | Recorded answers become one observation per (patient, protocol field, source message), so what a patient reported is queryable and chartable like any other signal. Sessions move through `open`, `awaiting_reply`, `re_engaging` and end in one of `completed`, `escalated`, `handed_off`, `abandoned` or `stopped`. A sweep on the policy worker closes anything that stalls (per-turn window, overall time to live, re-engagement count), so every interview reaches a terminal state and every patient who was texted gets an answer or a person. When a session closes it becomes an agent run in the dashboard's **Review Queue** and, when it surfaced something, an attention contribution on the episode. Reviewing the run clears the contribution; nothing else does. ## What it never does [#what-it-never-does] Four exclusions are always in force, on top of anything the program adds in `scope_fence`: * Do not diagnose, or offer any statement that functions as a diagnosis. * Do not give treatment, dosing or medication guidance of any kind. * Do not provide therapy, counselling or mental-health treatment. * Do not make independent therapeutic decisions; route to the care team instead. `permit_therapeutic` exists on the protocol so that opening that fence would be a typed, reviewable act; today the validator rejects `true` outright. The agent also never invents a question, never sends anything the gate withheld, never texts an address whose consent is not open, and never opens a second interview for a patient who already has one live. ## The protocol [#the-protocol] A program carries the interview as `spec.symptom_protocol`, a clinician-authored, versioned document. Its fields: | Field | Meaning | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id`, `version`, `condition` | Identity. A rule's `start_symptom_session` names the `id`. | | `opening_message`, `opening_field` | The first patient-facing message and the question it asks. The opening must carry the non-emergency-line disclaimer. | | `disclosure` | The AI-identity disclosure: `identity` (what the patient is talking to) and `human_contact` (how to reach a person). Required. | | `required_fields` | What the interview collects: `key` and a `field_type` (`bool`, `severity_scale` with `min` and `max`, an enum, `free_note`). | | `question_bank` | The vetted question per field; `pinned` questions are sent verbatim. | | `red_flags` | Program-specific deterministic rules: `id`, an `expression` in the same grammar as rule conditions, a `priority`, and `applies_to` (the triggering reading, inbound text, or structured values). | | `scope_fence` | Additive items on the never list. | | `routing` | `urgent_priority` and the verbatim `emergency_guidance` text. | | `completion` | The done condition and `closing_message`; optional `cannot_continue_message`. | | `session` | `per_turn_window_secs`, `overall_ttl_secs`, `max_reengagements`. | | `trigger` | When a session starts: `enabled`, `biosignal_slugs`, `expression`, `thresholds`, `cooldown_seconds`, `max_sessions_per_episode`, `max_reading_age_minutes`. Saving the program materializes this as the rule that opens sessions. | `GET /api/v1/episode-profiles/{id}/symptom-readiness` walks the program-scoped prerequisites (a valid protocol, a matching rule, an SMS sender, an active communication policy) and lists the patient-scoped ones it cannot answer (phone number, consent, no open session), so a green program is never mistaken for a delivery guarantee. ## The deterministic red-flag screen [#the-deterministic-red-flag-screen] Before the model sees an inbound message, a pure function screens the text against a fixed vocabulary. It has no database, no network, no configuration and no error path: there is nothing to fail toward except routing. A hit routes the interview to the care team and sends the emergency guidance without waiting for the model to choose to. Four categories are covered, each term traceable to a published clinical standard cited in code: | Category | Rules | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cardiac | `cardiac.chest_discomfort`, `cardiac.jaw_discomfort` | | Respiratory | `respiratory.breathing_difficulty`, `respiratory.shortness_of_breath`, `respiratory.cyanosis` | | Neurological | `neuro.balance_loss`, `neuro.coordination_loss`, `neuro.trouble_walking`, `neuro.vision_loss`, `neuro.trouble_seeing`, `neuro.face_droop`, `neuro.arm_weakness`, `neuro.slurred_speech`, `neuro.trouble_speaking` | | Self-harm | `self_harm.wanting_to_die`, `self_harm.kill_myself`, `self_harm.killing_myself`, `self_harm.no_reason_to_live`, `self_harm.hopeless`, `self_harm.unbearable_pain`, `self_harm.feeling_trapped`, `self_harm.burden_to_others` | Three properties are stated in code and should be read before relying on it: * **It is tuned for recall and will fire on messages that are not emergencies.** Negation is not suppressed: "no chest pain" fires. Matching is whole-word with a bounded inflection tolerance, and phrase rules allow qualifiers within a small window. * **The vocabulary is fixed and English only.** It matches the phrasings of the cited standards. Lay phrasings outside them ("my chest hurts", "can't breathe") and constitutional symptoms are not matched; add the phrasings your population uses as the protocol's own `red_flags` rules, which union with the screen. * **A hit carries no message text.** The record names the rule id and the category only, so patient text cannot leak into a log by accident. The screen runs at arrival, before the reply-coalescing window, so a fragmented message cannot delay it. It unions with the protocol's own `red_flags` rules. ## The gate [#the-gate] Every patient-facing message - a rule's notification, an adherence nudge, an agent message, a symptom question, a chat reply - is evaluated by the communication gate before it is sent, and the evaluation is recorded whether or not the message goes out. ### Three outcomes, and the missing-policy case [#three-outcomes-and-the-missing-policy-case] | Decision | Meaning | Delivered to a patient? | | ------------------ | ---------------------------------------------------- | ------------------------------------------------------------------ | | `allow` | No violation of the active policy | Yes | | `rewrite` | A violation the policy resolves by term substitution | **No.** Held for a person. Delivered only to a clinician audience. | | `block_and_review` | A violation the policy resolves by withholding | No. Held for a person. | | `policy_missing` | No active communication policy for the scope | No. Held for a person. The gate fails closed. | A rewrite is machine term substitution, not authored prose: a matched clinical term is swapped for the policy's wellness wording wherever it appears, which routinely produces sentences no human wrote. For that reason a rewrite bound for a patient is a withhold - the substituted text is stored for the reviewer to read beside the original, and it is never sent to the patient. For a clinician audience (a timeline insight) a rewrite may be delivered. The audience is derived from the channel, never inferred from the presence of a patient id. Patient channels: `patient_notification`, `adherence_nudge`, `agent_nudge`, `agent_notification`, `policy_notification`, `symptom_session_question`, and patient chat. Clinician channels: `agent_timeline_insight` and clinician chat. ### Review requirement [#review-requirement] A message needs a person when the decision withholds it, and otherwise according to the policy's `review_mode`: `none`, `on_violation` (the default), or `always`. A message that needs a person is recorded with `release_status: held_for_review` and appears in the dashboard's **Review Queue**. ### The hold and its release [#the-hold-and-its-release] A held record is released or rejected only by a signed-in person: * `POST /api/v1/compliance/checks/{check_id}/release` with `released_with` (`original` or `rewritten`; defaults to `rewritten` when one exists) and optional `notes`. The record becomes `released_after_approval`, or `release_delivery_failed` if the re-delivery did not happen, so the audit trail never claims a delivery that did not occur. Re-delivery is automated today for `agent_nudge` and `agent_notification`; other channels record the approval and leave delivery to the channel's own path. * `POST /api/v1/compliance/checks/{check_id}/reject` with optional `notes`. The record becomes `rejected_after_review`. `GET /api/v1/compliance/checks` lists records with filters (`communication_channel`, `communication_ref`, `conversation_id`, `decision`, `review_required`, `release_status`, `project_id`) and returns a release-status histogram beside the page. `GET /api/v1/compliance/checks/{check_id}` returns one record: decision, rationale, detected claims, original content, rewritten content, audience, review state, who released it and with which version. These routes are Admin scope. The symptom agent's own outbound path adds one more check the gate cannot: after the gate returns a deliverable body, the sender verifies the mandated text (the disclaimer, the opt-out instruction) survived, and refuses the send if it did not. The emergency guidance is mandated verbatim; a rewrite of it is treated as a withhold. ## Identity disclosure [#identity-disclosure] Every program with an agent carries one AI-identity disclosure in its spec: an `identity` line that says what the patient is talking to, and a `human_contact` line that says how to reach a person. The `identity` text must not read as a licensed human; naming who reviews the answers belongs in the opening message, and the human route belongs in `human_contact`. For the SMS interview the disclosure is part of the opening message, and the fact that it reached the patient is recorded as a gate decision keyed to the session. Whether a channel requires the disclosure is set per channel; when it is required and none is on record, the gate refuses substantive content. The consent invitation itself carries the disclosure text under a fixed version identifier, so a consent record names the wording the patient was shown. # Core Types (/docs/biosdk/api/core-types) The foundational data models used throughout the SDK. ## BioDevice [#biodevice] Represents a discovered or connected BLE device. ```swift public struct BioDevice: Equatable, Hashable { public let id: UUID public let name: String public init(id: UUID, name: String) } ``` | Property | Type | Description | | -------- | -------- | ----------------------------------- | | `id` | `UUID` | CoreBluetooth peripheral identifier | | `name` | `String` | Advertised device name | *** ## BioSample [#biosample] A single biosignal sample received from a device. ```swift public struct BioSample: Equatable { public let signalName: String public let data: SignalData public let timestamp: TimeInterval public init(signalName: String, data: SignalData, timestamp: TimeInterval) } ``` | Property | Type | Description | | ------------ | -------------- | ---------------------------------------------------------- | | `signalName` | `String` | Signal identifier (e.g., `"ecg"`, `"ppg"`, `"heart_rate"`) | | `data` | `SignalData` | The sample payload — waveform array or scalar value | | `timestamp` | `TimeInterval` | Unix timestamp of the sample | ### Convenience Properties [#convenience-properties] ```swift public var isECG: Bool { get } public var isPPG: Bool { get } public var isEDA: Bool { get } public var isTemp: Bool { get } public var isHeartRate: Bool { get } public var isSpO2: Bool { get } // Extract data public var waveform: [Double]? { get } // Non-nil for waveform signals public var scalarValue: Double? { get } // Non-nil for scalar signals ``` **Example:** ```swift sdk.events .compactMap { event -> BioSample? in guard case .sample(_, let sample) = event else { return nil } return sample } .filter { $0.isHeartRate } .sink { sample in if let bpm = sample.scalarValue { print("Heart rate: \(bpm) BPM") } } ``` *** ## SignalData [#signaldata] The payload of a `BioSample` — either a waveform array or a single scalar value. ```swift public enum SignalData: Equatable { case waveform([Double]) case scalar(Double) public var jsonValue: Any { get } } ``` | Case | Description | | --------------------- | -------------------------------------------- | | `.waveform([Double])` | Array of samples (ECG, PPG, EDA waveforms) | | `.scalar(Double)` | Single value (heart rate, temperature, SpO2) | *** ## TimeSeries [#timeseries] A timestamped array of values, used by chart components. ```swift public struct TimeSeries: Equatable { public let timestamp: TimeInterval public let values: [Double] public init(timestamp: TimeInterval, values: [Double]) } ``` | Property | Type | Description | | ----------- | -------------- | ---------------------------------- | | `timestamp` | `TimeInterval` | Unix timestamp for this data point | | `values` | `[Double]` | Signal values at this timestamp | *** ## BioDeviceSpec [#biodevicespec] Device specification sent to the backend when starting a session. ```swift public struct BioDeviceSpec: Codable, Sendable { public let user_device_id: Int? public let device_id: String? public let device_profile_id: String? public let make: String? public let model: String? public let firmware: String? public let sampling_profile_name: String? public let battery: Int? public init( user_device_id: Int? = nil, device_id: String?, device_profile_id: String? = nil, make: String?, model: String?, firmware: String?, sampling_profile_name: String?, battery: Int? ) } ``` *** ## BiosignalType [#biosignaltype] Enumeration of supported biosignal types. ```swift public enum BiosignalType: String, Hashable, CaseIterable { case ecg = "ecg" case ppg = "ppg" case eda = "eda" case temp = "temp" case heartRate = "heart_rate" case hrv = "hrv" case spo2 = "spo2" case accel = "accel" case gyro = "gyro" case respiratoryRate = "respiratory_rate" case bloodPressure = "blood_pressure" } ``` ```swift // Parse from a string public static func from(_ string: String) -> BiosignalType? ``` *** ## BioError [#bioerror] SDK-level errors. ```swift public enum BioError: Error { case notConfigured case invalidOperation(String) } ``` | Case | Description | | --------------------------- | --------------------------------------------- | | `.notConfigured` | SDK method called before initialization | | `.invalidOperation(String)` | Invalid operation attempted, with description | # Events (/docs/biosdk/api/events) The SDK communicates state changes through a Combine-based event system. Subscribe to `sdk.events` to receive all events. ```swift sdk.events.sink { event in switch event { case .lifecycle(let e): handleLifecycle(e) case .sample(let deviceId, let sample): handleSample(deviceId, sample) case .deviceState(let device, let conn, let stream): handleState(device, conn, stream) case .session(let e): handleSession(e) case .notification(let e): handleNotification(e) } } ``` ## BioEvent [#bioevent] Top-level event enum emitted by `BioSDKClient.events`. ```swift public enum BioEvent: Equatable { case lifecycle(BioLifecycleEvent) case sample(deviceId: String, BioSample) case deviceState(device: BioDevice, connection: BioConnectionState, stream: BioStreamState) case session(BioSessionEvent) case notification(BioNotificationEvent) } ``` | Case | Description | | ------------------------- | --------------------------------------------- | | `.lifecycle(...)` | Device discovered, connected, or disconnected | | `.sample(deviceId:, ...)` | Biosignal sample received from a device | | `.deviceState(...)` | Connection or streaming state changed | | `.session(...)` | Backend session lifecycle event | | `.notification(...)` | Push notification event | *** ## BioLifecycleEvent [#biolifecycleevent] Device discovery and connection lifecycle events. ```swift public enum BioLifecycleEvent: Equatable { case discovered(BioDevice) case connected(BioDevice) case disconnected(BioDevice, errorMessage: String?, errorCode: Int?, errorDomain: String?) } ``` | Case | Description | | ---------------------------- | ------------------------------------------------- | | `.discovered(device)` | A new device was found during scanning | | `.connected(device)` | Device successfully connected | | `.disconnected(device, ...)` | Device disconnected (with optional error details) | **Example:** ```swift sdk.events .compactMap { event -> BioDevice? in guard case .lifecycle(.connected(let device)) = event else { return nil } return device } .sink { device in print("Connected to \(device.name)") } ``` *** ## BioSessionEvent [#biosessionevent] Backend session lifecycle events. ```swift public enum BioSessionEvent: Equatable { case started(id: String, devices: [SessionDevice]) case resumed(id: String, devices: [SessionDevice]) case conflict(activeId: String, startedAt: Date?) case ended(id: String, ok: Bool) case endFailed(id: String, status: Int?) case recoveryAvailable(info: OrphanedSessionInfo) case forbidden case serverError(status: Int) case error(message: String) } ``` | Case | Description | | ---------------------------------- | ------------------------------------------------------- | | `.started(id:, devices:)` | New session created on the backend | | `.resumed(id:, devices:)` | Existing session resumed | | `.conflict(activeId:, startedAt:)` | HTTP 409 — another session is active | | `.ended(id:, ok:)` | Session ended (`ok` indicates clean shutdown) | | `.endFailed(id:, status:)` | Session end request failed | | `.recoveryAvailable(info:)` | An orphaned session from a previous launch was detected | | `.forbidden` | Authentication/authorization failure (HTTP 403) | | `.serverError(status:)` | Backend returned a server error | | `.error(message:)` | General session error | ### SessionDevice [#sessiondevice] Device info returned when a session starts or resumes. ```swift public struct SessionDevice: Equatable, Codable { public let ingest_device_id: String public let device_id: String public let device_profile_id: String? public let make: String? public let model: String? } ``` ### OrphanedSessionInfo [#orphanedsessioninfo] Information about a session that was left active from a previous app launch. ```swift public struct OrphanedSessionInfo: Equatable, Sendable { public let sessionId: String public let startedAt: Date public let deviceCount: Int public let totalChunks: Int } ``` *** ## BioNotificationEvent [#bionotificationevent] Events from the notification system. ```swift public enum BioNotificationEvent: Equatable { case received(BioNotification) case acknowledged(notificationId: String) case dismissed(notificationId: String) case connectionStateChanged(NotificationConnectionState) } ``` See the [Notifications API](notifications) reference for `BioNotification` and `NotificationConnectionState`. *** ## BioConnectionState [#bioconnectionstate] Connection state for a device. ```swift public enum BioConnectionState: String, Equatable, Codable { case discovered case connecting case discoveringServices case ready case disconnected case failed } ``` ### State Flow [#state-flow] ``` discovered → connecting → discoveringServices → ready → failed → disconnected ``` | State | Description | | ---------------------- | ------------------------------------------- | | `.discovered` | Device found during scan, not yet connected | | `.connecting` | Connection in progress | | `.discoveringServices` | Connected, discovering GATT services | | `.ready` | Fully connected with services discovered | | `.disconnected` | Device disconnected | | `.failed` | Connection attempt failed | *** ## BioStreamState [#biostreamstate] Streaming state for a device. ```swift public enum BioStreamState: String, Equatable, Codable { case idle case starting case streaming case stopping case stalled case unsupported } ``` ### State Flow [#state-flow-1] ``` idle → starting → streaming → stopping → idle → stalled → unsupported ``` | State | Description | | -------------- | --------------------------------------------- | | `.idle` | Not streaming | | `.starting` | Stream start command sent, waiting for data | | `.streaming` | Actively receiving biosignal data | | `.stopping` | Stream stop command sent | | `.stalled` | Stream started but no data received (timeout) | | `.unsupported` | Device does not support streaming | *** ## Subscribing to Events [#subscribing-to-events] ### Filter by Event Type [#filter-by-event-type] ```swift // Only session events let sessionEvents = sdk.events .compactMap { event -> BioSessionEvent? in guard case .session(let e) = event else { return nil } return e } // Only samples from a specific device let deviceSamples = sdk.events .compactMap { event -> BioSample? in guard case .sample(let id, let sample) = event, id == myDeviceId else { return nil } return sample } ``` ### Observe Device State Changes [#observe-device-state-changes] ```swift sdk.events .compactMap { event -> (BioDevice, BioConnectionState, BioStreamState)? in guard case .deviceState(let device, let conn, let stream) = event else { return nil } return (device, conn, stream) } .sink { device, connection, stream in print("\(device.name): \(connection) / \(stream)") } ``` # Images API (/docs/biosdk/api/images) The SDK supports uploading images and querying image data associated with users and sessions. ## Uploading Images [#uploading-images] Upload through `BioSDKClient`: ```swift // Upload a UIImage try await sdk.uploadImage(photo, userId: userId) // Upload raw data with MIME type try await sdk.uploadImageData( jpegData, mimeType: "image/jpeg", userId: userId, deviceId: "camera-001" ) ``` *** ## ImageIngestClient [#imageingestclient] Low-level image upload client. ```swift public class ImageIngestClient { // Upload raw image data public func uploadImageData( _ data: Data, mimeType: String, userId: UUID, deviceId: String ) async throws // Upload a UIImage (iOS only) public func uploadImage( _ image: UIImage, userId: UUID, deviceId: String, mimeType: String = "image/jpeg", compression: CGFloat = 0.8 ) async throws } ``` ### ImageError [#imageerror] ```swift public enum ImageError: Error, LocalizedError { case compressionFailed case uploadFailed case server(status: Int, body: String?) case network(underlying: Error) case invalidResponse } ``` *** ## ImageQueryClient [#imagequeryclient] Client for querying uploaded images. ```swift public final class ImageQueryClient { // List images with optional filters public func listImages( filter: ImageQueryFilter = ImageQueryFilter() ) async throws -> [BioImage] // Get a single image by ID public func getImage(id: String) async throws -> BioImage // Download the raw image data public func downloadImage(id: String) async throws -> Data } ``` ### ImageQueryFilter [#imagequeryfilter] Filter parameters for listing images. ```swift public struct ImageQueryFilter: Equatable, Sendable { public var xuserId: String? public var sessionId: String? public var dateRange: DateRangePreset? public var limit: Int? public var offset: Int? public init( xuserId: String? = nil, sessionId: String? = nil, dateRange: DateRangePreset? = nil, limit: Int? = nil, offset: Int? = nil ) } ``` ### DateRangePreset [#daterangepreset] ```swift public enum DateRangePreset: String, Sendable { case today = "today" case last7Days = "7days" case last30Days = "30days" case last90Days = "90days" } ``` ### ImageQueryError [#imagequeryerror] ```swift public enum ImageQueryError: Error, LocalizedError { case invalidHTTPResponse case badStatus(Int) case decodingFailed(String) case forbidden case notFound case serverError(Int) } ``` *** ## BioImage [#bioimage] An uploaded image with metadata and optional classifications. ```swift public struct BioImage: Codable, Identifiable, Equatable, Sendable { public let id: String public let xuserId: String public let sessionId: String? public let deviceId: String? public let capturedAt: Date? public let uploadedAt: Date public let mimeType: String public let sizeBytes: Int public let width: Int? public let height: Int? public let imageUrl: String? public let thumbnailUrl: String? public let classifications: [BioImageClassification]? } ``` | Property | Type | Description | | ------------------ | --------------------------- | ------------------------------------------------- | | `id` | `String` | Unique image identifier | | `xuserId` | `String` | User who owns the image | | `sessionId` | `String?` | Associated session (if uploaded during a session) | | `deviceId` | `String?` | Device that captured the image | | `capturedAt` | `Date?` | When the image was captured | | `uploadedAt` | `Date` | When the image was uploaded | | `mimeType` | `String` | MIME type (e.g., `"image/jpeg"`) | | `sizeBytes` | `Int` | File size in bytes | | `width` / `height` | `Int?` | Image dimensions | | `imageUrl` | `String?` | Full-resolution URL | | `thumbnailUrl` | `String?` | Thumbnail URL | | `classifications` | `[BioImageClassification]?` | ML classification results | *** ## BioImageClassification [#bioimageclassification] A classification result from the backend ML pipeline. ```swift public struct BioImageClassification: Codable, Equatable, Sendable { public let id: String public let imageId: String public let classification: String public let confidence: Double public let processingEngine: String? public let createdAt: Date } ``` | Property | Type | Description | | ------------------ | --------- | ------------------------------------- | | `id` | `String` | Classification record ID | | `imageId` | `String` | Parent image ID | | `classification` | `String` | Classification label | | `confidence` | `Double` | Confidence score (0.0–1.0) | | `processingEngine` | `String?` | ML engine that produced the result | | `createdAt` | `Date` | When the classification was generated | **Example — display classifications:** ```swift let images = try await imageClient.listImages( filter: ImageQueryFilter(xuserId: userId, limit: 10) ) for image in images { if let classifications = image.classifications { for c in classifications { print("\(c.classification): \(Int(c.confidence * 100))%") } } } ``` # BioLiveStore (/docs/biosdk/api/live-store) An `ObservableObject` that aggregates live biosignal data, device states, and session info for SwiftUI views. ```swift public final class BioLiveStore: ObservableObject ``` Access via `sdk.live`: ```swift let sdk = try await BioSDKClient.initialize(configuration: config) struct DashboardView: View { @ObservedObject var live: BioLiveStore var body: some View { Text("Session: \(live.currentSessionId ?? "none")") } } // Pass sdk.live to your view DashboardView(live: sdk.live) ``` ## Published Properties [#published-properties] ### Signal Data [#signal-data] | Property | Type | Description | | ----------------- | --------------------------------- | ---------------------------------------------- | | `signals` | `[String: [BioSample]]` | All samples grouped by signal name | | `signalsByDevice` | `[String: [String: [BioSample]]]` | Samples grouped by device ID, then signal name | ### Device State [#device-state] | Property | Type | Description | | ------------------------ | ------------------------------ | ------------------------------- | | `deviceNames` | `[String: String]` | Map of device ID → display name | | `deviceConnectionStates` | `[String: BioConnectionState]` | Connection state per device | | `deviceStreamStates` | `[String: BioStreamState]` | Stream state per device | | `deviceAutoStream` | `[String: Bool]` | Auto-stream enabled per device | | `deviceBattery` | `[String: Int]` | Battery percentage per device | ### Session State [#session-state] | Property | Type | Description | | ----------------------------- | ----------------- | --------------------------------------- | | `currentSessionId` | `String?` | Active backend session ID | | `conflictingSessionId` | `String?` | Session ID from a 409 conflict | | `conflictingSessionStartedAt` | `Date?` | When the conflicting session started | | `sessionError` | `String?` | Current session error message | | `sessionDevices` | `[SessionDevice]` | Devices enrolled in the current session | ## Convenience Accessors [#convenience-accessors] ```swift // Get samples for a specific signal public func samples(for signalName: String) -> [BioSample] // Get samples for a signal from a specific device public func samples(for signalName: String, deviceId: String) -> [BioSample] // List all signal names that have data public var availableSignals: [String] { get } // List all device IDs that have sent data public var availableDevices: [String] { get } ``` ## Chart Data [#chart-data] Pre-formatted data for BioUI chart components: ```swift public var ecg: [TimeSeries] { get } public var ppg: [TimeSeries] { get } public var eda: [TimeSeries] { get } public var temp: [TimeSeries] { get } ``` **Example with BioUI:** ```swift import BioUI struct LiveChartsView: View { @ObservedObject var live: BioLiveStore var body: some View { VStack { BioECGChart(data: live.ecg) BioPPGChart(data: live.ppg) } } } ``` ## Background Memory Management [#background-memory-management] Call these methods from your `SceneDelegate` or SwiftUI lifecycle to reduce memory usage when the app is backgrounded: ```swift // Trim in-memory sample buffers for background public func enterBackgroundMode() // Restore full sample buffering public func enterForegroundMode() ``` ## Data Ingestion [#data-ingestion] ```swift // Ingest a sample into the store (called automatically by the SDK) public func ingest(_ sample: BioSample, deviceId: String) ``` ## Device Management [#device-management] ```swift // Register a device name public func noteDevice(deviceId: String, name: String) // Update connection/stream state for a device public func updateDeviceState( deviceId: String, connection: BioConnectionState, stream: BioStreamState ) // Update battery level public func updateDeviceBattery(deviceId: String, battery: Int?) // Update auto-stream setting public func updateDeviceAutoStream(deviceId: String, enabled: Bool) // Mark a device as disconnected public func noteDeviceDisconnected(deviceId: String) // Restore persisted state (e.g., after app relaunch) public func restoreState( deviceNames: [String: String], deviceConnectionStates: [String: String], deviceStreamStates: [String: String], deviceBattery: [String: Int], deviceAutoStream: [String: Bool] ) ``` ## Session Updates [#session-updates] ```swift // Update the current session ID and devices public func updateSession(id: String?, devices: [SessionDevice] = []) // Set conflict state public func updateSessionConflict(activeId: String, startedAt: Date?) // Clear conflict state public func clearSessionConflict() // Set or clear a session error message public func updateSessionError(_ message: String?) // Reset all state (clears signals, device states, session info) public func reset() ``` # Notifications (/docs/biosdk/api/notifications) The SDK provides a real-time notification system for receiving alerts, updates, and actionable messages from the backend. ## BioNotificationsStore [#bionotificationsstore] Observable store for managing notifications. Access via `sdk.notifications`. ```swift public final class BioNotificationsStore: ObservableObject ``` ### Published Properties [#published-properties] | Property | Type | Description | | ----------------- | ----------------------------- | ------------------------------------- | | `notifications` | `[BioNotification]` | All received notifications | | `connectionState` | `NotificationConnectionState` | Current connection status | | `unreadCount` | `Int` | Count of unacknowledged notifications | ### Events [#events] ```swift // Combine publisher for notification events public var events: AnyPublisher { get } ``` ### Connection Management [#connection-management] ```swift // Connect to the notification service public func connect() // Disconnect from the notification service public func disconnect() // Retry connection immediately public func retryNow() ``` ### Notification Actions [#notification-actions] ```swift // Mark a notification as acknowledged public func acknowledge(notificationId: String) // Dismiss a notification public func dismiss(notificationId: String) // Clear all notifications public func clearAll() ``` ### FCM Token Management [#fcm-token-management] For integrating with Firebase Cloud Messaging: ```swift // Register an FCM token for push notifications public func registerFcmToken( token: String, deviceId: String, appVersion: String? = nil ) // Unregister an FCM token public func unregisterFcmToken(deviceId: String) // Handle an incoming FCM notification by ID public func handleFcmNotification(notificationId: String) ``` **Example:** ```swift // In your AppDelegate or notification handler func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { if let notificationId = userInfo["bio_notification_id"] as? String { sdk.notifications.handleFcmNotification(notificationId: notificationId) } } ``` *** ## BioNotification [#bionotification] A notification received from the backend. ```swift public struct BioNotification: Equatable, Codable { public let id: String public let notificationType: String public let priority: String public let title: String public let bodyPreview: String? public let body: String public let actionType: String? public let actionUrl: String? public let payload: [String: AnyCodableValue] public let createdAt: Date public let expiresAt: Date? public let episodeId: String? } ``` | Property | Type | Description | | ------------------ | --------------------------- | ------------------------------------------- | | `id` | `String` | Unique notification identifier | | `notificationType` | `String` | Type category (e.g., `"alert"`, `"info"`) | | `priority` | `String` | Priority level (e.g., `"high"`, `"normal"`) | | `title` | `String` | Display title | | `bodyPreview` | `String?` | Short preview text | | `body` | `String` | Full notification body | | `actionType` | `String?` | Action type (e.g., `"url"`, `"deep_link"`) | | `actionUrl` | `String?` | Action URL or deep link | | `payload` | `[String: AnyCodableValue]` | Arbitrary key-value metadata | | `createdAt` | `Date` | When the notification was created | | `expiresAt` | `Date?` | Expiration date (nil = no expiry) | | `episodeId` | `String?` | Associated episode/session ID | *** ## NotificationConnectionState [#notificationconnectionstate] Connection state for the notification service. ```swift public enum NotificationConnectionState: Equatable { case disconnected case connecting case connected case waitingToReconnect(delay: TimeInterval, attempt: Int, maxAttempts: Int) case failed(reason: String) } ``` | State | Description | | -------------------------- | ------------------------------------- | | `.disconnected` | Not connected | | `.connecting` | Connection in progress | | `.connected` | Connected and receiving notifications | | `.waitingToReconnect(...)` | Waiting before a retry attempt | | `.failed(reason:)` | Connection permanently failed | *** ## BioNotificationEvent [#bionotificationevent] Events emitted by the notification system. ```swift public enum BioNotificationEvent: Equatable { case received(BioNotification) case acknowledged(notificationId: String) case dismissed(notificationId: String) case connectionStateChanged(NotificationConnectionState) } ``` *** ## AnyCodableValue [#anycodablevalue] A type-safe wrapper for heterogeneous JSON values, used in notification payloads. ```swift public enum AnyCodableValue: Equatable, Codable { case string(String) case int(Int) case double(Double) case bool(Bool) case array([AnyCodableValue]) case dictionary([String: AnyCodableValue]) case null } ``` **Example — reading a notification payload:** ```swift if case .string(let level) = notification.payload["severity"] { print("Severity: \(level)") } ``` # OAuth (/docs/biosdk/api/oauth) The SDK supports connecting third-party data providers via OAuth 2.0 flows using `ASWebAuthenticationSession`. ## Setup [#setup] Configure OAuth before use: ```swift sdk.configureOAuth(callbackURLScheme: "myapp") ``` Then connect providers through `BioSDKClient`: ```swift try await sdk.connectOAuthProvider( "provider-slug", xuserId: userId, presentationContextProvider: self ) ``` *** ## OAuthManager [#oauthmanager] Manages OAuth provider connections. Access via `sdk.oauth` (available after calling `configureOAuth`). ```swift @MainActor public final class OAuthManager: NSObject, ObservableObject ``` ### Properties [#properties] | Property | Type | Description | | -------------------- | --------------------------------- | -------------------------------------------------------- | | `callbackURLScheme` | `String` | URL scheme for OAuth callbacks | | `connectionStatuses` | `[String: OAuthConnectionStatus]` | Status per provider slug (`@Published`) | | `availableProviders` | `[OAuthProviderConfig]` | Providers configured for the organization (`@Published`) | ### Events [#events] ```swift public var events: AnyPublisher { get } ``` ### Connection Management [#connection-management] ```swift // Start an OAuth flow for a provider public func connectProvider( _ provider: String, xuserId: String, presentationContextProvider: ASWebAuthenticationPresentationContextProviding ) async throws // Disconnect a provider public func disconnectProvider(_ provider: String) // Check if a provider is connected public func isConnected(_ provider: String) -> Bool ``` ### Status Refresh [#status-refresh] ```swift // Refresh connection statuses from the backend public func refreshConnectionStatuses( organizationId: Int, xuserId: String ) async throws // Refresh the list of available providers public func refreshAvailableProviders() async throws ``` *** ## OAuthProvider [#oauthprovider] Represents a third-party data provider. ```swift public struct OAuthProvider: Equatable, Hashable { public let slug: String public let displayName: String public init(slug: String, displayName: String) } ``` ### Built-in Providers [#built-in-providers] Pre-configured OAuth provider integrations are available as part of the AnyBio Enterprise plan. [Contact us](mailto:sales@anybio.io) to learn more about enabling third-party data provider connections for your organization. *** ## OAuthConnectionStatus [#oauthconnectionstatus] Connection state for a provider. ```swift public enum OAuthConnectionStatus: Equatable { case notConnected case connecting case connected case failed(Error) } ``` *** ## OAuthEvent [#oauthevent] Events emitted during OAuth flows. ```swift public enum OAuthEvent: Equatable { case connectionStarted(provider: String) case connectionSucceeded(provider: String) case connectionFailed(provider: String, error: String) case connectionCancelled(provider: String) } ``` **Example:** ```swift sdk.oauth?.events.sink { event in switch event { case .connectionSucceeded(let provider): print("\(provider) connected successfully") case .connectionFailed(let provider, let error): print("\(provider) failed: \(error)") case .connectionCancelled(let provider): print("\(provider) cancelled by user") default: break } } ``` *** ## OAuthProviderConfig [#oauthproviderconfig] Server-side provider configuration returned from the bootstrap API. ```swift public struct OAuthProviderConfig: Codable, Equatable, Identifiable { public let id: Int public let organizationId: Int public let providerConfigId: Int public let clientId: String public let enabled: Bool public let providerSlug: String public let providerName: String public var asProvider: OAuthProvider { get } } ``` *** ## OAuthToken [#oauthtoken] Token information for a connected provider. ```swift public struct OAuthToken: Codable, Equatable { public let id: Int public let oauthProviderId: Int public let xuserId: String public let tokenType: String? public let expiresAt: String? public let scope: String? public let providerUserId: String? public let providerUsername: String? public let isActive: Bool public let createdAt: String public let updatedAt: String public let lastSyncAt: String? public let providerSlug: String public let providerName: String } ``` *** ## OAuthError [#oautherror] Errors that can occur during OAuth operations. ```swift public enum OAuthError: Error, LocalizedError { case invalidURL case apiError(String) case authenticationSessionFailed(Error) case userCancelled case invalidResponse case missingAuthorizationURL case unauthorized case providerNotFound(String) case serverError(Int) } ``` | Case | Description | | ------------------------------------- | ----------------------------------- | | `.invalidURL` | Malformed OAuth URL | | `.apiError(String)` | Backend API returned an error | | `.authenticationSessionFailed(Error)` | `ASWebAuthenticationSession` failed | | `.userCancelled` | User dismissed the auth dialog | | `.invalidResponse` | Unexpected response format | | `.missingAuthorizationURL` | No authorization URL returned | | `.unauthorized` | Authentication required | | `.providerNotFound(String)` | Provider slug not recognized | | `.serverError(Int)` | Backend returned an HTTP error | # BioSDKClient (/docs/biosdk/api/sdk-client) The primary entry point for all BioSDK operations. Manages device scanning, connections, streaming sessions, image uploads, and more. ```swift public final class BioSDKClient ``` ## SDK Version [#sdk-version] ```swift public static let sdkVersion: String ``` ## Initialization [#initialization] ### Configuration [#configuration] ```swift public struct Configuration: Equatable { public let organizationKey: String? public let projectKey: String public init( organizationKey: String? = nil, projectKey: String ) } ``` ### Creating an Instance [#creating-an-instance] ```swift // Synchronous convenience initializer public convenience init(configuration: Configuration) // Async initializer — performs bootstrap (loads supported devices, org config) public static func initialize(configuration: Configuration) async throws -> BioSDKClient // Restore a previously persisted session (e.g., after background wake or app relaunch) public static func restoreIfNeeded(isBackground: Bool = true) throws -> BioSDKClient? ``` **Example:** ```swift let sdk = try await BioSDKClient.initialize( configuration: BioSDKClient.Configuration(projectKey: "my-project") ) ``` ## Properties [#properties] | Property | Type | Description | | ---------------------- | ---------------------------------------- | --------------------------------------------------- | | `ble` | `BioBLEManager` | Direct access to the BLE manager | | `live` | [`BioLiveStore`](live-store) | Observable data store for real-time UI | | `notifications` | [`BioNotificationsStore`](notifications) | Push notification store | | `session` | [`SessionManager`](session-management) | Session state manager | | `events` | `PassthroughSubject` | Combine publisher for all SDK events | | `sessionContext` | `BioSessionContext` | Current session context (`@Published`) | | `oauth` | [`OAuthManager?`](oauth) | OAuth manager (nil until configured) | | `currentSessionId` | `String?` | Active backend session ID | | `conflictingSessionId` | `String?` | Session ID from a 409 conflict | | `isScanning` | `Bool` | Whether BLE scanning is active | | `discoveredDevices` | `[BioDevice]` | Devices found during the current scan | | `clientInfo` | `[String: String]` | SDK version and device metadata sent with API calls | ## Scanning & Connecting [#scanning--connecting] ```swift // Start scanning for BLE devices public func startScan() // Stop scanning public func stopScan() // Connect to a discovered device public func connect(_ device: BioDevice) // Disconnect a specific device public func disconnect(_ device: BioDevice) // Disconnect all connected devices public func disconnectAll() ``` ## Streaming Control [#streaming-control] ```swift // Start streaming for a specific device (by BLE UUID) public func startStreaming(deviceId: UUID) // Stop streaming for a specific device public func stopStreaming(deviceId: UUID) // Enable/disable auto-streaming on connect for a device public func setAutoStream(deviceId: UUID, enabled: Bool) // Query current connection + stream state for a device public func deviceSessionState(deviceId: UUID) -> (BioConnectionState, BioStreamState)? // Check if auto-stream is enabled for a device public func autoStreamEnabled(deviceId: UUID) -> Bool? ``` ## Session Management [#session-management] ```swift // Enable or disable session mode public func setSessionMode(enabled: Bool) // Start a backend session for a user (with completion handler) public func startStreaming( for xUser: String, completion: @escaping (Result) -> Void ) // Start a backend session (fire-and-forget) public func startStreaming(for xUser: String) // Start only the backend session (without triggering BLE streaming) public func startBackendSession( for xUser: String, completion: @escaping (Result) -> Void ) // Stop all streaming and end the backend session public func stopStreaming() // End the active session without stopping BLE connections public func endActiveSession() // Check if the backend has an active session for the current user public func checkForActiveSession() async throws -> Bool // Detect an orphaned session from a previous app launch public func detectOrphanedSession() async throws -> OrphanedSessionInfo? // Resume an orphaned session public func resumeOrphanedSession() async throws -> Bool // End an orphaned session public func endOrphanedSession() ``` ### Conflict Resolution [#conflict-resolution] When a session start returns HTTP 409 (an active session already exists), the SDK emits a `.session(.conflict(...))` event. Use these methods to resolve: ```swift // Resume the existing conflicting session public func resumeConflictingSession() // End the conflicting session and start a new one public func endConflictingAndStartNew() // Cancel the pending conflict without action public func cancelPendingConflict() ``` ### SessionStartDisposition [#sessionstartdisposition] Returned as the error type when starting a session: ```swift public enum SessionStartDisposition: Error, Equatable { case new // New session created case resumed // Existing session resumed case conflict(activeId: String, startedAt: Date?) // 409 conflict } ``` ## Streaming Profiles [#streaming-profiles] ```swift // Activate a named streaming profile on a device public func activateStreamingProfile(deviceId: UUID, profileName: String) async throws // Deactivate the current streaming profile public func deactivateStreamingProfile(deviceId: UUID, executeStop: Bool = true) // List available streaming profiles for a device public func getStreamingProfiles(deviceId: UUID) -> [StreamingProfileUI] // Get the currently active profile name public func activeStreamingProfile(deviceId: UUID) -> String? ``` ## Device Operations [#device-operations] ```swift // Get available operations for a device public func getDeviceOperations(deviceId: UUID) -> [DeviceOperationUI] // Execute a named operation (e.g., "start_ecg", "calibrate") public func executeOperation(deviceId: UUID, operationName: String) async throws ``` ## Buffer & Upload Monitoring [#buffer--upload-monitoring] Advanced buffer and upload monitoring APIs are available as part of the AnyBio Enterprise plan. [Contact us](mailto:sales@anybio.io) to learn more about enabling these capabilities for your organization. ## Image Upload [#image-upload] ```swift // Upload a UIImage for a user public func uploadImage(_ image: UIImage, userId: UUID) async throws // Upload raw image data with a MIME type public func uploadImageData( _ data: Data, mimeType: String, userId: UUID, deviceId: String? = nil ) async throws ``` See the [Images API](images) reference for query and response types. ## User Management [#user-management] ```swift // Create a new external user public func createXUser( identifier: String, name: String ) async throws -> XUserHTTPClient.XUserResponse // Get or create an external user (idempotent) public func getOrCreateXUser( identifier: String, name: String ) async throws -> XUserHTTPClient.XUserResponse // Look up an existing external user public func getXUser(identifier: String) async throws -> XUserHTTPClient.XUserResponse ``` See the [Users API](users) reference for the `BioXUser` response type. ## OAuth [#oauth] ```swift // Configure OAuth with a callback URL scheme public func configureOAuth( callbackURLScheme: String, userTokenProvider: (() async -> String?)? = nil ) // Connect an OAuth provider (opens ASWebAuthenticationSession) public func connectOAuthProvider( _ provider: String, xuserId: String, presentationContextProvider: ASWebAuthenticationPresentationContextProviding ) async throws // Check if a provider is connected public func isOAuthProviderConnected(_ provider: String) -> Bool // Disconnect a provider public func disconnectOAuthProvider(_ provider: String) ``` See the [OAuth API](oauth) reference for full type details. ## Diagnostic Logging [#diagnostic-logging] Diagnostic logging and BLE packet capture APIs are available as part of the AnyBio Enterprise plan. [Contact us](mailto:sales@anybio.io) to learn more about enabling these capabilities for your organization. # Session Management (/docs/biosdk/api/session-management) The SDK manages backend sessions that track which devices are streaming data for a given user. Sessions persist across app backgrounding and device reconnections. ## SessionManager [#sessionmanager] Thread-safe session state manager. Access via `sdk.session`. ```swift public final class SessionManager: @unchecked Sendable ``` ### Properties [#properties] | Property | Type | Description | | ---------------- | ----------------------------------------- | ------------------------------------------ | | `state` | `SessionState` | Current session state (read-only snapshot) | | `statePublisher` | `PassthroughSubject` | Publishes state changes | ### Reading State [#reading-state] ```swift // Get a consistent snapshot of session state public func withState(_ operation: (SessionState) -> T) -> T ``` **Example:** ```swift let isActive = sdk.session.withState { $0.isActive } let deviceCount = sdk.session.withState { $0.connectedDeviceCount } ``` ### Lifecycle Methods [#lifecycle-methods] ```swift public func beginStartingSession(xuserId: String) public func sessionStarted( sessionId: String, devices: [(bleId: String, ingestDeviceId: String, name: String, deviceProfileId: String?, make: String?, model: String?)] ) public func sessionResumed( sessionId: String, devices: [(bleId: String, ingestDeviceId: String, name: String, deviceProfileId: String?, make: String?, model: String?)] ) public func beginEndingSession() public func sessionEnded() public func sessionOperationFailed() ``` ### Device Updates [#device-updates] ```swift public func deviceConnected(_ bleId: String, name: String) public func deviceDisconnected(_ bleId: String) public func deviceStreamStateChanged(_ bleId: String, state: BioStreamState) public func deviceBatteryUpdated(_ bleId: String, battery: Int) public func deviceStreamingProfileChanged(_ bleId: String, profileName: String?) ``` ### Configuration [#configuration] ```swift public func updateConfiguration( organizationKey: String?, projectKey: String?, ingestBaseURL: URL? ) ``` *** ## SessionState [#sessionstate] The complete state of the current session, including all enrolled devices. ```swift public struct SessionState: Codable, Sendable { public var lifecycle: SessionLifecycle public var sessionId: String? public var xuserId: String? public var startedAt: Date? public var devices: [String: DeviceSessionState] public var organizationKey: String? public var projectKey: String? public var ingestBaseURL: URL? public var lastPersistedAt: Date? public var lastProcessId: Int32? } ``` ### Computed Properties [#computed-properties] | Property | Type | Description | | ---------------------- | ------------------ | --------------------------------------------- | | `isActive` | `Bool` | Whether a session is currently active | | `shouldAutoReconnect` | `Bool` | Whether the SDK should auto-reconnect devices | | `connectedDeviceCount` | `Int` | Number of connected devices | | `streamingDeviceCount` | `Int` | Number of actively streaming devices | | `deviceIds` | `[String]` | All enrolled device IDs | | `deviceMapping` | `[String: String]` | BLE ID → ingest device ID mapping | ### Methods [#methods] ```swift // Reset all state to defaults public mutating func reset() ``` *** ## SessionLifecycle [#sessionlifecycle] The lifecycle stage of a session. ```swift public enum SessionLifecycle: String, Codable, Sendable { case idle case starting case active case paused case ending } ``` | State | Description | | ----------- | ----------------------------------------------- | | `.idle` | No active session | | `.starting` | Session start request in progress | | `.active` | Session is active and streaming | | `.paused` | Session paused (e.g., all devices disconnected) | | `.ending` | Session end request in progress | ```swift // Whether the session is in a live state (active or paused) public var isLive: Bool { get } ``` *** ## DeviceSessionState [#devicesessionstate] Per-device state within a session. ```swift public struct DeviceSessionState: Codable, Sendable { public var name: String public var ingestDeviceId: String? public var connectionState: BioConnectionState public var streamState: BioStreamState public var battery: Int? public var deviceProfileId: String? public var make: String? public var model: String? public var streamingProfileName: String? } ``` ### Computed Properties [#computed-properties-1] ```swift public var isConnected: Bool { get } // connectionState == .ready public var isStreaming: Bool { get } // streamState == .streaming ``` ### Initializer [#initializer] ```swift public init( name: String, ingestDeviceId: String? = nil, connectionState: BioConnectionState = .disconnected, streamState: BioStreamState = .idle, battery: Int? = nil, deviceProfileId: String? = nil, make: String? = nil, model: String? = nil, streamingProfileName: String? = nil ) ``` *** ## Buffer & Upload Types [#buffer--upload-types] Advanced buffer and upload monitoring types are available as part of the AnyBio Enterprise plan. [Contact us](mailto:sales@anybio.io) to learn more about enabling these capabilities for your organization. # Users API (/docs/biosdk/api/users) The SDK manages external users ("XUsers") that represent the end users of your application. XUsers are created in the AnyBio backend and referenced when starting sessions. ## Managing Users via BioSDKClient [#managing-users-via-biosdkclient] ```swift // Create a new user let user = try await sdk.createXUser( identifier: "user-123", name: "Jane Doe" ) // Get or create (idempotent) let user = try await sdk.getOrCreateXUser( identifier: "user-123", name: "Jane Doe" ) // Look up an existing user let user = try await sdk.getXUser(identifier: "user-123") ``` *** ## XUserHTTPClient [#xuserhttpclient] Low-level HTTP client for user operations. ```swift public final class XUserHTTPClient { public typealias XUserResponse = BioXUser public func createXUser(_ payload: CreateXUserPayload) async throws -> XUserResponse public func getOrCreateXUser(_ payload: CreateXUserPayload) async throws -> XUserResponse public func getXUser(identifier: String, projectKey: String) async throws -> XUserResponse } ``` ### CreateXUserPayload [#createxuserpayload] ```swift public struct CreateXUserPayload: Codable { public let identifier: String public let name: String public let project_key: String public init(identifier: String, name: String, project_key: String) } ``` ### XUserHTTPClientError [#xuserhttpclienterror] ```swift public enum XUserHTTPClientError: Error { case invalidHTTPResponse case badStatus(Int) case decodingFailed case forbidden case serverError(Int) case conflict case missingApiKey } ``` *** ## BioXUser [#bioxuser] The user model returned from the backend. ```swift public struct BioXUser: Codable, Identifiable, Sendable, Equatable, Hashable { public let id: String public let organizationId: Int public let identifier: String public let name: String public let genderTypeId: Int? public let contactPoints: [ContactPoint] public let birthDate: Date? public let address: Address? public let telecom: Telecom? public let active: Bool public let projectIds: [Int] public let totalSessions: Int public let numDevices: Int public let dataSentMb: Double } ``` | Property | Type | Description | | ---------------- | -------- | ---------------------------------- | | `id` | `String` | Backend user ID | | `organizationId` | `Int` | Organization the user belongs to | | `identifier` | `String` | Your application's user identifier | | `name` | `String` | Display name | | `active` | `Bool` | Whether the user is active | | `projectIds` | `[Int]` | Projects the user is enrolled in | | `totalSessions` | `Int` | Total recording sessions | | `numDevices` | `Int` | Number of registered devices | | `dataSentMb` | `Double` | Total data uploaded in MB | ### Convenience Properties [#convenience-properties] ```swift public var hasSessions: Bool { get } // totalSessions > 0 public var hasDevices: Bool { get } // numDevices > 0 public var dataSentFormatted: String { get } // Human-readable data size ``` ### Nested Types [#nested-types] #### ContactPoint [#contactpoint] ```swift public struct ContactPoint: Codable, Sendable { public let system: String? // e.g., "email", "phone" public let value: String? // e.g., "jane@example.com" public let use: String? // e.g., "home", "work" } ``` #### Address [#address] ```swift public struct Address: Codable, Sendable { public let line: [String]? public let city: String? public let state: String? public let postalCode: String? public let country: String? } ``` #### Telecom [#telecom] ```swift public struct Telecom: Codable, Sendable { public let system: String? public let value: String? } ``` *** ## Content Library [#content-library] The SDK also provides access to content library entries associated with your project. ### ContentLibraryClient [#contentlibraryclient] ```swift public final class ContentLibraryClient { public func list(projectId: Int? = nil) async throws -> [BioContentLibraryEntry] public func get(id: Int) async throws -> BioContentLibraryEntry } ``` ### BioContentLibraryEntry [#biocontentlibraryentry] ```swift public struct BioContentLibraryEntry: Codable, Identifiable, Equatable, Sendable { public let id: Int public let organizationId: Int public let projectId: Int public let title: String public let content: String? public let author: String? public let createdAt: Date public let updatedAt: Date } ``` ### ContentLibraryError [#contentlibraryerror] ```swift public enum ContentLibraryError: Error, LocalizedError { case invalidHTTPResponse case badStatus(Int) case decodingFailed(String) case forbidden case notFound case serverError(Int) } ``` # Apple Health Integration (/docs/biosdk/guides/apple-health) Sync Apple Health (HealthKit) data to AnyBio with two lines of code. BioSDK handles authorization, querying, mapping, and batch upload automatically. ## Quick Start [#quick-start] ```swift // Enable HealthKit (requests authorization for all supported types) try await sdk.enableHealthKit() // Sync recent data to AnyBio let result = try await sdk.syncHealthKit() print("Synced \(result.created) observations") // Optional: enable automatic background sync sdk.enableHealthKitBackgroundSync(frequency: .hourly) ``` ## Architecture [#architecture] ``` iOS App │ │ 1. sdk.enableHealthKit() │ → Requests HealthKit read authorization │ │ 2. sdk.syncHealthKit() │ → Queries HealthKit for new samples since last sync │ → Maps HK types → AnyBio biosignal slugs + LOINC codes │ HKQuantityType.heartRate → biosignal_slug: "Heart Rate" │ │ 4. Batch POST to AnyBio │ POST /api/v1/observations/batch │ Authorization: Bearer org_* │ source_slug: "apple_health" │ │ 5. Backend upserts, links to episodes, fires policies ``` Data stays on-device until explicitly synced. No server-to-server OAuth — Apple Health is read locally via HealthKit. ## HealthKit → AnyBio Mapping [#healthkit--anybio-mapping] | HealthKit Type | HK Identifier | AnyBio Biosignal | LOINC | Unit | | -------------------------- | -------------------------------------------------------- | ---------------------- | ------- | ----------- | | Heart Rate | `HKQuantityTypeIdentifier.heartRate` | Heart Rate | 8867-4 | bpm | | Heart Rate Variability | `HKQuantityTypeIdentifier.heartRateVariabilitySDNN` | Heart Rate Variability | 80404-7 | ms | | Resting Heart Rate | `HKQuantityTypeIdentifier.restingHeartRate` | Heart Rate | 8867-4 | bpm | | Walking Heart Rate Avg | `HKQuantityTypeIdentifier.walkingHeartRateAverage` | Heart Rate | 8867-4 | bpm | | Oxygen Saturation | `HKQuantityTypeIdentifier.oxygenSaturation` | Oxygen Saturation | 59408-5 | % | | Respiratory Rate | `HKQuantityTypeIdentifier.respiratoryRate` | Respiratory Rate | 9279-1 | breaths/min | | Body Temperature | `HKQuantityTypeIdentifier.bodyTemperature` | Body Temperature | 8310-5 | degC | | Blood Pressure (systolic) | `HKQuantityTypeIdentifier.bloodPressureSystolic` | Blood Pressure | 8480-6 | mmHg | | Blood Pressure (diastolic) | `HKQuantityTypeIdentifier.bloodPressureDiastolic` | Blood Pressure | 8462-4 | mmHg | | Blood Glucose | `HKQuantityTypeIdentifier.bloodGlucose` | Glucose (CGM) | 99504-3 | mg/dL | | Step Count | `HKQuantityTypeIdentifier.stepCount` | Step Count | 41950-7 | steps | | Active Energy Burned | `HKQuantityTypeIdentifier.activeEnergyBurned` | Calories Burned | 41979-6 | kcal | | Body Mass | `HKQuantityTypeIdentifier.bodyMass` | Body Weight | 29463-7 | kg | | Height | `HKQuantityTypeIdentifier.height` | Body Height | 8302-2 | cm | | BMI | `HKQuantityTypeIdentifier.bodyMassIndex` | Body Mass Index | 39156-5 | kg/m2 | | VO2 Max | `HKQuantityTypeIdentifier.vo2Max` | VO2max | 94122-9 | mL/kg/min | | Sleep Analysis | `HKCategoryTypeIdentifier.sleepAnalysis` | Sleep Metrics | 93832-4 | — | | Skin Temperature | `HKQuantityTypeIdentifier.appleSleepingWristTemperature` | Skin Temperature | 8310-5 | degC | | Walking Steadiness | `HKQuantityTypeIdentifier.appleWalkingSteadiness` | Gait Analysis | 95809-3 | % | | Falls | `HKCategoryTypeIdentifier.fallEvent` | Falls | 41952-1 | — | ## Batch Ingest Endpoint [#batch-ingest-endpoint] ``` POST /api/v1/observations/batch Authorization: Bearer org_your_key Content-Type: application/json ``` ### Request Body [#request-body] ```json { "observations": [ { "xuser_id": "550e8400-e29b-41d4-a716-446655440000", "biosignal_slug": "Heart Rate", "effective_datetime": "2026-04-04T14:30:00Z", "value_quantity": 72, "value_unit": "bpm", "source_slug": "apple_health", "metadata": { "hk_source": "com.apple.health", "hk_device": "Apple Watch Series 9" } }, { "xuser_id": "550e8400-e29b-41d4-a716-446655440000", "biosignal_slug": "Oxygen Saturation", "effective_datetime": "2026-04-04T14:30:00Z", "value_quantity": 98, "value_unit": "%", "source_slug": "apple_health" } ] } ``` ### Response [#response] ```json { "created": 2, "errors": [] } ``` If some items fail validation: ```json { "created": 1, "errors": [ { "index": 1, "error": "Unknown biosignal_slug: invalid_type" } ] } ``` **Status codes:** * `201` — All observations created successfully * `207` — Partial success (some created, some failed — check `errors` array) * `413` — Batch too large (max 1000 per request) * `403` — Unauthorized xuser\_id ## Example: Swift HealthKit Sync [#example-swift-healthkit-sync] ```swift import HealthKit let store = HKHealthStore() // 1. Request authorization let readTypes: Set = [ HKQuantityType(.heartRate), HKQuantityType(.oxygenSaturation), HKQuantityType(.stepCount), ] try await store.requestAuthorization(toShare: [], read: readTypes) // 2. Query recent heart rate samples let heartRateType = HKQuantityType(.heartRate) let since = Calendar.current.date(byAdding: .hour, value: -1, to: Date())! let predicate = HKQuery.predicateForSamples(withStart: since, end: Date()) let samples = try await withCheckedThrowingContinuation { (cont: CheckedContinuation<[HKQuantitySample], Error>) in let query = HKSampleQuery(sampleType: heartRateType, predicate: predicate, limit: 100, sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)]) { _, results, error in if let error { cont.resume(throwing: error) } else { cont.resume(returning: results as? [HKQuantitySample] ?? []) } } store.execute(query) } // 3. Map to AnyBio observations let observations = samples.map { sample in [ "xuser_id": xuserId, "biosignal_slug": "Heart Rate", "effective_datetime": ISO8601DateFormatter().string(from: sample.startDate), "value_quantity": sample.quantity.doubleValue(for: .count().unitDivided(by: .minute())), "value_unit": "bpm", "source_slug": "apple_health", "metadata": ["hk_device": sample.device?.name ?? "unknown"] ] as [String: Any] } // 4. POST batch to AnyBio var request = URLRequest(url: URL(string: "https://api.anybio.io/api/v1/observations/batch")!) request.httpMethod = "POST" request.setValue("Bearer \(orgKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: ["observations": observations]) let (data, response) = try await URLSession.shared.data(for: request) ``` ## Background Sync [#background-sync] For continuous sync, use HealthKit's background delivery: ```swift // Enable background delivery for heart rate store.enableBackgroundDelivery(for: heartRateType, frequency: .hourly) { success, error in if success { print("Background delivery enabled for heart rate") } } // Register an observer query let observerQuery = HKObserverQuery(sampleType: heartRateType, predicate: nil) { _, completionHandler, error in // New heart rate data available — trigger sync Task { await syncRecentSamples() completionHandler() } } store.execute(observerQuery) ``` ## Deduplication [#deduplication] The batch endpoint handles duplicates automatically. The unique key is: ``` (xuser_id, biosignal_id, source_id, effective_datetime, resolution, loinc_code) ``` If you sync the same HealthKit sample twice, the second insert is a no-op (or updates if values changed). Safe to re-sync overlapping time windows. ## Security Notes [#security-notes] * HealthKit data never leaves the device unless the user explicitly authorizes your app * Use `org_*` key for the batch endpoint (server-side or secure SDK storage) * Each observation is scoped to an xuser — cross-user access is impossible * Include `hk_source` and `hk_device` in metadata for provenance tracking ## Next Steps [#next-steps] * [Streaming](/docs/biosdk/guides/streaming) — Real-time BLE streaming (complementary to HealthKit) * [Session Management](/docs/biosdk/guides/session-management) — Backend session lifecycle * [Events](/docs/biosdk/guides/events) — Subscribe to BioSDK events # Background Mode (/docs/biosdk/guides/background-mode) Background streaming, session recovery, and overnight monitoring capabilities are available as part of the AnyBio Enterprise plan. [Contact us](mailto:sales@anybio.io) to learn more about enabling background mode for your organization. Background mode enables continuous biosignal streaming while your app is in the background — essential for overnight monitoring, extended research sessions, and clinical data capture. Enterprise customers receive full configuration support including: * BLE background state restoration * Automatic session recovery after app termination * Idle timer management during active streaming * Background upload scheduling # Configuration (/docs/biosdk/guides/configuration) ## SDK Initialization [#sdk-initialization] BioSDK is initialized asynchronously. The `initialize` call loads supported devices from the AnyBio platform and prepares the BLE subsystem. ```swift import BioSDK let config = BioSDKClient.Configuration( organizationKey: "org_your_key", projectKey: "proj_your_key" ) let sdk = try await BioSDKClient.initialize(configuration: config) ``` ## Configuration Options [#configuration-options] ### `organizationKey` [#organizationkey] Your organization identifier (prefixed with `org_`). Provided when you register with AnyBio. ### `projectKey` [#projectkey] Your project identifier (prefixed with `proj_`). Each project groups sessions, devices, and data under a single namespace. ## Environment.plist [#environmentplist] Create an `Environment.plist` file in your app bundle to configure environment-specific settings: ```xml BIO_INGEST_BASE_URL https://api.anybio.io DEVELOPER_KEY org_your_key PROJECT_KEY proj_your_key ENVIRONMENT production ``` ## OAuth Configuration (Optional) [#oauth-configuration-optional] If your app uses AnyBio's OAuth provider integration: ```swift sdk.configureOAuth( callbackURLScheme: "yourapp", userTokenProvider: { // Return the current user's access token from your keychain KeychainHelper.load(key: "accessToken") } ) ``` Register your callback URL scheme in `Info.plist`: ```xml CFBundleURLTypes CFBundleURLSchemes yourapp CFBundleURLName com.yourcompany.yourapp.oauth ``` ## Client Info [#client-info] The SDK automatically collects non-sensitive client metadata and sends it with session requests for diagnostics: * App name, version, and build number * OS name and version * Device model * SDK version Access this information at any time: ```swift let info = sdk.clientInfo // ["sdk_version": "2.0.0", "os_version": "17.2", "device_model": "iPhone 15 Pro", ...] ``` # Events (/docs/biosdk/guides/events) BioSDK uses a single Combine publisher to emit all SDK events. Subscribe to `sdk.events` to receive real-time updates about device lifecycle, biosignal data, connection state changes, and session activity. ```swift import Combine var cancellables = Set() sdk.events .sink { event in // Handle all event types } .store(in: &cancellables) ``` ## Event Types [#event-types] ### `BioEvent` [#bioevent] The top-level event enum: ```swift public enum BioEvent { case lifecycle(BioLifecycleEvent) case sample(deviceId: String, BioSample) case deviceState(device: BioDevice, connection: BioConnectionState, stream: BioStreamState) case session(BioSessionEvent) case notification(BioNotificationEvent) } ``` ### Lifecycle Events [#lifecycle-events] Emitted when devices are discovered, connected, or disconnected. ```swift public enum BioLifecycleEvent { case discovered(BioDevice) case connected(BioDevice) case disconnected(BioDevice, errorMessage: String?, errorCode: Int?, errorDomain: String?) } ``` ### Sample Events [#sample-events] Emitted for each biosignal data point received from a device. See the [Streaming guide](/docs/biosdk/guides/streaming) for details on signal types. ### Device State Events [#device-state-events] Emitted whenever a device's connection or streaming state changes. Each device has independent `(BioConnectionState, BioStreamState)` tracking. ### Session Events [#session-events] Emitted for session lifecycle changes on the AnyBio platform. ```swift public enum BioSessionEvent { case started(id: String, devices: [SessionDevice]) case resumed(id: String, devices: [SessionDevice]) case conflict(activeId: String, startedAt: Date?) case ended(id: String, ok: Bool) case endFailed(id: String, status: Int?) case recoveryAvailable(OrphanedSessionInfo) case forbidden case serverError(status: Int) case error(message: String) } ``` ### Notification Events [#notification-events] Emitted for push notification activity (if notifications are configured). ```swift public enum BioNotificationEvent { case received(BioNotification) case acknowledged(notificationId: String) case dismissed(notificationId: String) case connectionStateChanged(NotificationConnectionState) } ``` ## Filtering Events [#filtering-events] Use Combine operators to subscribe to specific event types: ```swift // Only heart rate samples sdk.events .compactMap { event -> Double? in guard case .sample(_, let sample) = event, sample.isHeartRate, let bpm = sample.scalarValue else { return nil } return bpm } .sink { bpm in print("HR: \(bpm) BPM") } .store(in: &cancellables) // Only connection state changes sdk.events .compactMap { event -> (BioDevice, BioConnectionState)? in guard case .deviceState(let device, let connection, _) = event else { return nil } return (device, connection) } .sink { device, state in print("\(device.name): \(state)") } .store(in: &cancellables) ``` # Image Ingest (/docs/biosdk/guides/image-ingest) BioSDK supports uploading images to the AnyBio platform alongside biosignal data. Images are associated with users and optionally with active sessions, enabling multimodal data capture — for example, photographing a skin condition while simultaneously recording EDA, or capturing wound images during a monitoring session. ## Uploading Images [#uploading-images] ### From a UIImage (iOS) [#from-a-uiimage-ios] The simplest way to upload an image on iOS: ```swift let image: UIImage = // from camera, photo picker, etc. let userId: UUID = // the authenticated xUser ID try await sdk.uploadImage(image, userId: userId) ``` This uses JPEG compression at 0.8 quality and automatically derives the device ID from the current device. ### From Raw Data (Cross-Platform) [#from-raw-data-cross-platform] For more control, or when working with non-UIImage sources: ```swift let imageData: Data = // PNG, JPEG, or HEIC bytes let userId: UUID = // the authenticated xUser ID try await sdk.uploadImageData( imageData, mimeType: "image/jpeg", userId: userId, deviceId: "optional-device-id" // nil = auto-generated ) ``` ### Supported Formats [#supported-formats] | MIME Type | Format | | ------------ | ---------------------------- | | `image/jpeg` | JPEG | | `image/png` | PNG | | `image/heic` | HEIC (Apple High Efficiency) | | `image/heif` | HEIF | ## Querying Images [#querying-images] Retrieve previously uploaded images using `ImageQueryClient`: ```swift let queryClient = ImageQueryClient( baseURL: sdk.state.resolvedIngestBaseURL, apiKey: sdk.state.organizationKey ?? "" ) ``` ### List Images [#list-images] ```swift let filter = ImageQueryClient.ImageQueryFilter( xuserId: "user-id", sessionId: "session-id", // optional: scope to a session dateRange: .last7Days, // optional: today, last7Days, last30Days, last90Days limit: 50, // optional: pagination offset: 0 // optional: pagination ) let images: [BioImage] = try await queryClient.listImages(filter: filter) for image in images { print("\(image.id): \(image.mimeType), \(image.sizeBytes) bytes") print(" Uploaded: \(image.uploadedAt)") if let classifications = image.classifications { for c in classifications { print(" Classification: \(c.classification) (\(c.confidence))") } } } ``` ### Get Image Metadata [#get-image-metadata] ```swift let image: BioImage = try await queryClient.getImage(id: "image-id") ``` ### Download Image File [#download-image-file] ```swift let data: Data = try await queryClient.downloadImage(id: "image-id") let uiImage = UIImage(data: data) ``` ## The BioImage Model [#the-bioimage-model] ```swift public struct BioImage: Codable, Identifiable { public let id: String public let xuserId: String public let sessionId: String? public let deviceId: String? public let capturedAt: Date? public let uploadedAt: Date public let mimeType: String public let sizeBytes: Int public let width: Int? public let height: Int? public let imageUrl: String? public let thumbnailUrl: String? public let classifications: [BioImageClassification]? } ``` ### Image Classifications [#image-classifications] When ML models are deployed on the AnyBio platform (via the [Model Registry](/docs/model-registry/overview)), uploaded images can be automatically classified. Results are attached to the `BioImage`: ```swift public struct BioImageClassification: Codable { public let id: String public let imageId: String public let classification: String // e.g., "rash", "healthy", "wound_stage_2" public let confidence: Double // 0.0 – 1.0 public let processingEngine: String? public let createdAt: Date } ``` ## Session Association [#session-association] Images uploaded during an active streaming session are automatically associated with that session via the `sessionId` field. This allows you to correlate images with the biosignal data captured at the same time — for example, viewing what ECG waveform was recording when a particular photo was taken. ## Error Handling [#error-handling] ```swift do { try await sdk.uploadImage(image, userId: userId) } catch ImageError.compressionFailed { // UIImage could not be compressed to the target format } catch ImageError.uploadFailed { // Upload request failed } catch ImageError.server(let status, let body) { // Server returned an error (e.g., 400, 413, 500) } catch ImageError.network(let underlying) { // Network connectivity issue } catch { print("Unexpected error: \(error)") } ``` ## Example: Photo Capture with Session Context [#example-photo-capture-with-session-context] A typical pattern — capture a photo while biosignal streaming is active: ```swift import SwiftUI import BioSDK struct CaptureView: View { let sdk: BioSDKClient let userId: UUID @State private var showCamera = false @State private var uploadStatus: String? var body: some View { VStack { Button("Capture Image") { showCamera = true } if let status = uploadStatus { Text(status) .font(.caption) .foregroundStyle(.secondary) } } .sheet(isPresented: $showCamera) { ImagePicker { image in Task { do { try await sdk.uploadImage(image, userId: userId) uploadStatus = "Uploaded successfully" } catch { uploadStatus = "Upload failed: \(error.localizedDescription)" } } } } } } ``` # Installation (/docs/biosdk/guides/installation) BioSDK is distributed as a pre-built binary XCFramework via Swift Package Manager. ## Swift Package Manager [#swift-package-manager] ### Xcode [#xcode] 1. Open your project in Xcode 2. **File > Add Package Dependencies...** 3. Enter the repository URL: ``` https://github.com/anybio/biosdk-ios-binary.git ``` 4. Select version rule: **Up to Next Major** from `2.0.0` 5. Add the **BioSDK** product to your app target ### Package.swift [#packageswift] ```swift dependencies: [ .package(url: "https://github.com/anybio/biosdk-ios-binary.git", from: "2.0.0"), ], targets: [ .target( name: "YourApp", dependencies: [ .product(name: "BioSDK", package: "biosdk-ios-binary"), ] ), ] ``` ## Info.plist Setup [#infoplist-setup] ### Bluetooth Permissions (Required) [#bluetooth-permissions-required] BioSDK uses CoreBluetooth to communicate with wearables. Add the Bluetooth usage description to your `Info.plist`: ```xml NSBluetoothAlwaysUsageDescription We use Bluetooth to connect to your wearable and stream health data. ``` ### Background Modes (Recommended) [#background-modes-recommended] For long-running biosignal capture that continues when your app is backgrounded, enable these background modes: ```xml UIBackgroundModes bluetooth-central processing remote-notification ``` Register the SDK's background task identifiers: ```xml BGTaskSchedulerPermittedIdentifiers io.anybio.upload io.anybio.session.maintenance ``` See the [Background Mode guide](/docs/biosdk/guides/background-mode) for more details. ## Requirements [#requirements] | Requirement | Version | | ----------- | ------- | | iOS | 16.0+ | | Swift | 5.9+ | | Xcode | 15.0+ | # Live Data Store (/docs/biosdk/guides/live-data-store) # BioLiveStore [#biolivestore] `BioLiveStore` is an `ObservableObject` that accumulates real-time biosignal data, device states, and session info into `@Published` properties. It's designed to drive SwiftUI views directly. ## Usage [#usage] Create a store and feed it events from the SDK: ```swift import BioSDK import Combine let store = BioLiveStore() var cancellables = Set() sdk.events .sink { event in switch event { case .sample(let deviceId, let sample): store.ingest(sample, deviceId: deviceId) case .deviceState(let device, let connection, let stream): store.updateDeviceState( deviceId: device.id.uuidString, connection: connection, stream: stream ) case .session(.started(let id, let devices)): store.updateSession(id: id, devices: devices) default: break } } .store(in: &cancellables) ``` ## Published Properties [#published-properties] ### Signal Data [#signal-data] ```swift // All signals by name (e.g., "ecg", "ppg", "hr") @Published var signals: [String: [BioSample]] // Signals organized by device @Published var signalsByDevice: [String: [String: [BioSample]]] // List of signal types currently being received var availableSignals: [String] ``` ### Device State [#device-state] ```swift // Device display names @Published var deviceNames: [String: String] // Per-device connection states @Published var deviceConnectionStates: [String: BioConnectionState] // Per-device streaming states @Published var deviceStreamStates: [String: BioStreamState] // Auto-stream enabled per device @Published var deviceAutoStream: [String: Bool] // Battery level per device (0-100) @Published var deviceBattery: [String: Int] ``` ### Session State [#session-state] ```swift // Active session ID @Published var currentSessionId: String? // Conflicting session ID (from 409 conflict) @Published var conflictingSessionId: String? // Session error message @Published var sessionError: String? ``` ## SwiftUI Integration [#swiftui-integration] Since `BioLiveStore` is an `ObservableObject`, inject it into your SwiftUI views: ```swift struct DashboardView: View { @ObservedObject var store: BioLiveStore var body: some View { VStack { if let ecgSamples = store.signals["ecg"] { BioECGChart(samples: ecgSamples) } ForEach(Array(store.deviceConnectionStates.keys), id: \.self) { deviceId in let name = store.deviceNames[deviceId] ?? "Unknown" let state = store.deviceConnectionStates[deviceId] ?? .discovered Text("\(name): \(state)") } } } } ``` ## Memory Management [#memory-management] The store manages memory automatically during background/foreground transitions: ```swift store.enterBackgroundMode() // Reduces memory footprint store.enterForegroundMode() // Restores full buffering store.reset() // Clear all accumulated data ``` ## Accessing Samples [#accessing-samples] ```swift // All samples for a signal type let ecg = store.samples(for: "ecg") // Samples from a specific device let deviceEcg = store.samples(for: "ecg", deviceId: "DEVICE-UUID") ``` # Scanning & Connecting (/docs/biosdk/guides/scanning-and-connecting) BioSDK discovers and connects to BLE wearables through a simple scan-and-connect flow. Discovered peripherals are classified by the biosignals they expose (heart rate, ECG, PPG, EDA, temperature, motion, and others) so your app can pick the right device for the session. ## Scanning for Devices [#scanning-for-devices] ```swift // Start BLE scanning sdk.startScan() // Check if currently scanning if sdk.isScanning { print("Scanning...") } // Stop scanning (saves battery) sdk.stopScan() ``` Discovered devices are emitted as `.lifecycle(.discovered(device))` events and accumulated in `sdk.discoveredDevices`: ```swift sdk.events.sink { event in if case .lifecycle(.discovered(let device)) = event { print("Found: \(device.name) (\(device.id))") } } ``` ## Connecting [#connecting] ```swift let device = sdk.discoveredDevices.first! sdk.connect(device) ``` Monitor connection progress through device state events: ```swift sdk.events.sink { event in if case .deviceState(let device, let connection, let stream) = event { switch connection { case .connecting: print("\(device.name) connecting...") case .discoveringServices: print("\(device.name) discovering services...") case .ready: print("\(device.name) ready to stream!") case .disconnected: print("\(device.name) disconnected") case .failed: print("\(device.name) connection failed") case .discovered: break } } } ``` ## Connection States [#connection-states] Each device progresses through these connection states: ``` .discovered → .connecting → .discoveringServices → .ready → .failed → .disconnected ``` | State | Description | | ---------------------- | ------------------------------------------------------- | | `.discovered` | Device found during scanning, not yet connected | | `.connecting` | BLE connection in progress | | `.discoveringServices` | Connected, discovering BLE services and characteristics | | `.ready` | Fully connected, services discovered — ready to stream | | `.failed` | Connection attempt failed | | `.disconnected` | Previously connected, now disconnected | ## Disconnecting [#disconnecting] ```swift // Disconnect a specific device sdk.disconnect(device) // Disconnect all connected devices sdk.disconnectAll() ``` ## Device Model [#device-model] The `BioDevice` type represents a discovered or connected peripheral: ```swift public struct BioDevice { public let id: UUID // CoreBluetooth peripheral identifier public let name: String // Advertised device name } ``` ## Querying Device State [#querying-device-state] Check a device's current connection and streaming state at any time: ```swift if let (connection, stream) = sdk.deviceSessionState(deviceId: device.id) { print("Connection: \(connection), Stream: \(stream)") } ``` ## Heart Rate Monitor Auto-Connect [#heart-rate-monitor-auto-connect] For apps that want to automatically connect to nearby standard heart rate peripherals (Bluetooth SIG Heart Rate profile): ```swift // Enable auto-connect for standard HR peripherals BioBLEManager.shared.enableAutoConnectHRM(true, maxConnections: 3) ``` This is useful in multi-device setups where a primary biosignal device is paired with one or more standard heart rate peripherals (chest straps, wristbands, or other Bluetooth SIG Heart Rate-compatible devices) to round out the signal coverage of a session. The same scan, connect, and streaming flow applies to any supported wearable: rings, chest straps, watches, patches, optical pulse sensors, glucose monitors, and others. Device-specific behavior is encapsulated in the underlying integration; your app code stays the same across devices. # Session Management (/docs/biosdk/guides/session-management) BioSDK automatically manages sessions with the AnyBio platform. A session is created when a device connects and streaming begins, and ended when streaming stops or the device disconnects. ## Session Lifecycle [#session-lifecycle] Sessions are managed automatically — the SDK starts a session on the backend when streaming begins and ends it when streaming stops. You receive session events through the SDK's event publisher: ```swift sdk.events.sink { event in if case .session(let sessionEvent) = event { switch sessionEvent { case .started(let id, let devices): print("Session started: \(id)") case .resumed(let id, let devices): print("Resumed existing session: \(id)") case .ended(let id, let ok): print("Session ended: \(id), success: \(ok)") case .conflict(let activeId, let startedAt): print("Conflict! Active session: \(activeId)") case .recoveryAvailable(let info): print("Orphaned session found: \(info.sessionId)") case .forbidden: print("Authorization failed") case .serverError(let status): print("Server error: \(status)") case .error(let message): print("Session error: \(message)") case .endFailed(let id, let status): print("Failed to end session \(id)") } } } ``` ## Conflict Handling [#conflict-handling] If a previous session is still active on the backend (e.g., the app was force-quit without ending the session), the SDK emits a `.conflict` event. You can resolve it in two ways: ```swift // Option 1: Resume the existing session sdk.resumeConflictingSession() // Option 2: End the old session and start a new one sdk.endConflictingAndStartNew() ``` The conflicting session ID is available on the SDK state: ```swift if let conflictId = sdk.state.conflictingActiveSessionId { // Present UI asking user to resume or start fresh } ``` ## Session Mode [#session-mode] Session mode controls whether the SDK automatically creates backend sessions. Disable it if you want to manage session lifecycle externally: ```swift // Disable automatic session management sdk.setSessionMode(enabled: false) // Re-enable sdk.setSessionMode(enabled: true) ``` ## Session Context [#session-context] The SDK maintains a published session context that you can observe in SwiftUI: ```swift // Observable session state sdk.sessionContext // BioSessionContext, @Published ``` ## Monitoring Active Sessions [#monitoring-active-sessions] Access the current session ID and check for conflicts: ```swift let currentId = sdk.state.deviceSessionId let hasConflict = sdk.state.conflictingActiveSessionId != nil ``` # Streaming Profiles (/docs/biosdk/guides/streaming-profiles) Streaming profiles define a named set of device operations that configure a device for a specific data capture scenario. Profiles are defined in the device's profile schema on the AnyBio platform and fetched at SDK initialization. ## Listing Available Profiles [#listing-available-profiles] ```swift let profiles = sdk.getStreamingProfiles(deviceId: device.id) for profile in profiles { print("\(profile.name): \(profile.description)") } ``` ## Activating a Profile [#activating-a-profile] Activating a profile executes its configured operations in order and enables auto-reconnect with profile replay: ```swift try await sdk.activateStreamingProfile( deviceId: device.id, profileName: "research" ) ``` Once activated, the profile is persisted — if the device disconnects and reconnects (or the app is relaunched), the SDK will replay the profile automatically. ## Deactivating a Profile [#deactivating-a-profile] ```swift sdk.deactivateStreamingProfile(deviceId: device.id) // Optionally skip the stop-streaming operation sdk.deactivateStreamingProfile(deviceId: device.id, executeStop: false) ``` ## Checking Active Profile [#checking-active-profile] ```swift if let active = sdk.activeStreamingProfile(deviceId: device.id) { print("Active profile: \(active)") } ``` ## Device Operations [#device-operations] For more granular control, you can list and execute individual device operations: ```swift // List available operations for a device let operations = sdk.getDeviceOperations(deviceId: device.id) for op in operations { print("\(op.name): \(op.description)") } // Execute a specific operation try await sdk.executeOperation( deviceId: device.id, operationName: "StartECG" ) ``` # Streaming (/docs/biosdk/guides/streaming) # Streaming Biosignals [#streaming-biosignals] Once a device reaches the `.ready` connection state, you can start streaming biosignal data. ## Start and Stop Streaming [#start-and-stop-streaming] ```swift // Start streaming from a connected device sdk.startStreaming(deviceId: device.id) // Stop streaming sdk.stopStreaming(deviceId: device.id) ``` ## Stream States [#stream-states] Each device tracks its streaming state independently: ``` .idle → .starting → .streaming → .stopping → .idle → .stalled → .unsupported ``` | State | Description | | -------------- | ------------------------------------------------ | | `.idle` | Not streaming | | `.starting` | Stream initialization in progress | | `.streaming` | Actively receiving biosignal data | | `.stopping` | Teardown in progress | | `.stalled` | Stream interrupted (e.g., temporary signal loss) | | `.unsupported` | Device does not support streaming | ## Receiving Samples [#receiving-samples] Biosignal samples arrive as `.sample` events on the SDK's event publisher: ```swift sdk.events.sink { event in if case .sample(let deviceId, let sample) = event { print("Signal: \(sample.signalName)") switch sample.data { case .waveform(let values): // Array of sample points (e.g., ECG, PPG, EDA waveforms) print(" \(values.count) samples") case .scalar(let value): // Single value (e.g., heart rate, temperature, SpO2) print(" Value: \(value)") } } } ``` ## Signal Types [#signal-types] The `BioSample` type includes convenience properties for identifying signal types: | Property | Signal | Data type | | -------------------- | ----------------------- | ----------- | | `sample.isECG` | Electrocardiogram | `.waveform` | | `sample.isPPG` | Photoplethysmography | `.waveform` | | `sample.isEDA` | Electrodermal activity | `.waveform` | | `sample.isTemp` | Skin temperature | `.scalar` | | `sample.isHeartRate` | Heart rate (BPM) | `.scalar` | | `sample.isSpO2` | Blood oxygen saturation | `.scalar` | ```swift sdk.events.sink { event in if case .sample(_, let sample) = event { if sample.isHeartRate, let bpm = sample.scalarValue { print("Heart rate: \(bpm) BPM") } if sample.isECG, let waveform = sample.waveform { // Feed to BioECGChart or your own visualization updateChart(waveform) } } } ``` ## Auto-Stream [#auto-stream] Enable auto-stream on a device to automatically start streaming whenever the device connects (including reconnections): ```swift sdk.setAutoStream(deviceId: device.id, enabled: true) // Check current setting let isAuto = sdk.autoStreamEnabled(deviceId: device.id) ``` ## Data Delivery [#data-delivery] The SDK buffers all biosignal data locally and uploads it to the AnyBio platform automatically. Data is persisted across app restarts, so no samples are lost even during network interruptions or app termination. # ECG Chart (/docs/bioui/charts/ecg-chart) # BioECGChart [#bioecgchart] Real-time electrocardiogram waveform visualization. ```swift import BioUI BioECGChart(samples: ecgSamples) ``` ## Usage with BioLiveStore [#usage-with-biolivestore] ```swift struct ECGView: View { @ObservedObject var store: BioLiveStore var body: some View { if let samples = store.signals["ecg"] { BioECGChart(samples: samples) .frame(height: 200) } else { Text("Waiting for ECG data...") .foregroundStyle(.secondary) } } } ``` ## Customization [#customization] Apply a custom theme: ```swift BioECGChart(samples: samples) .bioChartTheme(BioChartTheme( lineColor: .green, backgroundColor: .black, lineWidth: 1.5 )) ``` # EDA Chart (/docs/bioui/charts/eda-chart) # BioEDAChart [#bioedachart] Real-time electrodermal activity waveform visualization. ```swift import BioUI BioEDAChart(samples: edaSamples) ``` ## Usage with BioLiveStore [#usage-with-biolivestore] ```swift struct EDAView: View { @ObservedObject var store: BioLiveStore var body: some View { if let samples = store.signals["eda"] { BioEDAChart(samples: samples) .frame(height: 200) } else { Text("Waiting for EDA data...") .foregroundStyle(.secondary) } } } ``` # Heart Rate Chart (/docs/bioui/charts/hr-chart) # BioHRChart [#biohrchart] Heart rate trend line visualization showing BPM over time. ```swift import BioUI BioHRChart(samples: hrSamples) ``` ## Usage with BioLiveStore [#usage-with-biolivestore] ```swift struct HRTrendView: View { @ObservedObject var store: BioLiveStore var body: some View { if let samples = store.signals["hr"] { BioHRChart(samples: samples) .frame(height: 200) } else { Text("Waiting for heart rate data...") .foregroundStyle(.secondary) } } } ``` # PPG Chart (/docs/bioui/charts/ppg-chart) # BioPPGChart [#bioppgchart] Real-time photoplethysmography waveform visualization. ```swift import BioUI BioPPGChart(samples: ppgSamples) ``` ## Usage with BioLiveStore [#usage-with-biolivestore] ```swift struct PPGView: View { @ObservedObject var store: BioLiveStore var body: some View { if let samples = store.signals["ppg"] { BioPPGChart(samples: samples) .frame(height: 200) } else { Text("Waiting for PPG data...") .foregroundStyle(.secondary) } } } ``` # Temperature Chart (/docs/bioui/charts/temp-chart) # BioTempChart [#biotempchart] Skin temperature trend visualization. ```swift import BioUI BioTempChart(samples: tempSamples) ``` ## Usage with BioLiveStore [#usage-with-biolivestore] ```swift struct TemperatureView: View { @ObservedObject var store: BioLiveStore var body: some View { if let samples = store.signals["temp"] { BioTempChart(samples: samples) .frame(height: 200) } else { Text("Waiting for temperature data...") .foregroundStyle(.secondary) } } } ``` # Device List View (/docs/bioui/components/device-list-view) # BioDeviceListView [#biodevicelistview] A ready-made SwiftUI view for scanning, discovering, and connecting to BLE devices. Handles the full scan-and-connect flow with connection state indicators. ```swift import BioUI BioDeviceListView(sdk: sdk, store: store) ``` ## Usage [#usage] ```swift struct SetupView: View { let sdk: BioSDKClient @ObservedObject var store: BioLiveStore var body: some View { NavigationStack { BioDeviceListView(sdk: sdk, store: store) .navigationTitle("Devices") } } } ``` ## Features [#features] * Automatic BLE scanning with start/stop control * Device name and signal strength display * Connection state badges (discovered, connecting, ready, etc.) * Tap to connect / disconnect * Supports ring and HRM device types # Heart Rate Gauge (/docs/bioui/components/heart-rate-gauge) # BioHeartRateGauge [#bioheartrategauge] An animated gauge widget that displays the current heart rate in BPM. Designed for dashboard views and live monitoring screens. ```swift import BioUI BioHeartRateGauge(store: store) ``` ## Usage [#usage] ```swift struct DashboardView: View { @ObservedObject var store: BioLiveStore var body: some View { HStack { BioHeartRateGauge(store: store) .frame(width: 150, height: 150) VStack(alignment: .leading) { Text("Live Heart Rate") .font(.headline) Text("Data from all connected devices") .font(.caption) .foregroundStyle(.secondary) } } } } ``` ## Multi-Device Fusion [#multi-device-fusion] When multiple heart rate sources are connected (e.g., a ring and an HRM chest strap), `BioLiveStore` computes a fused BPM using the median of all active sources. `BioHeartRateGauge` displays this fused value automatically. # bio-checkin (/docs/bioui-web/components/bio-checkin) # `` [#bio-checkin] A patient-reported outcome (PRO) check-in widget that presents a step-by-step questionnaire and submits responses as observations. Supports both static configuration (via attributes) and profile-driven mode (auto-configured from the user's program). ```html ``` ## How It Works [#how-it-works] When `` receives context from a parent ``: 1. **Resolves configuration** — in profile-driven mode, reads question types, schedule, and questionnaire ID from the SDK config; in static mode, parses the `question-types` attribute 2. **Fetches prior scores** — calls `GET /api/v1/observations/latest` for each biosignal to show the user's last response 3. **Presents questions one at a time** — renders a scale (1–5 or 1–10 depending on question type) with step counter 4. **Submits all answers** — sends a single `POST /api/v1/episodes/:id/questionnaire-response` containing all items 5. **Shows results with trends** — the server compares each answer to the most recent prior observation and returns a trend (`increasing`, `decreasing`, or `stable`) 6. **Supports updates** — an "Update" button lets the user re-submit, and the next trend comparison will use the previous submission as the baseline ## Modes [#modes] ### Static Mode [#static-mode] Pass question types directly as an attribute. You control which questions appear: ```html ``` ### Profile-Driven Mode [#profile-driven-mode] Omit `question-types` and let the widget auto-configure from the user's program. The program's `spec.sdk.checkin` section defines available check-in contexts (e.g., `weekly`, `daily`), each with its own question types, schedule, and questionnaire ID. ```html ``` In profile-driven mode, the widget also reads the check-in status from the SDK config. If the check-in is already completed for the current period, it renders a summary view instead of the questionnaire form. ## Attributes [#attributes] | Attribute | Type | Required | Description | | ------------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------- | | `question-types` | `string` | No | JSON array of question type strings (e.g., `'["sleep","mood","energy"]'`). Omit for profile-driven mode. | | `episode-id` | `string` | No | Episode ID for submission. Falls back to the episode resolved by ``. | | `questionnaire-id` | `string` | No | Questionnaire identifier. Defaults to `"default"`. Overridden by profile config in profile-driven mode. | | `context_type` | `string` | No | Check-in context key (e.g., `"weekly"`, `"daily"`). In profile-driven mode, selects which check-in config to use. | ## Events [#events] | Event | Detail | Description | | ------------------------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `anybio:checkin:submit` | `{ eventId, questionnaireId, observations, episodeId, context }` | Fired after successful submission. `observations` is an array of created observation objects including `value_quantity`, `trend`, and `prior_value`. | | `anybio:checkin:error` | `{ error: string }` | Fired if submission fails. | | `anybio:checkin:dismiss` | `{}` | Fired when the user closes the check-in overlay without completing. | | `bio-checkin-complete` | *(same as anybio:checkin:submit)* | Backward-compatible alias. | | `bio-checkin-error` | *(same as anybio:checkin:error)* | Backward-compatible alias. | | `bio-checkin-dismiss` | *(same as anybio:checkin:dismiss)* | Backward-compatible alias. | ## Trend Indicators [#trend-indicators] After submission, each result row can display a trend arrow comparing the current score to the most recent prior observation for that biosignal: | Trend | Symbol | Color Variable | | --------------- | ---------- | ------------------------------- | | `increasing` | ↑ | `--bio-trend-improving` (green) | | `decreasing` | ↓ | `--bio-trend-declining` (red) | | `stable` | = | `--bio-trend-stable` (gray) | | *no prior data* | *(hidden)* | — | Trends are computed server-side in the questionnaire-response handler. When no prior observation exists, the `trend` field is `null` and no indicator is shown. ## CSS Classes [#css-classes] The widget renders with semantic CSS classes for styling. No Shadow DOM — standard CSS selectors work. | Class | Element | | --------------------------- | ---------------------------------- | | `.bio-checkin-overlay` | Full-screen backdrop (static mode) | | `.bio-checkin-modal` | Modal card | | `.bio-checkin-header` | Step counter + close button row | | `.bio-checkin-step` | "1 of 3" step text | | `.bio-checkin-close` | Close (×) button | | `.bio-checkin-question` | Question heading | | `.bio-checkin-prior` | "Last time: 7/10" prior score | | `.bio-checkin-scale` | Scale button container | | `.bio-checkin-scale-btn` | Individual scale button (1–10) | | `.bio-checkin-labels` | Low/High labels | | `.bio-checkin-summary` | Completed summary card | | `.bio-checkin-results` | Results list | | `.bio-checkin-result-item` | Individual result row | | `.bio-checkin-score` | Score value | | `.bio-checkin-trend` | Trend indicator (↑/↓/=) | | `.bio-checkin-submitted-at` | Timestamp | | `.bio-checkin-update-btn` | "Update" button | | `.bio-checkin-done-btn` | "Done" button (static mode) | ## CSS Custom Properties [#css-custom-properties] | Property | Default | Description | | ----------------------- | --------- | ------------------------------ | | `--bio-primary` | `#5a8a6a` | Score and heading accent color | | `--bio-surface` | `#ffffff` | Card/modal background | | `--bio-surface-muted` | `#f8fafc` | Result row background | | `--bio-border` | `#e2e8f0` | Scale button border | | `--bio-radius` | `8px` | Border radius for result items | | `--bio-radius-lg` | `12px` | Border radius for cards | | `--bio-shadow-md` | — | Summary card shadow | | `--bio-shadow-lg` | — | Modal shadow | | `--bio-trend-improving` | `#22c55e` | Increasing trend color | | `--bio-trend-stable` | `#64748b` | Stable trend color | | `--bio-trend-declining` | `#ef4444` | Decreasing trend color | ## Code Examples [#code-examples] ### Profile-Driven Weekly Check-in [#profile-driven-weekly-check-in] ```html ``` The widget reads the `weekly` entry from the profile's `spec.sdk.checkin` map. If the user has already completed the weekly check-in, it shows the summary with scores and trends. Otherwise it opens the questionnaire. ### With Event Handling [#with-event-handling] ```html ``` ### Custom Styling [#custom-styling] ```css /* Match your app's design system */ bio-checkin { --bio-primary: #1a73e8; --bio-radius-lg: 16px; --bio-trend-improving: #34a853; --bio-trend-declining: #ea4335; } /* Style the scale buttons */ .bio-checkin-scale-btn { width: 48px; height: 48px; } /* Customize the summary card */ .bio-checkin-summary { border: 1px solid var(--bio-border); } ``` ### Static Mode with All Options [#static-mode-with-all-options] ```html ``` ## Submission frequency [#submission-frequency] Submission frequency is not limited by this widget. # bio-health-metrics (/docs/bioui-web/components/bio-health-metrics) # `` [#bio-health-metrics] A self-contained health metrics widget that manages OAuth provider connections (starting with Fitbit) and displays synced metrics — steps, sleep, heart rate/HRV, stress, active minutes, and calories. ```html ``` ## How It Works [#how-it-works] When `` receives context from a parent ``: 1. **Resolves provider list** — in profile-driven mode, reads enabled health providers from the program's `spec.sdk.healthMetrics` section; in static mode, uses the `providers` attribute 2. **Shows connection state** — for each provider, displays a "Connect" button (disconnected) or the provider name with a status badge (connected) 3. **Handles OAuth** — clicking "Connect" initiates the OAuth flow for that provider; on success, stores the connection and emits `anybio:health:connected` 4. **Fetches metrics** — once connected, pulls the latest metrics for the requested `date` and renders metric cards for steps, sleep, heart rate/HRV, stress, active minutes, and calories 5. **Supports refresh** — a refresh button re-fetches metrics from the provider and emits `anybio:health:refreshed` 6. **Supports disconnect** — an "Unlink" action removes the provider connection and emits `anybio:health:disconnected` ## Modes [#modes] ### Profile-Driven Mode [#profile-driven-mode] Omit all attributes. The widget reads its configuration from the program's `spec.sdk.healthMetrics` section: ```html ``` The profile defines: * `providers` — array of enabled provider slugs (e.g., `["fitbit"]`) * `metrics` — array of metric keys to display (e.g., `["steps", "sleep", "heartRate"]`) * `compact` — whether to use the compact layout ### Static Mode [#static-mode] Pass configuration directly: ```html ``` ## Attributes [#attributes] | Attribute | Type | Required | Description | | ------------- | --------- | -------- | ----------------------------------------------------------- | | `episode-id` | `string` | No | Falls back to `` resolved episode. | | `date` | `string` | No | Date for metrics (YYYY-MM-DD). Defaults to today. | | `providers` | `string` | No | JSON array of provider slugs. Omit for profile-driven mode. | | `metrics` | `string` | No | JSON array of metric keys to show. Defaults to all six. | | `show-header` | `boolean` | No | Show/hide header bar. Defaults to `true`. | | `compact` | `boolean` | No | Compact 4-metric grid without progress bars or breakdowns. | ## Events [#events] | Event | Detail | Description | | ---------------------------- | ------------------------------ | --------------------------------------------------------------------- | | `anybio:health:connected` | `{ provider, providerUserId }` | OAuth completed. | | `anybio:health:disconnected` | `{ provider }` | Provider unlinked. | | `anybio:health:refreshed` | `{ provider, date, metrics }` | Metrics refreshed. | | `anybio:health:error` | `{ error, action }` | Error. `action`: `"connect"`, `"disconnect"`, `"refresh"`, `"fetch"`. | | `bio-health-connected` | *(alias)* | Backward-compatible. | | `bio-health-disconnected` | *(alias)* | Backward-compatible. | ## CSS Classes [#css-classes] The widget renders with semantic CSS classes for styling. No Shadow DOM — standard CSS selectors work. ### Layout [#layout] | Class | Element | | --------------------- | ---------------------------------------- | | `.bio-hm-container` | Outermost wrapper | | `.bio-hm-header` | Header bar with title and refresh button | | `.bio-hm-title` | Widget title text | | `.bio-hm-refresh-btn` | Refresh button | ### Provider Connection [#provider-connection] | Class | Element | | ------------------------ | ------------------------------------- | | `.bio-hm-providers` | Provider list container | | `.bio-hm-provider-row` | Single provider row (name + action) | | `.bio-hm-provider-name` | Provider display name | | `.bio-hm-provider-badge` | Connection status badge | | `.bio-hm-connect-btn` | "Connect" button (disconnected state) | | `.bio-hm-unlink-btn` | "Unlink" action button | ### Metrics Grid [#metrics-grid] | Class | Element | | ------------------------ | -------------------------------------- | | `.bio-hm-metrics` | Metrics card grid | | `.bio-hm-card` | Individual metric card | | `.bio-hm-card-icon` | Metric icon | | `.bio-hm-card-label` | Metric label (e.g., "Steps") | | `.bio-hm-card-value` | Primary metric value | | `.bio-hm-card-unit` | Unit text (e.g., "bpm", "hrs") | | `.bio-hm-card-progress` | Progress bar wrapper | | `.bio-hm-card-bar` | Progress bar fill | | `.bio-hm-card-breakdown` | Breakdown details (e.g., sleep stages) | ### States [#states] | Class | Element | | ----------------- | --------------------------------------- | | `.bio-hm-loading` | Applied to container while fetching | | `.bio-hm-empty` | Applied when no metrics available | | `.bio-hm-error` | Applied on fetch/connection error | | `.bio-hm-compact` | Applied when `compact` attribute is set | ## Code Examples [#code-examples] ### Profile-Driven [#profile-driven] ```html

Your Health Metrics

``` ### With Event Handling [#with-event-handling] ```html ``` ### Custom Styling [#custom-styling] ```css /* Match your app's design system */ bio-health-metrics { --bio-primary: #1a73e8; --bio-surface: #ffffff; --bio-radius: 12px; } /* Style the metric cards */ .bio-hm-card { border: 1px solid var(--bio-border); border-radius: var(--bio-radius); padding: 16px; } /* Custom connect button */ .bio-hm-connect-btn { background: var(--bio-primary); color: white; border-radius: 8px; padding: 8px 16px; } ``` ### Compact Mode [#compact-mode] ```html ``` Compact mode renders a 4-metric grid without progress bars or breakdown details — useful for dashboard summaries or sidebar widgets. # bio-journal (/docs/bioui-web/components/bio-journal) # `` [#bio-journal] A free-text journal widget that lets users write daily reflections in response to an optional prompt. Records each save as a `pro.journal` Observation on the shared timeline, with an optional history view of prior entries. ```html ``` ## How It Works [#how-it-works] When `` receives context from a parent ``: 1. **Resolves configuration** — in profile-driven mode, reads the prompt and status from the SDK config's `journal` section; in static mode, uses attribute defaults 2. **Loads existing entry** — fetches today's `pro.journal` observation (if any) and pre-populates the textarea so the user can edit 3. **Renders the prompt** — shows the configured prompt above the textarea 4. **Saves on submit** — posts to `POST /api/v1/xusers/{id}/observations/journal` with `text`, `prompt`, `day`, and `episode_id`; stores the response as `value_string` 5. **History view (optional)** — when `show-history` is set, exposes a "History" button that loads the most recent 20 entries ## Modes [#modes] ### Profile-Driven Mode [#profile-driven-mode] Omit all attributes. The widget reads its configuration from the program's `spec.sdk.journal` section: ```html ``` The profile defines: * `prompt` — text shown above the textarea * `schedule` — `"daily"` or `"weekly"` * `show_history` — whether the History button is enabled ### Static Mode [#static-mode] Pass configuration directly: ```html ``` ## Attributes [#attributes] | Attribute | Type | Required | Description | | -------------- | --------- | -------- | -------------------------------------------------------------------------- | | `prompt` | `string` | No | Prompt text shown above the textarea. Falls back to the SDK config prompt. | | `show-history` | `boolean` | No | When present, renders a History button that loads prior entries. | | `episode-id` | `string` | No | Episode ID. Falls back to the episode resolved by ``. | | `day` | `number` | No | Day index within the episode. Falls back to the episode's current day. | ## Events [#events] | Event | Detail | Description | | --------------------- | ----------------------------------------- | ------------------------------ | | `anybio:journal:save` | `{ text, observationId, day, episodeId }` | Fired after a successful save. | ## CSS Classes [#css-classes] | Class | Element | | ------------------------------ | ----------------------------------------- | | `.bio-journal` | Root container | | `.bio-journal-prompt` | Prompt text above the textarea | | `.bio-journal-textarea` | Free-text input | | `.bio-journal-footer` | Footer row with status and action buttons | | `.bio-journal-status` | "Saved 10:30 AM" status text | | `.bio-journal-saved` | Saved-timestamp span | | `.bio-journal-actions` | Button container | | `.bio-journal-history-btn` | History toggle button | | `.bio-journal-save-btn` | Save button | | `.bio-journal-history` | History panel container | | `.bio-journal-history-loading` | Loading state inside the history panel | | `.bio-journal-history-empty` | Empty state inside the history panel | | `.bio-journal-history-entry` | Single prior entry | | `.bio-journal-history-meta` | Day + date row for an entry | | `.bio-journal-history-text` | Entry body | ## Code Examples [#code-examples] ### Profile-Driven [#profile-driven] ```html

Daily Reflection

``` ### With History and Event Handling [#with-history-and-event-handling] ```html ``` ### Custom Styling [#custom-styling] ```css bio-journal { --bio-journal-textarea-min-height: 8rem; } .bio-journal-textarea { font-family: 'Georgia', serif; font-size: 1.05rem; } ``` ## Observation Shape [#observation-shape] Each save produces an Observation with: * `biosignal_name`: `pro.journal` * `value_string`: the entry text * `effective_datetime`: save time * `metadata.widget`: `bio-journal` * `metadata.day`: the episode day (when available) * `metadata.prompt`: the prompt that was shown (when available) Agents can query these via `GET /api/v1/observations?xuser_id=...&biosignal_name=pro.journal` to surface themes or trigger follow-up check-ins. # bio-mood (/docs/bioui-web/components/bio-mood) # `` [#bio-mood] A daily mood observation widget that lets users select their mood from a configurable emoji scale. Records the selection as an AnyBio observation with trend tracking across days. ```html ``` ## How It Works [#how-it-works] When `` receives context from a parent ``: 1. **Resolves configuration** — in profile-driven mode, reads the mood scale, labels, biosignal, and status from the SDK config's `mood` section; in static mode, uses attribute defaults 2. **Shows current state** — if mood has already been logged for the current day, displays "Mood logged" with a timestamp and the selected emoji highlighted 3. **Presents emoji scale** — renders buttons for each label in the scale (e.g., 😔 😐 🙂 😊 😄) 4. **Records observation** — on selection, posts an observation with `metadata: { widget: "bio-mood", day: N }` 5. **Supports updates** — clicking a different emoji after initial selection re-submits with the new value ## Modes [#modes] ### Profile-Driven Mode [#profile-driven-mode] Omit all attributes. The widget reads its configuration from the program's `spec.sdk.mood` section: ```html ``` The profile defines: * `scale` — number of options (e.g., `5`) * `labels` — emoji array (e.g., `["😔", "😐", "🙂", "😊", "😄"]`) * `biosignal` — the biosignal ID to record (e.g., `"mood"`) ### Static Mode [#static-mode] Pass configuration directly: ```html ``` ## Attributes [#attributes] | Attribute | Type | Required | Description | | ------------ | -------- | -------- | --------------------------------------------------------------------- | | `scale` | `number` | No | Number of mood options. Defaults to `5`. | | `labels` | `string` | No | JSON array of emoji labels. Defaults to `["😔","😐","🙂","😊","😄"]`. | | `biosignal` | `string` | No | Biosignal ID for the observation. Defaults to `"mood"`. | | `episode-id` | `string` | No | Episode ID. Falls back to the episode resolved by ``. | ## Events [#events] | Event | Detail | Description | | -------------------- | -------------------------------------- | --------------------------------------------------- | | `anybio:mood:submit` | `{ value, biosignal, episodeId, day }` | Fired on initial mood selection. | | `anybio:mood:update` | `{ value, biosignal, episodeId, day }` | Fired when mood is changed after initial selection. | | `bio-mood-complete` | *(same as anybio:mood:submit)* | Backward-compatible alias. | ## CSS Classes [#css-classes] | Class | Element | | -------------------- | ---------------------------------- | | `.bio-mood-buttons` | Button container (flex row) | | `.bio-mood-btn` | Individual mood button | | `.bio-mood-selected` | Applied to the selected button | | `.bio-mood-emoji` | Emoji text | | `.bio-mood-status` | "Mood logged 10:30 AM" status text | Each mood button sets a `--mood-color` CSS variable based on the mood index, using the `--bio-mood-*` palette: | Variable | Default | Mood | | -------------------- | --------- | --------------- | | `--bio-mood-great` | `#22c55e` | Best / happiest | | `--bio-mood-good` | `#84cc16` | Good | | `--bio-mood-okay` | `#eab308` | Okay | | `--bio-mood-bitoff` | `#f97316` | A bit off | | `--bio-mood-notbest` | `#ef4444` | Not my best | ## Code Examples [#code-examples] ### Profile-Driven [#profile-driven] ```html

How Do You Feel Today?

``` ### With Event Handling [#with-event-handling] ```html ``` ### Custom Styling [#custom-styling] ```css /* Larger emoji buttons */ .bio-mood-btn { padding: 12px 20px; } .bio-mood-emoji { font-size: 2.5rem; } /* Custom mood palette */ bio-mood { --bio-mood-great: #34a853; --bio-mood-notbest: #ea4335; } ``` # bio-progress-ring (/docs/bioui-web/components/bio-progress-ring) # `` [#bio-progress-ring] A circular progress indicator showing episode adherence — how many days the user has completed out of their current program day. Supports both static configuration and profile-driven mode. ```html ``` ## How It Works [#how-it-works] When `` receives context from a parent ``: 1. **Resolves progress data** — in profile-driven mode, reads `current_day`, `completed_days`, `total_days`, `adherence_pct`, and `streak` from the SDK config's `progress` section 2. **Renders an SVG ring** — the arc fill represents adherence percentage, animated on load 3. **Shows key metrics** — day count, total days, adherence percentage, and current streak 4. **Handles zero state** — when no days are completed, shows a friendly "Day N, just getting started!" message instead of "0 of N days" ## Modes [#modes] ### Profile-Driven Mode [#profile-driven-mode] Omit all attributes and let the widget auto-configure from the program: ```html ``` ### Static Mode [#static-mode] Pass progress data directly as attributes: ```html ``` ## Attributes [#attributes] | Attribute | Type | Required | Description | | ---------------- | -------- | -------- | ------------------------------------------------------------------- | | `size` | `number` | No | Diameter of the ring in pixels. Defaults to `120`. | | `stroke-width` | `number` | No | Width of the ring stroke in pixels. Defaults to `10`. | | `current-day` | `number` | No | Current program day. Falls back to profile-driven value. | | `completed-days` | `number` | No | Number of days completed. Falls back to profile-driven value. | | `total-days` | `number` | No | Total program days. Falls back to profile-driven value. | | `streak` | `number` | No | Current consecutive-day streak. Falls back to profile-driven value. | | `episode-id` | `string` | No | Episode ID. Falls back to the episode resolved by ``. | ## CSS Classes [#css-classes] | Class | Element | | ------------------------------ | ----------------------------------------- | | `.bio-progress-ring` | Outer container (flex column, centered) | | `.bio-progress-ring-label` | Day count row | | `.bio-progress-ring-day` | Bold day number | | `.bio-progress-ring-total` | "/ 30 days" suffix | | `.bio-progress-ring-adherence` | Adherence text or getting-started message | | `.bio-progress-ring-streak` | Streak badge (e.g., "3-day streak") | ## CSS Custom Properties [#css-custom-properties] | Property | Default | Description | | ------------------------ | --------- | --------------------------- | | `--bio-primary` | `#5a8a6a` | Ring fill color | | `--bio-border` | `#e2e8f0` | Ring track (unfilled) color | | `--bio-surface-muted-fg` | `#64748b` | Secondary text color | | `--bio-status-yellow` | `#eab308` | Streak badge color | ## Code Examples [#code-examples] ### Profile-Driven [#profile-driven] ```html ``` ### Custom Styling [#custom-styling] ```css bio-progress-ring { --bio-primary: #1a73e8; --bio-status-yellow: #f59e0b; } .bio-progress-ring-day { font-size: 2rem; } ``` # bio-provider (/docs/bioui-web/components/bio-provider) # `` [#bio-provider] The authentication context provider for all BioUI Web widgets. Wrap your widgets in a `` to supply credentials, resolve the user, and establish a shared API context. ```html ``` ## How It Works [#how-it-works] When `` connects to the DOM, it: 1. Validates that `api-key`, `project-key`, and `xuser-id` are all present 2. Calls `POST /api/v1/xusers` to resolve the external user ID to an AnyBio UUID 3. Stores the resolved context (auth headers + user UUID) and provides it to all child widgets via a Lit context 4. Dispatches `bio-provider-ready` once the context is available Child widgets wait for the provider context before making any API calls. If credentials change (e.g., a new `xuser-id`), the provider re-resolves and notifies children. ## Attributes [#attributes] | Attribute | Type | Required | Description | | ------------- | -------- | -------- | --------------------------------------------------------------------------------- | | `api-key` | `string` | Yes | Organization developer key. Sent as `Authorization: Bearer `. | | `project-key` | `string` | Yes | Project identifier. Sent as `X-Project-Key` header. | | `xuser-id` | `string` | Yes | External user ID from your system. Resolved to an AnyBio UUID via the xusers API. | | `base-url` | `string` | No | Override the default API base URL. Defaults to `https://api.anybio.io`. | ## Events [#events] | Event | Detail | Description | | -------------------- | -------------------- | --------------------------------------------------------------------------------------------------------- | | `bio-provider-ready` | `{ userId: string }` | Fired when the user has been resolved and the context is available. `userId` is the resolved AnyBio UUID. | | `bio-provider-error` | `{ error: string }` | Fired if user resolution fails (e.g., invalid API key, network error). | ## CSS Custom Properties [#css-custom-properties] `` does not render visible UI. It acts as a context boundary. However, you can set theme-level CSS custom properties on it and they will cascade to all child widgets: ```css bio-provider { --bio-color-primary: #1a73e8; --bio-color-surface: #ffffff; --bio-font-family: 'Inter', system-ui, sans-serif; --bio-radius: 12px; } ``` See the default theme file (`bioui-base.css`) for the full list of custom properties. ## Code Example [#code-example] ```html ``` ### Updating Credentials Dynamically [#updating-credentials-dynamically] ```javascript // When the logged-in user changes in your app: const provider = document.querySelector('bio-provider'); provider.setAttribute('xuser-id', newUserId); // The provider will re-resolve and fire bio-provider-ready again ``` # bio-task-list (/docs/bioui-web/components/bio-task-list) # `` [#bio-task-list] A daily task checklist that displays categorized tasks (exercise, nutrition, wellness) for the current program day. Tasks are checked off as observations, tracked server-side. Supports both static configuration and profile-driven mode. ```html ``` ## How It Works [#how-it-works] When `` receives context from a parent ``: 1. **Resolves task data** — in profile-driven mode, reads the task list from the SDK config's `tasks` section, which includes task definitions and completion status 2. **Renders categorized tasks** — groups tasks by category with icons and labels 3. **Handles completion** — when a user checks a task, sends a `POST` to record the observation with metadata (`widget: "bio-task-list"`, `day: N`) 4. **Handles unchecking** — when a user unchecks a task, sends a request to remove the observation 5. **Dispatches events** — fires `anybio:task:complete` and `anybio:task:uncomplete` for the host app to react ## Modes [#modes] ### Profile-Driven Mode [#profile-driven-mode] Omit the `day` attribute and let the widget auto-configure from the program: ```html ``` ### Static Mode [#static-mode] Pass the day number directly: ```html ``` ## Attributes [#attributes] | Attribute | Type | Required | Description | | ------------ | -------- | -------- | ------------------------------------------------------------------- | | `day` | `number` | No | Program day number. Omit for profile-driven mode. | | `episode-id` | `string` | No | Episode ID. Falls back to the episode resolved by ``. | ## Events [#events] | Event | Detail | Description | | ------------------------ | --------------------------------------- | --------------------------------- | | `anybio:task:complete` | `{ taskId, biosignal, day, episodeId }` | Fired when a task is checked off. | | `anybio:task:uncomplete` | `{ taskId, biosignal, day, episodeId }` | Fired when a task is unchecked. | | `bio-task-complete` | *(same as anybio:task:complete)* | Backward-compatible alias. | | `bio-task-uncomplete` | *(same as anybio:task:uncomplete)* | Backward-compatible alias. | ## CSS Classes [#css-classes] The widget renders with Light DOM — all elements are directly styleable. | Class | Element | | --------------------------- | ------------------------------ | | `.bio-task-category` | Category group container | | `.bio-task-category-header` | Category heading with icon | | `.bio-task-item` | Individual task row | | `.bio-task-checkbox` | Checkbox input | | `.bio-task-label` | Task description text | | `.bio-task-completed` | Applied to completed task rows | ## CSS Custom Properties [#css-custom-properties] | Property | Default | Description | | --------------------- | --------- | ------------------------- | | `--bio-primary` | `#5a8a6a` | Checkbox accent color | | `--bio-surface` | `#ffffff` | Task item background | | `--bio-surface-muted` | `#f8fafc` | Completed task background | | `--bio-border` | `#e2e8f0` | Task item border | | `--bio-radius` | `8px` | Task item border radius | ## Code Examples [#code-examples] ### Profile-Driven [#profile-driven] ```html ``` ### With Event Handling [#with-event-handling] ```html ``` ### Custom Styling [#custom-styling] ```css /* Circular checkboxes */ .bio-task-checkbox { border-radius: 50%; width: 24px; height: 24px; } /* Completed task styling */ .bio-task-completed .bio-task-label { text-decoration: line-through; opacity: 0.6; } ``` # AI Prompt Templates (/docs/bioui-web/guides/ai-prompt-templates) Copy-paste prompts for building with AnyBio using any AI coding tool — Cursor, Claude Code, ChatGPT, Lovable, Bolt, or others. Each prompt is self-contained with the context needed to produce working code. ## Web App: Add Health Widgets [#web-app-add-health-widgets] Works with: Cursor, Claude Code, ChatGPT, Bolt ``` I'm building a web app and want to add AnyBio health monitoring widgets. AnyBio BioUI Web uses CDN-based Web Components: - Script: https://cdn.anybio.io/biosdk-web/v1/bioui.js - CSS: https://cdn.anybio.io/biosdk-web/v1/bioui-base.css Authentication model: - Server-side: POST https://api.anybio.io/api/v1/sdk/token with Authorization: Bearer org_MY_KEY and body { external_id: "user-id", project_key: "proj_MY_KEY" } Returns: { xuser_token: "eyJ...", xuser_id: "...", expires_in: 3600 } - Client-side: Pass xuser_token to Available widgets: , , , , , Please: 1. Create a server-side route that mints xuser tokens (keep org_* key server-side only) 2. Add the CDN script and CSS to my page 3. Initialize with the token after user login 4. Add and widgets to my dashboard My framework is [Next.js / React / Vue / plain HTML — specify yours]. ``` ## iOS App: BLE Device Streaming [#ios-app-ble-device-streaming] Works with: Cursor, Claude Code, ChatGPT ``` I'm building an iOS app that streams biosignals from BLE wearables using AnyBio's BioSDK. BioSDK setup: - Swift Package: https://github.com/anybio/biosdk-ios-binary.git - Info.plist: NSBluetoothAlwaysUsageDescription, UIBackgroundModes: bluetooth-central SDK usage: import BioSDK let sdk = try await BioSDKClient.initialize( configuration: BioSDKClient.Configuration(organizationKey: "org_*", projectKey: "proj_*")) sdk.startScan() // Scan for BLE devices sdk.connect(device) // Connect to a BioDevice sdk.startStreaming(for: "user-id") // Start streaming data sdk.events.sink { event in ... } // Receive samples via Combine Event types: .lifecycle(.discovered/.connected/.disconnected), .sample(deviceId, sample), .deviceState(device, connection, stream) Please create a SwiftUI app with: 1. A device list view showing discovered BLE devices 2. Tap to connect, showing connection state 3. A real-time heart rate display from .sample events 4. Start/stop streaming controls ``` ## Lovable: Full Wellness App [#lovable-full-wellness-app] Works with: Lovable ``` Build a wellness app with AnyBio health widgets using Supabase for auth. Follow this integration pattern: 1. Create a Supabase Edge Function at supabase/functions/anybio-auth/index.ts that verifies the Supabase session, then calls POST https://api.anybio.io/api/v1/sdk/token with Authorization: Bearer [ANYBIO_API_KEY from secrets] and body { external_id: user.id, project_key: [ANYBIO_PROJECT_KEY from secrets] } 2. Add BioUI Web CDN to the app: 3. After login, call the Edge Function to get a token, then set it on 4. Add these pages: - Dashboard: + + - Check-in: for daily health questionnaires - Journal: for daily reflections The app should have a clean, modern design with a bottom tab bar. ``` ## API: Fetch Episodes and Artifacts [#api-fetch-episodes-and-artifacts] Works with: Cursor, Claude Code, ChatGPT ``` I need to fetch clinical episode data from AnyBio's REST API. Base URL: https://api.anybio.io Auth: Authorization: Bearer org_MY_KEY (or xuser token for user-scoped access) Endpoints: - GET /api/v1/episodes?xuser_id={id}&from={iso}&to={iso} → list of episodes - GET /api/v1/episodes/{id} → episode detail with artifacts array - Each artifact has: type (ecg_strip_pdf, fhir_bundle, etc.), signed_url, created_at Please create: 1. A TypeScript API client with functions for listEpisodes() and getEpisode() 2. An episodes list component showing type, duration, attention score (0-100) 3. An episode detail view with artifact download links 4. Error handling for 401 (auth) and 404 (not found) My framework is [specify yours]. ``` ## API: Usage Dashboard [#api-usage-dashboard] Works with: Cursor, Claude Code ``` I need to display AnyBio BPU (Biosignal Processing Unit) usage for my organization. Endpoint: GET https://api.anybio.io/api/v1/admin/usage/summary?window=30d Auth: Authorization: Bearer org_MY_KEY (admin scope required) Response shape: { plan: { tier: "sandbox", tier_display: "Sandbox", included_credits: 25000 }, credits: { total, data_ingest, session_time, signal_processing, ai_usage, model_inference, clinical_output }, ingest_gb: number, session_hours: number, active_xusers: number } Please create a usage dashboard page with: 1. A progress bar showing total BPU used vs plan limit 2. Category breakdown cards (Data Ingest, Session Time, AI/ML, Clinical Output, Active Users) 3. Color-coded progress (blue <75%, amber 75-90%, red >90%) ``` ## Program Builder: Conversational AI [#program-builder-conversational-ai] Works with: Cursor, Claude Code, ChatGPT ``` AnyBio has a Program Builder Agent: a conversational AI that helps design detection programs (what biosignal patterns to detect and when to create clinical episodes). The endpoint is: - POST /api/v1/profile-authoring/start → starts a new conversation - POST /api/v1/profile-authoring/conversations/{id}/stream → SSE streaming Please create a chat interface that: 1. Starts a program builder session on mount 2. Displays the streaming AI responses in real time (SSE) 3. Shows a text input for the user to describe what they want to detect 4. Has a clean chat bubble UI with user messages on the right, AI on the left Example opening prompt to the agent: "I want to detect when a patient's heart rate stays above 100 bpm for more than 10 minutes while they're at rest (low motion). This should create a resting tachycardia episode." ``` ## Tips for All Tools [#tips-for-all-tools] 1. **Always specify your framework** — the same task produces very different code for Next.js vs. plain HTML vs. SwiftUI 2. **Mention "server-side"** for token minting — AI tools default to client-side code which leaks keys 3. **Paste errors back** — AI tools are great at debugging their own output with AnyBio context 4. **Start with one widget** — get `` working first, then add more 5. **Reference the docs** — tell the tool to read `https://developer.anybio.io` for details ## Next Steps [#next-steps] * [Cursor Integration](/docs/bioui-web/guides/cursor-integration) — Full Cursor setup with `.cursorrules` * [Claude Code Integration](/docs/bioui-web/guides/claude-code-integration) — Full Claude Code setup with `ANYBIO.md` * [Lovable Integration](/docs/bioui-web/guides/lovable-integration) — Lovable-specific setup with Supabase # Authentication (/docs/bioui-web/guides/authentication) BioUI Web supports two auth modes. Choose the one that fits your architecture: | Mode | Best For | API Key in Browser? | | ---------------------------- | ----------------------------------------------------------------------------------------- | -------------------------- | | **Token mode** (recommended) | Production apps, third-party integrations, HIPAA-compliant deployments | No — key stays server-side | | **Direct mode** | Internal tools, prototyping, server-rendered apps where the key is injected at build time | Yes — visible in HTML | ## Token Mode (Recommended) [#token-mode-recommended] Your backend mints a short-lived JWT for the current user. The browser only sees a scoped, time-limited token — never your API key. ```html ``` ### How It Works [#how-it-works] 1. Your user logs in to your app (using your own auth — Supabase, Auth0, Firebase, etc.) 2. Your backend calls `POST /api/v1/sdk/token` with your Write-scoped API key and the user's external ID 3. AnyBio returns a short-lived JWT (1-hour expiry, RS256-signed, scoped to that user) 4. Your frontend passes the JWT to `` via `xuser-token` 5. All widget API calls use the JWT — no API key in the browser ### Backend Token Request [#backend-token-request] ```bash POST https://api.anybio.io/api/v1/sdk/token Authorization: Bearer org_your_write_key Content-Type: application/json { "external_id": "user-123", "project_key": "proj_your_key" } ``` Response: ```json { "xuser_token": "eyJhbGciOiJSUzI1NiJ9...", "xuser_id": "550e8400-e29b-41d4-a716-446655440000", "expires_in": 3600 } ``` ### Token Refresh [#token-refresh] The provider emits an `anybio:token:expiring` event 5 minutes before expiry. Listen for it and fetch a new token: ```javascript document.addEventListener('anybio:token:expiring', async () => { const { xuser_token } = await fetchNewToken(currentUser.id) document.getElementById('bio').setAttribute('xuser-token', xuser_token) }) ``` ### Platform Guides [#platform-guides] * [Lovable Integration](/docs/bioui-web/guides/lovable-integration) — Supabase Edge Function + Lovable ### `sdk-key` [#sdk-key] A public identifier for your project. Safe to include in browser HTML. It does not grant API access on its own. ### `xuser-token` [#xuser-token] A short-lived JWT returned by `POST /api/v1/sdk/token`. Contains the user's AnyBio UUID, project, and organization — all verified server-side on every request. *** ## Direct Mode [#direct-mode] For internal tools or prototyping, you can pass credentials directly. The API key is visible in the HTML source. ```html ``` ### `api-key` [#api-key] Your organization's developer key. Sent as `Authorization: Bearer ` on every API request. You can generate and rotate API keys in the AnyBio dashboard under **Settings > API Keys**. ### `project-key` [#project-key] Identifies the project within your organization. Each project has its own questionnaires, scoring rules, and user data. ### `xuser-id` [#xuser-id] Your application's external user identifier (e.g., a database primary key, Firebase UID, or Auth0 subject). When `` initializes, it calls `POST /api/v1/xusers` with the `xuser-id` value. The server resolves (or creates) the corresponding AnyBio UUID. *** ## Identity Ownership [#identity-ownership] Your application owns the user's identity in both modes. BioUI Web never shows a login screen or stores credentials on the client. This means: * **You control who has access.** Only users that your app authenticates can interact with the widgets. * **You can use any auth provider.** Firebase, Auth0, Cognito, Supabase, a custom backend — it does not matter. * **User data is scoped.** Each user under a given project maps to exactly one AnyBio user record. ## Security Considerations [#security-considerations] | Concern | Token Mode | Direct Mode | | ----------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------- | | API key exposure | Key stays server-side. Only a short-lived JWT reaches the browser. | Key is visible in HTML. Use scoped permissions and rotate regularly. | | Token lifetime | 1-hour JWT with automatic expiry warning event. | N/A — API key doesn't expire (rotate manually). | | Cross-user access | Impossible — JWT is scoped to one user. | Depends on API key scope. | | HTTPS | Always required. CDN and API calls use HTTPS. | Same. | | CORS | Wildcard origin allowed for JWT-authenticated requests. | Requires registered domain in ALLOWED\_ORIGINS. | # Bolt Integration (/docs/bioui-web/guides/bolt-integration) Add health widgets to your Bolt app in three steps. This guide walks through setting up the BioUI Web SDK inside a Bolt (bolt.new) project using a Next.js API route for secure token-based authentication. ## Prerequisites [#prerequisites] * An AnyBio account with a **project key** (`proj_*`) and a **Write-scoped API key** (`org_*`) * A Bolt project using the **Next.js** template (recommended) or any framework that supports server-side routes ## Architecture [#architecture] ``` Bolt App (Next.js in StackBlitz WebContainer) │ │ 1. User interacts with your app │ │ 2. App calls Next.js API route │ POST /api/anybio-auth │ │ 3. API route calls AnyBio (server-side, secret key) │ POST https://api.anybio.io/api/v1/sdk/token │ Authorization: Bearer org_your_write_key │ │ 4. AnyBio returns short-lived JWT (1 hour) │ { xuser_token: "eyJ...", xuser_id: "...", expires_in: 3600 } │ │ 5. App passes token to │ │ │ 6. Widgets make API calls using the JWT │ Authorization: Bearer eyJ... ``` Your Write-scoped API key never leaves the server. The browser only sees a short-lived, user-scoped JWT. ## Step 1: Create the API Route [#step-1-create-the-api-route] In your Bolt project, create a Next.js API route that mints AnyBio xuser tokens. Create the file `app/api/anybio-auth/route.ts`: ```typescript import { NextRequest, NextResponse } from "next/server"; const ANYBIO_API_KEY = process.env.ANYBIO_API_KEY!; const ANYBIO_PROJECT_KEY = process.env.ANYBIO_PROJECT_KEY!; export async function POST(request: NextRequest) { // In production, verify the caller is authenticated with your app's auth. // For prototyping in Bolt, you can use a simple user ID. const { userId } = await request.json(); if (!userId) { return NextResponse.json({ error: "Missing userId" }, { status: 400 }); } const res = await fetch("https://api.anybio.io/api/v1/sdk/token", { method: "POST", headers: { Authorization: `Bearer ${ANYBIO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ external_id: userId, project_key: ANYBIO_PROJECT_KEY, }), }); if (!res.ok) { const body = await res.text(); return NextResponse.json( { error: `AnyBio error: ${body}` }, { status: 502 } ); } const data = await res.json(); return NextResponse.json(data); } ``` Then add your secrets to the Bolt project's `.env.local`: ``` ANYBIO_API_KEY=org_your_write_key_here ANYBIO_PROJECT_KEY=proj_your_project_key_here ``` > **Bolt tip:** In Bolt's StackBlitz environment, create `.env.local` via the file explorer. Environment variables prefixed with `NEXT_PUBLIC_` are exposed to the browser — your API key does NOT have that prefix, so it stays server-side. ## Step 2: Add the SDK to Your App [#step-2-add-the-sdk-to-your-app] In your Bolt project's root layout or `app/layout.tsx`, add the BioUI Web SDK: ```tsx import Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children}

Daily Check-in

Thanks! Your check-in has been recorded.
``` ## What's Next [#whats-next] * [`` Reference](/docs/bioui-web/components/bio-checkin) — Full attribute and event reference * [``](/docs/bioui-web/components/bio-provider) — Show the user's progress after check-in * [Authentication](/docs/bioui-web/guides/authentication) — Credential and identity details # Claude Code Integration (/docs/bioui-web/guides/claude-code-integration) Use Claude Code (Anthropic's coding agent) to build AnyBio-powered apps. This guide covers setting up an `ANYBIO.md` context file so Claude Code understands the platform, and prompt patterns for common tasks. ## Prerequisites [#prerequisites] * An AnyBio account with a **project key** (`proj_*`) and an **API key** (`org_*`) * [Claude Code](https://claude.ai/code) CLI installed * A project you want to add biosignal capabilities to ## Step 1: Add `ANYBIO.md` [#step-1-add-anybiomd] Create an `ANYBIO.md` file at your project root. Claude Code automatically reads markdown files as context. Download the template: [`ANYBIO.md` for Claude Code](/templates/ANYBIO.md) Or create it manually: ```markdown # AnyBio Integration ## What is AnyBio? Biosignal infrastructure platform. Handles BLE device connectivity, real-time signal processing, FHIR output, and clinical AI. You build the product — AnyBio handles the pipeline. ## Authentication - `org_*` key: Server-side only. Used to mint tokens and manage resources. - `proj_*` key: Identifies your project. Used alongside org key. - `sdk_*` key: Browser-safe. Limited to bootstrap and widget initialization. - XUser token: Short-lived JWT (1hr). Minted server-side via POST /api/v1/sdk/token. IMPORTANT: Never put org_* keys in client-side code. ## API Base URL https://api.anybio.io ## Key Endpoints - POST /api/v1/sdk/token — Mint xuser token (body: { external_id, project_key }) - GET /api/v1/xusers — List users - GET /api/v1/episodes — List episodes (query: xuser_id, from, to) - GET /api/v1/episodes/:id — Episode detail with artifacts - POST /api/v1/charts/query — Chart data query - GET /api/v1/admin/usage/summary — BPU usage summary - OpenAPI spec: https://api.anybio.io/api-docs ## BioUI Web (browser widgets) CDN-based, zero build step. Add to any HTML page: Available widgets: bio-health-metrics, bio-mood, bio-checkin, bio-journal, bio-task-list, bio-progress-ring, bio-trend-card, bio-trend-chart ## BioSDK (iOS) Swift Package: https://github.com/anybio/biosdk-ios-binary.git import BioSDK let sdk = try await BioSDKClient.initialize( configuration: BioSDKClient.Configuration(organizationKey: "org_*", projectKey: "proj_*")) sdk.startScan() sdk.connect(sdk.discoveredDevices.first!) sdk.startStreaming(for: "user-id") ## Security Rules - org_* keys are server-side only - Mint xuser tokens via server-side route, pass to client - All /api/v1/* endpoints require authentication ``` ## Step 2: Use Claude Code with AnyBio [#step-2-use-claude-code-with-anybio] With `ANYBIO.md` in your project, Claude Code has full context. Start a session and try: ### Adding BioUI Web widgets [#adding-bioui-web-widgets] ``` Add AnyBio health monitoring widgets to my Next.js app. Create a server-side API route at /api/anybio-token that mints xuser tokens, then add a dashboard page with bio-health-metrics and bio-mood widgets that initialize after login. ``` ### Building an iOS app [#building-an-ios-app] ``` Create a SwiftUI view that uses BioSDK to scan for BLE devices, display them in a list, connect on tap, and show real-time heart rate. Use the events publisher to handle .sample events. ``` ### Fetching episodes and clinical data [#fetching-episodes-and-clinical-data] ``` Add an episodes page that fetches episodes from the AnyBio API for the current user. Show each episode's type, duration, attention score, and link to any artifact PDFs. Use the /api/v1/episodes endpoint. ``` ### Creating a usage dashboard [#creating-a-usage-dashboard] ``` Add a usage page that shows the org's BPU consumption. Call /api/v1/admin/usage/summary and display a progress bar against the plan limit, plus a breakdown by category (data ingest, session time, AI, clinical output). ``` ## Step 3: Using with `/docs` Context [#step-3-using-with-docs-context] You can tell Claude Code to fetch the docs directly: ``` Read the AnyBio BioUI Web quickstart at https://developer.anybio.io/bioui-web/quickstart and implement that pattern in my app. ``` Or reference the OpenAPI spec: ``` Fetch the OpenAPI spec at https://api.anybio.io/api-docs and create TypeScript types for the Episode and XUser models. ``` ## Tips [#tips] * **Claude Code reads `ANYBIO.md` automatically** — keep it at your project root * **Be specific about server vs. client** — say "create a server-side API route" to prevent key leakage * **Iterate on errors** — paste error output and Claude Code will fix with AnyBio context * **Use for boilerplate** — token minting routes, widget initialization, data fetching are great tasks * **Review security** — always verify that `org_*` keys are not in client bundles ## Troubleshooting [#troubleshooting] | Issue | Solution | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Claude Code doesn't know AnyBio APIs | Check that `ANYBIO.md` is at your project root, not in a subdirectory. | | Generated code puts `org_*` key in client | Tell it: "The org key must stay server-side. Create an API route to mint xuser tokens." | | Authentication errors (401/403) | Verify you're using the right key type. Widget endpoints need xuser tokens, admin endpoints need org keys. | | Widgets don't render | Ensure CDN script is loaded before widgets, and `` wraps all widget elements. | ## Next Steps [#next-steps] * [AI Prompt Templates](/docs/bioui-web/guides/ai-prompt-templates) — Copy-paste prompts for common tasks * [BioUI Web Quickstart](/docs/bioui-web/quickstart) — Manual setup * [BioSDK Quickstart](/docs/biosdk/quickstart) — iOS native integration * [Cursor Integration](/docs/bioui-web/guides/cursor-integration) — Cursor IDE setup # Cursor Integration (/docs/bioui-web/guides/cursor-integration) Use Cursor's AI agent to build AnyBio-powered apps. This guide covers setting up `.cursorrules` so Cursor understands the AnyBio platform, and prompt patterns that produce working integrations. ## Prerequisites [#prerequisites] * An AnyBio account with a **project key** (`proj_*`) and an **API key** (`org_*`) * [Cursor](https://cursor.sh) installed * A project you want to add biosignal capabilities to ## Step 1: Add `.cursorrules` [#step-1-add-cursorrules] Create a `.cursorrules` file at your project root. This gives Cursor's AI context about AnyBio's APIs and conventions. Download the template: [`.cursorrules` for AnyBio](/templates/anybio.cursorrules) Or create it manually: ````text # AnyBio Integration Context ## Platform AnyBio is a biosignal infrastructure platform. It handles BLE device connectivity, real-time signal processing, FHIR output, and clinical AI — so you build the product, not the pipeline. ## Authentication Model - Organization API key: `org_*` (server-side only, never expose to browser) - Project key: `proj_*` (identifies the project) - SDK key: `sdk_*` (browser-safe, limited scope) - XUser token: short-lived JWT for end-user access (minted server-side) ## Key API Endpoints (base: https://api.anybio.io) - POST /api/v1/sdk/token — Mint xuser token (requires org_* key, server-side) - GET /api/v1/xusers — List users in project - GET /api/v1/episodes — List episodes for a user - GET /api/v1/episodes/:id — Episode detail with artifacts - POST /api/v1/charts/query — Query chart data - GET /api/v1/bootstrap — SDK bootstrap config (device profiles) - POST /ingest/raw-packet — Protobuf biosignal ingest (SDK handles this) ## BioSDK (iOS) — BLE device streaming ```swift import BioSDK let sdk = try await BioSDKClient.initialize( configuration: BioSDKClient.Configuration(organizationKey: "org_*", projectKey: "proj_*")) sdk.startScan() sdk.connect(sdk.discoveredDevices.first!) sdk.startStreaming(for: "user-id") ```` ## BioUI Web — Browser widgets (CDN, zero build step) [#bioui-web--browser-widgets-cdn-zero-build-step] ```html ``` ## Available BioUI Web Widgets [#available-bioui-web-widgets] * `` — Wearable data (steps, sleep, HR, stress) * `` — Mood tracking * `` — PRO questionnaires * `` — Daily journaling * `` — Task tracking * `` — Circular progress * `` — Single biosignal trend * `` — Multi-biosignal trends ## Security Rules [#security-rules] * NEVER put org\_\* keys in client-side code (browser, mobile app bundles) * Use sdk\_\* or xuser tokens for client-side authentication * Mint xuser tokens server-side (edge function, API route, serverless function) * All API calls to /api/v1/\* require authentication ## Documentation [#documentation] * Full docs: [https://developer.anybio.io](https://developer.anybio.io) * OpenAPI spec: [https://api.anybio.io/api-docs](https://api.anybio.io/api-docs) * BioSDK iOS: [https://developer.anybio.io/biosdk/quickstart](https://developer.anybio.io/biosdk/quickstart) * BioUI Web: [https://developer.anybio.io/bioui-web/quickstart](https://developer.anybio.io/bioui-web/quickstart) ``` ## Step 2: Use Cursor with AnyBio With `.cursorrules` in place, Cursor's AI understands AnyBio's APIs. Try these prompts: ### Adding BioUI Web widgets to an existing app > Add AnyBio health widgets to my dashboard page. Use the BioUI Web CDN. Create a server-side route at /api/anybio-token that mints xuser tokens using my org key, then initialize the widgets client-side after login. ### Building an iOS app with BioSDK > Create a SwiftUI view that scans for BLE devices, shows a list, and lets the user tap to connect and start streaming. Use BioSDK. Show real-time heart rate from the .sample events. ### Querying episode data > Add an episodes list page that fetches from /api/v1/episodes for the current user, shows episode type, duration, and attention score. Include a detail view that displays the episode timeline and any artifact PDFs. ## Step 3: Point to the OpenAPI Spec For Cursor's `@docs` feature, you can reference the AnyBio OpenAPI spec directly: 1. Open Cursor Settings > Features > Docs 2. Add: `https://api.anybio.io/api-docs` 3. Now use `@AnyBio` in prompts to reference the full API schema ## Tips - **Start with BioUI Web** if you're building a web app — it's CDN-based with zero build step - **Start with BioSDK** if you're building a native iOS app - **Always create tokens server-side** — Cursor sometimes puts API keys in client code; review and move to a server route - **Use `@docs`** to let Cursor read the OpenAPI spec for endpoint details - **Iterate in Composer** — paste error messages back and Cursor will fix them with AnyBio context ## Troubleshooting | Issue | Solution | |---|---| | Cursor puts `org_*` key in React code | Tell it: "Move the AnyBio org key to a server-side API route. Only use sdk_* or xuser tokens client-side." | | Widgets don't render | Ensure `bio-provider` wraps all widgets and has valid credentials. Check browser console. | | Cursor doesn't know AnyBio APIs | Verify `.cursorrules` is at your project root. Try `@docs` with the OpenAPI URL. | | Authentication errors (401) | Check that you're using the right key type for the endpoint. SDK endpoints need `org_*`, widget endpoints need xuser tokens. | ## Next Steps - [AI Prompt Templates](/docs/bioui-web/guides/ai-prompt-templates) — Copy-paste prompts for common integration tasks - [BioUI Web Quickstart](/docs/bioui-web/quickstart) — Manual setup without AI tools - [BioSDK Quickstart](/docs/biosdk/quickstart) — iOS native integration - [Lovable Integration](/docs/bioui-web/guides/lovable-integration) — Lovable-specific setup ``` # Lovable Integration (/docs/bioui-web/guides/lovable-integration) Add health widgets with HIPAA-compliant architecture to your Lovable app in three steps. This guide walks through setting up the BioUI Web SDK with Lovable's Supabase backend for secure token-based authentication. ## Prerequisites [#prerequisites] * An AnyBio account with a **project key** (`proj_*`) and a **Write-scoped API key** (`org_*`) * A Lovable project with Supabase connected ## Architecture [#architecture] ``` Lovable App (browser) │ │ 1. User logs in via your app's auth │ │ 2. App calls Supabase Edge Function │ POST /functions/v1/anybio-auth │ │ 3. Edge Function calls AnyBio (server-side, secret key) │ POST https://api.anybio.io/api/v1/sdk/token │ Authorization: Bearer org_your_write_key │ │ 4. AnyBio returns short-lived JWT (1 hour) │ { xuser_token: "eyJ...", xuser_id: "...", expires_in: 3600 } │ │ 5. App passes token to │ │ │ 6. Widgets make API calls using the JWT │ Authorization: Bearer eyJ... ``` Your Write-scoped API key never leaves the server. The browser only sees a short-lived, user-scoped JWT. ## Step 1: Create the Supabase Edge Function [#step-1-create-the-supabase-edge-function] In your Lovable project, create a Supabase Edge Function that mints AnyBio xuser tokens. Create the file `supabase/functions/anybio-auth/index.ts`: ```typescript import { serve } from 'https://deno.land/std/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' serve(async (req) => { // Verify the caller is authenticated with your Supabase project const authHeader = req.headers.get('Authorization') if (!authHeader) { return new Response(JSON.stringify({ error: 'Missing auth' }), { status: 401 }) } const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!, { global: { headers: { Authorization: authHeader } } } ) const { data: { user }, error: authError } = await supabase.auth.getUser() if (authError || !user) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 }) } // Mint an AnyBio xuser token for this user const res = await fetch('https://api.anybio.io/api/v1/sdk/token', { method: 'POST', headers: { 'Authorization': `Bearer ${Deno.env.get('ANYBIO_API_KEY')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ external_id: user.id, project_key: Deno.env.get('ANYBIO_PROJECT_KEY'), }), }) if (!res.ok) { const body = await res.text() return new Response(JSON.stringify({ error: `AnyBio error: ${body}` }), { status: 502 }) } const data = await res.json() return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' }, }) }) ``` Then store your secrets: ```bash supabase secrets set ANYBIO_API_KEY=org_your_write_key_here supabase secrets set ANYBIO_PROJECT_KEY=proj_your_project_key_here ``` Deploy the function: ```bash supabase functions deploy anybio-auth ``` ## Step 2: Add the SDK to Your App [#step-2-add-the-sdk-to-your-app] In your Lovable app's `index.html`, add the BioUI Web SDK: ```html ``` Then add the provider and widgets to your page: ```html ``` The widgets won't render until a valid `xuser-token` is set on the provider. ## Step 3: Initialize After Login [#step-3-initialize-after-login] After your user authenticates, call the Edge Function to get a token and pass it to the provider: ```typescript import { supabase } from './supabaseClient' async function initBioWidgets() { const { data: { session } } = await supabase.auth.getSession() if (!session) return const { data, error } = await supabase.functions.invoke('anybio-auth', { headers: { Authorization: `Bearer ${session.access_token}` }, }) if (error) { console.error('Failed to get AnyBio token:', error) return } const provider = document.getElementById('bio') provider?.setAttribute('xuser-token', data.xuser_token) } // Call after login or on app load if session exists initBioWidgets() ``` ## Token Refresh [#token-refresh] Tokens expire after 1 hour. Listen for the expiry warning and refresh automatically: ```typescript document.addEventListener('anybio:token:expiring', async () => { const { data: { session } } = await supabase.auth.getSession() if (!session) return const { data } = await supabase.functions.invoke('anybio-auth', { headers: { Authorization: `Bearer ${session.access_token}` }, }) if (data?.xuser_token) { document.getElementById('bio')?.setAttribute('xuser-token', data.xuser_token) } }) ``` ## Working with Lovable's AI Agent [#working-with-lovables-ai-agent] When using Lovable's AI coding agent, you can point it to this documentation. Here's a prompt template: > Add AnyBio BioUI Web health widgets to my app. Follow the integration guide at [https://developer.anybio.io/bioui-web/guides/lovable-integration](https://developer.anybio.io/bioui-web/guides/lovable-integration). The SDK CDN URL is [https://cdn.anybio.io/biosdk-web/v1/bioui.js](https://cdn.anybio.io/biosdk-web/v1/bioui.js). My project key ID is `proj_XXX`. Create a Supabase Edge Function called `anybio-auth` that mints xuser tokens, then add `` and `` widgets to the dashboard page. ## Available Widgets [#available-widgets] | Widget | Tag | Purpose | | -------------- | ---------------------- | ------------------------------------------------ | | Health Metrics | `` | Wearable data (steps, sleep, heart rate, stress) | | Journal | `` | Daily journaling with prompts | | Mood | `` | Mood tracking with configurable scales | | Check-in | `` | PRO questionnaires | | Task List | `` | Daily task tracking | | Progress Ring | `` | Circular progress visualization | | Trend Card | `` | Single biosignal trend summary | | Trend Chart | `` | Multi-biosignal trend charts | ## Security Notes [#security-notes] * Your AnyBio Write-scoped API key (`org_*`) stays in the Supabase Edge Function — never exposed to the browser * The `xuser-token` is a short-lived JWT (1-hour expiry, RS256-signed) * Each token is scoped to a single user — cross-user access is impossible * The Edge Function verifies Supabase auth before minting a token, so only your authenticated users can get AnyBio access ## Troubleshooting [#troubleshooting] | Issue | Solution | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | Widgets show "Loading..." indefinitely | Check that `xuser-token` is set on the provider. Open DevTools and verify the Edge Function returns a token. | | Edge Function returns 401 | Verify `ANYBIO_API_KEY` is set correctly in Supabase secrets. The key must have Write scope. | | CORS errors | The AnyBio API allows cross-origin requests when using JWT tokens. Make sure you're using `xuser-token` mode, not `api-key` mode. | | Token expired | Listen for the `anybio:token:expiring` event and refresh. See [Token Refresh](#token-refresh) above. | # Custom Algorithms (/docs/dsp/guides/custom-algorithms) Most applications get what they need from the [built-in modality extractors](/docs/dsp/guides/signal-processing-pipeline) - HR, HRV, PRV, plus the discrete biosignals in the [catalog](/docs/biosignals/catalog). For applications that need something specific (a custom feature, a proprietary algorithm, an FDA-cleared metric from a partner), the platform supports running custom DSP algorithms as part of the pipeline. ## Two ways custom algorithms reach your stream [#two-ways-custom-algorithms-reach-your-stream] There are two paths a custom algorithm reaches your application: 1. **Algorithm marketplace.** Algorithms published by AnyBio's algorithm partners. You opt your organization in via an entitlement; from then on, the algorithm runs against your matching streams alongside the built-in extractors. Outputs surface as feature series and observations like everything else. See [Algorithm Marketplace](/docs/algorithm-marketplace) (gated availability). 2. **Direct authoring.** For applications building proprietary signal-processing logic, the platform supports authoring and running your own algorithms scoped to your organization. This is in private preview. [Contact us](https://anybio.io/contact) if you have a custom algorithm in mind. ## What runs the algorithm [#what-runs-the-algorithm] Custom algorithms run inside a sandboxed runtime on the AnyBio platform - not on the device, not in the browser. This keeps clients lightweight and ensures the algorithm has access to the full stream history (filters that need a few seconds of warm-up, rolling windows that span minutes, etc.). Algorithms declare: * **Input signals** - which biosignals from the catalog they consume (e.g., `ECG`, `PPG`, or a combination). * **Sampling rate** - the rate at which the algorithm expects input samples. * **Output signals** - the feature series the algorithm produces, with names, units, and cadences. The platform routes a stream to a custom algorithm whenever the stream contains the algorithm's declared input signals. Outputs land on the same `/streams/{streamId}/features` endpoint as built-in features. ## Provenance and traceability [#provenance-and-traceability] Outputs from custom algorithms are tagged with a `method_code` identifying the algorithm and its version, the same way built-in algorithms are tagged: ```json { "biosignal_id": 12, "value": 32, "unit": "ms", "method_code": "algorithm:partner_hrv_v2" } ``` This gives downstream consumers (clinical reviewers, compliance audits, research workflows) a clear record of which algorithm produced each value. Versions advance when the algorithm is updated; historical values keep their original method code so prior analyses remain reproducible. ## Verification [#verification] For algorithms published through the marketplace, the platform exposes a public-key endpoint that lets entitled customers verify the algorithm binary they're executing matches what the publisher signed: ```http GET /api/v1/algorithms/{algorithmId}/public-key ``` Returns the public key in PEM form. Combined with the algorithm's signature metadata, this gives external auditors a way to confirm that the algorithm bytes haven't been altered between the publisher and the runtime. See [Algorithm Marketplace](/docs/algorithm-marketplace) for the full verification flow. ## Limits and constraints [#limits-and-constraints] Custom algorithms run in a sandboxed runtime on the platform, and each execution returns at most 16 KB of output. For heavier workloads (model inference on long windows, multi-stream fusion at high rates), [contact us](https://anybio.io/contact) to discuss the right execution path. ## Quality and validation [#quality-and-validation] Quality masks are attached by the built-in extractors (see [Signal Processing Pipeline](/docs/dsp/guides/signal-processing-pipeline)). Custom algorithm outputs do not carry a `qmask`; each value is stored with the algorithm's identity and version, and values outside the catalog's valid range for the output biosignal are rejected. # Getting Started (/docs/dsp/guides/getting-started) This guide walks through the practical case: a stream is flowing, the DSP layer is running, and your application wants the heart rate, HRV, or other derived features that come out of it. If you haven't yet captured a stream, start with the [BioSDK quickstart](/docs/biosdk/quickstart). DSP runs automatically against any incoming waveform stream; you don't need to enable it. ## Querying a feature series [#querying-a-feature-series] Every signal stream that produces derived features exposes them through a single endpoint: ```http GET /api/v1/streams/{streamId}/features ``` ```typescript import { fetchStreamFeatures } from "@anybio/sdk"; const features = await fetchStreamFeatures(streamId); const hr = features.find((f) => f.name === "hr_ecg"); if (hr) { console.log(`${hr.values.length} HR samples at ${hr.cadence_hz} Hz`); console.log(`Latest: ${hr.values[hr.values.length - 1]} bpm`); } ``` Each feature series carries five fields: ```typescript type FeatureSeries = { name: string; // "hr_ecg", "hrv_rmssd", "prv_sdnn_5min", ... ts_ns: number[]; // nanosecond timestamps, one per value values: number[]; // feature values cadence_hz: number; // output sample rate qmask: number[]; // quality mask, one per value (0=good, 1=suspect, 2=bad) }; ``` The timestamps are nanosecond-precision and aligned with the source stream's clock, so feature series can be plotted alongside the raw waveform without resampling. ## What features come from what biosignal [#what-features-come-from-what-biosignal] Map a biosignal abbreviation to its feature outputs: | If your stream contains... | You get these features | | ------------------------------------------------------- | ----------------------------------------------------------------------- | | ECG | `hr_ecg`, `hrv_rmssd`, `hrv_sdnn_5min` | | PPG | `hr_ppg`, `prv_rmssd`, `prv_sdnn_5min` | | EDA, TEMP, HR (device-reported), other discrete signals | Direct observations - see [Biosignals: Lookup](/docs/biosignals/lookup) | Discrete signals (anything emitted by the device at 1 Hz or slower) skip DSP and become observations directly. ## Reading the quality mask [#reading-the-quality-mask] Every feature value has a `qmask` byte: | Value | Meaning | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `0` | Good. Use the value. | | `1` | Suspect. The value was interpolated because no beats were detected nearby, or its window had too few beats to compute it. Use with caution; consider downweighting. | | `2` | Bad. The value is outside the physiological range for the feature. Skip in aggregations. | A simple consumer pattern: ```typescript const cleanValues = hr.values.filter((_, i) => hr.qmask[i] === 0); const meanHR = cleanValues.reduce((a, b) => a + b, 0) / cleanValues.length; ``` ## Live vs. historical [#live-vs-historical] The same `/streams/{streamId}/features` endpoint works for both live and completed streams. For live streams, poll periodically or subscribe to updates via WebSocket. For completed streams, the full feature series is available as soon as the stream closes. For raw waveform data in real time, subscribe to the WebSocket feed - see [BioSDK streaming](/docs/biosdk/guides/streaming). ## Provenance [#provenance] Every feature value is tagged with the algorithm that produced it. When the same feature is later persisted as an observation, the `method_code` field records this: ```json { "biosignal_id": 1, "value": 72, "method_code": "algorithm:ecg_v1" } ``` The version suffix advances when an algorithm is updated. Historical values keep their original `method_code` so reanalysis is reproducible. # Signal Processing Pipeline (/docs/dsp/guides/signal-processing-pipeline) This guide explains the DSP architecture - generic primitives, modality extractors, and how they compose. You don't need to know any of this to consume the output (see [Getting Started](/docs/dsp/guides/getting-started)), but it helps when reasoning about latency, fidelity, or extending the platform. ## Pipeline at a glance [#pipeline-at-a-glance] ``` raw stream ─┐ ├─► modality extractor ─► feature series ─► observations device meta ─┘ (qmask attached) (FHIR-aligned) ``` A waveform stream lands at the platform via the ingest endpoint. The platform routes the stream to the modality extractor that matches its signal type (ECG, PPG, etc.). The extractor produces one or more named feature series, each at its own cadence, each with a quality mask. Feature series are persisted as observations on the same biosignal catalog you query elsewhere. ## Generic primitives [#generic-primitives] Modality extractors are built on a small set of generic primitives. These are reusable across signal types. ### Butterworth filters [#butterworth-filters] Zero-phase bidirectional Butterworth filters for both bandpass and lowpass. Zero-phase means the filter is applied forward and backward, so the output has no phase distortion - important when downstream stages do peak detection or alignment. Typical bands: * ECG: 5-20 Hz bandpass (isolates the QRS complex while suppressing baseline wander and high-frequency noise). * PPG: 0.5-4 Hz bandpass (covers the cardiac component while rejecting respiration and motion). ### Peak detection [#peak-detection] Adaptive-threshold peak finder. The threshold tracks the local signal envelope, so it works across changing amplitudes (different subjects, electrode placements, perfusion levels). Used for R-peaks (ECG) and pulse feet (PPG). A refractory period prevents double-counting: 300 ms for ECG, 250 ms for PPG. These map to upper bounds on heart rate of about 200 bpm and 240 bpm respectively. ### Spectral analysis [#spectral-analysis] FFT-based primitives for periodogram and band-power calculations. Used selectively where time-domain methods aren't appropriate (e.g., respiratory rate estimation from waveform modulation). ## Modality extractors [#modality-extractors] A modality extractor is a pipeline that consumes one signal type and emits a fixed set of features. ### ECG extractor [#ecg-extractor] ``` ECG (250-500 Hz) → Butterworth 5-20 Hz bandpass → Pan-Tompkins R-peak detection → R-R intervals ├→ hr_ecg (1 Hz) ├→ hrv_rmssd (0.2 Hz, 5s rolling window) └→ hrv_sdnn_5min (0.2 Hz, 5min rolling window) ``` Pan-Tompkins is the standard production algorithm for QRS detection. Output features are time-stamped at the centroid of their window, so they align cleanly with the original waveform. ### PPG extractor [#ppg-extractor] ``` PPG (100-250 Hz) → Butterworth 0.5-4 Hz bandpass → Pulse-foot detection (adaptive peak finder, inverted) → Pulse-to-pulse intervals ├→ hr_ppg (1 Hz) ├→ prv_rmssd (0.2 Hz, 5s rolling) └→ prv_sdnn_5min (0.2 Hz, 5min rolling) ``` PPG pulse foot - the trough preceding the systolic upstroke - is the most stable fiducial under varying perfusion. Output structure mirrors ECG so the two can be cross-validated when both are available on the same subject. ## Quality mask propagation [#quality-mask-propagation] Every feature value from a built-in extractor carries a `qmask` byte (0 = good, 1 = suspect, 2 = bad), computed from checks in the extractor: * **Suspect (1):** the value was interpolated because no beats were detected nearby, or its window had too few beats. * **Bad (2):** the value is outside the physiological range for the feature. Suspect or bad values are still emitted - downstream consumers can choose whether to filter them - but they're never silently dropped. ## Output format [#output-format] Every modality extractor emits one or more `FeatureSeries`: ```typescript type FeatureSeries = { name: string; // unique per extractor (e.g., "hr_ecg") ts_ns: number[]; // nanosecond timestamps values: number[]; // feature values cadence_hz: number; // output rate qmask: number[]; // quality mask }; ``` Plus optional event series (e.g., `rpeaks`, `pulse_feet`) - timestamped events without a continuous value. Useful for overlaying markers on a waveform plot. Plus algorithm metadata recording the algorithm name, version, and parameters used for the run. This is what becomes `method_code` on the resulting observations. ## Latency [#latency] The pipeline is windowed but streaming. End-to-end latency from a sample arriving at ingest to the corresponding feature value being available depends on the largest window the feature uses: * `hr_ecg`, `hr_ppg` (1 Hz): \~1-2 seconds. * `*_rmssd` (5s window): \~5-7 seconds. * `*_sdnn_5min` (5 min window): \~5-6 minutes after stream start; rolling thereafter. Rolling windows continue to update at the cadence shown above once their warm-up period is complete. ## Extending the pipeline [#extending-the-pipeline] The catalog of biosignals and the set of modality extractors are intentionally extensible. New biosignals are added through the catalog; new extractors are wired into the platform when we onboard a new signal type. See [Custom Algorithms](/docs/dsp/guides/custom-algorithms) for the customer-side perspective on running custom DSP against your streams. # Testing & Validating Outputs (/docs/dsp/guides/testing-algorithms) DSP outputs are only useful if you can trust them. This guide covers the practical tools the platform exposes for validating feature series - both built-in and custom. ## Reading the quality mask [#reading-the-quality-mask] Every feature value carries a `qmask` byte. The mask is the platform's view of how trustworthy that value is: | Value | Meaning | Typical cause | | ----- | ------- | --------------------------------------------------------------------------------------- | | `0` | Good | Beats detected near the value, enough beats in the window, value in physiological range | | `1` | Suspect | Interpolated (no beats detected nearby), or too few beats in the window | | `2` | Bad | Value outside the physiological range for the feature | A common pattern when reporting aggregates: ```typescript function meanHR(series: FeatureSeries): number { const clean = series.values.filter((_, i) => series.qmask[i] === 0); return clean.reduce((a, b) => a + b, 0) / clean.length; } ``` For real-time UI, you might still display suspect values but de-emphasize them visually. For clinical aggregations and exports, drop bad values and flag suspect ones. ## Comparing against reference [#comparing-against-reference] If you have a reference signal for the same subject (e.g., a clinical-grade ECG monitor running alongside a wearable), the platform makes side-by-side comparison straightforward: 1. Capture both streams in the same session. 2. Each stream produces its own feature series under a unique `streamId`. 3. Query both, align on the nanosecond timestamps in `ts_ns`, and compute the metric of interest (mean error, Bland-Altman, etc.). Because timestamps are nanosecond-aligned with the source stream's clock, the only step you need is to interpolate to a common cadence if the two streams have different rates. ## Synthetic streams [#synthetic-streams] For testing without a wearable on a subject, the platform accepts pre-recorded waveform data submitted through the same ingest path as a live device. Common use cases: * Replay a known-good ECG against the pipeline to verify HR and HRV outputs match expected values. * Inject a segment with dropped or implausible beats and confirm the affected values are flagged in the quality mask. * Stress-test downstream consumers with high-cadence feature series before deploying. Reach out if you need help setting up a synthetic stream for your project - we have validated test fixtures (ECG strips with annotated R-peaks, PPG segments with known HR) that we can share. ## Validating a custom algorithm [#validating-a-custom-algorithm] If you're running a custom algorithm against your streams (see [Custom Algorithms](/docs/dsp/guides/custom-algorithms)), the same approach applies: query the values the algorithm produces and compare them against reference data. Quality masks are attached by the built-in extractors, not to custom algorithm outputs. The `method_code` field on every output value identifies the algorithm and version that produced it, so you can run multiple algorithm versions in parallel and compare their outputs apples-to-apples. ## Reproducibility [#reproducibility] Algorithm versions are immutable. When an algorithm is updated, existing observations keep their original `method_code` and the new version emits under a new code (e.g., `algorithm:ecg_v1` to `algorithm:ecg_v2`). This means historical analyses always run against the same algorithm version that produced the original output - the platform doesn't silently re-run features under a newer algorithm. If you need to reanalyze historical data under a newer algorithm, the platform supports explicit reprocessing. [Contact us](https://anybio.io/contact) for the API surface; this is currently a managed operation rather than self-service. # Getting Started (/docs/model-registry/guides/getting-started) # Getting Started with the Model Registry [#getting-started-with-the-model-registry] ## Overview [#overview] The Model Registry allows you to: 1. **Register** ML models that process biosignal data 2. **Version** models as you iterate on training and architecture 3. **Deploy** specific versions to run against live or historical session data 4. **Monitor** model performance and output quality