How to Scrape Vitals Data with an API
Learn how to retrieve authorized vital-sign data from FHIR and consumer health APIs, preserve clinical context, and handle gaps safely.
Direct answer: “Vitals data” does not come from one universal scraping endpoint. First identify the system of record: a clinical EHR that exposes FHIR Observation resources, a wearable or consumer-health platform with its own user-scoped data types, or an intermediary service. Then obtain the provider-approved bearer token and patient or user consent, query the provider’s documented resource or data type, parse values with their codes and units, and retain timestamps, device and measurement context. Treat missing readings as missing data.
This guide shows an authorized API workflow for FHIR R4 and the Google Health API examples documented for vitals. Provider versions, scopes, enabled resources, pagination, rate limits and approval requirements vary, so verify each detail against the target system’s current documentation before production use.
1. Choose the source system before writing code
The source determines identity, consent, authorization, schema and historical coverage.
| Source | Typical resource or path | What to confirm |
|---|---|---|
| EHR or clinical record | FHIR R4 /Observation |
FHIR version, patient authorization, supported search parameters, profiles, pagination and local restrictions |
| Consumer health or wearable platform | Provider-specific data type and data-point endpoints | User consent, OAuth scopes, supported devices, timestamp rules, source reconciliation and retention |
| Intermediary platform | Its normalized API | Which upstream systems are connected, mapping rules, provenance and data freshness |
Do not copy a demonstration host, patient ID or token into production. API access is authorized access: use the provider’s approval and consent flow rather than unauthenticated scraping.
2. Know the vital-sign structures
FHIR identifies measurements with terminology codes and represents the value, unit, status and time in an Observation. Common R4 vital-sign codes include:
| Measurement | LOINC code | Typical unit or structure |
|---|---|---|
| Heart rate | 8867-4 |
/min |
| Respiratory rate | 9279-1 |
/min |
| Oxygen saturation | 2708-6; pulse oximetry may use 59408-5 |
% |
| Body temperature | 8310-5 |
Celsius or Fahrenheit, with site and device context when supplied |
| Blood pressure panel | 85354-9 |
Components: systolic 8480-6, diastolic 8462-4 |
| Body weight | 29463-7 |
Quantity with unit |
| BMI | 39156-5 |
Quantity |
Blood pressure is commonly a panel. Parse its systolic and diastolic components explicitly; do not assume it is one scalar observation. Preserve the original code, unit, effective time, status, device, measurement site, body position and other qualifiers before normalizing.
3. Query FHIR R4 observations
The FHIR vital-sign quick start uses a bearer token and a patient plus category=vital-signs search. A date-bounded search can add date parameters, and a code-targeted search can request one or more LOINC codes. See the FHIR R4 vital-sign profiles and the US Vital Signs Implementation Guide.
Basic request with cURL
curl --fail-with-body \
-H "Authorization: Bearer $FHIR_TOKEN" \
-H "Accept: application/fhir+json" \
"https://fhir.example.org/fhir/Observation?patient=PATIENT_ID&category=vital-signs"
Date and code filters
# Observations in a date range
curl --fail-with-body \
-H "Authorization: Bearer $FHIR_TOKEN" \
-H "Accept: application/fhir+json" \
"https://fhir.example.org/fhir/Observation?patient=PATIENT_ID&category=vital-signs&date=ge2026-01-01&date=lt2026-02-01"
# Heart rate only
curl --fail-with-body \
-H "Authorization: Bearer $FHIR_TOKEN" \
-H "Accept: application/fhir+json" \
"https://fhir.example.org/fhir/Observation?patient=PATIENT_ID&code=8867-4"
Some servers require a category and code system syntax, support different date search behavior, or return a paginated Bundle. Follow the server’s CapabilityStatement and implementation guide.
Python: request and extract measurements
import os
import requests
base = "https://fhir.example.org/fhir"
params = {
"patient": os.environ["FHIR_PATIENT_ID"],
"category": "vital-signs",
"date": "ge2026-01-01",
}
headers = {
"Authorization": f"Bearer {os.environ['FHIR_TOKEN']}",
"Accept": "application/fhir+json",
}
r = requests.get(f"{base}/Observation", params=params, headers=headers, timeout=30)
r.raise_for_status()
bundle = r.json()
for entry in bundle.get("entry", []):
obs = entry.get("resource", {})
code = obs.get("code", {}).get("coding", [{}])[0].get("code")
effective = obs.get("effectiveDateTime") or obs.get("effectivePeriod")
value = obs.get("valueQuantity")
if value:
print({"code": code, "effective": effective,
"value": value.get("value"), "unit": value.get("unit"),
"status": obs.get("status")})
for component in obs.get("component", []):
c = component.get("code", {}).get("coding", [{}])[0].get("code")
q = component.get("valueQuantity", {})
print({"code": c, "effective": effective,
"value": q.get("value"), "unit": q.get("unit")})
Node.js: request a FHIR Bundle
const base = 'https://fhir.example.org/fhir';
const p = new URLSearchParams({
patient: process.env.FHIR_PATIENT_ID,
category: 'vital-signs',
date: 'ge2026-01-01'
});
const res = await fetch(`${base}/Observation?${p}`, {
headers: {
Authorization: `Bearer ${process.env.FHIR_TOKEN}`,
Accept: 'application/fhir+json'
}
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const bundle = await res.json();
for (const {resource: obs = {}} of bundle.entry ?? []) {
const code = obs.code?.coding?.[0]?.code;
console.log(code, obs.valueQuantity, obs.component, obs.effectiveDateTime);
}
4. Follow pagination and preserve provenance
FHIR search results are Bundles. Continue through the Bundle’s link with relation next until no next link remains. Store the source URL, resource ID, version or last-updated value when supplied, original JSON, code system, code, unit, status, effective time and all qualifiers. Normalize units only after retaining that source representation so a conversion or mapping can be audited.
def iter_fhir_bundle(url, headers):
while url:
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
bundle = response.json()
yield from (e["resource"] for e in bundle.get("entry", []))
url = next((x["url"] for x in bundle.get("link", [])
if x.get("relation") == "next"), None)
Expect blood-pressure panels with one or both components, observations with statuses such as amended or entered-in-error, and measurements that use effectivePeriod rather than a single timestamp. Decide how your application handles each status with the provider’s profile.
5. Query a consumer health API
Consumer platforms expose their own user-scoped paths and data types. Google’s official vitals guide shows paths such as /v4/users/me/dataTypes/heart-rate/dataPoints and /v4/users/me/dataTypes/oxygen-saturation/dataPoints, with startTime, endTime and bearer authorization. Required scopes, supported devices and operations are listed in the Google Health API vitals documentation.
curl --fail-with-body \
-H "Authorization: Bearer $GOOGLE_TOKEN" \
"https://health.googleapis.com/v4/users/me/dataTypes/heart-rate/dataPoints?startTime=2026-01-01T00:00:00Z&endTime=2026-02-01T00:00:00Z"
Keep the API’s sample time, value fields and metadata such as motion context, sensor location and recording method. A list operation can contain overlapping source intervals; Google documents a reconcile operation for a consolidated stream. That behavior is specific to that API and should not be generalized to FHIR.
6. Handle gaps, duplicates and conflicting sources
- Gaps: Some APIs omit invalid minutes, and device availability differs. Missing oxygen-saturation minutes, for example, do not prove normal oxygenation.
- Duplicates: The same measurement may arrive from multiple devices or source intervals. Keep source identifiers and define a deterministic reconciliation policy.
- Conflicts: Do not silently overwrite values. Retain each observation and record which source your application selected.
- Units: Convert only with an explicit, tested rule and retain the original unit.
- Consent changes: Expired or revoked grants must stop collection and trigger the provider’s reauthorization flow.
7. Security, reliability and cost controls
- Keep access and refresh tokens in a secrets manager; never put them in browser JavaScript, logs or URLs.
- Request the narrowest scopes needed for the vital types and date range.
- Use HTTPS, bounded timeouts, retry with exponential backoff for transient 429 and 5xx responses, and honor
Retry-After. - Use idempotent checkpoints based on source IDs and timestamps so retries do not duplicate records.
- Respect provider rate limits, pagination limits and local health-system approval rules. API pricing and quotas are provider-specific; confirm them in the current contract or documentation.
- Encrypt stored health data, restrict operator access and retain audit records according to your organization’s requirements.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 Unauthorized | Missing, expired or wrong-audience bearer token | Renew the provider-approved token and verify the authorization header and environment |
| 403 Forbidden | Consent, scope, patient permission or health-system approval is missing | Request the documented scope and complete the provider’s authorization flow |
| 404 Not Found | Wrong base URL, API version, resource path or data type | Check the provider’s current discovery or API documentation |
| 200 with an empty Bundle | No matching data, incorrect patient identity, date filter or unsupported category | Verify identity, broaden the date range, inspect the CapabilityStatement and test a known observation |
| Blood pressure value missing | It is a panel whose values are in component |
Parse systolic and diastolic component codes explicitly |
| Only recent readings appear | Device history, retention, consent or local policy limits coverage | Ask the provider what historical range and devices are available; do not infer older values |
| Repeated records | Pagination retries or overlapping source intervals | Deduplicate by provider ID and retain source metadata |
| 429 or intermittent 5xx | Quota or transient service failure | Back off, honor retry headers, checkpoint progress and alert after bounded retries |
9. Or skip the browser setup
If you need screenshots of a vitals dashboard, report or API-rendered page after retrieving the data, ScreenshotNeo provides a single screenshot request. Its consent step accepts cookie banners and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; bot checks, blank pages, timeouts, failed loads and cache hits are not billed, with the result explained by response headers. It also offers an MCP server for AI agents and supports PNG, JPEG, WebP and PDF output. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/vitals-dashboard -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/vitals-dashboard"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/vitals-dashboard' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Every feature is on every plan: 1,000 screenshots a month are free with no card, and paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
10. FAQ
Is there one standard vitals API?
No. FHIR Observation is a common clinical route, while consumer platforms expose provider-specific data types and paths.
Can I treat an empty response as a normal reading?
No. It can indicate no matching data, a filter problem, device gaps or access restrictions.
Should blood pressure be stored as one number?
Usually no. Store the panel and its systolic and diastolic components, including units and context.
Do I need patient consent for an EHR integration?
Follow the EHR and organization’s authorization requirements. A valid token alone does not establish permission for every use.
How often should I poll?
Use the provider’s documented update behavior, quotas and webhooks where available. Checkpoint results and back off on throttling rather than polling aggressively.


