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
nullon the first submission to a group — with no prior history the result is equivalent to a one-off analysis. Detect that case by checkingbaseline_scorefornull, 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, 0–1. 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, 0–1. 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, 0–1. |
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.
Related pages
- 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.