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
HTTPS endpoint to receive events.
Event types to subscribe to. Currently: ["interview.completed"].
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"]
}'
201 — Created
200 — Updated
{
"url" : "https://your-app.com/webhooks/userjourneys" ,
"events" : [ "interview.completed" ],
"signing_secret" : "whsec_5b69ff6a12af94c6e3901a061180ca73"
}
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.
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.
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.
Header Value Content-Typeapplication/jsonX-Webhook-Eventinterview.completedX-Webhook-SignatureHMAC-SHA256 signature of the request body
Payload
ISO 8601 timestamp of when the event was sent.
Interview session ID (UUID).
Experiment this interview belongs to.
Public interview URL for this experiment.
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.
{
"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
}
}
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.
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" );
});
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:
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" );
});