# Authentication Source: https://docs.userjourneys.ai/api/authentication Authenticate API requests with a project API key. Every API request must include a project API key in the `Authorization` header. ```bash theme={null} Authorization: Bearer uj_live_your_key_here ``` ## Creating an API key Go to **Settings** in the sidebar, then select the **API** tab. Click **Create Key**, enter a name (e.g., "Production"), and click **Create**. Copy the key immediately. It starts with `uj_live_` and will not be shown again. API keys are shown once at creation and cannot be retrieved later. Store your key securely. ## Using your key Include it as a Bearer token in the `Authorization` header of every request: ```bash cURL theme={null} curl -H "Authorization: Bearer uj_live_your_key_here" \ https://app.userjourneys.ai/api/v1/experiments ``` ```javascript Node.js theme={null} const response = await fetch( "https://app.userjourneys.ai/api/v1/experiments", { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); ``` ```python Python theme={null} import requests response = requests.get( "https://app.userjourneys.ai/api/v1/experiments", headers={"Authorization": "Bearer uj_live_your_key_here"}, ) ``` Each API key is scoped to a single project. All endpoints return `401 Unauthorized` if the key is missing or invalid. # Chat Messages Source: https://docs.userjourneys.ai/api/chat-messages List the visible transcript of a chat — text, thinking, tool calls, and tool results, in chronological order. Messages belong to a [chat](/api/chats). This endpoint returns the customer-visible transcript: user messages, assistant replies, tool calls, and tool results, in the order they occurred. Internal runtime parts (compaction, container uploads, hidden branches) and internal metadata (session IDs, model names, branching info) are never returned. Tool calls and results are normalized across transports — you'll never need to branch on where a tool ran. *** ## `GET /v1/chats/:id/messages` List visible messages for a chat in chronological order (oldest first). Unlike `/v1/chats` and `/v1/releases` — which return newest-first — messages are ordered ascending so you can read a transcript top-to-bottom without reversing the list. ### Request Bearer token. See [Authentication](/api/authentication). Chat ID (UUID). Number of messages to return. Default `20`, max `100`. Cursor for pagination. Pass the `id` of the last message from the previous page. ```bash cURL theme={null} curl https://app.userjourneys.ai/api/v1/chats/7af6cf75-20ec-49f4-b113-929c01dfbe45/messages \ -H "Authorization: Bearer uj_live_your_key_here" ``` ```javascript Node.js theme={null} const chatId = "7af6cf75-20ec-49f4-b113-929c01dfbe45"; const response = await fetch( `https://app.userjourneys.ai/api/v1/chats/${chatId}/messages`, { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); const { data, has_more } = await response.json(); ``` ```python Python theme={null} import requests chat_id = "7af6cf75-20ec-49f4-b113-929c01dfbe45" response = requests.get( f"https://app.userjourneys.ai/api/v1/chats/{chat_id}/messages", headers={"Authorization": "Bearer uj_live_your_key_here"}, ) data = response.json() ``` ```json 200 theme={null} { "object": "list", "data": [ { "id": "a1b2c3d4-5678-90ab-cdef-111111111111", "object": "message", "chat_id": "7af6cf75-20ec-49f4-b113-929c01dfbe45", "role": "user", "parts": [ { "type": "text", "text": "Why did onboarding drop this week?" } ], "sources": null, "feedback": null, "usage": null, "created_at": "2026-04-10T15:18:03.000Z", "completed_at": "2026-04-10T15:18:03.000Z" }, { "id": "b2c3d4e5-6789-01ab-cdef-222222222222", "object": "message", "chat_id": "7af6cf75-20ec-49f4-b113-929c01dfbe45", "role": "assistant", "parts": [ { "type": "thinking", "thinking": "Pulling the onboarding funnel..." }, { "type": "tool_use", "id": "tool_q1", "name": "query_bigquery", "summary": "Weekly onboarding funnel", "input": { "sql": "SELECT step, count(*) FROM onboarding GROUP BY 1" }, "state": "output_available" }, { "type": "tool_result", "tool_use_id": "tool_q1", "content": { "rows": [{ "step": "signup", "count": 312 }] }, "is_error": false }, { "type": "text", "text": "The signup → verify step lost 18% compared to last week." } ], "sources": [ { "id": "src_1", "description": "Weekly onboarding funnel", "rows": 312, "query": "SELECT step, count(*) FROM onboarding GROUP BY 1" } ], "feedback": "positive", "usage": { "input_tokens": 1420, "output_tokens": 362, "cache_read_input_tokens": 800, "cache_creation_input_tokens": 0, "cost_usd": 0.0087 }, "created_at": "2026-04-10T15:18:09.000Z", "completed_at": "2026-04-10T15:18:18.212Z" } ], "has_more": false } ``` Returns `404` if the chat doesn't exist, doesn't belong to your project, or is excluded from the public chats API (internal, eval, or release-generated). Returns `400` with `Invalid parameters` when pagination inputs are malformed — for example, `limit` outside `1..100` or a non-UUID `starting_after`. The response body includes a `details` array with the Zod issue for each offending field. Returns `400` with `No such message: ` if `starting_after` parses as a UUID but is not a visible message in this chat. *** ## Message object Message ID (UUID). `"message"` ID of the parent [chat](/api/chats). `"user"`, `"assistant"`, or `"system"`. `system` is rare in public output. Ordered content blocks. See [Part types](#part-types). Citations referenced by an assistant message. `null` when there are none. Source identifier. Human-readable description. Row count when available. The query body. `null` when absent, or when truncated (see `truncated`). Present and `true` only when `query` was nulled because it exceeded the size cap. Absent on non-truncated sources. Check this to disambiguate "no query stored" from "query too large". Present alongside `truncated`. Currently always `"size_limit_32kb"`. `"positive"`, `"negative"`, or `null`. User thumbs-up/down from the UI. Token and cost metrics for assistant messages. `null` on user/system messages or when metrics were not recorded. ISO 8601 timestamp — when the message was recorded. ISO 8601 timestamp — when streaming finished. `null` while still streaming. *** ## Part types ### `text` ```json theme={null} { "type": "text", "text": "..." } ``` ### `thinking` Internal reasoning shown to the user in a collapsible section. ```json theme={null} { "type": "thinking", "thinking": "..." } ``` ### `redacted_thinking` Marker for reasoning that was redacted. No content. ```json theme={null} { "type": "redacted_thinking" } ``` ### `file` User-uploaded file attached to a message. ```json theme={null} { "type": "file", "url": "https://...", "media_type": "application/pdf", "filename": "brief.pdf" } ``` `filename` is `null` if none was provided. ### `tool_use` A tool call made by the assistant. All tool transports (server tools, MCP tools) normalize to this single shape. ```json theme={null} { "type": "tool_use", "id": "tool_abc", "name": "query_bigquery", "summary": "Weekly retention cohort", "input": { "sql": "SELECT ..." }, "state": "output_available" } ``` One of `input_available`, `output_available`, `output_error`, `output_denied`. Transient internal lifecycle states (e.g. streaming input, approval requests) are mapped to `input_available`. ### `tool_result` A tool result for a previous `tool_use`. All result transports (web search, code execution, MCP) normalize to this shape. ```json theme={null} { "type": "tool_result", "tool_use_id": "tool_abc", "content": { "rows": [...] }, "is_error": false } ``` *** ## Tool payload stability The **shapes inside `tool_use.input` and `tool_result.content` are tool-specific** and may evolve as tools change. Don't hard-code field paths — read them defensively. Everything else in the response is stable and versioned. *** ## Truncation Large payloads are capped to keep response sizes predictable. When a `tool_use.input`, `tool_result.content`, or `sources[].query` exceeds 32 KB of JSON-encoded UTF-8: ```json theme={null} { "type": "tool_result", "tool_use_id": "tool_abc", "content": null, "is_error": false, "truncated": true, "truncation_reason": "size_limit_32kb" } ``` Check `truncated === true` rather than hardcoding the size — the threshold may change. Truncated sources surface the same way — `query` becomes `null` with matching `truncated`/`truncation_reason` fields: ```json theme={null} { "id": "src_1", "description": "Weekly retention cohort", "rows": null, "query": null, "truncated": true, "truncation_reason": "size_limit_32kb" } ``` The full content for truncated parts is not retrievable in this version. *** ## Pagination ```json theme={null} { "object": "list", "data": [...], "has_more": true } ``` When `has_more` is `true`, pass `starting_after` with the last message's `id` to get the next page. Results are ordered **oldest first**, so walking pages with `starting_after` traverses the transcript forward in time. # Chats Source: https://docs.userjourneys.ai/api/chats List and retrieve chats — access customer-visible chat metadata and message counts. A chat is a conversation thread in your project. The public chats API returns customer-visible chat metadata only. It does not return message bodies or tool/runtime internals. Only customer-visible chats are returned. Internal chats, evaluation chats, and release-generated chats are excluded from this API. *** ## `GET /v1/chats` List customer-visible chats for your project. Returns chats in descending `created_at` order, newest chats first. Pagination is intentionally based on `created_at` so list traversal stays stable even while a chat continues receiving new messages. ### Request Bearer token. See [Authentication](/api/authentication). Number of chats to return. Default `20`, max `100`. Cursor for pagination. Pass the `id` of the last chat from the previous page. ```bash cURL theme={null} curl https://app.userjourneys.ai/api/v1/chats \ -H "Authorization: Bearer uj_live_your_key_here" ``` ```javascript Node.js theme={null} const response = await fetch( "https://app.userjourneys.ai/api/v1/chats", { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); const { data, has_more } = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://app.userjourneys.ai/api/v1/chats", headers={"Authorization": "Bearer uj_live_your_key_here"}, ) data = response.json() ``` ```json 200 theme={null} { "object": "list", "data": [ { "id": "7af6cf75-20ec-49f4-b113-929c01dfbe45", "object": "chat", "title": "Investigate the spike in onboarding drop-off", "source": "web", "is_streaming": false, "created_at": "2026-04-10T15:18:03.000Z", "updated_at": "2026-04-10T15:24:18.000Z" } ], "has_more": false } ``` This endpoint returns metadata only. To fetch the transcript, use [`GET /v1/chats/:id/messages`](/api/chat-messages). *** ## `GET /v1/chats/:id` Retrieve a single customer-visible chat with metadata and visible message count. ### Request Bearer token. See [Authentication](/api/authentication). Chat ID (UUID). ```bash cURL theme={null} curl https://app.userjourneys.ai/api/v1/chats/7af6cf75-20ec-49f4-b113-929c01dfbe45 \ -H "Authorization: Bearer uj_live_your_key_here" ``` ```javascript Node.js theme={null} const response = await fetch( "https://app.userjourneys.ai/api/v1/chats/7af6cf75-20ec-49f4-b113-929c01dfbe45", { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); const chat = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://app.userjourneys.ai/api/v1/chats/7af6cf75-20ec-49f4-b113-929c01dfbe45", headers={"Authorization": "Bearer uj_live_your_key_here"}, ) chat = response.json() ``` ```json 200 theme={null} { "id": "7af6cf75-20ec-49f4-b113-929c01dfbe45", "object": "chat", "title": "Investigate the spike in onboarding drop-off", "source": "web", "is_streaming": false, "message_count": 12, "created_at": "2026-04-10T15:18:03.000Z", "updated_at": "2026-04-10T15:24:18.000Z" } ``` This endpoint does not return message bodies. `message_count` counts visible messages only. Returns `404` if the chat doesn't exist, doesn't belong to your project, or is excluded from the public chats API. *** ## Chat object Chat ID (UUID). `"chat"` Chat title, or `null` if the thread has not been titled. Where the chat originated. Example values include `"web"`, `"analyst"`, and `"mcp"`. Internal-only sources like `"eval"` and `"releases"` are never returned by this API. Whether the chat is currently streaming a response. Number of visible messages in the chat. Only returned by `GET /v1/chats/:id`. To fetch the actual messages, see [`GET /v1/chats/:id/messages`](/api/chat-messages). ISO 8601 timestamp for when the chat was created. ISO 8601 timestamp for the most recent update to the chat. # Experiments (Deprecated) Source: https://docs.userjourneys.ai/api/experiments This endpoint is deprecated. Use /v1/studies instead. **Deprecated** — This endpoint will be removed after **July 1, 2026**. Use [`GET /v1/studies`](/api/studies) instead, which provides the same capacity and usage data with a cleaner response format. ## `GET /v1/experiments` Returns project-level usage and per-experiment interview stats for all active experiments. All responses include `Deprecation` and `Sunset` headers. See [`GET /v1/studies`](/api/studies) for the replacement. # API Overview Source: https://docs.userjourneys.ai/api/index Programmatic access to your userjourneys.ai project — manage studies, fetch interviews, inspect releases and chats, and configure webhooks. The userjourneys.ai API lets you programmatically access your project data — check study capacity, fetch interviews, inspect releases and chats, and receive real-time webhook notifications when interviews complete. ## Base URL ``` https://app.userjourneys.ai/api/v1 ``` ## Endpoints | Endpoint | Method | Description | | ------------------------------------------------------- | -------------------- | ------------------------------------------------- | | [`/studies`](/api/studies) | `GET` | List studies and check interview capacity | | [`/studies/:id`](/api/studies#get-v1studiesid) | `GET` | Get study details, questions, and interview count | | [`/interviews`](/api/interviews) | `GET` | List interviews for a study | | [`/interviews/:id`](/api/interviews#get-v1interviewsid) | `GET` | Get interview with full transcript | | [`/releases`](/api/releases) | `GET` | List releases for your project | | [`/releases/:id`](/api/releases#get-v1releasesid) | `GET` | Get release details and linked chat ID | | [`/chats`](/api/chats) | `GET` | List customer-visible chats for your project | | [`/chats/:id`](/api/chats#get-v1chatsid) | `GET` | Get chat metadata and visible message count | | [`/chats/:id/messages`](/api/chat-messages) | `GET` | List messages for a chat | | [`/webhooks`](/api/webhooks) | `PUT` `GET` `DELETE` | Configure webhook notifications | ## Error handling | Status | Meaning | | ------ | -------------------------- | | `200` | Success | | `201` | Resource created | | `204` | Deleted successfully | | `400` | Invalid request body | | `401` | Missing or invalid API key | | `404` | Resource not found | All responses return JSON (except `204`). ## Pagination List endpoints return paginated results using cursor-based pagination: ```json theme={null} { "object": "list", "data": [...], "has_more": true } ``` Pass `limit` to control page size (default `20`, max `100`). When `has_more` is `true`, pass `starting_after` with the `id` of the last item to get the next page. # Interviews Source: https://docs.userjourneys.ai/api/interviews List and retrieve individual interviews — access transcripts, summaries, and structured question answers. An interview is a single conversation between an AI interviewer and a respondent. Each interview belongs to a [study](/api/studies). *** ## `GET /v1/interviews` List interviews for a study. Filter by status, reference ID, or quality score. ### Request Bearer token. See [Authentication](/api/authentication). Study ID (UUID). Only interviews belonging to this study are returned. Filter by status. Common values: `"completed"`, `"started"`. Filter by the reference ID you passed via `?reference_id=` on the interview link. Use this to find a specific user's interview. Filter by quality: `"insightful"`, `"successful"`, or `"unsuccessful"`. Number of interviews to return. Default `20`, max `100`. Cursor for pagination. Pass the `id` of the last interview from the previous page. ```bash cURL theme={null} curl "https://app.userjourneys.ai/api/v1/interviews?study_id=e5f6a7b8-1234-56cd-ef78-222222222222" \ -H "Authorization: Bearer uj_live_your_key_here" ``` ```javascript Node.js theme={null} const studyId = "e5f6a7b8-1234-56cd-ef78-222222222222"; const response = await fetch( `https://app.userjourneys.ai/api/v1/interviews?study_id=${studyId}`, { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); const { data, has_more } = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://app.userjourneys.ai/api/v1/interviews", headers={"Authorization": "Bearer uj_live_your_key_here"}, params={"study_id": "e5f6a7b8-1234-56cd-ef78-222222222222"}, ) data = response.json() ``` ```json 200 theme={null} { "object": "list", "data": [ { "id": "a1b2c3d4-5678-90ab-cdef-111111111111", "object": "interview", "study_id": "e5f6a7b8-1234-56cd-ef78-222222222222", "status": "completed", "reference_id": "user_12345", "respondent_email": null, "language": "en", "quality_score": "insightful", "duration_secs": 580, "headline": "User loves the onboarding but struggles with payment", "summary": "The respondent had a positive first impression of the product...", "started_at": "2026-03-11T14:20:00.000Z", "completed_at": "2026-03-11T14:30:00.456Z", "created_at": "2026-03-11T14:20:00.000Z" } ], "has_more": false } ``` Filter by `reference_id` to find a specific user's interview: `?study_id=...&reference_id=user_12345` *** ## `GET /v1/interviews/:id` Retrieve a single interview with the full transcript and structured question answers. ### Request Bearer token. See [Authentication](/api/authentication). Interview ID (UUID). This is the `interview_id` from the [webhook payload](/api/webhooks#event-interviewcompleted). ```bash cURL theme={null} curl https://app.userjourneys.ai/api/v1/interviews/a1b2c3d4-5678-90ab-cdef-111111111111 \ -H "Authorization: Bearer uj_live_your_key_here" ``` ```javascript Node.js theme={null} const response = await fetch( "https://app.userjourneys.ai/api/v1/interviews/a1b2c3d4-5678-90ab-cdef-111111111111", { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); const interview = await response.json(); // interview.transcript — full conversation // interview.question_answers — structured per-question data ``` ```python Python theme={null} import requests response = requests.get( "https://app.userjourneys.ai/api/v1/interviews/a1b2c3d4-5678-90ab-cdef-111111111111", headers={"Authorization": "Bearer uj_live_your_key_here"}, ) interview = response.json() # interview["transcript"] — full conversation # interview["question_answers"] — structured per-question data ``` ```json 200 theme={null} { "id": "a1b2c3d4-5678-90ab-cdef-111111111111", "object": "interview", "study_id": "e5f6a7b8-1234-56cd-ef78-222222222222", "status": "completed", "reference_id": "user_12345", "respondent_email": null, "language": "es", "quality_score": "insightful", "duration_secs": 580, "headline": "User loves the onboarding but struggles with payment", "summary": "The respondent had a positive first impression of the product...", "started_at": "2026-03-11T14:20:00.000Z", "completed_at": "2026-03-11T14:30:00.456Z", "created_at": "2026-03-11T14:20:00.000Z", "transcript": [ { "role": "agent", "message": "Gracias por unirte hoy. Cuéntame sobre tu experiencia.", "translated": "Thanks for joining today. Tell me about your experience.", "time_in_call_secs": 0 }, { "role": "user", "message": "El proceso fue bastante sencillo al principio...", "translated": "The process was pretty straightforward at first...", "time_in_call_secs": 4 } ], "question_answers": [ { "question_index": 0, "question_text": "What was your first impression of the product?", "answer_summary": "Found it intuitive but hit a snag at the payment step", "answer_excerpt": [ { "role": "user", "message": "It was pretty smooth until..." } ] } ] } ``` Returns `404` if the interview doesn't exist or doesn't belong to your project. *** ## Interview object Interview ID (UUID). `"interview"` The study this interview belongs to. `"completed"`, `"started"`, or `"processing"`. Custom identifier passed via `?reference_id=` on the interview link. Email entered during or after the interview. `null` if not collected. Detected language code (e.g. `"en"`, `"es"`, `"de"`). `"insightful"`, `"successful"`, or `"unsuccessful"`. Interview length in seconds. AI-generated one-line summary. AI-generated paragraph summary. Interview start time (ISO 8601). Interview end time (ISO 8601). Record creation time (ISO 8601). ### Detail-only fields These fields are only included in `GET /v1/interviews/:id` responses: Full conversation transcript, ordered chronologically. `"agent"` or `"user"`. The message text in the original language. English translation. Only present for non-English interviews. Seconds into the interview when this message occurred. Structured answers extracted from the transcript, one per interview question. Question position (0-indexed). The question that was asked. AI-generated summary of the respondent's answer. Relevant transcript excerpt. # Releases Source: https://docs.userjourneys.ai/api/releases List and retrieve releases — access merged pull request metadata and the linked chat for each release. A release represents a merged pull request in your project. Each release includes basic GitHub metadata and a `chat_id` pointing to the chat where the release analysis happened. *** ## `GET /v1/releases` List releases for your project. Returns releases in descending `merged_at` order, newest first. ### Request Bearer token. See [Authentication](/api/authentication). Filter by status: `"active"` or `"dismissed"`. Number of releases to return. Default `20`, max `100`. Cursor for pagination. Pass the `id` of the last release from the previous page. ```bash cURL theme={null} curl https://app.userjourneys.ai/api/v1/releases \ -H "Authorization: Bearer uj_live_your_key_here" ``` ```javascript Node.js theme={null} const response = await fetch( "https://app.userjourneys.ai/api/v1/releases", { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); const { data, has_more } = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://app.userjourneys.ai/api/v1/releases", headers={"Authorization": "Bearer uj_live_your_key_here"}, ) data = response.json() ``` ```json 200 theme={null} { "object": "list", "data": [ { "id": "6a9f3b0d-6a8c-4cde-9e47-9d53f78a3d56", "object": "release", "status": "active", "chat_id": "7af6cf75-20ec-49f4-b113-929c01dfbe45", "github_pr_number": 482, "github_pr_title": "Ship release summary emails", "github_pr_url": "https://github.com/acme/product/pull/482", "github_pr_author": "alex", "merged_at": "2026-04-10T15:22:13.000Z", "last_analysis_at": "2026-04-10T15:25:44.000Z", "created_at": "2026-04-10T15:22:20.000Z" } ], "has_more": false } ``` Use `status=active` if you only want releases that are still active in the release workflow. *** ## `GET /v1/releases/:id` Retrieve a single release with full pull request metadata. ### Request Bearer token. See [Authentication](/api/authentication). Release ID (UUID). ```bash cURL theme={null} curl https://app.userjourneys.ai/api/v1/releases/6a9f3b0d-6a8c-4cde-9e47-9d53f78a3d56 \ -H "Authorization: Bearer uj_live_your_key_here" ``` ```javascript Node.js theme={null} const response = await fetch( "https://app.userjourneys.ai/api/v1/releases/6a9f3b0d-6a8c-4cde-9e47-9d53f78a3d56", { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); const release = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://app.userjourneys.ai/api/v1/releases/6a9f3b0d-6a8c-4cde-9e47-9d53f78a3d56", headers={"Authorization": "Bearer uj_live_your_key_here"}, ) release = response.json() ``` ```json 200 theme={null} { "id": "6a9f3b0d-6a8c-4cde-9e47-9d53f78a3d56", "object": "release", "status": "active", "chat_id": "7af6cf75-20ec-49f4-b113-929c01dfbe45", "github_pr_number": 482, "github_pr_title": "Ship release summary emails", "github_pr_description": "Adds release summary emails and related delivery metrics.", "github_pr_url": "https://github.com/acme/product/pull/482", "github_pr_author": "alex", "merged_at": "2026-04-10T15:22:13.000Z", "last_analysis_at": "2026-04-10T15:25:44.000Z", "created_at": "2026-04-10T15:22:20.000Z" } ``` Returns `404` if the release doesn't exist or doesn't belong to your project. *** ## Release object Release ID (UUID). `"release"` `"active"` or `"dismissed"`. ID of the linked [chat](/api/chats). GitHub pull request number. GitHub pull request title. GitHub pull request description. Only returned by `GET /v1/releases/:id`. GitHub pull request URL. GitHub username or author handle for the pull request. ISO 8601 timestamp for when the pull request was merged. ISO 8601 timestamp for the last release analysis run, or `null` if analysis has not run yet. ISO 8601 timestamp for when the release record was created. # Studies Source: https://docs.userjourneys.ai/api/studies List and retrieve your interview studies — check capacity, limits, and whether a study is accepting responses. A study is a research configuration: the questions, target audience, and settings for a set of interviews. Each study has a public interview link that you send to respondents. *** ## `GET /v1/studies` List all active studies for your project. Returns studies with their current usage and whether they're accepting new responses. ### Request Bearer token. See [Authentication](/api/authentication). Number of studies to return. Default `20`, max `100`. Cursor for pagination. Pass the `id` of the last study from the previous page. ```bash cURL theme={null} curl https://app.userjourneys.ai/api/v1/studies \ -H "Authorization: Bearer uj_live_your_key_here" ``` ```javascript Node.js theme={null} const response = await fetch( "https://app.userjourneys.ai/api/v1/studies", { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); const { data, has_more } = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://app.userjourneys.ai/api/v1/studies", headers={"Authorization": "Bearer uj_live_your_key_here"}, ) data = response.json() ``` ```json 200 theme={null} { "object": "list", "data": [ { "id": "e5f6a7b8-1234-56cd-ef78-222222222222", "object": "study", "name": "Onboarding Feedback", "status": "active", "interview_link": "https://app.userjourneys.ai/i/xK9mR2pQ", "accepting_responses": true, "supported_languages": ["en", "es"], "limits": { "monthly": { "limit": 200, "used": 45 }, "weekly": { "limit": 50, "used": 12 } }, "created_at": "2026-03-01T00:00:00.000Z" } ], "has_more": false } ``` Cache this response for a few minutes. Study capacity doesn't change often, and caching avoids unnecessary API calls. *** ## `GET /v1/studies/:id` Retrieve a single study with full details including questions, research goal, and interview count. ### Request Bearer token. See [Authentication](/api/authentication). Study ID (UUID). ```bash cURL theme={null} curl https://app.userjourneys.ai/api/v1/studies/e5f6a7b8-1234-56cd-ef78-222222222222 \ -H "Authorization: Bearer uj_live_your_key_here" ``` ```javascript Node.js theme={null} const response = await fetch( "https://app.userjourneys.ai/api/v1/studies/e5f6a7b8-1234-56cd-ef78-222222222222", { headers: { Authorization: "Bearer uj_live_your_key_here", }, } ); const study = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://app.userjourneys.ai/api/v1/studies/e5f6a7b8-1234-56cd-ef78-222222222222", headers={"Authorization": "Bearer uj_live_your_key_here"}, ) study = response.json() ``` ```json 200 theme={null} { "id": "e5f6a7b8-1234-56cd-ef78-222222222222", "object": "study", "name": "Onboarding Feedback", "status": "active", "interview_link": "https://app.userjourneys.ai/i/xK9mR2pQ", "accepting_responses": true, "supported_languages": ["en", "es"], "limits": { "monthly": { "limit": 200, "used": 45 }, "weekly": { "limit": 50, "used": 12 } }, "research_goal": "Understand user onboarding experience", "product_name": "Acme App", "questions": [ { "text": "What was your first impression of the product?" }, { "text": "What almost stopped you from signing up?" } ], "interview_count": 45, "created_at": "2026-03-01T00:00:00.000Z" } ``` Returns `404` if the study doesn't exist or doesn't belong to your project. *** ## Study object Study ID (UUID). `"study"` Display name. `"active"`, `"paused"`, or `"draft"`. Public URL for respondents. Append `?reference_id=your_user_id` to track who completes the interview. `true` when the study is active and within all usage limits. Language codes the study supports (e.g. `["en", "es"]`). Monthly interview cap. `null` if unlimited. Interviews used in the current billing period. Weekly interview cap. `null` if unlimited. Interviews used this week (Mon–Sun UTC). ISO 8601 timestamp. ### Detail-only fields These fields are only included in `GET /v1/studies/:id` responses: The research objective for this study. The product being researched. The interview questions. The question text. Total number of completed interviews for this study. # Webhooks Source: https://docs.userjourneys.ai/api/webhooks Receive real-time notifications when interviews complete, with duration, quality score, and respondent data. Get notified when an interview completes. Configure a webhook URL and we'll POST a signed payload with interview results every time a respondent finishes. *** ## `PUT /v1/webhooks` Create or update the webhook configuration for your project. One webhook per project. Returns the webhook configuration. On first creation, includes the `signing_secret`. ### Request Bearer token. See [Authentication](/api/authentication). HTTPS endpoint to receive events. Event types to subscribe to. Currently: `["interview.completed"]`. ```bash cURL theme={null} curl -X PUT https://app.userjourneys.ai/api/v1/webhooks \ -H "Authorization: Bearer uj_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/userjourneys", "events": ["interview.completed"] }' ``` ```javascript Node.js theme={null} const response = await fetch( "https://app.userjourneys.ai/api/v1/webhooks", { method: "PUT", headers: { Authorization: "Bearer uj_live_your_key_here", "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://your-app.com/webhooks/userjourneys", events: ["interview.completed"], }), } ); const data = await response.json(); // data.signing_secret — save this, only shown once ``` ```python Python theme={null} import requests response = requests.put( "https://app.userjourneys.ai/api/v1/webhooks", headers={"Authorization": "Bearer uj_live_your_key_here"}, json={ "url": "https://your-app.com/webhooks/userjourneys", "events": ["interview.completed"], }, ) data = response.json() # data["signing_secret"] — save this, only shown once ``` ```json 201 — Created theme={null} { "url": "https://your-app.com/webhooks/userjourneys", "events": ["interview.completed"], "signing_secret": "whsec_5b69ff6a12af94c6e3901a061180ca73" } ``` ```json 200 — Updated theme={null} { "url": "https://your-app.com/webhooks/userjourneys", "events": ["interview.completed"] } ``` The `signing_secret` is only returned on first creation (201). Store it securely — you'll need it to verify payloads. Lost or need to rotate your secret? Delete the webhook and create a new one. Deletion only requires your API key. *** ## `GET /v1/webhooks` Retrieve the current webhook configuration. Returns the URL and subscribed events. Does not return the signing secret. ```bash theme={null} curl https://app.userjourneys.ai/api/v1/webhooks \ -H "Authorization: Bearer uj_live_your_key_here" ``` Returns `404` if no webhook is configured. *** ## `DELETE /v1/webhooks` Remove the webhook configuration. ```bash theme={null} curl -X DELETE https://app.userjourneys.ai/api/v1/webhooks \ -H "Authorization: Bearer uj_live_your_key_here" ``` Returns `204` on success. *** ## Event: `interview.completed` Sent after interview processing completes, typically **5–30 seconds** after the respondent finishes. ### Headers | Header | Value | | --------------------- | ----------------------------------------- | | `Content-Type` | `application/json` | | `X-Webhook-Event` | `interview.completed` | | `X-Webhook-Signature` | HMAC-SHA256 signature of the request body | ### Payload `"interview.completed"` ISO 8601 timestamp of when the event was sent. Interview ID (UUID). Use this with [`GET /v1/interviews/:id`](/api/interviews#get-v1interviewsid) to fetch the full transcript. Study this interview belongs to. Matches the `id` in [`GET /v1/studies`](/api/studies). Study display name. Deprecated alias for `interview_id`. Use `interview_id` instead. Deprecated alias for `study_id`. Use `study_id` instead. Deprecated alias for `study_name`. Use `study_name` instead. Public interview URL for this experiment. `"completed"` Interview start time (ISO 8601). Interview end time (ISO 8601). Interview length in seconds. `"insightful"`, `"successful"`, or `"unsuccessful"`. Detected language code (e.g. `"en"`, `"es"`, `"de"`). Custom identifier passed via the `?reference_id=` query parameter on the interview link. Use this to match completions back to users in your system. Email entered during or after the interview. `null` if not collected. Respondent ID, if triggered via a respondent-specific link. ```json interview.completed theme={null} { "event": "interview.completed", "timestamp": "2026-03-11T14:32:00.123Z", "data": { "interview_id": "a1b2c3d4-5678-90ab-cdef-111111111111", "study_id": "e5f6a7b8-1234-56cd-ef78-222222222222", "study_name": "Onboarding Feedback", "session_id": "a1b2c3d4-5678-90ab-cdef-111111111111", "experiment_id": "e5f6a7b8-1234-56cd-ef78-222222222222", "experiment_name": "Onboarding Feedback", "interview_link": "https://app.userjourneys.ai/i/xK9mR2pQ", "status": "completed", "started_at": "2026-03-11T14:20:00.000Z", "completed_at": "2026-03-11T14:30:00.456Z", "duration_secs": 580, "quality_score": "insightful", "language": "en", "reference_id": "user_12345", "respondent_email": null, "respondent_id": null } } ``` ### Retries If your endpoint returns a non-2xx status or times out (10s limit), we retry up to 3 times: | Attempt | Delay | | --------- | ---------- | | 1st retry | 10 seconds | | 2nd retry | 1 minute | | 3rd retry | 5 minutes | After all retries are exhausted, the event is dropped. Use `session_id` to deduplicate in case you receive the same event more than once. *** ## Verifying signatures Every webhook includes an `X-Webhook-Signature` header. Verify it to confirm the request came from userjourneys.ai. ```javascript Node.js theme={null} import crypto from "node:crypto"; function verifyWebhookSignature(body, signature, secret) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(body).digest("hex"); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // Express example app.post("/webhooks/userjourneys", express.raw({ type: "application/json" }), (req, res) => { const signature = req.headers["x-webhook-signature"]; const isValid = verifyWebhookSignature(req.body, signature, SIGNING_SECRET); if (isValid === false) { return res.status(401).send("Invalid signature"); } const event = JSON.parse(req.body); // Handle the event... res.status(200).send("OK"); }); ``` ```python Python theme={null} import hashlib import hmac def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) # Flask example @app.route("/webhooks/userjourneys", methods=["POST"]) def handle_webhook(): signature = request.headers.get("X-Webhook-Signature") is_valid = verify_webhook_signature( request.data, signature, SIGNING_SECRET ) if not is_valid: return "Invalid signature", 401 event = request.get_json() # Handle the event... return "OK", 200 ``` Use timing-safe comparison (`crypto.timingSafeEqual`, `hmac.compare_digest`) to prevent timing attacks. *** ## Typical integration Mark users after they complete an interview so you stop showing the prompt: ```javascript theme={null} app.post("/webhooks/userjourneys", express.raw({ type: "application/json" }), async (req, res) => { const signature = req.headers["x-webhook-signature"]; if (verifyWebhookSignature(req.body, signature, SIGNING_SECRET) === false) { return res.status(401).send("Invalid signature"); } const { event, data } = JSON.parse(req.body); if (event === "interview.completed" && data.reference_id) { await db.users.update({ where: { id: data.reference_id }, data: { interview_completed: true }, }); } res.status(200).send("OK"); }); ``` # GitHub App Setup Source: https://docs.userjourneys.ai/chat/github-app Connect a GitHub repository so the UserJourneys AI agent can read code, make changes, and create draft pull requests. Connect the UserJourneys GitHub App to the repository you want the AI agent to use. After setup, the agent can read your codebase, prepare code changes, and create draft pull requests for your review. ## Before you start You need: * Access to the UserJourneys project you want to configure * Permission to install GitHub Apps on the target GitHub account or organization * The repository you want UserJourneys to use The GitHub App is the recommended connection method. Use a Personal Access Token only when your organization cannot install the GitHub App. ## Connect the GitHub App Sign in to UserJourneys settings. You can also open **Settings** from the sidebar in the app. Select the **Setup** tab. In the **GitHub** section, click **Connect GitHub App**. GitHub opens the UserJourneys GitHub App installation page. Choose the personal account or organization that owns the repository. Choose the repository UserJourneys should use. Select only the repository you want the AI agent to work with unless your team intentionally wants to grant broader access. Click **Install** or **Save** in GitHub. GitHub sends you back to UserJourneys. If UserJourneys shows a repository picker, select the repository and click **Use Repository**. The **GitHub** section shows the connected repository name. The AI agent can now use the connected repository to read code and create draft pull requests for approved code changes. ## Troubleshooting Ask a GitHub organization owner or app manager to install the app, or request permission to install GitHub Apps for the organization. The GitHub App may not have access to any repositories. In UserJourneys, click **Reload Repositories**. If the list is still empty, reinstall or update the GitHub App in GitHub and grant access to the repository you want to connect. Go to **Settings > Setup > GitHub**, click **Disconnect**, then connect the GitHub App again and choose the correct repository. Use **Personal Access Token** from the GitHub section in UserJourneys. Prefer the GitHub App when possible because it gives your team clearer installation ownership and repository-scoped access. # AI Agent Source: https://docs.userjourneys.ai/chat/index Ask product questions in natural language and get answers backed by your data. The AI agent answers product questions using your connected data sources. Ask in plain language -- it writes queries, runs analysis, and returns results as tables, charts, and summaries directly in the conversation. ## How it works Open the chat from the sidebar or press **Cmd+.** on any page to open the side panel. The side panel is context-aware -- if you're looking at a funnel or an interview, the agent already knows what's on your screen. Ask a question, and the agent figures out which data sources to use. Follow up to refine, drill into segments, or take the analysis in a different direction. The full conversation history is preserved. Try broad questions like "How is my product doing?" -- the agent runs multiple queries and assembles a health check across your key metrics. ## What the agent can do The agent's capabilities depend on which data sources are connected to your project. Your team's connections are configured during onboarding. ### Always available * **AI interviews** -- Create interview studies, analyze transcripts, synthesize themes. See [Interviews](/interviews) for the full guide. * **Text analysis** -- Analyze survey responses, feedback, and support tickets for patterns. * **Visualizations** -- Charts, tables, metric cards, and journey graphs rendered inline. * **Ticket generation** -- Turn analysis findings into actionable tickets for your team. ### With analytics data When your project has BigQuery, [PostHog](/funnels/posthog), or [Mixpanel](/funnels/mixpanel) connected: * **SQL queries** -- Retention cohorts, funnel analysis, conversion tracking, custom metrics. * **Predictive models** -- Churn prediction, activation drivers, feature importance rankings. * **Session replays** -- Find and watch recordings filtered by user behavior. * **Experiments** -- List running A/B tests, analyze statistical significance, run power calculations. ### With other integrations * **GitHub** -- [Read your codebase, implement changes, and create draft pull requests](/chat/github-app). * **App database** -- Query business entities directly -- users, orders, subscriptions, revenue. * **Pages** -- Build interactive dashboards with live data that update automatically. * **File export** -- Package results as CSV, PDF, images, or presentations. ## Using from other tools You can also interact with the agent from external AI tools like Claude Code, Cursor, or Claude Desktop. Go to **Settings > Integrations > AI Assistants** for setup instructions. # Success metrics Source: https://docs.userjourneys.ai/experiments/metrics Configure success metrics to measure whether an experiment's treatment moved user behavior in the direction you intended. A success metric is a measurable outcome an experiment is trying to change. userjourneys.ai supports four types, each answering a different product question with a different formula. This guide covers what each type computes, when to use it, and how the results are produced. ## Metric types at a glance | Type | Answers | Formula | Typical use | | ----------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------- | --------------------------------- | | [Conversion](#conversion) | Did the user trigger the event at least once? | `triggered ÷ exposed` | Activation, signup, first action | | [Events per user](#events-per-user) | How many events per user? | `Σ events ÷ exposed` | Volume: clicks, views, purchases | | [Events per user per active day](#events-per-user-per-active-day) | How intensely when engaged? | `Σ(events ÷ active days) ÷ exposed` | Engagement depth, session quality | | [Retention](#retention) | Did the user clear a threshold within a window? | `threshold met ÷ exposed` | Habit formation, stickiness | All four denominators are the **total number of exposed users** — users who never triggered the event contribute 0. This is [intent-to-treat analysis](#zero-filling), the correct statistical frame for A/B testing. ## Conversion Measures the fraction of exposed users who fired the event at least once. ### Formula ``` Rate = users_who_triggered ÷ users_exposed ``` ### Example A variant exposes 1,200 users. 340 of them fire `signup_completed` at least once. ``` Conversion rate = 340 ÷ 1,200 = 28.3% ``` ### When to use Activation funnels, first-time actions, any binary "did it happen" outcome. **Don't use it** when volume matters. A user firing the event ten times contributes the same as a user firing it once. For volume, use [Events per user](#events-per-user). ## Events per user Measures the average number of events each exposed user fired. ### Formula ``` Mean = Σ(events across all users) ÷ users_exposed ``` Non-participants count as 0 in the numerator but remain in the denominator — a **zero-filled mean** taken over everyone exposed, not only those who engaged. ### Example A variant exposes 1,200 users. 200 fire 600 events total; the remaining 1,000 fire none. ``` Mean = 600 ÷ 1,200 = 0.5 events / user ``` ### When to use Volume metrics: clicks, page views, messages sent, purchases. A user firing the event ten times contributes ten times as much as a user firing it once. **Don't use it** when engagement intensity matters more than cumulative volume. A user firing ten events on one day and a user firing one event on each of ten days contribute the same here. For intensity, use [Events per user per active day](#events-per-user-per-active-day). ## Events per user per active day Measures the average per-active-day event rate, computed per user, then averaged across the exposed population. ### Formula ``` For each user: rate = SUM(events) ÷ COUNT(DISTINCT active_days) Mean = Σ(rate) ÷ users_exposed ``` Each user contributes their own daily rate rather than their raw total. Non-participants contribute 0. ### Example Two users, same event, same experiment window: | User | Events | Active days | Per-user rate | | ----- | -----: | ----------: | ------------: | | Alice | 10 | 1 | 10 | | Bob | 10 | 10 | 1 | Alice and Bob fired the same number of events, but their per-day rates differ by 10×. This metric captures that asymmetry; [Events per user](#events-per-user) does not. ### When to use Engagement intensity, session quality, and any question where "when users come, they come hard" matters more than "how many total interactions." **Don't use it** when you care about total volume. Use [Events per user](#events-per-user). When these two mean metrics show similar numbers, the denominators still differ — total events vs. a sum of per-user daily rates — and meaningful differences typically appear at the 2nd or 3rd decimal. Low-participation experiments can make them look numerically close even when answering different questions. ## Retention Measures the fraction of exposed users who met a frequency threshold within a specified post-exposure window. ### Formula ``` Rate = users_who_met_threshold ÷ users_exposed ``` Configure the threshold as "at least **N** events in days **X** through **Y** after exposure." ### Example Threshold: *at least 2 events in days 0–7*. Out of 1,200 exposed users, 180 meet it. ``` Retention rate = 180 ÷ 1,200 = 15.0% ``` ### When to use Habit formation, stickiness, and any question about whether users keep engaging within a specific time window. **Don't use it** for one-time actions ([Conversion](#conversion) is simpler) or when you need the number of interactions ([Events per user](#events-per-user)). ## How results are computed The sections below document the statistical machinery. Read them to interpret edge cases; skip them to just use the numbers. ### Zero-filling All four metric types divide by the total exposed user count, not just participants. Exposed users who never trigger the event contribute 0 to the numerator. This is **intent-to-treat (ITT) analysis**: the experiment measures the effect of *assigning* users to a variant, not the effect on users who engaged. Restricting to engaged users selects on outcome and biases the result. Practical consequence: with low participation (say 3%), the median pins to 0 because more than half of the variant contributes 0. The Participating column shows how many users contributed non-zero values. ### Winsorization `Events per user` and `Events per user per active day` cap each user's per-user value at the variant's **99.9th percentile** before summing. This bounds the influence of extreme outliers without removing them entirely. `Conversion` and `Retention` are booleans per user; there's no value to cap. ### Significance testing | Metric type | Test | | --------------------------------------------------- | --------------------------------- | | `Conversion`, `Retention` | Two-proportion Z-test (two-sided) | | `Events per user`, `Events per user per active day` | Welch's t-test (two-sided) | The **q-value** column shows p-values adjusted via Benjamini–Hochberg, which controls the false-discovery rate across all metrics on the experiment. Treat `q < 0.05` as statistically significant. ### CUPED variance reduction CUPED (Controlled-experiment Using Pre-Experiment data) uses each user's pre-exposure behavior as a covariate to shrink variance. Typical reduction is 10–40%, which means experiments reach significance with fewer samples. Enable CUPED per metric by setting `cuped_pre_exposure_days` to the number of pre-exposure days to use (e.g. `7` or `14`). Only applies to `Events per user` and `Events per user per active day`. ## Troubleshooting Low participation. If 3% of exposed users engaged, the remaining 97% contribute 0. The mean is diluted and the median pins to 0. Check the Participating column. When it's a small fraction of Users, the metric is dominated by zero-filled non-participants. Options: * Run the experiment longer to accumulate more participants. * Switch to [Retention](#retention) if the question is "how many users crossed a threshold." * Switch to [Conversion](#conversion) if a binary "did they engage" is enough. The two metrics differ only when users vary in how many days they were active. If every engaged user fires the event on exactly one day, the two reduce to the same quantity (events ÷ 1 = events). To see a meaningful difference, pick an event that can repeat across days — session-level activity, daily check-ins, repeated clicks. The experiment is underpowered: either the sample is too small or the effect size is too small relative to variance. Options: * Increase traffic allocation or extend the run to accumulate more exposures. * Enable [CUPED](#cuped-variance-reduction) on mean metrics to reduce variance. * Pick a metric with less variance — [Conversion](#conversion) is typically less noisy than [Events per user](#events-per-user). * Verify the metric is one the treatment is expected to move. # Amplitude Setup Source: https://docs.userjourneys.ai/funnels/amplitude Find your Amplitude project keys and connect them to userjourneys.ai Connect the Amplitude project that contains the event history you want to analyze. You do not need to create a new Amplitude project unless you want to test with a separate sandbox first. Do not send API keys or secret keys in email, Slack, Linear, or support chat. Use the secure UserJourneys connection link your contact gives you, your company's password manager, or another approved secret-sharing tool. ## Before you start Open the secure UserJourneys connection link your contact gave you. Keep it open while you follow this guide. The connection form asks for: * Amplitude API key * Amplitude secret key You do not need to enter the Amplitude project name, project ID, or data region. UserJourneys detects the region from the key pair. ## What you need * An Amplitude project API key * An Amplitude project secret key * Permission to send one validation event, if you want us to verify the full ingest path ## Find your Amplitude keys Open your Amplitude workspace: * US workspace: app.amplitude.com/login * EU workspace: app.eu.amplitude.com/login Click the gear icon in the top-right, then select **Organization settings**. Amplitude Organization settings page showing Projects in the Workspace sidebar Select **Projects** in the left sidebar. Amplitude Projects page showing project rows to select Select the project that contains the event data you want UserJourneys to analyze. In the project's **General** tab, find **Project Details**. Click **Show** next to **Secret Key**, then paste that value into the UserJourneys connection form. Amplitude Project Details card showing API Key Manage and Secret Key Show controls Treat the secret key like a password. Anyone with the API key and secret key can export project event data. Click **Manage** next to **API Key**. On **API and Secret Keys**, copy the key value for the same project and paste it into the UserJourneys connection form. Submit the form after both values are filled in. After we receive the keys, we validate that the Export API can read settled hourly event exports before enabling ingestion. ## Validation event For a brand-new or empty Amplitude project, we may ask permission to send one synthetic validation event. Amplitude accepts the event immediately, but the Export API can take a few hours before the event appears in hourly exports. For a project that already has historical events, we can usually validate the read path immediately using older settled export hours. ## Troubleshooting Use an Amplitude account that can view project keys, then reopen the project's General tab. You do not need to provide the region. UserJourneys tests both Export API regions and uses the one that authenticates. Tell us your approximate events per day, and whether any single hour can exceed 4 GB of compressed export data. If hourly Export API reads are too large, we will discuss an existing warehouse export path instead. # Mixpanel Setup Source: https://docs.userjourneys.ai/funnels/mixpanel Connect Mixpanel to UserJourneys with a Service Account and cohort webhook sync. ## Create your Service Account Go to your Mixpanel project, click the **gear icon** in the bottom-left, then select **Project Settings**. In the Project Settings page, click the **Service Accounts** tab on the right side. Click **Create Service Account**. * Give it a name (e.g. "UserJourneys Export") * Set the role to **Analyst** Click **Create**. You'll see a **username** and **secret**. Copy both immediately — the secret won't be shown again. Go back to UserJourneys, paste the username and secret into the Mixpanel connection form, and click **Connect**. UserJourneys detects your project automatically — no project ID or region to enter. If the service account can access more than one project, you'll be asked to pick which one to connect. ## Sync cohorts to UserJourneys Cohort sync is optional and set up separately from connecting your account. It lets Mixpanel push cohort membership to UserJourneys for use in Study Launch Rules. In the Mixpanel connection settings, expand **Cohort sync (optional)** and click **Set up cohort sync**. UserJourneys shows the webhook URL, the Basic Auth username, and a one-time secret. Copy the secret immediately — it's shown only once. If you lose it, use **Regenerate secret**; regenerating invalidates the previous secret until you update it in Mixpanel. In Mixpanel, open the cohort sync/integrations area and create a custom webhook destination. Paste the UserJourneys webhook URL as the destination URL, enable Basic Auth, and paste the displayed username and secret. For each Mixpanel cohort you want to use for Study Launch Rules, sync or export that cohort to the UserJourneys webhook destination. Include `mixpanel_distinct_id`. If your app user id differs from Mixpanel distinct id, also include that app user id as `reference_id`. Saved cohorts can now appear as UserJourneys Audiences and can be connected to Studies through [Study Launch Rules](/interviews/targeting-rules). # PostHog Setup Source: https://docs.userjourneys.ai/funnels/posthog Follow these steps to create and configure your PostHog API key with userjourneys.ai ## Creating your API key Go to eu.posthog.com/settings/user-api-keys. If your project is in the US, change the URL prefix to us.posthog.com. Open the Personal API keys section and select Create personal API key. PostHog API Keys settings page showing the Create personal API key button * Select Projects * Choose the project you want to integrate Create personal API key form showing project selection and access tabs Select **MCP Server** from the Scopes dropdown. PostHog Scopes dropdown with MCP Server selected Click Create key and paste it into the form at userjourneys. You're all set # Guides Source: https://docs.userjourneys.ai/guides/index Step-by-step walkthroughs for common userjourneys.ai integrations. Practical guides that walk you through common integration patterns end-to-end. | Guide | What you'll build | | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | [Rolling out in-app interviews](/guides/rolling-out-in-app-interviews) | Stage a React Native interview rollout from a dev build to an internal pilot to a wider audience | | [Track interview completions](/guides/track-completions) | Detect when users finish an interview via webhook and update your product in real time | # Rolling out in-app interviews Source: https://docs.userjourneys.ai/guides/rolling-out-in-app-interviews Stage the rollout of React Native in-app interviews: test in a dev build, ship to an internal-only audience, verify on team phones, then widen. Roll out in-app interviews in stages instead of enabling a rule for everyone at once. Each stage limits who can be prompted, so you catch integration problems on your own devices before real users ever see a prompt. ## 1. Test in a dev build Validate the SDK end-to-end in a development build on both platforms before any release build carries a live rule. * Install the SDK and run the app once so it contacts the project's SDK API. The **SDK not connected yet** warning on the Targeting tab disappears once `sdk_connected` flips, which confirms the app reached UserJourneys. * Walk the full [Test your integration](/react-native-interviews/testing) checklist on iOS and Android: event wrapping, WebView rendering, microphone permission, browser fallback, and attribution. * Use a fresh throwaway `referenceId` per run so limits, cooldowns, and dismissals do not block re-testing. ## 2. Ship an internal-only rule in the release build Put a live rule in front of only your team first. * Sync a small internal cohort (your team's `referenceId`s) and create an **audience** [Study Launch Rule](/interviews/targeting-rules) targeting just that cohort, or use an app-event rule whose event you can trigger on demand. * Set the study to **Active** and the rule to **Active**. Keep the audience internal-only at this stage. ## 3. Verify on team phones Install the release build on team devices and confirm real prompts appear. * Confirm the prompt shows for internal users and opens the interview in-app. * Confirm unrelated events do not prompt, and dismissing clears the prompt. * If nothing prompts, work through [Why isn't anyone getting prompted?](/interviews/targeting-rules#why-isnt-anyone-getting-prompted) — most often the study is not activated, the event name does not match exactly, or the SDK is not connected. ## 4. Widen the audience Once internal verification passes, widen the rule's audience (or enable the app-event rule for everyone) and watch the first sessions land before expanding further. Tune priority, cooldown, and max starts per person as you scale. # Track Interview Completions Source: https://docs.userjourneys.ai/guides/track-completions Detect when users complete an interview via webhook, fetch the transcript, and update your product in real time. A common pattern: you want to ask certain users for feedback, but once they've completed the interview, stop showing the prompt. This guide walks through the full integration — from checking capacity, to handling the webhook, to fetching the transcript. ## What you'll build 1. Check if your study is accepting responses before showing the prompt 2. Open the interview in a new tab when the user clicks 3. Receive a webhook when the interview completes 4. Fetch the transcript via API 5. Mark the user in your database so the prompt doesn't appear again *** ## Prerequisites * A userjourneys.ai account with an active study * An [API key](/api/authentication) * A server that can receive HTTPS POST requests (for the webhook) *** ## Step 1: Configure the webhook Set up a webhook so userjourneys.ai notifies your server when an interview completes. You only need to do this once per project. ```bash theme={null} curl -X PUT https://app.userjourneys.ai/api/v1/webhooks \ -H "Authorization: Bearer uj_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/userjourneys", "events": ["interview.completed"] }' ``` The response includes a `signing_secret` — save it. You'll need it to verify incoming webhooks. ```json theme={null} { "url": "https://your-app.com/webhooks/userjourneys", "events": ["interview.completed"], "signing_secret": "whsec_5b69ff6a12af94c6e3901a061180ca73" } ``` The signing secret is only returned once. Store it securely (e.g. in an environment variable). If you lose it, delete the webhook and create a new one. See [Webhooks](/api/webhooks) for the full API reference. *** ## Step 2: Check study capacity Before showing the prompt, verify that the study can accept responses. This prevents showing a prompt that leads to a closed study. ```javascript theme={null} const API_KEY = process.env.USERJOURNEYS_API_KEY; async function getStudy(name) { const response = await fetch( "https://app.userjourneys.ai/api/v1/studies", { headers: { Authorization: `Bearer ${API_KEY}` } } ); const { data } = await response.json(); return data.find((s) => s.name === name); } const study = await getStudy("Onboarding Feedback"); if (study?.accepting_responses) { // Show the prompt to the user } ``` Cache this response for a few minutes. Study capacity doesn't change often, and caching avoids unnecessary API calls. See [Studies](/api/studies) for the full API reference. *** ## Step 3: Show the prompt and open the interview On your frontend, show a prompt to users who haven't completed the interview yet. When they click, open the interview link in a new tab. ```javascript theme={null} // Check your database to see if the user already completed the interview const user = await getUser(userId); if (!user.interview_completed && study.accepting_responses) { showInterviewPrompt({ message: "We'd love your feedback — it takes about 10 minutes.", // Append ?reference_id= so the webhook tells you who completed it link: `${study.interview_link}?reference_id=${encodeURIComponent(user.id)}`, // Open in a new tab so the user stays in your product target: "_blank", }); } ``` The `interview_link` comes from the study object you fetched in Step 2. It looks like `https://app.userjourneys.ai/i/xK9mR2pQ`. Appending `?reference_id=` to the link passes the user's identity through to the webhook payload. This is how you match a completed interview back to the user who clicked — even if multiple users are interviewing at the same time. You can pass any identifier: a user ID, email, UUID, or whatever your system uses. *** ## Step 4: Handle the webhook and fetch the transcript When a user finishes the interview, userjourneys.ai sends a POST request to your webhook URL. Verify the signature, fetch the transcript, then update your database. ```javascript theme={null} import crypto from "node:crypto"; import express from "express"; const SIGNING_SECRET = process.env.USERJOURNEYS_WEBHOOK_SECRET; const API_KEY = process.env.USERJOURNEYS_API_KEY; function verifySignature(body, signature, secret) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(body).digest("hex"); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } app.post( "/webhooks/userjourneys", express.raw({ type: "application/json" }), async (req, res) => { const signature = req.headers["x-webhook-signature"]; if (!verifySignature(req.body, signature, SIGNING_SECRET)) { return res.status(401).send("Invalid signature"); } const { event, data } = JSON.parse(req.body); if (event === "interview.completed" && data.reference_id) { // Fetch the full transcript const transcript = await fetch( `https://app.userjourneys.ai/api/v1/interviews/${data.interview_id}`, { headers: { Authorization: `Bearer ${API_KEY}` } } ); const interview = await transcript.json(); // Save transcript and mark user as completed await db.users.update({ where: { id: data.reference_id }, data: { interview_completed: true, interview_transcript: interview.transcript, interview_summary: interview.summary, }, }); } res.status(200).send("OK"); } ); ``` Always verify the signature before processing the webhook. Use `crypto.timingSafeEqual` (Node.js) or `hmac.compare_digest` (Python) to prevent timing attacks. ### What the payload looks like ```json theme={null} { "event": "interview.completed", "timestamp": "2026-03-11T14:32:00.123Z", "data": { "interview_id": "a1b2c3d4-5678-90ab-cdef-111111111111", "study_id": "e5f6a7b8-1234-56cd-ef78-222222222222", "study_name": "Onboarding Feedback", "interview_link": "https://app.userjourneys.ai/i/xK9mR2pQ", "status": "completed", "started_at": "2026-03-11T14:20:00.000Z", "completed_at": "2026-03-11T14:30:00.456Z", "duration_secs": 580, "quality_score": "insightful", "language": "en", "reference_id": "user_12345", "respondent_email": null, "respondent_id": null } } ``` See [Webhooks — Event: interview.completed](/api/webhooks#event-interviewcompleted) for the full payload reference. *** ## Step 5: Stop showing the prompt Once your database is updated, the check from Step 3 handles the rest — `user.interview_completed` is now `true`, so the prompt won't appear again. That's it. The full flow: 1. User sees the prompt -> clicks -> interview opens in a new tab 2. User completes the interview -> userjourneys.ai sends a webhook 3. Your server verifies the signature -> fetches the transcript -> marks the user as completed 4. Next time the user loads the page -> no prompt *** ## Testing Use a tool like [webhook.site](https://webhook.site) to inspect webhook payloads during development. Point your webhook URL there, complete a test interview, and verify the payload arrives. Once you're seeing payloads, switch the URL to your real server and test the full flow end-to-end. # Getting Started Source: https://docs.userjourneys.ai/index Get started with userjourneys.ai Choose where to start based on what you want to do. Collect user feedback through AI-powered voice conversations that feel natural and extract deeper insights. **Available on all plans** Connect PostHog to visualize funnels, analyze drop-offs, and watch session replays of users who didn't convert. **Pro plan** # Creating and Managing Experiments Source: https://docs.userjourneys.ai/interviews/experiments Set up interview experiments using a 6-step wizard with AI-powered prefill, custom questions, voice settings, and more. An experiment is a configured interview that you share with users. Each experiment has its own [questions](/interviews/questions), settings, and collected responses. ## Creating an Experiment You create experiments through a 6-step wizard. Choose how to set up your experiment: * **Prefill using AI** — Automatically generates interview questions from your website. Paste a URL and the AI extracts product context, then suggests relevant questions. * **Create manually** — Start with a blank experiment and configure everything yourself. Define your research goals: * **Research goal** (required) — Describe what you want to learn from this experiment. * **Respondent type** — Choose **Existing user** (interviewing your own customers) or **Discovery** (interviewing potential users or people outside your product). * **Additional context** (optional) — Extra details about who you're interviewing, such as user segment, role, or experience level. Configure the AI interviewer's identity and voice: * **Name** — What the AI introduces itself as. Defaults to "Alex". * **Voice gender** — Male or Female. * **Voice age** — Any, Young, Middle, or Old. * **Interview languages** — Select from 32 supported languages. * **Voice selection** — Choose a specific voice per language. Provide product details: * **Experiment name** (required) — Internal identifier to organize your interviews. Not shown to users. * **Product name** (required) — Your product's name, used in the `{product}` placeholder variable. * **Product context** (optional) — Background information about your product that helps the AI ask relevant follow-up questions. Include what your product does, target audience, key features, and common use cases. Set up your interview questions and opening message: * **Interview questions** — Add, edit, and reorder questions by dragging. Each question has a text field, optional internal notes, and a dig deep toggle. See [Writing Good Interview Questions](/interviews/questions) for details. * **Opening message** — Customize how the AI greets users. Supports these placeholders: | Placeholder | Replaced with | | ---------------- | ------------------------ | | `{"{name}"}` | Interviewer name | | `{"{product}"}` | Product name | | `{"{question}"}` | First interview question | **Example:** ``` Hey, I'm [name]. Thanks for joining — this will only take a few minutes. Let's start: [question] ``` * **Interview reward** (optional) — Text shown to users after completing the interview. Use this for thank-you messages or reward codes. **Example:** ``` Thanks for your feedback. Here's 20% off your next purchase: FEEDBACK20 ``` ## Managing Experiments Right-click an experiment or use the actions menu to access these options: | Action | Behavior | | --------------------- | ------------------------------------------------------------------------------------------ | | **Edit** | Opens the experiment configuration page. Changes apply to new interviews only. | | **Pause / Activate** | Toggle whether the experiment accepts new responses. Existing data stays accessible. | | **Rename** | Rename the experiment inline without opening the full editor. | | **Export interviews** | Downloads all interview transcripts as markdown files. | | **Delete** | Removes the experiment and its associated ElevenLabs agent. Interview data is preserved. | | **Test** | Opens the interview in a new tab in test mode — responses don't count toward usage limits. | | **Copy link** | Copies the production URL for sharing with real users. | Editing an experiment doesn't retroactively change completed interviews. If you significantly change your questions, consider creating a new experiment instead. # Interviews Overview Source: https://docs.userjourneys.ai/interviews/index Collect user feedback through AI-powered voice conversations that feel natural and extract deeper insights. Interviews are AI-powered voice conversations that collect user feedback. Instead of typing responses to a survey, users speak naturally while an AI interviewer guides the conversation, asks follow-up questions, and digs deeper when answers are vague. ## Key Characteristics Users speak via microphone, not text. This captures tone, emotion, and more natural responses. The AI weaves your questions into a flowing dialogue rather than reading them verbatim. When answers are vague, the AI pushes for specifics and examples automatically. Conversations are recorded, transcribed, and analyzed. Insights aggregate across interviews. ## Quick Start Set up your interview with [questions](/interviews/questions) you want answered and optional [product context](/interviews/experiments#product-context). Send the interview link directly to users, or launch a [PostHog in-app survey](/setup/posthog) to reach them inside your product. Watch [themes](/interviews/insights) emerge as interviews complete. ## Pricing Interview usage is based on completed interviews. Test mode interviews (via the [test link](/interviews/experiments)) don't count toward your limits. Use the test link to preview your interview experience before sharing with real users. # Understanding Themes & Results Source: https://docs.userjourneys.ai/interviews/insights Learn how themes are extracted from interviews and how to use them to improve your product. Themes are patterns automatically extracted across multiple interviews. Rather than reviewing each interview individually, the results page surfaces what matters most in two views. ## When Themes Appear Themes need enough data to identify patterns. **You need 3-5 completed interviews** before meaningful themes emerge. Once you have enough interviews: * Themes generate automatically * They regenerate as new interviews come in * No manual action needed on your part If you're not seeing themes yet, check how many successful interviews you have. Interviews marked as [unsuccessful](/interviews/quality) don't count toward theme generation. ## Results Page Layout The results page header shows the total respondent count, an **Export interviews** button, and a **Translate all** toggle for multilingual studies. Below the header, two tabs organize your results: ### Goals View (Default) The Goals view displays your research goal at the top, followed by themes extracted across all interviews. Each theme card includes: * **Title** summarizing the theme * **Sentiment** indicator (green for Positive, red for Negative, gray for Neutral) * **Description** elaborating on the pattern * **Interview count** with percentage showing how many unique interviews mentioned this theme * **Citations** — expandable carousel of quote cards linking directly to the conversation history Click "Show citations" on any theme to expand a highlight reel of direct quotes. Each quote card links to the exact moment in the conversation, so you can hear the original audio. ### Questions View The Questions view lets you drill into responses per question using a **question selector dropdown** (Question N of M). For each question, you see: * **Themes** specific to that question with sentiment and citation counts * **Individual response cards** showing each participant's answer text * A **translation indicator** on response cards from non-English interviews * **Expandable transcript context** to see the full exchange around each answer ## Sentiment Reference | Color | Sentiment | Meaning | | ----- | --------- | ---------------------------------------- | | Green | Positive | Users expressed satisfaction or praise | | Red | Negative | Users expressed frustration or criticism | | Gray | Neutral | Observation without strong sentiment | ## Sharing Results You can share your results via public link at three levels of granularity: Share a link to the full results page, including all themes and responses across both views. Share a link scoped to a single question's themes and responses. Share a link to one theme, including its description, sentiment, and citations. ## Using Themes Effectively **Prioritize by sentiment.** Red (negative) themes often point to friction worth fixing. Green (positive) themes show what to preserve — don't accidentally break what users love. **Listen to the source.** Expand citations on any theme to see the exact moments users said something relevant. Click through to the conversation to hear the original audio and read the full transcript. **Watch for patterns over time.** As more interviews complete, themes become more reliable. A theme mentioned by one user might be an outlier; the same theme from ten users is a signal. Share theme citations with your team by playing them in meetings. Hearing real users describe their experience is more compelling than summarizing it yourself. # Multi-Language Support Source: https://docs.userjourneys.ai/interviews/languages Run interviews in 32 languages and view translated results in English. Reach users in their preferred language. Interviews can be conducted in 32 languages with automatic translation for your review. ## Supported Languages ## How It Works Select which languages to offer when creating your [experiment](/interviews/experiments). Choose a voice for each enabled language. Your voice gender and age preferences determine which voices are available. Your questions and first message are automatically translated. Before starting, users choose their preferred language. The entire conversation happens in the user's selected language. Transcripts are auto-translated to English for your review. ## Translation Toggle When viewing non-English interviews: * **Default:** Shows English translation * **Toggle:** Switch to see the original language This lets you verify translations or review with team members who speak the original language. ## Dubbed Audio Non-English interviews can be dubbed to English, making it easy to share feedback with English-speaking teammates. Dubbed audio is AI-generated. Toggle to the original to hear the user's actual voice and emotional tone. # Quality Evaluation Source: https://docs.userjourneys.ai/interviews/quality How interview quality is automatically evaluated across three tiers and what it means for your insights. Not all interviews provide equal value. The system automatically evaluates each interview into one of three quality tiers: **High signal**, **Successful**, or **Poor quality**. ## Quality Tiers | Tier | Badge | Default Visibility | Included in Insights | | ---------------- | ------------ | ------------------ | -------------------- | | **High signal** | Yellow star | Shown | Yes | | **Successful** | None | Shown | Yes | | **Poor quality** | Orange badge | Hidden | No | ### High Signal Interviews marked as high signal contain particularly insightful feedback. These are highlighted with a yellow star badge so you can prioritize reviewing them. ### Successful Standard-quality interviews with useful responses. These form the bulk of your data and are included in [insights](/interviews/insights). ### Poor Quality Interviews where the feedback wasn't useful. Common causes: * **Very short duration** — Interviews under 2 minutes typically lack substance * **Off-topic discussion** — User didn't address your questions * **Technical issues** — Audio problems or disconnections interrupted the conversation * **One-word answers** — Only brief, surface-level responses throughout * **Uncooperative behavior** — User was joking, testing the system, or not engaging genuinely Poor quality interviews are excluded from [insights](/interviews/insights) to prevent low-quality responses from skewing your patterns. ## Filtering Conversations On the conversation history page, use the filter toggles to control which interviews you see: * **High signal only** — Show only interviews marked as high signal * **Show test** — Show or hide test interviews * **Show low quality** — Show or hide poor quality interviews ## Manual Quality Marking You can override the automatic evaluation from any conversation's detail view: 1. Open the conversation detail page 2. Click the actions dropdown 3. Mark the interview as **high signal**, **poor quality**, or **test** This is useful when the automatic evaluation misses a particularly valuable interview or flags one incorrectly. If you're seeing many poor quality interviews, review your experiment setup. Clearer [questions](/interviews/questions), better [product context](/interviews/experiments#product-context), or a different audience might help. # Writing Good Interview Questions Source: https://docs.userjourneys.ai/interviews/questions Learn how to write effective interview questions that the AI can use to guide meaningful conversations. Questions in interviews are conversation goals, not scripts. The AI uses them as topics to explore but won't read them verbatim. This means how you write questions matters for getting useful feedback. ## Working with Questions You add and manage questions in the **Content** step of the experiment wizard. * **Drag-and-drop reordering** — Drag questions to change their order. * **Press Enter** to add a new question below the current one. * Use the `{product}` variable in question text — it auto-replaces with your product name at interview time. ### Dig Deep Toggle Enable **dig deep** on a question to force the AI to ask follow-up questions on that topic. Use this for your highest-priority research areas where surface-level answers aren't enough. Reserve dig deep for 1-3 questions. Enabling it on every question makes interviews feel long and repetitive. ### Per-Question Notes Each question has an optional **notes** field for internal context. These notes are sent to the AI interviewer but never shown to the user. Use them to: * Explain what you're trying to learn from the question * Give the AI hints about what follow-ups to ask * Provide context about why this question matters **Example:** * **Question:** "How do you currently handle reporting?" * **Note:** "We're evaluating whether to build an export feature. Try to understand what tools they use today and what's frustrating about their current workflow." ## Write Open-Ended Questions Open-ended questions invite detailed responses and give users room to share their genuine experience. **Good questions:** * "What made you decide to try our product?" * "What was confusing about getting started?" * "What's one thing we could improve?" * "How do you typically use this feature?" **Avoid yes/no questions:** | Instead of... | Try... | | ------------------------------ | ----------------------------------------------------- | | "Did you like the onboarding?" | "How did you feel about the onboarding experience?" | | "Was the pricing clear?" | "What was your reaction to our pricing?" | | "Is the product easy to use?" | "Walk me through how you use the product day-to-day." | ## Question Guidelines This gives enough depth for meaningful insights without making interviews too long. Users typically spend 5-10 minutes in an interview. The AI follows your question sequence but skips questions the user already addressed naturally. Put your most important questions early in case the conversation runs long. Instead of "What do you think about our product?" try "What problem were you trying to solve when you found us?" The more focused your question, the more actionable the feedback. Think about what decisions you're trying to make, then write questions that would give you the information to make them. ## Next Steps Once you've written your questions, [create an experiment](/interviews/experiments) to start collecting feedback. # Study Launch Rules Source: https://docs.userjourneys.ai/interviews/targeting-rules Define targeting rules that let the React Native Interviews SDK prompt eligible users to start a voice interview from app events or synced Mixpanel audiences. Study Launch Rules connect a study to the people who should be offered it. A rule says who becomes eligible — through a tracked app event or a synced Mixpanel audience — and how the installed React Native Interviews SDK prompts them. You manage rules in **Configuration → Targeting rules** on a study, and you can change them without shipping a new app release. ## How a rule reaches a user ```text theme={null} You create a rule (event or audience) -> the installed SDK loads active rules for the project -> a tracked event matches, or a synced audience member loads config -> UserJourneys checks eligibility (priority, cooldown, max starts) -> the SDK shows the in-app prompt -> the user accepts -> the interview opens in-app ``` The SDK evaluates a tracked event locally first and ignores events that no rule targets. Eligibility, priority, cooldown, and start limits are enforced on the server, so changing a rule takes effect for installed apps without an update. ## Trigger types Every rule has one trigger, fixed when the rule is created. Fires when a user triggers a named event in your mobile app, such as `Order Completed`. The SDK matches the event name and asks UserJourneys to resolve a launch. Makes everyone in a synced Mixpanel cohort eligible. When a user's `referenceId` matches an active audience member, the SDK receives a pending prompt while loading config. ## Set up targeting rules In the dashboard, go to **Settings → API keys** and copy the **Interviews SDK key**. This public key is the only credential the mobile SDK needs. Add the SDK to your app and create the client with that key. See the [React Native quickstart](/react-native-interviews/quickstart). For an app-event rule, make sure your app tracks the event through the wrapped analytics client. For an audience rule, sync the Mixpanel cohort first — see [Mixpanel cohort sync](/funnels/mixpanel). Open the study, go to **Configuration → Targeting rules**, and click **Add rule**. Choose the trigger, pick the event or audience, write the prompt copy, set pacing limits, and set the status to **Active**. The tab appears once your app has contacted the SDK API — run the app once after installing the SDK. The event picker suggests events UserJourneys has already observed for your project. You can also type an event name the catalog has not seen yet. Once an app has contacted the project's SDK API with your key, the **SDK not connected yet** warning disappears and active rules can fire. ## What the user sees For an `SDK prompt` rule, the SDK shows an in-app card with your prompt heading, message, an accept button, and a dismiss button. Accepting opens the interview in-app; dismissing clears the prompt without opening anything. Empty copy fields fall back to the SDK defaults. A `Direct link` rule makes the study available without showing this prompt card. ## Field reference An internal label to find the rule later. Users never see it. **Active** runs the rule now, **Draft** saves it without launching, and **Paused** turns it off temporarily. Archiving removes a rule from the study. **SDK prompt** shows an in-app card the user can accept. **Direct link** makes the study available without a prompt. When several rules match the same person at the same time, the rule with the **higher number wins**. Default 100; range 0–10000. The minimum time before the same person can be prompted again. 0 means no wait; maximum 30 days, in seconds. How many times one person can start this study from this rule. Default 1; range 1–100. The text on the in-app prompt card for `SDK prompt` rules: a prompt heading (up to 120 characters), a prompt message (up to 500 characters), an accept button label, and a dismiss button label. Any field left blank uses the SDK default. Optional advanced mapping of metadata keys to respondent fields (for example `traits.plan`) to attach to each interview. Leave it empty unless you need it. ## When several rules match Rules are evaluated highest priority first. For an app event, the highest priority rule that targets that event name is used. For a synced audience, audiences are evaluated highest priority first. After a rule is selected, its cooldown and max-starts limits decide whether the person is prompted; if the limit is reached or the cooldown is active, the launch is not eligible. ## Before the SDK is installed Rules are delivered through the Interviews SDK. A rule can only reach someone after an app has contacted the project's SDK API with your Interviews SDK key. The **Targeting rules** tab appears in study configuration once your app has connected, or once the project already has launch rules. Before that, start from **Settings → API keys** to get the Interviews SDK key and install the SDK. If rules exist while the SDK is not connected, the tab shows an **SDK not connected yet** warning. Rules created before the SDK is installed (for example through the chat agent) are stored and ready, but they will not fire until the SDK is connected. ## Why isn't anyone getting prompted? If a rule is live but no one is seeing the prompt, work through this checklist from the most common cause down: * **The study is not activated.** A rule on a study that is not active never prompts anyone. Activate the study first. * **The rule is paused, a draft, or archived.** Only `Active` rules fire. Check the rule's status. * **The event name does not match.** App-event rules match the tracked event name exactly, including spaces and capitalization. `Order Completed` does not match `order_completed`. * **The person hit the max starts or is in cooldown.** Once a person reaches the rule's max starts per person, or while the cooldown is active, they are not eligible again. See [Testing your rules](#testing-your-rules) to test with a fresh person. * **The person dismissed a recent prompt.** A recently dismissed prompt does not immediately reappear. * **The person is not in the synced audience.** For audience rules, the person's `referenceId` must match an active member of the synced cohort. Confirm the cohort sync ran and includes that person. * **The SDK is not installed or connected.** Rules are delivered through the Interviews SDK. If the **SDK not connected yet** warning is showing, no app has contacted the project's SDK API with your key — install the SDK and run the app once. ## Testing your rules Limits, cooldowns, and dismissals are keyed off the `referenceId`, so once a test person has hit a limit or a cooldown, that same id will not prompt again. To re-run a rule cleanly, use a fresh throwaway `referenceId` for each test run — for example `dev-test-1`, then `dev-test-2` — so the new id has no start history, cooldown, or dismissal. Switch back to your real `referenceId` before you release. An invite link is a plumbing test that bypasses launch rules: it opens the interview directly without checking event match, audience membership, priority, cooldown, or max starts. Use an invite link to confirm the interview itself works, and use a fresh `referenceId` to confirm the rule's targeting and eligibility. For the full in-app validation checklist (event wrapping, WebView, microphone, fallback, attribution), see [Test your integration](/react-native-interviews/testing). ## Managing rules with the chat agent The chat agent can list, create, update, and archive Study Launch Rules for you. When it manages rules it also reports whether the SDK is connected, so it can warn you when a rule cannot reach anyone yet because the SDK is not installed. ## Related Install the SDK and create the client with your Interviews SDK key. Sync Mixpanel cohorts so they appear as audiences for audience rules. # Troubleshooting Source: https://docs.userjourneys.ai/interviews/troubleshooting Solutions for common issues with interviews, microphone access, and audio playback. ## No insights appearing [Insights](/interviews/insights) require multiple completed interviews to identify patterns. You'll need 3-5 successful interviews before insights generate. They appear automatically—no action needed on your part. ## Microphone permission denied Users must grant microphone access to participate in interviews. If permission was denied: Click the lock or info icon in the browser address bar. Look for "Microphone" in the site permissions list. Change the permission from "Block" to "Allow". Refresh to apply the new permission. ## User can't start interview Check these common causes: | Issue | Solution | | ------------------- | ----------------------------------------- | | Experiment paused | Resume the experiment from your dashboard | | Usage limit reached | Upgrade plan or wait for limit reset | | Microphone denied | Follow steps above to grant permission | | Unsupported browser | Use Chrome, Firefox, Safari, or Edge | ## Audio not playing If interview audio won't play: * Check browser audio permissions for the site * Try a different browser * Note: Some failed interviews may not have audio available ## Interview marked "Poor quality" The system determined the feedback wasn't useful for insights. Common causes: * Very short conversation (under 2 minutes) * Off-topic discussion * One-word answers throughout Enable the **Show low quality** filter on the conversation history page to see the transcript and understand why. You can also manually override the quality rating from the conversation detail view's actions dropdown. See [Quality Evaluation](/interviews/quality) for more details. # Audience prompts Source: https://docs.userjourneys.ai/react-native-interviews/audience-prompts Launch in-app interviews for synced Mixpanel cohort members without generating one link per user. Audience prompts are for users who match a synced analytics cohort. They do not require an app event at the moment of launch and they do not require you to generate a short link for every user ahead of time. ## Flow ```text theme={null} Mixpanel cohort membership changes -> Mixpanel sends cohort members to UserJourneys -> UserJourneys stores active audience members -> An interview Study Launch Rule connects that audience to a study -> app fetches SDK config with referenceId -> matching audience member materializes a respondent short code -> SDK receives pendingInvite -> app shows the native prompt -> accepted prompt opens WebView or browser fallback ``` The mobile app only uses the public SDK key and the current `referenceId`. Private Mixpanel credentials, webhook secrets, and project API keys stay on the server side. ## Identity `referenceId` is the join key between the installed app user and the synced audience member. Use the same stable person id your app uses for analytics, such as Mixpanel `distinct_id`, or your canonical app user id when that is what you sync to UserJourneys. UserJourneys also matches against the provider member id when `reference_id` is not present on the synced member. That lets teams start with Mixpanel `distinct_id` and move to a canonical app user id later without changing the SDK surface. ## Prompt behavior Audience membership does not push a user directly into an interview. The SDK sets a pending invite and `InterviewHost` shows the in-app prompt. Accepting the prompt resolves the respondent short code and opens the in-app WebView or the server-selected browser fallback. ## Attribution When an audience prompt is materialized, UserJourneys stores: * the Study Launch Rule id; * the study id; * the audience id; * the audience member id; * the respondent short code; * the app `referenceId`; * primitive, allowlisted audience traits used for context. That same respondent short code works for in-app WebView launches and browser fallback, so attribution is preserved if the app cannot open the WebView path. ## What changes after setup After the SDK is installed and the cohort sync is configured, UserJourneys can change which synced audience prompts which study by updating Study Launch Rules. The app does not need a release for each new study, prompt copy change, or event-to-study mapping change. # Customize the prompt Source: https://docs.userjourneys.ai/react-native-interviews/customize-prompt Use the built-in host prompt or render your own prompt for pending React Native interview invites. Event-triggered interviews and synced audience prompts show an in-app popup before opening the interview. By default, `InterviewHost` renders that prompt for you. ```tsx AppRoot.tsx theme={null} ``` The prompt appears when a configured tracked event or synced audience match creates a pending invite. Accepting the prompt starts the invite. Dismissing it clears the invite without opening the interview. ## Customize copy Prompt copy is configured on the [Study Launch Rule](/interviews/targeting-rules) in UserJourneys. The app does not need a release when the prompt title, body, primary action label, or dismiss label changes. ## Render your own prompt Use `useInterviews` when you need a custom native prompt component. Disable the built-in host prompt so the app does not render two prompts. ```tsx theme={null} import { InterviewHost, useInterviews, } from "@userjourneys/interviews-react-native"; export function InterviewInviteSheet() { const { pendingInvite } = useInterviews(interviews); if (pendingInvite == null) return null; return ( interviews.dismissPendingInvite()} onStart={() => void interviews.startPendingInvite()} /> ); } export function AppRoot() { return ( <> ); } ``` The prompt fields map directly to the [Study Launch Rule](/interviews/targeting-rules) fields you set in the dashboard: `title` is the rule's title, `message` is its body, `startLabel` is its primary action label, and `dismissLabel` is its dismiss label, so changing the dashboard copy updates your custom prompt without an app release. Keep `InterviewHost` mounted once near the app root. The prompt only controls whether a pending invite starts; the host owns the full-screen WebView after the invite starts. ## Direct link rules do not show a prompt card A [Study Launch Rule](/interviews/targeting-rules) can use either the `SDK prompt` surface or the `direct_link` surface. A `direct_link` rule makes the study available without showing a prompt card at all, so no pending invite is created and neither the built-in nor a custom prompt renders. Use `direct_link` when you drive the user into the interview yourself — for example from your own in-app entry point — instead of asking the SDK to show an in-app prompt. ## Link invites do not use the prompt HTTPS invite links open from a user tap in Intercom, email, push, or another channel. Because the user already selected the invite, `handleLink` resolves the link and opens the in-app WebView or browser fallback directly. # Event-triggered interviews Source: https://docs.userjourneys.ai/react-native-interviews/event-triggered-interviews Use the React Native SDK to launch UserJourneys interviews from an existing analytics track client. Use the analytics wrapper when your app already sends product events through one central client with a `track(eventName, properties)` method. This is the Mixpanel path and also works for app-owned analytics helpers with the same method shape. Analytics providers with different event APIs should call `interviews.track(eventName, properties)` from the app's central analytics helper instead. ## Flow ```text theme={null} analytics.track("Order Completed") -> original analytics call runs (return value preserved) -> SDK checks the cached interview trigger config (loaded at startup) -> unrelated events are ignored locally -> matching events create a pendingInvite from local config — no network call -> app shows a native prompt instantly -> user accepts -> SDK resolves the signed launch with UserJourneys -> server returns webview, external_browser, or not_eligible -> WebView or browser fallback opens ``` The SDK does not replace your analytics provider. It observes the same event stream from one wrapper point and preserves the original `track` return value. Interview config, API, or network failures are reported through diagnostics and do not block the original analytics call. ## Track client shape The wrapped client must have a `track` method whose first argument is the event name and whose second argument is the event properties object. ```ts theme={null} type TrackMethodClient = { track: ( eventName: string, properties?: Record, ...extraArgs: unknown[] ) => unknown; }; ``` `wrapTrackClient` returns a proxy with the same surface as the original client. All properties other than `track` pass through unchanged. When `track` is called, the wrapper: * calls the original analytics client first; * returns the original `track` result; * reads the event name and allowlisted properties; * creates a pending invite locally from the cached trigger config when the event can launch an interview — with no network call, so the prompt appears instantly. The signed launch is resolved later, only when the user accepts the prompt. ## Server-controlled triggers Study Launch Rules own trigger config, eligibility, interview selection, and fallback behavior after the app integration. The SDK loads active event trigger config for the project, caches it for the server-provided TTL, and ignores unrelated analytics events locally. Matching events create a pending invite instead of opening an interview without user action. You do not need an app release when UserJourneys changes which study is active for an event. The app does not define trigger rules. If UserJourneys changes the event-to-study mapping, targeting, fallback policy, or active study, the server config changes and the installed SDK keeps working. To create an event trigger, add an app-event [Study Launch Rule](/interviews/targeting-rules) on the study and set its event name. ## Metadata Only pass allowlisted primitive metadata. ```ts theme={null} const interviews = createInterviewClient({ publicKey: "INTERVIEWS_PUBLIC_KEY", referenceId: user.id, mapTrackProperties: (_eventName, properties) => ({ total: typeof properties?.total === "number" ? properties.total : null, currency: typeof properties?.currency === "string" ? properties.currency : null, }), }); ``` Nested analytics payloads, tokens, emails, and private vendor ids should stay out of SDK metadata. ## Prompt before opening Matching events create a pending invite. The mounted `InterviewHost` shows the in-app popup for that invite. Accepting the prompt opens the in-app WebView or browser fallback. Dismissing the prompt clears the invite without opening anything. See [Customize the prompt](/react-native-interviews/customize-prompt) to render your own prompt component. # How launches work Source: https://docs.userjourneys.ai/react-native-interviews/how-launches-work Understand the supported React Native interview launch paths: tracked app events, synced audience prompts, and HTTPS invite links. The React Native Interviews SDK supports three launch paths. ```text theme={null} Tracked app event -> native prompt -> in-app WebView or browser fallback Synced audience membership -> native prompt -> in-app WebView or browser fallback HTTPS invite link -> in-app WebView or browser fallback ``` For event-triggered interviews, the prompt is the handoff before the WebView opens. Customizing the prompt changes that handoff UI, not the launch model. ## Event-triggered interviews Event-triggered interviews start from an analytics event your app already sends. An active Study Launch Rule connects the event name to the study that should run. ```text theme={null} analytics.track("Order Completed") -> original analytics call runs -> SDK checks cached interview trigger config (loaded at startup) -> unrelated event: ignored locally -> matching event: SDK creates pendingInvite from local config — no network call -> InterviewHost shows the in-app prompt instantly -> user accepts -> SDK resolves the signed launch decision with UserJourneys (the only network call) -> InterviewHost opens the web interview in-app ``` The in-app popup is rendered by `InterviewHost`. It appears instantly from the trigger config the SDK loaded at startup — a matching event creates the pending invite locally, with no network round-trip, so the prompt never lags behind the user's tap. The signed launch is resolved only when the user accepts the prompt. The SDK does not push a user directly from a tracked event into the interview without that prompt. If UserJourneys decides the installed app should not open the interview in-app, accepting the prompt opens the server-provided browser fallback through React Native `Linking.openURL`. ## Synced audience prompts Audience prompts start from synced cohort membership, not from an app event. ```text theme={null} Mixpanel sends cohort members to UserJourneys -> UserJourneys stores active audience members -> active Study Launch Rule connects the audience to a study -> SDK fetches config with referenceId -> matching audience member materializes a respondent short code -> SDK sets pendingInvite -> InterviewHost shows the in-app prompt -> user accepts -> SDK resolves source="audience" with the respondent invite code -> InterviewHost opens the web interview in-app ``` The public mobile SDK never syncs cohorts and never carries private API keys. Cohort sync runs through UserJourneys integration endpoints and authenticated provider webhooks. The mobile app only sends the same `referenceId` used by the synced audience member. ## HTTPS invite links Invite links are for Intercom, email, push, SMS, and other message channels. ```text theme={null} User taps https://app.userjourneys.ai/i/{inviteCode} -> app opens through Universal Links or App Links when installed -> SDK resolves the invite code with UserJourneys -> supported app: WebView opens in-app -> unsupported or missing app: browser interview opens ``` The link contains routing context. It does not contain conversation tokens, voice agent ids, room ids, or private runtime config. Universal Links/App Links require both sides of the handshake: your app declares `app.userjourneys.ai`, and UserJourneys authorizes your app identifiers on that domain. Without that domain authorization, the same HTTPS invite link still works as the browser fallback. ## What UserJourneys controls After the app integrates the SDK, UserJourneys controls: * which tracked events are active interview triggers; * which Study Launch Rules connect events or audiences to studies; * which synced audience members have pending prompts; * eligibility and targeting for a launch; * which interview runs for a trigger or invite; * whether the app receives an in-app WebView launch or browser fallback. The app controls: * where the wrapped analytics client is exported; * how `referenceId` is derived; * which primitive event properties are mapped into interview metadata; * whether the default prompt or a custom pending-invite prompt is rendered; * app-side Universal Links/App Links setup for HTTPS invite links. See [Study Launch Rules](/interviews/targeting-rules) for how to create and target the rules that connect events and audiences to studies. # Invite links Source: https://docs.userjourneys.ai/react-native-interviews/invite-links Configure Universal Links and App Links so UserJourneys interview invites open in-app when supported and fall back to web otherwise. Send normal HTTPS links from Intercom, email, push, SMS, or any other channel. ```text theme={null} https://app.userjourneys.ai/i/{inviteCode} ``` `/i/` is the canonical public participant path: it opens the web interview in a browser and is intercepted in-app when the SDK is installed. The app also intercepts `https://app.userjourneys.ai/interviews/{inviteCode}` as a deep link, but that prefix has no public browser route — prefer `/i/` for any link a recipient might open in a browser. The link contains routing context only. It does not contain conversation tokens, voice agent ids, room ids, or private Interview config. ## App handling Configure your app for Universal Links on iOS and App Links on Android for `app.userjourneys.ai`, then forward incoming URLs to the SDK. For iOS, enable the associated domain: ```text theme={null} applinks:app.userjourneys.ai ``` For Android, add an HTTPS intent filter for: ```text theme={null} https://app.userjourneys.ai/i/* https://app.userjourneys.ai/interviews/* ``` UserJourneys must also authorize your app on the `app.userjourneys.ai` domain. Provide: * Apple Team ID and iOS bundle ID. * Android application ID and SHA-256 signing certificate fingerprint. If Android uses separate debug, staging, and production signing keys, provide the fingerprints for the builds that should open production invite links. ```tsx theme={null} Linking.getInitialURL().then((url) => { if (url != null) void interviews.handleLink(url); }); Linking.addEventListener("url", (event) => { void interviews.handleLink(event.url); }); ``` ## Fallback behavior The same link works when the app is not installed. The browser opens the web interview route. When the app is installed and supported, UserJourneys returns a signed WebView launch URL and the SDK opens it in-app. When the app is unsupported or the server chooses not to open in-app, UserJourneys returns an external browser URL and the SDK opens it with React Native `Linking.openURL`. ## Intercom Intercom is a delivery channel in this setup. It can send the HTTPS invite link, but it does not need to know the interview runtime, WebView URL, or voice configuration. ## Replacing Intercom delivery If your app uses synced audience prompts, Intercom is not required to get the user into the interview. UserJourneys receives synced audience members from the configured integration, the SDK fetches pending prompts for the current `referenceId`, and the native prompt opens the in-app WebView when the user accepts. Keep HTTPS invite links for channels where you still want an explicit message: email, push, SMS, Intercom, or support workflows. The same respondent short code preserves attribution in-app and in the browser fallback. # Mixpanel setup Source: https://docs.userjourneys.ai/react-native-interviews/mixpanel Use Mixpanel events and cohorts with the UserJourneys React Native interview SDK. Use this recipe when Mixpanel is the analytics client your app already imports and calls from product code. There are two supported paths: * event-triggered interviews from the app's existing `mixpanel.track(...)` calls; * synced cohort prompts from Mixpanel cohorts imported into UserJourneys. The interview SDK does not replace Mixpanel. Your app still tracks product events in Mixpanel, and UserJourneys decides which configured events or synced cohorts should show an interview prompt. ## Wrap the exported Mixpanel client Create the interviews client in the same module where you export your Mixpanel instance. ```ts analytics.ts theme={null} import { createInterviewClient } from "@userjourneys/interviews-react-native"; const rawMixpanel = new Mixpanel("MIXPANEL_TOKEN", true); export const interviews = createInterviewClient({ publicKey: "INTERVIEWS_PUBLIC_KEY", referenceId: currentUser.id, metadata: { app_version: appVersion, }, mapTrackProperties: (_eventName, properties) => ({ total: typeof properties?.total === "number" ? properties.total : null, currency: typeof properties?.currency === "string" ? properties.currency : null, }), }); export const mixpanel = interviews.wrapTrackClient(rawMixpanel); ``` Your existing product code keeps using the exported client: ```ts theme={null} mixpanel.track("Order Completed", { total: 50, currency: "USD", }); ``` ## What UserJourneys receives For event-triggered launches, UserJourneys receives only: * the event name; * the `referenceId` configured on the SDK; * the primitive metadata returned by `mapTrackProperties`; * app-level `metadata`, if configured. Nested Mixpanel payloads, private provider fields, tokens, emails, and raw user profiles are not forwarded unless you explicitly map them. ## Sync Mixpanel cohorts For cohort-based prompts, configure Mixpanel to send saved cohort membership to the UserJourneys Mixpanel cohort sync endpoint. UserJourneys stores active audience members and Study Launch Rules connect those audiences to studies. The mobile app does not call Mixpanel cohort APIs and does not carry private Mixpanel credentials. It only sends the public SDK key and the current `referenceId` when loading config. See [Audience prompts](/react-native-interviews/audience-prompts) for the runtime flow, and [Study Launch Rules](/interviews/targeting-rules) to connect a synced audience to a study. ## Mount the interview UI Mount the host once near your app root. It owns the built-in prompt and the full-screen WebView. ```tsx AppRoot.tsx theme={null} import { InterviewHost, } from "@userjourneys/interviews-react-native"; import { interviews } from "./analytics"; export function AppRoot() { return ( <> ); } ``` When Interview config has an active trigger for a Mixpanel event, the SDK creates a pending invite and the host shows the native prompt. Starting the prompt opens the web interview in-app. # Get started Source: https://docs.userjourneys.ai/react-native-interviews/quickstart Install the UserJourneys React Native SDK and launch interviews from tracked events, synced audiences, and HTTPS invite links. The React Native SDK opens the canonical web interview inside your app with `react-native-webview`. It does three things: * It wraps one existing analytics `track` client so the SDK can observe selected app events. * It shows a native prompt before starting an event-triggered or synced-audience interview. * It resolves Interview HTTPS invite links from Intercom, email, push, or any other channel. You keep your analytics SDK and messaging tools. After the first integration, UserJourneys controls which Study Launch Rules, synced audiences, events, and invite links should launch an interview. ## Install ```bash theme={null} npm install @userjourneys/interviews-react-native react-native-webview react-native-screens ``` You do not install a separate UserJourneys core package. The shared interview runtime is bundled into `@userjourneys/interviews-react-native`; `react-native-webview` and `react-native-screens` are listed separately because they are native modules linked into your app binary. ## Requirements The SDK supports React Native `>=0.82.0`, which matches the `react-native-screens` 4.x peer requirement. Apps must use iOS deployment target 15.1 or newer and Android `minSdkVersion` 24 or newer. Expo Go can run JavaScript-only app code, but a production integration should be validated in the same development build, EAS build, or bare React Native app that users receive so WebView, deep links, and microphone permissions match the release build. ## Add microphone permissions The in-app interview runs in `react-native-webview`, but voice capture still uses native app permissions. For iOS, add `NSMicrophoneUsageDescription` to the app `Info.plist`. For Android, add both permissions to the app manifest: ```xml theme={null} ``` Expo apps can declare the same values through `android.permissions` and `ios.infoPlist` in app config. ## Get your SDK key In the dashboard, go to **Settings → API keys** and copy the **Interviews SDK key**. This public key is the only credential the mobile SDK needs. Pass it as `publicKey` when you create the client below. ## Create the client Create the interview client next to your central analytics setup. If that client exposes `track(eventName, properties)`, wrap it once. ```tsx analytics.ts theme={null} import { createInterviewClient } from "@userjourneys/interviews-react-native"; import { rawAnalytics } from "./existing-analytics"; export const interviews = createInterviewClient({ publicKey: "INTERVIEWS_PUBLIC_KEY", referenceId: currentUser.id, metadata: { app_version: appVersion, }, mapTrackProperties: (_eventName, properties) => ({ total: typeof properties?.total === "number" ? properties.total : null, currency: typeof properties?.currency === "string" ? properties.currency : null, }), }); export const analytics = interviews.wrapTrackClient(rawAnalytics); ``` You do not need to update every existing `track` call. Wrap the single analytics client your app already imports, then keep calling that client normally. When the signed-in user changes, call `setUser` so triggers and invites follow the new identity (any in-flight launch from the previous user is discarded): ```ts theme={null} interviews.setUser({ referenceId: currentUser.id }); // sign in / switch account interviews.setUser({ referenceId: null }); // sign out ``` See the [`setUser` reference](/react-native-interviews/reference#setuser) for details. If your analytics provider uses a different method name or argument shape, keep that provider call unchanged and call `interviews.track(eventName, properties)` from the same central analytics helper. ## Mount the host once Render the host once near your app root. Eligible analytics events and synced audience matches show the built-in native prompt first. Accepted prompts and explicit invite links open the interview in-app. ```tsx AppRoot.tsx theme={null} import { InterviewHost, } from "@userjourneys/interviews-react-native"; import { interviews } from "./analytics"; export function AppRoot() { return ( <> ); } ``` The built-in prompt is a small native invite sheet. The host renders accepted interviews as a full-screen WebView. The interview UI itself comes from the same interview web embed used in the browser, so welcome, setup, active interview, completion, unavailable, and error states stay visually aligned. Synced audience prompts use the same prompt and host. When the current `referenceId` matches an active audience member connected to a Study Launch Rule, the SDK receives a pending invite during config load and shows the prompt. ## Keep tracking normally Your app keeps calling analytics as it does today. ```tsx CheckoutCompleteButton.tsx theme={null} analytics.track("Order Completed", { total: 50, currency: "USD", }); ``` When UserJourneys has an active trigger for that event, the SDK creates a pending invite and the prompt appears. If the user starts the invite, the SDK opens the in-app WebView. If the installed app cannot handle the WebView path, UserJourneys returns an external browser fallback URL and the prompt starts that fallback through React Native `Linking.openURL`. Which events and audiences launch an interview is controlled by Study Launch Rules in the dashboard. See [Study Launch Rules](/interviews/targeting-rules) to create and target them. If your central analytics client is Mixpanel, see the [Mixpanel setup](/react-native-interviews/mixpanel). ## Handle HTTPS invite links Configure Universal Links/App Links for `https://app.userjourneys.ai/i/*` and `https://app.userjourneys.ai/interviews/*`, then pass incoming URLs to the SDK. UserJourneys also needs to authorize your app from the `app.userjourneys.ai` domain before those HTTPS links can open your installed app. Provide these app identifiers: * Apple Team ID and iOS bundle ID. * Android application ID and the SHA-256 signing certificate fingerprint for each release signing key that should open links. Until those identifiers are active on the UserJourneys domain, the same HTTPS links still work as browser fallback links. ```tsx links.ts theme={null} import { Linking } from "react-native"; import { interviews } from "./analytics"; Linking.getInitialURL().then((url) => { if (url != null) void interviews.handleLink(url); }); Linking.addEventListener("url", (event) => { void interviews.handleLink(event.url); }); ``` If the app is installed and supported, the link opens the in-app WebView. If the app is missing or unsupported, the same HTTPS link opens the browser interview. # API reference Source: https://docs.userjourneys.ai/react-native-interviews/reference Reference for UserJourneys React Native SDK options, event wrapping, link handling, and WebView host props. Most apps use one setup function and one mounted host. The host owns the built-in native prompt and the full-screen WebView. ```tsx theme={null} import { createInterviewClient, InterviewHost, } from "@userjourneys/interviews-react-native"; ``` ## createInterviewClient Creates a wrapper around your existing analytics client and returns link handling utilities. ```ts theme={null} const interviews = createInterviewClient({ publicKey: "INTERVIEWS_PUBLIC_KEY", referenceId: currentUser.id, }); ``` The Interviews public SDK key from Settings -> API Keys. It is scoped to loading interview triggers and launching eligible in-app interviews. Your stable person id. Use the same id you use for your analytics person id, such as a Mixpanel `distinct_id`, or your canonical user id. Optional override for opening the server-provided browser interview URL. Defaults to React Native `Linking.openURL`. Optional allowlist mapper for event properties. Only JSON primitive metadata is accepted. Optional app-level primitive metadata added to eligible event and link launch requests. Optional low-level diagnostics for logging config loads, ignored events, fallbacks, and WebView bridge errors. ## setUser Updates the current respondent identity at runtime — call it on sign-in, sign-out, or account switch. The SDK refetches trigger config for the new identity and discards any in-flight launch from the previous user, so a pending invite never opens for the wrong person. ```ts theme={null} // sign in / switch account interviews.setUser({ referenceId: currentUser.id }); // sign out — clear the identity interviews.setUser({ referenceId: null }); ``` The stable person id for the new identity (same value you pass to `createInterviewClient`). Pass `null` to clear it on sign-out. Optional app-level primitive metadata for the new identity, merged into eligible launch requests. ## wrapTrackClient Wraps a client that already exposes a `track(eventName, properties, ...extraArgs)` method, such as the Mixpanel React Native client or an app-owned analytics helper with the same method shape. ```ts theme={null} export const analytics = interviews.wrapTrackClient(rawAnalytics); ``` The wrapper returns a proxy with the same surface as the original client. Non-`track` properties pass through unchanged. The original analytics call still runs first, and the wrapper preserves the `track` return value. UserJourneys receives the event name and mapped context in the background and decides whether to create a pending invite. Interview config, API, or network failures are reported through `onDiagnostic`; they do not change the wrapped analytics return value. If your analytics provider uses a different event method, call that provider and then call `interviews.track(eventName, properties)` from the same analytics helper instead of using `wrapTrackClient`. The SDK does not call UserJourneys for every analytics event. It loads the active trigger config for the project, caches it for the server-provided TTL, and ignores unrelated events locally. A matching event creates a pending invite locally from the cached config — with no network call — so the prompt appears instantly. The signed launch is resolved with the server only when the user accepts the prompt. Matching events do not open the interview immediately. They set `snapshot.pendingInvite`. By default, `InterviewHost` renders the prompt for that invite. ## useInterviews Subscribes to the SDK snapshot so you can render a custom native prompt. When using a custom prompt, mount `InterviewHost` with `showPrompt={false}`. ```tsx theme={null} const { pendingInvite, activeLaunch } = useInterviews(interviews); ``` `pendingInvite` is set after an eligible analytics event or synced audience match. `activeLaunch` is set when an invite starts or an explicit link opens in-app. ## startPendingInvite Starts the current pending invite. ```ts theme={null} await interviews.startPendingInvite(); ``` Returns: ```ts theme={null} type StartPendingInviteResult = | "webview" | "external_browser" | "not_eligible" | "expired" | "none"; ``` `webview` means the full-screen host can render the in-app interview. `external_browser` means the SDK opened the fallback URL. `expired` means the signed launch expired before the user accepted. `not_eligible` means the server declined the launch after the user accepted a pending audience prompt. `none` means no pending invite was available or launch resolution failed without breaking the app. ## dismissPendingInvite Clears the current pending invite without opening the interview. ```ts theme={null} interviews.dismissPendingInvite(); ``` ## handleLink Handles HTTPS invite links from Intercom, email, push, or any other channel. ```ts theme={null} await interviews.handleLink("https://app.userjourneys.ai/i/dS8FFCgu"); ``` Supported formats: * `https://app.userjourneys.ai/i/{inviteCode}` * `https://app.userjourneys.ai/interviews/{inviteCode}` Returns: ```ts theme={null} type InterviewLinkResult = | { handled: true; mode: "webview" | "external_browser" | "not_eligible" } | { handled: false; reason: "not_interview_link" | "resolve_failed" }; ``` ## InterviewHost Mounts the built-in prompt and full-screen WebView host. ```tsx theme={null} { analytics.track("Interview Started", { sessionId }); }} /> ``` The client returned by `createInterviewClient`. Optional app-controlled visibility gate. The host still requires an active pending invite or active server launch decision. Controls whether the built-in pending-invite prompt is rendered. Defaults to `true`. Set to `false` when rendering your own prompt with `useInterviews`. Optional extra origins that the WebView may navigate to. The launch and fallback origins are allowed automatically. Called when the web interview reports that the embed loaded. Called when the interview session starts. Called when the session completes. Called when the participant leaves before completion. Called when the participant closes the in-app interview. Called for WebView load failures, invalid bridge messages, blocked navigation, or interview runtime errors reported by the embed. ## referenceId and triggerEvent `referenceId` answers who the respondent is. Use your analytics person id or canonical user id. `triggerEvent` answers what app event opened the interview. It is attribution context; Interview targeting and eligibility are server-controlled. ## Attribution fields SDK snapshots and callbacks can include these server-provided attribution fields: The Study Launch Rule that created the pending invite or launch. The study that UserJourneys selected for the launch. The synced audience that matched the current `referenceId`, when the launch started from an audience prompt. Apps normally do not construct these values. They come from UserJourneys so in-app WebView launches and browser fallback sessions keep the same attribution. ## Public API boundary The SDK uses two versioned public endpoints: * `GET /api/v1/interview-participant/config` * `POST /api/v1/interview-participant/launches` * `POST /api/v1/interview-participant/prompt-events` The app never calls bootstrap, conversation-token, vendor, or raw interview runtime endpoints. `config` returns server-controlled trigger rules. `launches` returns a launch decision object: `webview`, `external_browser`, or `not_eligible`. `prompt-events` records invite prompt funnel phases for server-side analytics. ## Package boundary Install `@userjourneys/interviews-react-native` in your app. The SDK includes the shared interview runtime in its published JavaScript and type output, so there is no separate UserJourneys core package for app teams to install or version. # Test your integration Source: https://docs.userjourneys.ai/react-native-interviews/testing Confirm analytics-triggered launches, invite links, WebView rendering, fallback, and attribution before releasing your app. Use this checklist before releasing an app version with UserJourneys React Native SDK. Opening a browser interview link is not enough. A React Native SDK validation should run inside your app so event wrapping, Universal Links/App Links, WebView rendering, microphone permissions, fallback behavior, and attribution are tested together. ## Before you start Prepare a build and a test user that can safely create interview sessions: * An iOS or Android app build that includes the SDK and `react-native-webview`. * A non-production interview study with an active Study Launch Rule for the test event. * If you use cohort prompts, one active Study Launch Rule for a non-production synced audience. * A known user id that matches the id your analytics tool uses for the same person. * One analytics event that should launch an interview. * One synced audience member for the same known user id, if you use cohort prompts. * One Interview HTTPS invite link. * Universal Links/App Links authorized on `app.userjourneys.ai` for the app build you are testing. ## App build Confirm the app can load the in-app WebView host: * The app installs and opens on a simulator, emulator, or physical device. * `InterviewHost` is mounted once near the app root. * Expo apps are validated in a development build, EAS build, or prebuild output. * Android builds include WebView microphone permissions. * iOS builds can request microphone permission from the WebView interview. ## Analytics launch Run the same product action a real user would take. Complete the app action that sends the configured analytics event. Verify your existing analytics dashboard still receives the event. The configured event should show the native interview prompt. Nearby unrelated events should keep tracking normally without showing a prompt or opening an interview. Accepting the prompt should open the in-app WebView. Dismissing the prompt should clear it without opening an interview. If UserJourneys returns an external browser decision for the app version or device, accepting the prompt should open the fallback URL with React Native `Linking.openURL` instead of leaving the user stuck. The app should not need changes at every existing `track` call. The SDK is installed once around your central analytics client and UserJourneys controls which events are active for interviews. Launch limits, cooldowns, and dismissals are keyed off the `referenceId`, so a test user that already hit a limit or cooldown will not prompt again. To re-run a rule cleanly, use a fresh throwaway `referenceId` for each test run — for example `dev-test-1`, then `dev-test-2` — so the new id has no start history, cooldown, or dismissal. Switch back to your real `referenceId` before releasing. ## Invite links Validate the link path separately from analytics events. The link should open your app through Universal Links or App Links and present the in-app WebView when the app version is supported. If the link opens only in the browser with the app installed, confirm UserJourneys has your Apple Team ID, iOS bundle ID, Android application ID, and Android SHA-256 signing certificate fingerprint. The same HTTPS link should still open the web interview in a browser when the app is not installed or the app cannot handle the link. Your app should ignore unrelated links and keep its normal link behavior. ## Synced audience prompts Validate the cohort path separately from analytics events. Sync the test user's `referenceId` or provider member id into a non-production Mixpanel cohort connected to UserJourneys. The SDK config request should include the same `referenceId` and receive a `pending_prompts` entry from UserJourneys. Accepting the prompt should resolve `source: "audience"` with the respondent short code, Study Launch Rule id, and audience id, then open the in-app WebView or the server-selected fallback. The mobile app should only use the public SDK key. Cohort sync must use a private project API key or authenticated provider webhook outside the app. ## Interview experience Walk through the visible interview states: * Loading state while the WebView loads the signed launch URL. * Welcome screen. * Environment and microphone setup. * Active voice interview. * Completed state. * Leave or abandon flow. * Microphone-denied state. * Limit or unavailable state, if your test interview is configured for it. The in-app interview should match the browser participant experience because both surfaces render the same web embed. Use screenshot comparison against the browser embed for release checks when visual drift is a risk. ## Attribution After the test session, confirm the session is attached to the right user and launch source in UserJourneys. You should be able to identify: * The app user or analytics person id. * Whether the session came from an analytics event or invite link. * The event name, Study Launch Rule, audience, or invite that started the session. * The study that ran. * The session outcome: started, completed, abandoned, screened out, unavailable, or fallback. ## Failure cases Test the cases users are likely to hit: * Offline or poor network. * WebView load failure. * Microphone permission denied. * User grants microphone permission after initially denying it. * User dismisses the native prompt. * Unsupported app version or device runtime. * Interview unavailable or limit reached. * User leaves before completing the interview. The app should not crash in these cases. It should either show the appropriate interview state, return the user to the app, or open the fallback interview URL. ## Compatibility | Surface | Minimum | Notes | | -------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------- | | React Native | `0.70` | SDK peer range. | | React | `18.1` | Match your React Native version's React peer range. | | React Native WebView | `13.16` | In-app interview host. | | Expo | Development build | Validate the release-like build users receive. | | iOS | App build with WebView and microphone permission | Use the deployment target compatible with your app. | | Android | App build with WebView microphone permissions | Include `RECORD_AUDIO` and `MODIFY_AUDIO_SETTINGS` through the native app config. | ## Package verification Before shipping a new SDK version, UserJourneys validates the package with: * TypeScript build and typecheck for the private core and React Native package. * Unit tests for trigger config, launch decisions, invite links, fallback, and WebView bridge handling. * Consumer compile against the packed public package. * Package verification that the published tarball does not depend on the private separate core package. ## Ready to release Treat the integration as ready when: * The configured analytics event shows the native prompt before opening. * Accepting the prompt opens the in-app WebView or browser fallback. * Dismissing the prompt does not open an interview. * Unrelated analytics events do not open interviews. * Synced audience membership shows the native prompt for the matching `referenceId`. * HTTPS invite links open in-app when supported and web when unsupported. * Microphone permission, leave, completion, and fallback states behave correctly. * UserJourneys records the correct user identity, launch source, and outcome. # Support & Contact Source: https://docs.userjourneys.ai/support How to reach userjourneys.ai — product help, security disclosures, privacy requests, and abuse reports — with response targets and what to include. Pick the channel that matches what you need. We respond within the targets below, and we take every message seriously. ## Contact channels | What you need | Where to send it | Target response | | ---------------------------------------------- | ----------------------------------------------------------- | --------------- | | Product help, bug reports, general questions | [support@userjourneys.ai](mailto:support@userjourneys.ai) | 2 business days | | Security vulnerabilities and incidents | [security@userjourneys.ai](mailto:security@userjourneys.ai) | Within 24 hours | | Privacy and data subject requests (GDPR, CCPA) | [privacy@userjourneys.ai](mailto:privacy@userjourneys.ai) | Within 30 days | | Abuse or acceptable use violations | [abuse@userjourneys.ai](mailto:abuse@userjourneys.ai) | 2 business days | | Sales, demos, and trials | [Book a demo](https://cal.com/userjourneys/demo) | — | These response targets apply to standard users. Customers with an executed Master Service Agreement or enterprise SLA may have different commitments as defined in their contract; where a signed agreement conflicts with the targets above, the signed agreement governs. ## What you can report Use the channels above to let us know about: * Bugs and unexpected behavior * Service failures or degraded performance * Security vulnerabilities or suspicious activity * Privacy concerns and data subject requests * Feature requests and product feedback * Complaints about our service or personnel * Requests for information about how our platform works ## Business hours We handle routine inquiries **Monday through Friday, 9am–6pm Pacific Time**, excluding US public holidays. Security reports are triaged outside business hours when appropriate; other inquiries received outside business hours are picked up the next business day. ## Security vulnerability disclosure We welcome reports from security researchers and customers who identify vulnerabilities in our platform. ### How to report Send vulnerability reports to [security@userjourneys.ai](mailto:security@userjourneys.ai). Include: * A description of the issue and its potential impact * Steps to reproduce, ideally with a proof of concept * Affected URLs, endpoints, or components * Your assessment of severity, if you have one ### What to expect We acknowledge all security reports within 24 hours. We classify the finding by severity (critical, high, medium, or low) and open an internal tracking ticket. We remediate within the service-level agreements defined in our internal Vulnerability Management Policy, prioritized by severity. We follow up when a fix is deployed and coordinate any public disclosure with you. ### Safe harbor We will not pursue legal action against security researchers who: * Make a good-faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our services * Only interact with accounts they own or have explicit permission to access * Do not access, modify, exfiltrate, or delete data that is not their own * Avoid attacks that could harm the reliability or integrity of our services, such as denial-of-service, spam, or social engineering of our employees or customers * Report the vulnerability promptly and give us a reasonable time to respond before any public disclosure ## Privacy and data subject requests If you are a California resident, an EU or UK data subject, or otherwise entitled to make a data subject request under applicable privacy law, email [privacy@userjourneys.ai](mailto:privacy@userjourneys.ai) with: * The type of request (access, correction, deletion, portability, or opt-out) * The email address associated with your account * Any additional information needed to verify your identity We respond to verifiable requests within 30 days, or within any shorter period required by law. See our [Privacy Policy](https://userjourneys.ai/privacy) for the full description of your rights. ## Company and legal entity userjourneys.ai is a product of **KSP Studio, Inc.** **Mailing address** KSP Studio, Inc.
2261 Market Street STE 86809
San Francisco, CA 94114
United States For privacy-related postal correspondence, please include "Attn: Privacy" on the envelope. ## Related resources How we collect, use, and protect personal information. The agreement governing use of the userjourneys.ai platform. Guides for interviews, funnels, and integrations. Talk to the team about enterprise use cases.