Longitudinal API
Most analyze endpoints score one recording on its own. The Longitudinal API adds a second question: how does this recording compare to the same person's previous ones?
You collect recordings into a group, and Amplifier maintains a personal baseline for that group. Each new submission can then be scored against that history — returning a baseline, the deviation from it, and a trend — alongside the usual per-recording result.
Two ways to read a group:
| Endpoint | Answers |
|---|---|
POST .../analyze/longitudinal | How does this recording compare to the subject's history? |
GET /v2/groups/{group_id}/longitudinal | What does the whole trajectory look like so far? |
Info
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.
Concepts
Group. A caller-named container for jobs, scoped to your account. You choose the group_id — a stable identifier from your own system (a user id, a participant code) works well. Groups are created explicitly with POST /v2/groups, or implicitly the first time you submit audio to one.
Raw vs. longitudinal analyze. Every analyze endpoint has two group-aware forms:
.../groups/{group_id}/analyze— registers the job into the group, but scores the recording on its own..../groups/{group_id}/analyze/longitudinal— registers the job and scores it against the group's history.
Both contribute to the group's history once the job reaches done, and both cost the same — the two forms bill and rate-limit identically. The only difference is the response: the raw form returns the standard result, the longitudinal form adds the baseline and deviation fields. Use the raw form when a given call does not need them.
Recording time. Pass recorded_at when the audio was captured earlier than it was uploaded. Longitudinal ordering and time-decay use it; it falls back to the job's created_at when omitted.
History window. Group computations read a bounded window: completed jobs recorded within the last 270 days, capped at the 1,000 most recent. Everything the trajectory endpoint returns — data_points included — comes from that window (done_job_count excepted; it counts the whole group), and recency-weighting applies within it, so the oldest readings in the window influence the baseline least.
Credits and rate limits. Each analyze endpoint bills and rate-limits exactly like its own non-group counterpart: the model and use-case variants like POST /v2/models/{model_name}/analyze, the sign and condition variants like POST /v2/signs/{sign_name}/analyze. Group lifecycle, membership, and state endpoints consume no credits. See Billing & Cost.
POST /v2/groups
Creating a group up front is optional — the analyze endpoints create one implicitly when the group_id is new. Use this endpoint when you want to register the group as a discrete step.
Request Body
Requests use Content-Type: application/json.
| Field | Type | Required | Description |
|---|---|---|---|
| group_id | string | Required | Your identifier for the group. Must be unique within your account. Up to 512 characters. Must start with a letter or digit; remaining characters may also include . _ ~ : - |
Response
Returns 201 Created.
| Field | Type | Description |
|---|---|---|
group_id | string | The group identifier you supplied. |
job_count | integer | Jobs currently in the group. 0 for a new group. |
created_at | string | ISO 8601 creation timestamp. |
Example
curl -X POST https://api.amplifierhealth.com/v2/groups \
-H "X-Account-ID: your-account-id" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"group_id": "daily-checkins"}'Example response
{
"group_id": "daily-checkins",
"job_count": 0,
"created_at": "2026-05-29T10:00:00Z"
}Errors
| HTTP | Condition |
|---|---|
| 409 | A group with this group_id already exists for your account. |
| 400 | group_id is empty, too long, or contains unsupported characters. |
GET /v2/groups
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | Optional | 0-based page number. Default: 0. |
Response
| Field | Type | Description |
|---|---|---|
groups | array | Group objects, described below. |
page | integer | The page returned. |
total_pages | integer | Total pages available. |
Each object in groups:
| Field | Type | Description |
|---|---|---|
group_id | string | The group identifier. |
job_count | integer | Jobs in the group, in any status. |
done_job_count | integer | Jobs that have completed and contribute to group computations. |
earliest_recorded_at | string | null | Earliest recording time in the group. |
latest_recorded_at | string | null | Most recent recording time in the group. |
created_at | string | When the group was created. |
updated_at | string | When the group last changed — membership edits and recomputes of its cached state both move it. |
Example
curl -X GET "https://api.amplifierhealth.com/v2/groups" \
-H "X-Account-ID: your-account-id" \
-H "X-API-Key: your-api-key"Example response
{
"groups": [
{
"group_id": "daily-checkins",
"job_count": 12,
"done_job_count": 10,
"earliest_recorded_at": "2026-05-01T08:30:00Z",
"latest_recorded_at": "2026-05-29T08:30:00Z",
"created_at": "2026-05-01T09:00:00Z",
"updated_at": "2026-05-29T09:00:00Z"
}
],
"page": 0,
"total_pages": 1
}DELETE /v2/groups/{group_id}
The jobs themselves are kept — they remain available through GET /v2/jobs/{job_id}. Only the group and its computed state are removed.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
group_id | string | The group to delete. |
Response
| Field | Type | Description |
|---|---|---|
group_id | string | The group that was deleted. |
deleted | boolean | true on success. |
Example
curl -X DELETE https://api.amplifierhealth.com/v2/groups/daily-checkins \
-H "X-Account-ID: your-account-id" \
-H "X-API-Key: your-api-key"Example response
{ "group_id": "daily-checkins", "deleted": true }Errors
| HTTP | Condition |
|---|---|
| 404 | No group with this group_id exists for your account. |
GET /v2/groups/{group_id}/jobs
Always synchronous. For the full result of any listed job, call GET /v2/jobs/{job_id}.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
group_id | string | The group to list. |
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | Optional | 0-based page number. Default: 0. |
| from | string | Optional | ISO 8601 date. Include jobs recorded at or after this time. |
| to | string | Optional | ISO 8601 date. Include jobs recorded at or before this time. |
| status | string | Optional | Filter by the membership status recorded for the job: queued, running, done, failed, or timed-out. Jobs registered at submission are recorded as queued and move to done on completion, so filter on done to select the jobs that feed group computations. |
Response
| Field | Type | Description |
|---|---|---|
group_id | string | The group listed. |
total_jobs | integer | Jobs matching the filters. |
jobs | array | Job metadata: job_id, status, recorded_at, created_at, use_case, condition, audio_duration_seconds. |
page | integer | The page returned. |
total_pages | integer | Total pages available. |
Example
curl -X GET "https://api.amplifierhealth.com/v2/groups/daily-checkins/jobs?status=done" \
-H "X-Account-ID: your-account-id" \
-H "X-API-Key: your-api-key"Example response
{
"group_id": "daily-checkins",
"total_jobs": 10,
"jobs": [
{
"job_id": "job_aaa",
"status": "done",
"recorded_at": "2026-05-01T08:30:00Z",
"created_at": "2026-05-01T09:14:00Z",
"use_case": null,
"condition": null,
"audio_duration_seconds": 27.4
},
{ "...": "..." }
],
"page": 0,
"total_pages": 1
}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. |
POST /v2/groups/{group_id}/jobs
Use this to backfill a group from existing jobs. To register a job into a group at submission time instead, use the analyze extensions.
Each job_id is handled independently and reported in exactly one of added, skipped, or not_found — a job id that isn't found leaves the rest of the batch unaffected, and the request still returns 200.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
group_id | string | The group to add jobs to. |
Request Body
Requests use Content-Type: application/json.
| Field | Type | Required | Description |
|---|---|---|---|
| job_ids | string[] | Required | Job IDs to add. Between 1 and 100 per request. |
Response
| Field | Type | Description |
|---|---|---|
status | string | Always "accepted". |
added | string[] | Job IDs added to the group. |
skipped | object | Job ID → reason, for jobs already in the group ("already_in_group"). |
not_found | string[] | Job IDs that don't exist for your account. |
not_yet_done | string[] | Added jobs that are still queued or running. They contribute to group computations once they reach done. This is a subset of added. |
Example
curl -X POST https://api.amplifierhealth.com/v2/groups/daily-checkins/jobs \
-H "X-Account-ID: your-account-id" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"job_ids": ["job_ddd", "job_eee", "job_fff", "job_ggg", "job_hhh"]}'Example response
{
"status": "accepted",
"added": ["job_ddd", "job_eee", "job_hhh"],
"skipped": { "job_fff": "already_in_group" },
"not_found": ["job_ggg"],
"not_yet_done": ["job_hhh"]
}Errors
| HTTP | Condition |
|---|---|
| 404 | No group with this group_id exists for your account. |
DELETE /v2/groups/{group_id}/jobs/{job_id}
Removing a job clears the group's stored longitudinal and aggregate results, so the next read recomputes them without that job.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
group_id | string | The group to remove from. |
job_id | string | The job to remove. |
Example
curl -X DELETE https://api.amplifierhealth.com/v2/groups/daily-checkins/jobs/job_bbb \
-H "X-Account-ID: your-account-id" \
-H "X-API-Key: your-api-key"Example response
{ "status": "accepted", "removed": "job_bbb" }Errors
| HTTP | Condition |
|---|---|
| 404 | The group, or this job's membership in it, does not exist. |
Analyze extensions
Four endpoints mirror the standard analyze endpoints, adding groups/{group_id} to the path. They register the job into the group at submission — creating the group if it is new — and score the recording on its own, without historical context.
| Endpoint | Path parameter | Result shape |
|---|---|---|
POST /v2/models/{model_name}/groups/{group_id}/analyze | model_name | result.summary + result.signals[] |
POST /v2/signs/{sign_name}/groups/{group_id}/analyze | sign_name | result.signal |
POST /v2/use-cases/{use_case_name}/groups/{group_id}/analyze | use_case_name | result.summary + result.signals[] |
POST /v2/conditions/{condition_name}/groups/{group_id}/analyze | condition_name | result.signal |
Request Body
Requests use Content-Type: multipart/form-data.
| Field | Type | Required | Description |
|---|---|---|---|
| audio | file | Required | Audio file (WAV, FLAC, MP3, or M4A). Max 32 MB per file. See the Audio Requirements guide for size and format limits. |
| recorded_at | string | Optional | 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 | Optional | Whether to apply speaker diarization. Default: false. |
| webhook_url | string | Optional | Per-request webhook URL, overriding the account-level webhook for this job. Must be provided together with webhook_secret_key — include both or omit both. |
| webhook_secret_key | string | Optional | Required when webhook_url is provided. Signs the webhook payload (HMAC-SHA256) so your server can verify it came from Amplifier. |
Response
The standard v2 job object, plus two fields:
| Field | Type | Description |
|---|---|---|
group_id | string | The group this job was registered into. |
recorded_at | string | null | The capture time you supplied, or null. |
Example
curl -X POST https://api.amplifierhealth.com/v2/models/apex/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"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"
}Longitudinal analyze
The same four endpoints with /longitudinal appended. These score the new recording against the group's history, adding baseline and deviation fields to each signal. The job is registered into the group as well, so it becomes part of the history for later calls.
| Endpoint | Path parameter |
|---|---|
POST /v2/models/{model_name}/groups/{group_id}/analyze/longitudinal | model_name |
POST /v2/signs/{sign_name}/groups/{group_id}/analyze/longitudinal | sign_name |
POST /v2/use-cases/{use_case_name}/groups/{group_id}/analyze/longitudinal | use_case_name |
POST /v2/conditions/{condition_name}/groups/{group_id}/analyze/longitudinal | condition_name |
Request Body
Identical to the analyze extensions above.
Longitudinal result fields
On model and use-case jobs these appear on each result.signals[] entry; on sign and condition jobs, on the singular result.signal.
| Field | Type | Description |
|---|---|---|
score, level, flagged | — | Unchanged from every other v2 job: this recording's own reading. |
latest_score | number | null | The smoothed current estimate after this recording — precision- and time-weighted across the window. May differ from the raw score. |
baseline_score | number | null | The subject's personal baseline, blended toward a population prior while personal history is still accumulating. |
deviation_from_baseline | number | null | latest_score − baseline_score. Positive means above the subject's own baseline. |
anomaly | boolean | 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 | null | Deviation expressed in units of the subject's own variability. |
population_z | number | 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/apex/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"Example completed job
Poll GET /v2/jobs/{job_id} until status is done. The job read returns the standard v2 job object — group_id and recorded_at are returned on the submission response above, 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": { "...": "..." }
}
}GET /v2/groups/{group_id}/longitudinal
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.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
group_id | string | The group to read. |
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| signal | string | Optional | Return only this signal, matched against signals[].name. Repeat the parameter for several signals. |
| from | string | Optional | ISO 8601 date. Show data points from this time onward. Filters the stored result for display; summary statistics are unchanged. |
| to | string | Optional | 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 | 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 analyze extension 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 | 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 | 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 | null | The subject's personal baseline, blended toward a population prior while personal history is still accumulating. |
latest_score | number | 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 | null | Latest deviation in units of the subject's own variability. |
population_z | number | null | The subject's current position relative to the cohort. null when no population reference is available. |
status | string | ok | establishing_baseline | 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 | 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". |
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"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 computing
{ "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 /v2/groups/{group_id}/aggregate
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. |
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"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. |
Errors
The analyze endpoints on this page return the same codes as POST /v2/models/{model_name}/analyze — including AUDIO_TOO_SHORT, AUDIO_TOO_LONG, UNSUPPORTED_FORMAT, and AUDIO_POOR_QUALITY.
Group-specific conditions:
| HTTP | Condition |
|---|---|
| 400 | group_id in the request body is empty, too long, or contains unsupported characters (POST /v2/groups); or from/to is not a valid ISO 8601 datetime. |
| 404 | The group, job, or membership does not exist for your account, or a group_id in the path is malformed. |
| 409 | POST /v2/groups was called with a group_id that already exists. |
For the full list and retry guidance, see Error Reference.
