Device ingest
The cellular device webhook (shared secret, per project), image ingest, waveform captures, and the provenance fields every observation carries - measured time versus received time.
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 and wearable connections under Connect devices.
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/<provider>/{project_key}
Authorization: <the exact value you configured>
Content-Type: application/jsonThe 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
- The hub sends a fixed
Authorizationheader on every POST. You configure the expected value per project when you add the source (Integrations, Connected Sources, Add Data Source, field Authorization), or withPOST /api/v1/oauth_providers(webhook_secret,webhook_enabled). - The stored value must be the entire header value, scheme included (for example
Basic <base64>orBearer <token>). 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
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
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 for it end to end.
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
POST /ingest/image (Write scope; the token must carry an environment) stores a photograph or screenshot against a patient for later classification.
{
"xuser_id": "<patient uuid>",
"device_id": "phone-01",
"ts_ms": 1789740300000,
"image_data": [255, 216, 255, 224, ...],
"mime_type": "image/jpeg"
}image_datais the raw bytes as a JSON array;mime_typemust be one ofimage/jpeg,image/png,image/heic,image/heif;ts_msis 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 is400. - 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
uploadImagecompresses to JPEG for you; see the BioSDK image guide.
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 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
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
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
POST /ingest/raw-packetand the ingest session routes under/api/v1/ingest/sessionsare the SDK's streaming path; see Stream on iOS.POST /api/v1/observationsand/batchwrite observations directly (Write scope) and link only to theepisode_idsyou 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 withlatency_classset.
Export
FHIR R4 bundles validated against US Core in CI, HL7 v2 ORU over MLLP, the Epic adapter proven against Epic's developer sandbox, and a generic FHIR R4 server target.
Messaging and consent
SMS opt-in and decline, the consent ledger, STOP and START handling, the per-patient message budget, the identity disclosure, and what a program must have before it can text anyone.