Frank Public API — Developer Guide
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.
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.
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.