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

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
curl https://api.hifrank.ai/v1/products \
  -H "Authorization: Bearer frank_sk_live_..."

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

FieldTypeNotes
namerequiredstringMax 60 characters. Must be unique across your account — a duplicate returns 409.
descriptionrequiredstringMax 1500 characters. What the product is.
categoryrequiredenumOne of the 16 values below.
briefoptionalstringContext the interviewer uses. Defaults to description.
urlsoptionalstring[]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
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

FieldTypeNotes
productIdalways presentstring (uuid)Use this to link the product to a research.
namealways presentstringAs sent.
descriptionalways presentstring | nullAs sent.
categoryalways presentstringOne of the category values.
briefalways presentstringSent value, or the description.
urlsalways presentstring[]Empty array when none were sent.
createdAtalways presentstring (ISO 8601)UTC, e.g. 2026-01-01T10:00:00.000Z.

Response

JSON
{
  "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.

Request body

FieldTypeNotes
namerequiredstringNon-empty; whitespace is trimmed.
descriptionrequiredstringWhat you want to learn.
goalsrequiredstring[]At least one non-empty goal.
topicsrequiredobject[]At least one topic — see the topic fields below.
researchTypeoptionalenumdiscovery, product_experience or retention_growth.
botTypesoptionalenum[]Any of audio, video, chat. Publishing sets this to ["audio"].
greetingMessageoptionalobjectOpening line per channel: chat, audio, video — all optional strings.
promptsoptionalobjectOverrides the generated prompts: voice, video, chat, tts_style — all optional strings. Sending these stops Frank from auto-updating them later.

topics[] — each topic

FieldTypeNotes
namerequiredstringTopic heading.
messageoptionalstringIntro the interviewer reads before the topic.
questionsrequiredobject[]At least one question.

topics[].questions[] — each question

FieldTypeNotes
textrequiredstringThe question itself.
followUpDepthoptionalstringHow hard to probe, e.g. shallow, medium, deep. Free text — it is guidance for the interviewer, not a fixed set.
probingoptionalstringWhat to dig into, e.g. ask for specific examples.
tagsoptionalstringYour own labels for grouping, e.g. onboarding,pricing.

Request

curl
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

FieldTypeNotes
researchIdalways presentstring (uuid)Use it for link, publish and interview calls.
namealways presentstring | nullAs sent.
statusalways presentstringdraft on create, active after publish, stopped when halted.
typealways presentstring | nullThe researchType you sent, else null.
goalsalways presentstring[]As sent.
productIdalways presentstring (uuid) | nullnull until you link a product.
createdAtalways presentstring (ISO 8601)UTC.

Response

JSON
{
  "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

FieldTypeNotes
productIdrequiredstring (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
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

FieldTypeNotes
languagerequiredenumISO 639-1 code from the list below. Case-insensitive.
personaoptionalenumolivia (default) or frank — the interviewer's voice and name.
durationoptionalintegerTarget interview length in minutes. Minimum 1.
promptsoptionalobjectSame shape as on create; overrides the generated prompts.

language values

zhChinese
enEnglish
frFrench
deGerman
itItalian
jaJapanese
koKorean
ptPortuguese
esSpanish
multiMultilanguage — follows the participant

Request

curl
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

FieldTypeNotes
interviewIdalways presentstring (uuid)Fetch the transcript with it.
researchIdalways presentstring (uuid)The parent research.
statusalways presentstring | nullscheduled, in_progress, completed, incomplete or dropped.
typealways presentstringaudio, video or chat.
participantalways presentobjectname and email, each a string or null when the participant stayed anonymous.
startedAtalways presentstring (ISO 8601) | nullnull if it never started.
endedAtalways presentstring (ISO 8601) | nullnull while in progress.
durationalways presentinteger | nullLength in seconds.

Response

JSON
[
  {
    "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

FieldTypeNotes
sequencealways presentinteger1-based turn order.
rolealways presentstringassistant (the interviewer) or user (the participant).
textalways presentstringWhat was said.
startTimestampalways presentstring (ISO 8601)When the turn started.
endTimestampalways presentstring (ISO 8601)When the turn ended.

Response

JSON
{
  "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.

JSON
{
  "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"
  }
}
StatusCodeWhen
400validation_errorMalformed id, or the body failed validation
401invalid_api_keyMissing, invalid, or revoked key
404not_foundNot yours, or does not exist
409conflictProduct name taken, or research already has a product
429rate_limitedRate 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.