REST API
The REST API covers the participant side of a study from your own systems: launch a study for your own people, add people to it, prepare and download their personal links, name one person, read the list, and read a study's shareable links. Everything else (creating and designing studies, reading results and analysis) is on the MCP server, which takes the same key.
Basics
- Base URL:
https://research.underheard.ai - Authentication:
Authorization: Bearer <key>, an API key. Writes need a key that can edit. - Bodies: JSON,
Content-Type: application/json. Responses are JSON unless noted. - Study id: the second segment of a study's address,
/studies/<study id>/recruit, orresearch_list_studiesover MCP. - Limits: a request body is at most 12 MB for people, 2 KB for links. A list holds at most 10,000 people.
Every response carries Cache-Control: private, no-store.
Launch a study for your own people
POST /api/research/studies/<study id>/launch
{ "targetCompletes": 50, "mode": "voice" }
Launches a study whose participants are your own people, as the Launch button on Recruit does: the newest published guide is pinned and fieldwork opens. Nothing is bought and nobody is contacted. targetCompletes is how many responses to collect (1 to 10,000). mode is optional (text, voice or mixed): voice for a voice-led guide and text otherwise.
The study's brief must say the participants are your own people (recruitmentIntent bring-own) and carry the decision, the audience and the experience mode, and the guide must be published. A study recruiting from the Underheard panel is refused with 422: it launches on Recruit, where the order and its cost are shown.
Response:
{
"studyId": "811a7a3b-6a3a-4171-bc0f-2ef868a77ba5",
"launched": true,
"recruitmentPlanId": "...",
"fieldwork": { "status": "active", "mode": "voice", "targetCompletes": 50, "launchedAt": "2026-10-23T09:00:00.000Z", "instrumentRevisionId": "..." },
"links": {
"study": { "path": "/take/<token>", "url": "https://research.underheard.ai/take/<token>", "enabled": true, "availability": "open" },
"test": { "path": "/take/<token>", "url": "https://research.underheard.ai/take/<token>", "availability": "open", "interviews": [] }
}
}
Calling it again after launch changes nothing and answers launched: false with the same links.
Add people to a study
POST /api/research/own-people
{
"action": "add-people",
"studyId": "811a7a3b-6a3a-4171-bc0f-2ef868a77ba5",
"mode": "add",
"source": "HubSpot list: Gold customers",
"defaultCallingCode": "41",
"people": [
{ "name": "Rami Haddad", "phone": "+971 50 123 4567", "email": "rami@example.com", "segments": { "Company": "Aurora Beauty", "Country": "UAE" } },
{ "name": "Lina Khoury", "email": "lina@example.com", "segments": { "Company": "Cedar Labs" } }
]
}
| Field | Required | Meaning |
|---|---|---|
action | yes | add-people |
studyId | yes | The study the list belongs to. |
people | yes | 1 to 10,000 people. |
mode | no | add (default) keeps the current list and skips people already on it; replace starts the list again. |
source | no | A label for where the list came from, shown above the list. Defaults to "the API". |
defaultCallingCode | no | Digits only, no plus, for example 971. Used for numbers written with a single leading zero. Defaults to the first of the study's markets that has one. |
Per person:
| Field | Required | Meaning |
|---|---|---|
name | no | Used in the greeting and in an invitation. |
phone | no | Any readable form. +, 00 and a leading 0 with a defaultCallingCode all resolve; anything else without a country code is left out rather than guessed. |
email | no | Normalised to lower case. |
segments | no | Up to 40 of your own labels, each with a string or a number. They become the segments you compare in Fieldwork and Results. |
A person needs a phone number or an email. One with neither is left out.
Response:
{ "added": 2, "skipped": 0, "people": 10, "leftOut": { "noContact": 0, "unreadablePhone": 0, "duplicatesInRequest": 0 }, "segments": ["company", "country"] }
added is how many were written; skipped how many were already on the list by phone or email; people the size of the list afterwards; leftOut the rows nobody could be reached on.
Prepare personal links
POST /api/research/own-people
{ "action": "prepare-links", "studyId": "<study id>", "personIds": ["p_1a2b3c4d5e6f"] }
Prepares a personal link for every person on the list who has none, or for the personIds given. The study has to be launched and fieldwork active. Links stay valid for 7 days; calling again reissues expired ones and leaves live ones alone.
Response:
{ "prepared": 120, "alreadyLinked": 30, "rejected": { "quota": 0, "target": 0 }, "liveLinks": 150 }
The links themselves are not in this response: fetch them with links-csv.
Download the links
POST /api/research/own-people
{ "action": "links-csv", "studyId": "<study id>" }
Returns text/csv with the columns Name, Phone, Email, one column per segment, Personal link, Link expires. One row per person with a live link. Send each person their own link through your own channel.
A link for one named person
POST /api/research/own-people
{ "action": "person-link", "studyId": "<study id>", "name": "Nina Roth" }
Adds a person with that name (no contact details) to the list and returns their personal link once:
{ "personId": "p_9f8e7d6c5b4a", "name": "Nina Roth", "link": "https://research.underheard.ai/respond/<token>", "expiresAt": "2026-10-26T09:00:00.000Z" }
Read the list
POST /api/research/own-people
{ "action": "list-csv", "studyId": "<study id>" }
Returns text/csv with Name, Phone, Email, one column per segment, and Opted out.
GET /api/research/own-people?studyId=<study id>
Returns the list as JSON: roster (the people, their segments, their links), tracking (how far each person got: not-invited, link-issued, invited, opened, started, completed), funnel (the counts), launched and liveLinks.
A study's shareable links
GET /api/research/study-links?studyId=<study id>
{
"study": { "path": "/take/<token>", "url": "https://research.underheard.ai/take/<token>", "enabled": true, "availability": "open" },
"test": { "path": "/take/<token>", "url": "https://research.underheard.ai/take/<token>", "availability": "open", "interviews": [] }
}
availability is one of open, not-launched, paused, closed, or not-own-people (a study launched before own people had a place in every plan; its path is null). enabled is whether the study link is turned on.
POST /api/research/study-links
{ "action": "set-enabled", "studyId": "<study id>", "enabled": false }
{ "action": "replace", "studyId": "<study id>", "kind": "study" }
set-enabled turns the study link off or on; the same address keeps working when turned on again. replace makes a new study link or test link (kind is study or test); the old address stops working.
Browser-only actions
With a key, the own-people route refuses import (the mapping drawer's upload), delete, opt-out and send with 403. Deleting a list, opting someone out and sending messages stay on screen, where the researcher sees the count and the message before pressing. So does launching a panel study.
Errors
| Status | When |
|---|---|
400 | The body is not JSON, a field is missing or malformed, or no person in the request has a usable phone or email. |
401 | No key and no session, or a key that is revoked, expired or unknown. |
403 | A read key on a write, a browser-only action with a key, a key whose owner left the workspace, or a browser request from another origin. |
404 | The study does not exist or is not in the key's workspace. |
409 | The study is not launched or fieldwork is not active, the list would pass 10,000 people, the study is full, the record changed under a concurrent write, or (on launch) the brief is incomplete, the guide is not published, or a prepared plan has another target. |
413 | The body is too large. Split it. |
422 | On launch: the study recruits from a panel. Launch it on Recruit. |
Every error body is { "error": "..." } in plain words.
Example
curl -X POST https://research.underheard.ai/api/research/own-people \
-H "Authorization: Bearer uh_research_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "add-people",
"studyId": "811a7a3b-6a3a-4171-bc0f-2ef868a77ba5",
"people": [
{ "name": "Rami Haddad", "phone": "+971501234567", "segments": { "Company": "Aurora Beauty" } },
{ "name": "Lina Khoury", "email": "lina@example.com", "segments": { "Company": "Cedar Labs" } }
]
}'
Then launch, prepare the links and download them:
curl -X POST https://research.underheard.ai/api/research/studies/811a7a3b-6a3a-4171-bc0f-2ef868a77ba5/launch \
-H "Authorization: Bearer uh_research_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "targetCompletes": 50 }'
curl -X POST https://research.underheard.ai/api/research/own-people \
-H "Authorization: Bearer uh_research_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "action": "prepare-links", "studyId": "811a7a3b-6a3a-4171-bc0f-2ef868a77ba5" }'
curl -X POST https://research.underheard.ai/api/research/own-people \
-H "Authorization: Bearer uh_research_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "action": "links-csv", "studyId": "811a7a3b-6a3a-4171-bc0f-2ef868a77ba5" }' -o personal-links.csv