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.

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_..."

Manage API keys

Typical flow

  1. POST /v1/products — describe what you're researching.
  2. POST /v1/researches — goals + topics/questions. Comes back as a draft.
  3. PUT /v1/researches/{id}/product — attach the product (optional).
  4. PUT /v1/researches/{id}/publish — pick interviewer + language; becomes active and can take interviews.
  5. GET /v1/researches/{id}/interviews then GET /v1/interviews/{id} — read transcripts.

Products

POST/v1/products

Create a product

REQUEST BODY


Field Type Notes

name string Max 60 characters. Must be unique across your account — a duplicate returns 409.

required

description string Max 1500 characters. What the product is.

required

category enum One of the 16 values below.

required

brief string Context the interviewer uses. Defaults to description.

optional

urls string [] Up to 10 http(s) URLs.

optional

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

productId string (uuid) Use this to link the product to a research.

always present

name string As sent.

always present

description string | null As sent.

always present

category string One of the category values.

always present

brief string Sent value, or the description.

always present

urls string[] Empty array when none were sent.

always present

createdAt string (ISO 8601) UTC, e.g. 2026-01-01T10:00:00.000Z.

always present

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"
}

GET/v1/products

List products

Returns an array of the product object above, newest first. No parameters.

GET/v1/products/{id}

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.

PUT/v1/products/{id}

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

POST/v1/researches

Create a research

Creates the research immediately as a draft. It cannot take interviews until you publish it.

Field Type Notes

name string Non-empty; whitespace is trimmed.

required

description string What you want to learn.

required

goals string [] At least one non-empty goal.

required

topics object [] At least one topic — see the topic fields below.

required

researchType enum discovery, product_experience or retention_growth.

optional

botTypes enum [] Any of audio, video, chat. Publishing sets this to ["audio"].

optional

greetingMessage object Opening line per channel: chat, audio, video — all optional strings.

optional

prompts object Overrides the generated prompts: voice, video, chat, tts_style — all optional strings. Sending these stops Frank from auto-updating them later.

optional

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

name string Topic heading.

required

message string Intro the interviewer reads before the topic.

optional

questions object [] At least one question.

required

TOPICS[].QUESTIONS[] — EACH QUESTION

Field Type Notes

text string The question itself.

required

followUpDepth string How hard to probe, e.g. shallow, medium, deep. Free text — it is guidance for the interviewer, not a fixed set.

optional

probing string What to dig into, e.g. ask for specific examples.

optional

tags string Your own labels for grouping, e.g. onboarding,pricing.

optional

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

researchId string (uuid) Use it for link, publish and interview calls.

always present

name string | null As sent.

always present

status string draft on create, active after publish, stopped when halted.

always present

type string | null The researchType you sent, else null.

always present

goals string [] As sent.

always present

productId string (uuid) | null null until you link a product.

always present

createdAt string (ISO 8601) UTC.

always present

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"
}

PUT/v1/researches/{id}/product

Link a product

REQUEST BODY

Field Type Notes

productId string (uuid) A product you own.

required

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-..." }'

PUT/v1/researches/{id}/publish

Publish (activate)

Sets the interviewer and flips the research to active so it can take voice interviews.

REQUEST BODY

Field Type Notes

language enum ISO 639-1 code from the list below. Case-insensitive.

required

persona enum olivia (default) or frank — the interviewer's voice and name.

optional

duration integer Target interview length in minutes. Minimum 1.

optional

prompts object Same shape as on create; overrides the generated prompts.

optional

LANGUAGE VALUES

zh Chinese

en English

fr French

de German

it Italian

ja Japanese

ko Korean

pt Portuguese

es Spanish

multi Multi language — 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.

GET/v1/researches

List researches

Returns an array of the research object above, newest first. No parameters.

Interviews & transcripts

GET/v1/researches/{researchId}/interviews

List a research's interviews

Summaries only, newest first — use it to discover interview ids.

RESPONSE· ARRAY OF

Field Type Notes

interviewId string (uuid) Fetch the transcript with it.

always present

researchId string (uuid) The parent research.

always present

status string | null scheduled, in_progress, completed, incomplete or dropped.

always present

type string audio, video or chat.

always present

participant object name and email, each a string or null when the participant stayed anonymous.

always present

startedAt string (ISO 8601) | null null if it never started.

always present

endedAt string (ISO 8601) | null null while in progress.

always present

duration integer | null Length in seconds.

always present

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/v1/interviews/{id}

Get an interview + transcript

Every field from the summary above, plus the full transcript in order.

TRANSCRIPT[] — EACH TURN

Field Type Notes

sequence integer 1-based turn order.

always present

role string assistant (the interviewer) or user (the participant).

always present

text string What was said.

always present

startTimestamp string (ISO 8601) When the turn started.

always present

endTimestamp string (ISO 8601) When the turn ended.

always present

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.