Skip to content
Tendral Health

Client API

Private preview

Read 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:read and client: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 404 NOT_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 400 INVALID_PARAMETER, and error.details.parameter names it.
  • While the client API is switched off, every endpoint answers 503 CLIENT_API_DISABLED. Retry later.

Endpoints

EndpointPathScope
List programsGET /v1/client/programsclient:programs:read
Get a programGET /v1/client/programs/{programId}client:programs:read
Responses per dayGET /v1/client/programs/{programId}/seriesclient:programs:read
Get program audienceGET /v1/client/programs/{programId}/audienceclient:programs:read
List study questionsGET /v1/client/studies/{surveyId}/questionsclient:programs:read
Get study resultsGET /v1/client/studies/{surveyId}/resultsclient:responses:read
List responsesGET /v1/client/programs/{programId}/responsesclient:responses:read
Get a responseGET /v1/client/responses/{responseId}client:responses:read
List what changedGET /v1/client/changesclient: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:

FieldType
program_iduuid
organization_iduuid
namestring
program_statusstring
study_countinteger
collectedinteger
is_completeboolean
is_collectingboolean
has_previews_waitingboolean
has_scheduled_startboolean
curl
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

ParameterInRequiredValue
programIdpathyesstring uuid.

Response

One resource, with object naming it. Fields:

FieldType
program_iduuid
namestring
descriptionstring or null
is_completeboolean
is_collectingboolean
has_previews_waitingboolean
has_scheduled_startboolean
study_countinteger
clinician_countinteger
collectedinteger
collected_last_14_daysinteger
new_last_24hinteger
new_last_7dinteger
first_collected_atdate-time or null
last_collected_atdate-time or null
goalsarray of object (goal_id, target_count, due_on, label)
next_step_bodystring or null
next_step_atdate-time or null
studiesarray of object (survey_id, title, status, collected, review_state, start_at, end_at, target_count)
curl
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

ParameterInRequiredValue
programIdpathyesstring uuid.
timeZonequerynostring matching ^[A-Za-z0-9_+/-]+$.

Response

A list (object: "list"); every row is in data, on one page. Each row:

FieldType
daydate (YYYY-MM-DD)
collected_on_dayinteger
cumulativeinteger
curl
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

ParameterInRequiredValue
programIdpathyesstring uuid.

Response

One resource, with object naming it. Fields:

FieldType
audiencesarray of object (specialties, states, activity_list)
respondents_by_statearray of object (state, clinicians)
respondents_by_specialtyarray of object (code, label, clinicians)
curl
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

ParameterInRequiredValue
surveyIdpathyesstring uuid.

Response

A list (object: "list"); every row is in data, on one page. Each row:

FieldType
question_idstring
parent_idstring or null
positioninteger
sub_positioninteger or null
textstring or null
typestring or null
scale_visualizationstring or null
case_idstring or null
optionsarray of object (value, text) or null
scale_pointsinteger or null
scale_optionsarray of any JSON value or null
allow_otherboolean
allow_noneboolean
none_textstring or null
itemsarray of object (value, text) or null
scale_headersarray of string or null
scale_labelsobject (start, end) or null
yes_textstring or null
no_textstring or null
point_labelsarray of string or null
curl
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

ParameterInRequiredValue
surveyIdpathyesstring uuid.
specialtyquerynostring 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:

FieldType
question_idstring
parent_idstring or null
responsesinteger
cliniciansinteger
answeredinteger
countsarray of object (value, count)
unlistedinteger
item_countsarray of object
curl
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

ParameterInRequiredValue
programIdpathyesstring uuid.
surveyIdquerynostring uuid.
specialtyquerynostring matching ^[0-9A-Z]{9}X$, repeatable. Repeat specialty for each value.
sincequerynostring date-time.
cursorquerynostring. next_cursor from the previous page, unchanged. Omit it for the first page.
limitquerynointeger 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:

FieldType
response_iduuid
survey_iduuid
study_titlestring
completed_atdate-time or null
first_visible_atdate-time or null
sort_atdate-time
specialty_codestring or null
specialty_labelstring or null
practice_statestring or null
answersobject of any JSON value
unmapped_answer_countinteger
curl
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

ParameterInRequiredValue
responseIdpathyesstring uuid.

Response

One resource, with object naming it. Fields:

FieldType
response_iduuid
survey_iduuid
study_titlestring
completed_atdate-time or null
first_visible_atdate-time or null
sort_atdate-time
specialty_codestring or null
specialty_labelstring or null
practice_statestring or null
answersobject of any JSON value
unmapped_answer_countinteger
curl
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

ParameterInRequiredValue
sincequeryyesstring date-time.

Response

A list (object: "list"); every row is in data, on one page. Each row:

When kind is "responses":

FieldType
program_iduuid
program_namestring
occurred_atdate-time
kind"responses"
countinteger
detailobject (by_study)

When kind is "goal_met":

FieldType
program_iduuid
program_namestring
occurred_atdate-time
kind"goal_met"
count1
detailobject (goal_id, target_count, due_on, label)

When kind is "next_step":

FieldType
program_iduuid
program_namestring
occurred_atdate-time
kind"next_step"
count1
detailobject (body)

When kind is "review_requested":

FieldType
program_iduuid
program_namestring
occurred_atdate-time
kind"review_requested"
countinteger
detailobject (studies)
curl
curl -H "Authorization: Bearer $TENDRAL_API_KEY" "https://api.tendralhealth.com/v1/client/changes"