ScreenshotNeo

BlogHow-to

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.

By the ScreenshotNeo team1 October 20267 min read

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.

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.