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 opening the interview, to handling the webhook that tells you it’s done.
What you’ll build
- Check if your experiment is accepting interviews before showing the prompt
- Open the interview in a new tab when the user clicks
- Receive a webhook when the interview completes
- Mark the user in your database so the prompt doesn’t appear again
Prerequisites
- A userjourneys.ai account with an active experiment
- An API key
- A server that can receive HTTPS POST requests (for 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 experiment capacity
Before showing the prompt, verify that the experiment can accept interviews. This prevents showing a prompt that leads to a closed experiment.
const API_KEY = process.env.USERJOURNEYS_API_KEY;
async function getExperiment(name) {
const response = await fetch(
"https://app.userjourneys.ai/api/v1/experiments",
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const { experiments } = await response.json();
return experiments.find((e) => e.name === name);
}
const experiment = await getExperiment("Onboarding Feedback");
if (experiment?.accepting_interviews) {
// Show the prompt to the user
}
Cache this response for a few minutes. Interview capacity doesn’t change often, and caching avoids unnecessary API calls.
See Experiments 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 && experiment.accepting_interviews) {
showInterviewPrompt({
message: "We'd love your feedback — it takes about 10 minutes.",
// Append ?reference_id= so the webhook tells you who completed it
link: `${experiment.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 experiment 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
When a user finishes the interview, userjourneys.ai sends a POST request to your webhook URL. Verify the signature, then update your database.
import crypto from "node:crypto";
import express from "express";
const SIGNING_SECRET = process.env.USERJOURNEYS_WEBHOOK_SECRET;
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) {
await db.users.update({
where: { id: data.reference_id },
data: { interview_completed: true },
});
}
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": {
"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
}
}
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:
- User sees the prompt → clicks → interview opens in a new tab
- User completes the interview → userjourneys.ai sends a webhook
- Your server verifies the signature → marks the user as completed
- 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.