Skip to main content
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
  • 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.
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.
{
  "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 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.
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 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.
// 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.
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

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