Skip to content
AmplifierDocs
Esc
navigateopen⌘Jpreview
On this page

Longitudinal API

Group-aware analyze, trajectory, and cohort aggregate endpoints.

These endpoints analyze audio in the context of a group and read that group’s history.

A group is a caller-named container for jobs. Create and manage membership with the Groups API. For what a group is, how raw and longitudinal analyze differ, how recorded_at is used, and the history window, see Longitudinal concepts.

Call What it does
Analyze into a group (raw) Registers the job in the group and scores the recording on its own.
Longitudinal analyze Registers the job and scores it against that subject’s history.
Get group trajectory Reads the whole trajectory so far for one subject.
Get group aggregate Cohort statistics across many subjects.

Note A group used with the longitudinal endpoints represents one subject. Personal baselines are per-subject, so a group that mixes people describes no one in particular. Amplifier stores no personal identifiers and cannot verify subject identity — keeping a group to a single subject is the caller’s responsibility. For cohort-level statistics across many subjects, use GET /v2/groups/{group_id}/aggregate.

Analyze into a group (model)

POST /v2/models/{model_name}/groups/{group_id}/analyze

Submit audio using a named model and register the job into a group. Returns a job object immediately; poll GET /v2/jobs/{job_id} or use a webhook for the completed result.

This is the raw group-aware form: it registers the job into the group — creating the group if it is new — and scores the recording on its own, without historical context. The completed result carries result.summary and result.signals[].

Path Parameters

Parameter Type Description
model_name string The named model to run. See Valid model names.
group_id string The group to register the job into. Created if it is new.

Request Body

Requests use Content-Type: multipart/form-data.

Field Type Required Description
audio file No Audio file (WAV, FLAC, MP3, or M4A). Max 32 MB per file. Provide this or audio_upload_ref. See Audio Requirements for size and format limits.
audio_upload_ref string No The upload_ref value from POST /v2/audio/uploads. Provide this or an audio file.
recorded_at string No ISO 8601 time the audio was captured. Used for temporal ordering in longitudinal computation. Falls back to the job’s created_at when omitted. Accepted as supplied, since capture time cannot be confirmed server-side.
diarize boolean No Whether to apply speaker diarization. Default: false. See Speaker Diarization.
webhook_url string No Per-request webhook URL, overriding the account-level webhook for this job. Optional on its own in the schema. When you set it, also send webhook_secret_key.
webhook_secret_key string No Optional on its own in the schema. Required in practice when webhook_url is provided. Signs the webhook payload (HMAC-SHA256). See Jobs, Polling, and Webhooks.

Response

The standard job object described in Jobs API, plus two fields:

Field Type Description
group_id string The group this job was registered into.
recorded_at string or null The capture time you supplied, or null.

Example

curl -X POST https://api.amplifierhealth.com/v2/models/pulse/groups/daily-checkins/analyze \
  -H "X-Account-ID: your-account-id" \
  -H "X-API-Key: your-api-key" \
  -F "audio=@recording.wav;type=audio/wav" \
  -F "recorded_at=2026-05-01T08:30:00Z"
const fs = require("fs");

const form = new FormData();
form.append("audio", new Blob([fs.readFileSync("recording.wav")], { type: "audio/wav" }), "recording.wav");
form.append("recorded_at", "2026-05-01T08:30:00Z");

const response = await fetch("https://api.amplifierhealth.com/v2/models/pulse/groups/daily-checkins/analyze", {
  method: "POST",
  headers: {
    "X-Account-ID": process.env.AMPLIFIER_ACCOUNT_ID,
    "X-API-Key": process.env.AMPLIFIER_API_KEY,
  },
  body: form,
});

const data = await response.json();
# Requires httpx: pip install httpx
import os
import httpx

with open("recording.wav", "rb") as f:
    audio_bytes = f.read()

response = httpx.post(
  "https://api.amplifierhealth.com/v2/models/pulse/groups/daily-checkins/analyze",
  headers={
      "X-Account-ID": os.environ["AMPLIFIER_ACCOUNT_ID"],
      "X-API-Key": os.environ["AMPLIFIER_API_KEY"],
  },
  files={"audio": ("recording.wav", audio_bytes, "audio/wav")},
  data={
      "recorded_at": "2026-05-01T08:30:00Z"
  },
)

data = response.json()

Example response

{
  "job_id": "job_aaa",
  "status": "queued",
  "group_id": "daily-checkins",
  "recorded_at": "2026-05-01T08:30:00Z",
  "created_at": "2026-05-01T09:14:00Z"
}

Errors

Same codes as POST /v2/models/{model_name}/analyze, plus the group conditions below.

Analyze into a group (sign)

POST /v2/signs/{sign_name}/groups/{group_id}/analyze

Submit audio for a single sign and register the job into a group. Returns a job object immediately.

Identical in behaviour, request body, and response to POST /v2/models/{model_name}/groups/{group_id}/analyze, with two differences: the path parameter is sign_name rather than model_name, and the completed result carries a singular result.signal.

Path Parameters

Parameter Type Description
sign_name string The sign to analyze. See Valid sign names.
group_id string The group to register the job into. Created if it is new.

Example

curl -X POST https://api.amplifierhealth.com/v2/signs/stress/groups/daily-checkins/analyze \
  -H "X-Account-ID: your-account-id" \
  -H "X-API-Key: your-api-key" \
  -F "audio=@recording.wav;type=audio/wav" \
  -F "recorded_at=2026-05-01T08:30:00Z"
const fs = require("fs");

const form = new FormData();
form.append("audio", new Blob([fs.readFileSync("recording.wav")], { type: "audio/wav" }), "recording.wav");
form.append("recorded_at", "2026-05-01T08:30:00Z");

const response = await fetch("https://api.amplifierhealth.com/v2/signs/stress/groups/daily-checkins/analyze", {
  method: "POST",
  headers: {
    "X-Account-ID": process.env.AMPLIFIER_ACCOUNT_ID,
    "X-API-Key": process.env.AMPLIFIER_API_KEY,
  },
  body: form,
});

const data = await response.json();
# Requires httpx: pip install httpx
import os
import httpx

with open("recording.wav", "rb") as f:
    audio_bytes = f.read()

response = httpx.post(
  "https://api.amplifierhealth.com/v2/signs/stress/groups/daily-checkins/analyze",
  headers={
      "X-Account-ID": os.environ["AMPLIFIER_ACCOUNT_ID"],
      "X-API-Key": os.environ["AMPLIFIER_API_KEY"],
  },
  files={"audio": ("recording.wav", audio_bytes, "audio/wav")},
  data={
      "recorded_at": "2026-05-01T08:30:00Z"
  },
)

data = response.json()

Errors

Same codes as POST /v2/signs/{sign_name}/analyze, plus the group conditions below.

Longitudinal analyze (model)

POST /v2/models/{model_name}/groups/{group_id}/analyze/longitudinal

Submit audio using a named model and score it against the group’s history. Returns a job object immediately; the completed result carries baseline and deviation fields.

The job is registered into the group as well, so it becomes part of the history for later calls.

Path Parameters

Same as POST /v2/models/{model_name}/groups/{group_id}/analyze.

Request Body

Identical to POST /v2/models/{model_name}/groups/{group_id}/analyze.

Response

The submission response is the same job object, with group_id and recorded_at. Once the job reaches done, each result.signals[] entry carries the longitudinal fields below. On sign jobs they appear on the singular result.signal.

Field Type Description
score, level, flagged Unchanged from any other job: this recording’s own reading. See Response Schema and Interpreting Results.
latest_score number or null The smoothed current estimate after this recording — precision- and time-weighted across the window. May differ from the raw score.
baseline_score number or null The subject’s personal baseline, blended toward a population prior while personal history is still accumulating.
deviation_from_baseline number or null latest_score − baseline_score. Positive means above the subject’s own baseline.
anomaly boolean or null true when the change against the subject’s own norm is significant. Slower-moving signs need the change to persist across two distinct timepoints before this is set.
z_score number or null Deviation expressed in units of the subject’s own variability.
population_z number or null The subject’s current position relative to the cohort. null when no population reference is available for a signal.

Note All of the longitudinal fields above are null on the first submission to a group — with no prior history the result is equivalent to a one-off analysis. Detect that case by checking baseline_score for null, and treat the other longitudinal fields as available only when it is non-null. Baselines become meaningful as spaced recordings accumulate.

Example

curl -X POST https://api.amplifierhealth.com/v2/models/pulse/groups/subject-8f2a/analyze/longitudinal \
  -H "X-Account-ID: your-account-id" \
  -H "X-API-Key: your-api-key" \
  -F "audio=@recording.wav;type=audio/wav" \
  -F "recorded_at=2026-05-29T08:30:00Z"
const fs = require("fs");

const form = new FormData();
form.append("audio", new Blob([fs.readFileSync("recording.wav")], { type: "audio/wav" }), "recording.wav");
form.append("recorded_at", "2026-05-29T08:30:00Z");

const response = await fetch("https://api.amplifierhealth.com/v2/models/pulse/groups/subject-8f2a/analyze/longitudinal", {
  method: "POST",
  headers: {
    "X-Account-ID": process.env.AMPLIFIER_ACCOUNT_ID,
    "X-API-Key": process.env.AMPLIFIER_API_KEY,
  },
  body: form,
});

const data = await response.json();
# Requires httpx: pip install httpx
import os
import httpx

with open("recording.wav", "rb") as f:
    audio_bytes = f.read()

response = httpx.post(
  "https://api.amplifierhealth.com/v2/models/pulse/groups/subject-8f2a/analyze/longitudinal",
  headers={
      "X-Account-ID": os.environ["AMPLIFIER_ACCOUNT_ID"],
      "X-API-Key": os.environ["AMPLIFIER_API_KEY"],
  },
  files={"audio": ("recording.wav", audio_bytes, "audio/wav")},
  data={
      "recorded_at": "2026-05-29T08:30:00Z"
  },
)

data = response.json()

Example response

Poll GET /v2/jobs/{job_id} until status is done. The job read returns the standard job object — group_id and recorded_at are returned on the submission response, so keep them alongside the job_id you stored:

{
  "job_id": "job_xxx",
  "status": "done",
  "created_at": "2026-05-29T09:00:00Z",
  "completed_at": "2026-05-29T09:00:45Z",
  "result": {
    "summary": { "...": "..." },
    "signals": [
      {
        "name": "stress",
        "score": 0.71,
        "level": "elevated",
        "flagged": true,
        "latest_score": 0.68,
        "baseline_score": 0.35,
        "deviation_from_baseline": 0.33,
        "anomaly": true,
        "z_score": 2.4,
        "population_z": 1.93
      }
    ],
    "audio_quality": { "...": "..." },
    "extended_metrics": { "...": "..." }
  }
}

Errors

Same codes as POST /v2/models/{model_name}/analyze, plus the group conditions below.

Longitudinal analyze (sign)

POST /v2/signs/{sign_name}/groups/{group_id}/analyze/longitudinal

Submit audio for a single sign and score it against the group’s history.

Identical in behaviour, request body, and longitudinal fields to POST /v2/models/{model_name}/groups/{group_id}/analyze/longitudinal, with two differences: the path parameter is sign_name rather than model_name, and the longitudinal fields appear on the singular result.signal.

Example

curl -X POST https://api.amplifierhealth.com/v2/signs/stress/groups/subject-8f2a/analyze/longitudinal \
  -H "X-Account-ID: your-account-id" \
  -H "X-API-Key: your-api-key" \
  -F "audio=@recording.wav;type=audio/wav" \
  -F "recorded_at=2026-05-29T08:30:00Z"
const fs = require("fs");

const form = new FormData();
form.append("audio", new Blob([fs.readFileSync("recording.wav")], { type: "audio/wav" }), "recording.wav");
form.append("recorded_at", "2026-05-29T08:30:00Z");

const response = await fetch("https://api.amplifierhealth.com/v2/signs/stress/groups/subject-8f2a/analyze/longitudinal", {
  method: "POST",
  headers: {
    "X-Account-ID": process.env.AMPLIFIER_ACCOUNT_ID,
    "X-API-Key": process.env.AMPLIFIER_API_KEY,
  },
  body: form,
});

const data = await response.json();
# Requires httpx: pip install httpx
import os
import httpx

with open("recording.wav", "rb") as f:
    audio_bytes = f.read()

response = httpx.post(
  "https://api.amplifierhealth.com/v2/signs/stress/groups/subject-8f2a/analyze/longitudinal",
  headers={
      "X-Account-ID": os.environ["AMPLIFIER_ACCOUNT_ID"],
      "X-API-Key": os.environ["AMPLIFIER_API_KEY"],
  },
  files={"audio": ("recording.wav", audio_bytes, "audio/wav")},
  data={
      "recorded_at": "2026-05-29T08:30:00Z"
  },
)

data = response.json()

Errors

Same codes as POST /v2/signs/{sign_name}/analyze, plus the group conditions below.

Get group trajectory

GET /v2/groups/{group_id}/longitudinal

Return the subject’s trajectory over the group’s history window: per-signal history, baseline, latest estimate, and trend.

This endpoint follows a polling pattern. The first call for a group — or the first after its membership changes — returns status: "running" while the trajectory is computed. Poll until status is done.

Everything returned comes from the history window — completed jobs recorded within the last 270 days, capped at the 1,000 most recent — including data_points. The one exception is done_job_count, which counts every completed job in the group. See Longitudinal concepts.

Path Parameters

Parameter Type Description
group_id string The group to read.

Query Parameters

Parameter Type Required Description
signal string No Return only this signal, matched against signals[].name. Repeat the parameter for several signals.
from string No ISO 8601 date. Show data points from this time onward. Filters the stored result for display; summary statistics are unchanged.
to string No ISO 8601 date. Show data points up to this time. Filters the stored result for display; summary statistics are unchanged.

Response

Field Type Description
status string running while computing, done when ready, failed if the computation could not complete.
group_id string The group read.
computed_at string When this result was computed.
min_data_points_met boolean Convenience roll-up over every signal in the group: true only when all of them have an established baseline, and false when the group has no signals yet. It is not narrowed by the signal filter, so read the per-signal min_data_points_met as the source of truth.
min_data_points_required integer The configured minimum number of independent readings (3). Signals with no population reference are held at establishing_baseline until it is met; where a population reference exists the baseline starts from it and firms up as readings accumulate. Read the per-signal status for where a given signal actually stands. Recordings taken close together pool into roughly one independent reading, so three recordings minutes apart count as about one.
done_job_count integer Completed jobs in the group.
signals array Per-signal trajectory objects, described below.

Each object in signals:

Field Type Description
name string Signal name. For the apex and harbor models a few signs are reported under a model-specific name on job results and under the canonical name here, so match on the value this endpoint returns rather than joining by the job result’s name.
data_points array Usable readings ordered by recording time: job_id, recorded_at, score, level, flagged, anomaly.
data_points[].anomaly boolean or null The anomaly value recorded when that job was scored in this group, stored rather than recomputed. null means no anomaly was determined for that job and signal — either the job was added to the group after the fact and never scored in it, or it was scored with no basis for an anomaly yet: no prior history in the group, or no established baseline for that signal at the time. Jobs submitted through a raw group analyze endpoint are scored and do get a stored value; it is only their own returned result that omits the longitudinal fields.
trajectory.direction string up, down, flat, or insufficient_data. flat is returned unless the fitted change clears a deadband set by the signal’s own variability. insufficient_data is returned until enough spaced readings are available to fit a trend.
trajectory.slope number or null Change in score per day. null when direction is insufficient_data.
trajectory.resolvable boolean Whether a trend was actually fitted. Gate any trend display on this rather than on data_points_used.
trajectory.data_points_used integer Time bins the trend was fitted over, not a raw job count. Populated even when resolvable is false.
baseline_score number or null The subject’s personal baseline, blended toward a population prior while personal history is still accumulating.
latest_score number or null The smoothed current estimate. May differ from the most recent raw score.
change_absolute number Net change across the stored series in the window: last reading minus first. The from and to filters affect display only, not this value.
flagged_rate number Share of the stored readings that were flagged, 01. Computed over the whole series in the window; the from and to filters do not narrow it.
z_score number or null Latest deviation in units of the subject’s own variability.
population_z number or null The subject’s current position relative to the cohort. null when no population reference is available.
status string ok, establishing_baseline, or inconclusive. ok once the baseline is established, and establishing_baseline while it is still forming. A signal with no usable readings in the window is omitted from signals[] entirely rather than returned with a status, so treat an absent signal as “nothing measured in this window” — it can disappear between polls if its readings age out. inconclusive is reserved for a reading that could not be attributed to a known signal.
min_data_points_met boolean Whether this signal’s baseline is established.
baseline_personal_weight number or null How much of baseline_score comes from this subject rather than the population prior, 01. Useful for deciding when to present the baseline as “your usual”.

level values inside data_points are defined in Interpreting Results.

Example

curl -X GET "https://api.amplifierhealth.com/v2/groups/daily-checkins/longitudinal?signal=stress" \
  -H "X-Account-ID: your-account-id" \
  -H "X-API-Key: your-api-key"
const response = await fetch("https://api.amplifierhealth.com/v2/groups/daily-checkins/longitudinal?signal=stress", {
  headers: {
    "X-Account-ID": process.env.AMPLIFIER_ACCOUNT_ID,
    "X-API-Key": process.env.AMPLIFIER_API_KEY,
  },
});

const data = await response.json();
# Requires httpx: pip install httpx
import os
import httpx

response = httpx.get(
  "https://api.amplifierhealth.com/v2/groups/daily-checkins/longitudinal?signal=stress",
  headers={
      "X-Account-ID": os.environ["AMPLIFIER_ACCOUNT_ID"],
      "X-API-Key": os.environ["AMPLIFIER_API_KEY"],
  },
)

data = response.json()

Example response

{
  "status": "done",
  "group_id": "daily-checkins",
  "computed_at": "2026-05-29T09:00:00Z",
  "min_data_points_met": true,
  "min_data_points_required": 3,
  "done_job_count": 10,
  "signals": [
    {
      "name": "stress",
      "data_points": [
        { "job_id": "job_aaa", "recorded_at": "2026-05-01T08:30:00Z", "score": 0.22, "level": "low",       "flagged": false, "anomaly": null  },
        { "job_id": "job_bbb", "recorded_at": "2026-05-15T08:30:00Z", "score": 0.61, "level": "moderate", "flagged": true,  "anomaly": false },
        { "job_id": "job_ccc", "recorded_at": "2026-05-29T08:30:00Z", "score": 0.81, "level": "elevated", "flagged": true,  "anomaly": false }
      ],
      "trajectory": { "direction": "up", "slope": 0.0211, "data_points_used": 3, "resolvable": true },
      "baseline_score": 0.28,
      "latest_score": 0.81,
      "change_absolute": 0.59,
      "flagged_rate": 0.67,
      "z_score": 1.9,
      "population_z": 2.37,
      "status": "ok",
      "min_data_points_met": true,
      "baseline_personal_weight": 0.37
    }
  ]
}

While the trajectory is still being computed:

{ "status": "running", "group_id": "daily-checkins" }

Errors

HTTP Condition
400 from or to is not a valid ISO 8601 datetime.
404 No group with this group_id exists for your account.

A 200 response with status: "failed" carries an error object with code and message. The failed state is held for about five minutes before a retry triggers a fresh computation, so poll on that interval rather than in a tight loop.

Get group aggregate

GET /v2/groups/{group_id}/aggregate

Return cohort-level statistics across the completed jobs in a group’s history window: level distribution, mean, and spread per signal.

This is the many-subjects view. It describes the group as a population rather than tracking one person, so it is the right endpoint for a group that intentionally holds recordings from several people. It follows the same polling pattern as the trajectory endpoint.

Statistics count usable readings only, drawn from the same history window as the trajectory endpoint.

Path Parameters

Parameter Type Description
group_id string The group to summarize.

Response

Field Type Description
status string running, done, or failed.
group_id string The group read.
computed_at string When this result was computed.
job_count integer Completed jobs in the history window.
signals array Per-signal statistics, described below.

Each object in signals:

Field Type Description
name string Signal name, the same identifier the trajectory endpoint returns. For the apex and harbor models a few signs are reported under a model-specific name on job results and under the canonical name here, so match on the value this endpoint returns rather than joining by the job result’s name.
distribution object Share of readings at each level — none, low, consider, moderate, elevated. Values sum to 1.0. See Interpreting Results.
mean_score number Mean score across usable readings.
std_score number Standard deviation across the same readings.
flagged_rate number Share of readings flagged for this signal, 01.
job_count integer Completed jobs that produced a usable reading for this signal. May be lower than the group’s total.

Example

curl -X GET "https://api.amplifierhealth.com/v2/groups/study-cohort-a/aggregate" \
  -H "X-Account-ID: your-account-id" \
  -H "X-API-Key: your-api-key"
const response = await fetch("https://api.amplifierhealth.com/v2/groups/study-cohort-a/aggregate", {
  headers: {
    "X-Account-ID": process.env.AMPLIFIER_ACCOUNT_ID,
    "X-API-Key": process.env.AMPLIFIER_API_KEY,
  },
});

const data = await response.json();
# Requires httpx: pip install httpx
import os
import httpx

response = httpx.get(
  "https://api.amplifierhealth.com/v2/groups/study-cohort-a/aggregate",
  headers={
      "X-Account-ID": os.environ["AMPLIFIER_ACCOUNT_ID"],
      "X-API-Key": os.environ["AMPLIFIER_API_KEY"],
  },
)

data = response.json()

Example response

{
  "status": "done",
  "group_id": "study-cohort-a",
  "computed_at": "2026-05-28T10:05:00Z",
  "job_count": 150,
  "signals": [
    {
      "name": "stress",
      "distribution": {
        "none": 0.32,
        "low": 0.22,
        "consider": 0.21,
        "moderate": 0.18,
        "elevated": 0.07
      },
      "mean_score": 0.41,
      "std_score": 0.18,
      "flagged_rate": 0.46,
      "job_count": 150
    }
  ]
}

Errors

HTTP Condition
404 No group with this group_id exists for your account.

Group error conditions

Analyze endpoints return the same audio and auth codes as POST /v2/models/{model_name}/analyze. Shared codes: Errors.

Group-specific conditions:

HTTP Condition
400 from or to is not a valid ISO 8601 datetime.
404 The group does not exist for your account, or a group_id in the path is malformed.

For the full list and retry guidance, see Errors.

  • Groups API — create groups and manage job membership.
  • Longitudinal concepts — groups, baselines, deviation, trend, and the history window as concepts.
  • Billing and Cost — group analyze endpoints bill exactly like their non-group counterparts; group lifecycle, membership, and state endpoints consume no credits.
  • Response Schema — the standard fields every job result carries.

Was this page helpful?