Frank Public API — Developer Guide
API documentation
Create products and researches, publish them for interviews, and pull transcripts — programmatically. All requests and responses are camelCase JSON; all timestamps are ISO 8601 UTC strings.
Authentication
Send your secret key as a bearer token on every request. Keys are created under API keys and shown only once.
- Base URL
- https://api.hifrank.ai
- Prefix
- /v1
- Header
- Authorization: Bearer frank_sk_live_...
First request
curl https://api.hifrank.ai/v1/products \
-H "Authorization: Bearer frank_sk_live_..."
Typical flow
POST /v1/products— describe what you're researching.POST /v1/researches— goals + topics/questions. Comes back as a draft.PUT /v1/researches/{id}/product— attach the product (optional).PUT /v1/researches/{id}/publish— pick interviewer + language; becomes active and can take interviews.GET /v1/researches/{id}/interviewsthenGET /v1/interviews/{id}— read transcripts.
Products
Create a product
Request body
| Field | Type | Notes |
|---|---|---|
| namerequired | string | Max 60 characters. Must be unique across your account — a duplicate returns 409. |
| descriptionrequired | string | Max 1500 characters. What the product is. |
| categoryrequired | enum | One of the 16 values below. |
| briefoptional | string | Context the interviewer uses. Defaults to description. |
| urlsoptional | string[] | Up to 10 http(s) URLs. |
category values
saas_software
mobile_app
apparel_accessories
beauty_personal_care
home_living
electronics_consumer_tech
sports_outdoor
toys_hobbies
food_beverages
health_wellness
pet_products
automotive_accessories
office_stationery
online_services
offline_services
other
Request
curl -X POST https://api.hifrank.ai/v1/products \
-H "Authorization: Bearer frank_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Notes",
"description": "A note-taking app for teams",
"category": "saas_software",
"urls": ["https://acme.example"]
}'
Response · 201 Created
| Field | Type | Notes |
|---|---|---|
| productIdalways present | string (uuid) | Use this to link the product to a research. |
| namealways present | string | As sent. |
| descriptionalways present | string | null | As sent. |
| categoryalways present | string | One of the category values. |
| briefalways present | string | Sent value, or the description. |
| urlsalways present | string[] | Empty array when none were sent. |
| createdAtalways present | string (ISO 8601) | UTC, e.g. 2026-01-01T10:00:00.000Z. |
Response
{
"productId": "d91c8f22-3b7e-4c21-9f10-6a2b8c4d5e70",
"name": "Acme Notes",
"description": "A note-taking app for teams",
"category": "saas_software",
"brief": "A note-taking app for teams",
"urls": ["https://acme.example"],
"createdAt": "2026-01-01T10:00:00.000Z"
}
List products
Returns an array of the product object above, newest first. No parameters.
Get a product
Returns one product object. id must be a uuid — anything else is a 400; a product that isn't yours is a 404.
Update a product
Send only the fields you want to change — omitted fields keep their current value. Same field rules and types as create; all are optional here. Returns the updated product object.
Researches
Create a research
Creates the research immediately as a draft. It cannot take interviews until you publish it.
Request body
| Field | Type | Notes |
|---|---|---|
| namerequired | string | Non-empty; whitespace is trimmed. |
| descriptionrequired | string | What you want to learn. |
| goalsrequired | string[] | At least one non-empty goal. |
| topicsrequired | object[] | At least one topic — see the topic fields below. |
| researchTypeoptional | enum | discovery, product_experience or retention_growth. |
| botTypesoptional | enum[] | Any of audio, video, chat. Publishing sets this to ["audio"]. |
| greetingMessageoptional | object | Opening line per channel: chat, audio, video — all optional strings. |
| promptsoptional | object | Overrides the generated prompts: voice, video, chat, tts_style — all optional strings. Sending these stops Frank from auto-updating them later. |
topics[] — each topic
| Field | Type | Notes |
|---|---|---|
| namerequired | string | Topic heading. |
| messageoptional | string | Intro the interviewer reads before the topic. |
| questionsrequired | object[] | At least one question. |
topics[].questions[] — each question
| Field | Type | Notes |
|---|---|---|
| textrequired | string | The question itself. |
| followUpDepthoptional | string | How hard to probe, e.g. shallow, medium, deep. Free text — it is guidance for the interviewer, not a fixed set. |
| probingoptional | string | What to dig into, e.g. ask for specific examples. |
| tagsoptional | string | Your own labels for grouping, e.g. onboarding,pricing. |
Request
curl -X POST https://api.hifrank.ai/v1/researches \
-H "Authorization: Bearer frank_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Trial churn discovery",
"description": "Why trial users do not upgrade",
"researchType": "discovery",
"goals": ["Understand upgrade blockers"],
"topics": [
{
"name": "Onboarding",
"message": "Let us start with your first few days.",
"questions": [
{
"text": "How was your first week?",
"followUpDepth": "deep",
"probing": "Ask for specific moments of friction",
"tags": "onboarding"
},
{ "text": "What confused you early on?" }
]
}
]
}'
Response · 201 Created
| Field | Type | Notes |
|---|---|---|
| researchIdalways present | string (uuid) | Use it for link, publish and interview calls. |
| namealways present | string | null | As sent. |
| statusalways present | string | draft on create, active after publish, stopped when halted. |
| typealways present | string | null | The researchType you sent, else null. |
| goalsalways present | string[] | As sent. |
| productIdalways present | string (uuid) | null | null until you link a product. |
| createdAtalways present | string (ISO 8601) | UTC. |
Response
{
"researchId": "6b2f8c10-1a4d-4e88-b0c3-2f7a9d1e4b56",
"name": "Trial churn discovery",
"status": "draft",
"type": "discovery",
"goals": ["Understand upgrade blockers"],
"productId": null,
"createdAt": "2026-01-01T10:00:00.000Z"
}
Link a product
Request body
| Field | Type | Notes |
|---|---|---|
| productIdrequired | string (uuid) | A product you own. |
A product can back many researches; a research holds at most one. Sending the product that's already linked is a no-op (200). Linking a research that already has a different product returns 409 — there is no re-link. Returns the research object.
Request
curl -X PUT https://api.hifrank.ai/v1/researches/6b2f8c10-.../product \
-H "Authorization: Bearer frank_sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "productId": "d91c8f22-..." }'
Publish (activate)
Sets the interviewer and flips the research to active so it can take voice interviews.
Request body
| Field | Type | Notes |
|---|---|---|
| languagerequired | enum | ISO 639-1 code from the list below. Case-insensitive. |
| personaoptional | enum | olivia (default) or frank — the interviewer's voice and name. |
| durationoptional | integer | Target interview length in minutes. Minimum 1. |
| promptsoptional | object | Same shape as on create; overrides the generated prompts. |
language values
zh | Chinese |
en | English |
fr | French |
de | German |
it | Italian |
ja | Japanese |
ko | Korean |
pt | Portuguese |
es | Spanish |
multi | Multilanguage — follows the participant |
Request
curl -X PUT https://api.hifrank.ai/v1/researches/6b2f8c10-.../publish \
-H "Authorization: Bearer frank_sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "persona": "olivia", "language": "en", "duration": 15 }'
Returns the research object with status set to active.
List researches
Returns an array of the research object above, newest first. No parameters.
Participants
A research keeps a list of known participants. Give one private context and, when they later join an interview with the matching email, the interviewer receives that context and asks sharper questions. The context is never shown to the participant.
Add or update a participant
Upserts a single participant by email. Sending the same email again updates that entry rather than creating a duplicate.
Request body
| Field | Type | Notes |
|---|---|---|
| emailrequired | string | Matched trimmed and case-insensitive. |
| nameoptional | string | The participant's name. |
| contextrequired | string | Private interviewer notes, plain text, up to 1,000 characters. Required when adding a participant; on an update you may omit it to keep the stored value. It can never be blanked — remove the participant instead. Over 1,000 characters returns 400 and stores nothing. |
Request
curl -X PUT https://api.hifrank.ai/v1/researches/6b2f8c10-.../participants \
-H "Authorization: Bearer frank_sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "bob@example.com", "name": "Bob", "context": "Former enterprise customer — probe on pricing and onboarding." }'
Response
| Field | Type | Notes |
|---|---|---|
| researchIdalways present | string (uuid) | The parent research. |
| emailalways present | string | The stored, normalized email. |
| namealways present | string | null | The participant's name. |
| hasContextalways present | boolean | Whether usable private context is stored. |
| createdAtalways present | string (ISO 8601) | When the entry was created. |
| updatedAtalways present | string (ISO 8601) | When it was last updated. |
Draft, active and stopped researches accept participants. A deleted research returns 409.
Remove a participant
Removes the participant and their private context. Past interviews they already completed are left untouched. Returns 204 with no body; an email that isn't on the list returns 404.
Request body
| Field | Type | Notes |
|---|---|---|
| emailrequired | string | The participant to remove. Matched trimmed and case-insensitive. |
Request
curl -X DELETE https://api.hifrank.ai/v1/researches/6b2f8c10-.../participants \
-H "Authorization: Bearer frank_sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "bob@example.com" }'
Website widget
Every research has three widget formats — floating, inline and popup — each with its own embed ID and settings. The protected domains and the widget's on/off switch are changed in the dashboard only.
List widget formats
The three formats with their embed IDs, plus the research's protected domains and whether the widget is switched on.
Request
curl https://api.hifrank.ai/v1/researches/6b2f8c10-.../embeds \
-H "Authorization: Bearer frank_sk_live_..."
Response
| Field | Type | Notes |
|---|---|---|
| researchIdalways present | string (uuid) | The research. |
| protectedDomainsalways present | string[] | The sites the widget may run on. |
| activatedalways present | boolean | Whether the widget is switched on. |
| formatsalways present | { type, embedId }[] | One entry each for floating, inline and popup. |
Get a widget format
One format's settings, its install snippet — exactly what the Code tab shows — how to install it, and whether it will run. type is floating, inline or popup.
Request
curl https://api.hifrank.ai/v1/researches/6b2f8c10-.../embeds/floating \
-H "Authorization: Bearer frank_sk_live_..."
Response
| Field | Type | Notes |
|---|---|---|
| embedIdalways present | string | This format's key; it only works with this format's snippet. |
| settingsalways present | object | Intro copy, button labels, launcher position (floating only), identity mode and fields, consent options and appearance. |
| snippetalways present | string | The install snippet. |
| installalways present | object | Where the snippet goes, what the page needs, what single-page apps must do, the identity placeholders to replace, and a link to the setup guide. |
| runnablealways present | boolean | Whether participants can take an interview once it's installed. |
| blockersalways present | { code, message }[] | What stops them: research_draft, research_stopped, embed_revoked, widget_off, no_protected_domain, out_of_credits. out_of_credits leaves the widget visible but stops interviews from starting; the others keep it hidden. |
Change a widget format
Send any subset of settings; everything else keeps its stored value and the other formats are untouched. Unknown fields, domain and activated return 400.
Request
curl -X PUT https://api.hifrank.ai/v1/researches/6b2f8c10-.../embeds/floating \
-H "Authorization: Bearer frank_sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "floatingAlign": "left", "startingContent": { "title": "How was checkout?" } }'
Response (in addition to the GET fields)
| Field | Type | Notes |
|---|---|---|
| snippetChangedalways present | boolean | True only when the identify call in the snippet changed — replace the installed snippet then. |
| snippetChangedNotealways present | string | The same, in a sentence. |
Title up to 50 characters, description 350, button labels 40. A deleted research returns 409.
Interviews & transcripts
List a research's interviews
Summaries only, newest first — use it to discover interview ids.
Response · array of
| Field | Type | Notes |
|---|---|---|
| interviewIdalways present | string (uuid) | Fetch the transcript with it. |
| researchIdalways present | string (uuid) | The parent research. |
| statusalways present | string | null | scheduled, in_progress, completed, incomplete or dropped. |
| typealways present | string | audio, video or chat. |
| participantalways present | object | name and email, each a string or null when the participant stayed anonymous. |
| startedAtalways present | string (ISO 8601) | null | null if it never started. |
| endedAtalways present | string (ISO 8601) | null | null while in progress. |
| durationalways present | integer | null | Length in seconds. |
Response
[
{
"interviewId": "2b7f8c10-7c1d-4a90-9e63-1d5c8b3a2f41",
"researchId": "6b2f8c10-1a4d-4e88-b0c3-2f7a9d1e4b56",
"status": "completed",
"type": "audio",
"participant": { "name": "Jane", "email": "jane@acme.com" },
"startedAt": "2026-01-01T10:00:00.000Z",
"endedAt": "2026-01-01T10:20:00.000Z",
"duration": 1200
}
]
Get an interview + transcript
Every field from the summary above, plus the full transcript in order.
transcript[] — each turn
| Field | Type | Notes |
|---|---|---|
| sequencealways present | integer | 1-based turn order. |
| rolealways present | string | assistant (the interviewer) or user (the participant). |
| textalways present | string | What was said. |
| startTimestampalways present | string (ISO 8601) | When the turn started. |
| endTimestampalways present | string (ISO 8601) | When the turn ended. |
Response
{
"interviewId": "2b7f8c10-...",
"researchId": "6b2f8c10-...",
"status": "completed",
"type": "audio",
"participant": { "name": "Jane", "email": "jane@acme.com" },
"startedAt": "2026-01-01T10:00:00.000Z",
"endedAt": "2026-01-01T10:20:00.000Z",
"duration": 1200,
"transcript": [
{
"sequence": 1,
"role": "assistant",
"text": "Thanks for joining — how was your first week?",
"startTimestamp": "2026-01-01T10:00:04.000Z",
"endTimestamp": "2026-01-01T10:00:08.000Z"
},
{
"sequence": 2,
"role": "user",
"text": "Honestly, setup took longer than I expected.",
"startTimestamp": "2026-01-01T10:00:09.000Z",
"endTimestamp": "2026-01-01T10:00:14.000Z"
}
]
}
Errors & rate limits
Every error uses the same envelope. field is present on validation errors and names the offending property. Every response carries an x-request-id header — include it when reporting an issue.
{
"error": {
"code": "validation_error",
"message": "language must be one of: zh, en, fr, de, it, ja, ko, pt, es, multi",
"field": "language",
"requestId": "req_1a2b3c"
}
}
| Status | Code | When |
|---|---|---|
400 | validation_error | Malformed id, or the body failed validation |
401 | invalid_api_key | Missing, invalid, or revoked key |
404 | not_found | Not yours, or does not exist |
409 | conflict | Product name taken, or research already has a product |
429 | rate_limited | Rate limit hit — wait Retry-After seconds |
Requests are rate limited per key. On 429 the Retry-After header tells you how many seconds to wait. Anything that isn't yours returns 404 rather than 403, so ids can't be probed.