Early access
Client API
Private previewRead your organization's programs, studies and collected responses from your own systems. Every endpoint is a GET, reads only, and answers JSON. The API has exactly the reads your dashboard shows, through the same code, so a number here and on screen cannot differ.
Keys
- Any login of your organization makes a key on the Developers page of the Tendral dashboard. You see the key once; Tendral keeps only its hash.
- Send it as a bearer token:
Authorization: Bearer tk_live_…. - A key reads your own organization's data only. No parameter names an organization; the key decides it.
- A key belongs to your organization, not to the person who made it: it keeps working when they leave, until someone revokes it on the Developers page.
- Every client key carries
client:programs:readandclient:responses:read. Each endpoint names the one it needs.
What the API never returns
- A respondent's name or NPI. A response carries the clinician's specialty and practice state, never who they are.
- A response a clinician has not completed, one from a clinician whose identity is not verified, or one not yet delivered to you.
- A program, study or response that is hidden from you or belongs to another organization: its id answers
404NOT_FOUND, the same as an id that does not exist.
Calling it
- Call it from your server. No endpoint sends CORS headers, so a browser page cannot call it, and a key must never be in one.
- Every answer carries
Cache-Control: private, no-store. Do not cache answers on a shared cache. - Each key may make 100 requests per minute (Rate limits).
- A parameter an endpoint does not take, a repeated one, or a malformed one is
400INVALID_PARAMETER, anderror.details.parameternames it. - While the client API is switched off, every endpoint answers
503CLIENT_API_DISABLED. Retry later.
Endpoints
| Endpoint | Path | Scope |
|---|---|---|
| List programs | GET /v1/client/programs | client:programs:read |
| Get a program | GET /v1/client/programs/{programId} | client:programs:read |
| Responses per day | GET /v1/client/programs/{programId}/series | client:programs:read |
| Get program audience | GET /v1/client/programs/{programId}/audience | client:programs:read |
| List study questions | GET /v1/client/studies/{surveyId}/questions | client:programs:read |
| Get study results | GET /v1/client/studies/{surveyId}/results | client:responses:read |
| List responses | GET /v1/client/programs/{programId}/responses | client:responses:read |
| Get a response | GET /v1/client/responses/{responseId} | client:responses:read |
| List what changed | GET /v1/client/changes | client:programs:read |
List programs
GET /v1/client/programs · scope client:programs:read
The programs this client can see, newest first. For each: its id, its organization, its name, program_status (the status an admin recorded; it does not say whether the program is collecting), study_count (its studies the client can see: live or closed, or sent for the client's review), collected (its responses the client can see: complete, from an identity-verified clinician, and delivered) and four facts from its studies: is_complete (every study closed), is_collecting (a study is live), has_previews_waiting (a study waits for the client's review) and has_scheduled_start (a study not yet live has a future start).
Parameters
No parameters.
Response
A list (object: "list"); every row is in data, on one page. Each row:
| Field | Type |
|---|---|
program_id | uuid |
organization_id | uuid |
name | string |
program_status | string |
study_count | integer |
collected | integer |
is_complete | boolean |
is_collecting | boolean |
has_previews_waiting | boolean |
has_scheduled_start | boolean |
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/programs"Get a program
GET /v1/client/programs/{programId} · scope client:programs:read
One program this client can see, as exactly one row. Its program_id, name and description. Four facts from its studies: is_complete (every study closed, or the program marked complete), is_collecting (a study is live), has_previews_waiting (a study waits for the client's review) and has_scheduled_start (a study not yet live has a future start). Counts are of responses the client can see (complete, from an identity-verified clinician, and delivered) in the studies it can see (live or closed, or sent for its review): collected (all), collected_last_14_days (the 14 UTC calendar days that end today, today included), new_last_24h and new_last_7d (by when each response first became visible to the client), clinician_count (distinct clinicians who answered) and study_count. first_collected_at and last_collected_at are ISO 8601 times, null when there are no responses. goals: each live goal, target_count responses by due_on (YYYY-MM-DD), with an optional label. next_step_body and next_step_at: the program's latest "Now" line and when it was written, or null. studies: each study it can see, with survey_id, title, status, collected, review_state (the latest review event: sent_for_review, approved or changes_requested; null when none), start_at / end_at, and target_count (how many responses the study is set to collect; null when it has no target). A program that does not exist, is hidden, or is not this client's is refused. It never returns a response rate, an invitation count, a pool size or a price.
Parameters
| Parameter | In | Required | Value |
|---|---|---|---|
programId | path | yes | string uuid. |
Response
One resource, with object naming it. Fields:
| Field | Type |
|---|---|
program_id | uuid |
name | string |
description | string or null |
is_complete | boolean |
is_collecting | boolean |
has_previews_waiting | boolean |
has_scheduled_start | boolean |
study_count | integer |
clinician_count | integer |
collected | integer |
collected_last_14_days | integer |
new_last_24h | integer |
new_last_7d | integer |
first_collected_at | date-time or null |
last_collected_at | date-time or null |
goals | array of object (goal_id, target_count, due_on, label) |
next_step_body | string or null |
next_step_at | date-time or null |
studies | array of object (survey_id, title, status, collected, review_state, start_at, end_at, target_count) |
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/programs/<programId>"Responses per day
GET /v1/client/programs/{programId}/series · scope client:programs:read
One program's responses per day, from the day of its first visible response to today, in timeZone (an IANA name such as America/New_York; UTC when left out). One row per day: day (YYYY-MM-DD), collected_on_day (responses that first became visible to the client that day; a response not yet stamped counts today) and cumulative (the running total). Days with none are listed with 0; no rows means the program has no visible responses. It counts only responses the client can see (complete, from an identity-verified clinician, and delivered) in the studies it can see. A time zone that does not exist is refused as invalid input; a program that does not exist, is hidden, or is not this client's is refused. It never returns a rate or a forecast.
Parameters
| Parameter | In | Required | Value |
|---|---|---|---|
programId | path | yes | string uuid. |
timeZone | query | no | string matching ^[A-Za-z0-9_+/-]+$. |
Response
A list (object: "list"); every row is in data, on one page. Each row:
| Field | Type |
|---|---|
day | date (YYYY-MM-DD) |
collected_on_day | integer |
cumulative | integer |
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/programs/<programId>/series"Get program audience
GET /v1/client/programs/{programId}/audience · scope client:programs:read
Who one program targets and who answered, as exactly one row. audiences: each audience the survey admits, with specialties (NUCC taxonomy code and label), states (an empty list means every state) and activity_list (true when the audience is also selected by treatment activity; the list itself is never named). respondents_by_state and respondents_by_specialty: distinct clinicians whose responses the client can see, by practice state and by primary specialty from the NPI registry, largest group first; clinicians with no registry entry are counted under null. A program that does not exist, is hidden, or is not this client's is refused. It never returns a pool size, an invitation count, a vendor or list name, or a rate.
Parameters
| Parameter | In | Required | Value |
|---|---|---|---|
programId | path | yes | string uuid. |
Response
One resource, with object naming it. Fields:
| Field | Type |
|---|---|
audiences | array of object (specialties, states, activity_list) |
respondents_by_state | array of object (state, clinicians) |
respondents_by_specialty | array of object (code, label, clinicians) |
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/programs/<programId>/audience"List study questions
GET /v1/client/studies/{surveyId}/questions · scope client:programs:read
The questions of one study this client can see, in order: one row per question, plus one row per sub-question of a compound question (parent_id is its question; sub_position its place). Each row: question_id (the key of its answer in a response's answers), position, text, type, scale_visualization, case_id, options (each choice's value and text, or null), scale_points (5 when the study set none), scale_options (a labelled scale's words, the study's own or the default ones; null for other scales), and what the question offered: allow_other ("Other"), allow_none ("None of the above") with none_text, items (best/worst items and grid statements, each value and text), scale_headers (a grid's columns, in order), scale_labels (a scale's end words), yes_text / no_text, and point_labels (the word shown on each point of a compound part's scale, in point order, or null). Each question is read from the newest version of the study that a visible response answered, else from the study's current setup. A study that does not exist, is not live, closed or sent for the client's review, is in a hidden program, or is not this client's is refused. It never returns answers, counts or rates.
Parameters
| Parameter | In | Required | Value |
|---|---|---|---|
surveyId | path | yes | string uuid. |
Response
A list (object: "list"); every row is in data, on one page. Each row:
| Field | Type |
|---|---|
question_id | string |
parent_id | string or null |
position | integer |
sub_position | integer or null |
text | string or null |
type | string or null |
scale_visualization | string or null |
case_id | string or null |
options | array of object (value, text) or null |
scale_points | integer or null |
scale_options | array of any JSON value or null |
allow_other | boolean |
allow_none | boolean |
none_text | string or null |
items | array of object (value, text) or null |
scale_headers | array of string or null |
scale_labels | object (start, end) or null |
yes_text | string or null |
no_text | string or null |
point_labels | array of string or null |
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/studies/<surveyId>/questions"Get study results
GET /v1/client/studies/{surveyId}/results · scope client:responses:read
How the responses this client can see answered each question of one study: one row per question, plus one row per sub-question of a compound question (parent_id is its question), in the order of study_questions. Each row: question_id, parent_id, responses and clinicians (the totals counted, the same on every row), answered (how many of those responses answered the question), counts (a list of { value, count }: every option of the study's newest questions in their order, zeros included, then other when the question offers "Other" and none when it offers "None of the above", each at zero when nobody picked it; yes and no; each scale word, or each point from 1 to scale_points; each number, ascending), unlisted (answers, picks or cells that are not on that list — including a pick of "Other" or "None of the above" the question did not offer) and item_counts (best/worst: per item, { item, shown, best, worst } — how many sets showed it and how often it was picked most and least; a grid: per statement, { item, counts } with every column's count; empty for other questions). Free text, the old multi-dimension grid and a compound question's own row give answered only, with no counts. Filter: specialty (up to 50 NUCC taxonomy codes, matched on the clinician's primary specialty; an empty list is no filter). A study with no questions has no rows. A study that does not exist, is not live, closed or sent for the client's review, is in a hidden program, or is not this client's is refused. It returns counts only: never what a clinician wrote, and no percents or averages.
Parameters
| Parameter | In | Required | Value |
|---|---|---|---|
surveyId | path | yes | string uuid. |
specialty | query | no | string matching ^[0-9A-Z]{9}X$, repeatable. Repeat specialty for each value. |
Response
A list (object: "list"); every row is in data, on one page. Each row:
| Field | Type |
|---|---|
question_id | string |
parent_id | string or null |
responses | integer |
clinicians | integer |
answered | integer |
counts | array of object (value, count) |
unlisted | integer |
item_counts | array of object |
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/studies/<surveyId>/results"List responses
GET /v1/client/programs/{programId}/responses · scope client:responses:read
One page of a program's responses this client can see (complete, from an identity-verified clinician, and delivered), in the studies it can see, newest completed first: ordered by sort_at (the completed time, or the created time when none was recorded) then response_id, both descending. Filters: surveyId (one study), specialty (up to 50 NUCC taxonomy codes, matched on the clinician's primary specialty; an empty list is no filter) and since (only responses that first became visible after this ISO 8601 time). limit: 1 to 500 rows, 100 when left out. A walk returns each row once; a later call with since can return a response again (at least once), so de-duplicate by response_id. Each row: response_id, survey_id, study_title, completed_at, first_visible_at (null until stamped), sort_at, specialty_code and specialty_label, practice_state, answers (keyed by question_id from study_questions; only answers to known questions) and unmapped_answer_count (the answers left out). A program or study that does not exist, is hidden, or is not this client's is refused. It never returns an email, a fraud or quality score, a payout or a rate.
Parameters
| Parameter | In | Required | Value |
|---|---|---|---|
programId | path | yes | string uuid. |
surveyId | query | no | string uuid. |
specialty | query | no | string matching ^[0-9A-Z]{9}X$, repeatable. Repeat specialty for each value. |
since | query | no | string date-time. |
cursor | query | no | string. next_cursor from the previous page, unchanged. Omit it for the first page. |
limit | query | no | integer from 1 to 500. Default 100. |
Response
A list (object: "list"), one page at a time: pass next_cursor back as ?cursor= while has_more is true. Each row:
| Field | Type |
|---|---|
response_id | uuid |
survey_id | uuid |
study_title | string |
completed_at | date-time or null |
first_visible_at | date-time or null |
sort_at | date-time |
specialty_code | string or null |
specialty_label | string or null |
practice_state | string or null |
answers | object of any JSON value |
unmapped_answer_count | integer |
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/programs/<programId>/responses"Get a response
GET /v1/client/responses/{responseId} · scope client:responses:read
One response this client can see, as exactly one row: the same row responses_page returns (response_id, survey_id, study_title, completed_at, first_visible_at, sort_at, specialty_code, specialty_label, practice_state, answers keyed by question_id, unmapped_answer_count). A response that does not exist, is not visible to the client (not complete, not identity-verified or not delivered), is in a hidden program or study, or is not this client's is refused. It never returns an email, a fraud or quality score, a payout or a rate.
Parameters
| Parameter | In | Required | Value |
|---|---|---|---|
responseId | path | yes | string uuid. |
Response
One resource, with object naming it. Fields:
| Field | Type |
|---|---|
response_id | uuid |
survey_id | uuid |
study_title | string |
completed_at | date-time or null |
first_visible_at | date-time or null |
sort_at | date-time |
specialty_code | string or null |
specialty_label | string or null |
practice_state | string or null |
answers | object of any JSON value |
unmapped_answer_count | integer |
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/responses/<responseId>"List what changed
GET /v1/client/changes · scope client:programs:read
What changed after since (an ISO 8601 time) across every program this client can see, newest first. One row per program and kind, with program_id, program_name, occurred_at (the latest time behind the row), count and detail: responses (count = new responses the client can see; detail.by_study = [{ survey_id, title, count }]), goal_met (count = 1; detail = the goal: goal_id, target_count, due_on, label), next_step (count = 1; detail.body = the program's new "Now" line) and review_requested (count = studies; detail.studies = [{ survey_id, title }] waiting for the client's review). A response is new when it first became visible after since; one not yet stamped counts as new, so it can be counted again on a later call. No rows means nothing changed. It never returns respondent identity, a pool size, an invitation count or a rate.
Parameters
| Parameter | In | Required | Value |
|---|---|---|---|
since | query | yes | string date-time. |
Response
A list (object: "list"); every row is in data, on one page. Each row:
When kind is "responses":
| Field | Type |
|---|---|
program_id | uuid |
program_name | string |
occurred_at | date-time |
kind | "responses" |
count | integer |
detail | object (by_study) |
When kind is "goal_met":
| Field | Type |
|---|---|
program_id | uuid |
program_name | string |
occurred_at | date-time |
kind | "goal_met" |
count | 1 |
detail | object (goal_id, target_count, due_on, label) |
When kind is "next_step":
| Field | Type |
|---|---|
program_id | uuid |
program_name | string |
occurred_at | date-time |
kind | "next_step" |
count | 1 |
detail | object (body) |
When kind is "review_requested":
| Field | Type |
|---|---|
program_id | uuid |
program_name | string |
occurred_at | date-time |
kind | "review_requested" |
count | integer |
detail | object (studies) |
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/changes"