> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userjourneys.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Track Interview 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"
}
```

<Warning>
  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.
</Warning>

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

<Tip>
  Cache this response for a few minutes. Study capacity doesn't change often, and caching avoids unnecessary API calls.
</Tip>

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");
  }
);
```

<Warning>
  Always verify the signature before processing the webhook. Use `crypto.timingSafeEqual` (Node.js) or `hmac.compare_digest` (Python) to prevent timing attacks.
</Warning>

### 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.
