Skip to main content
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

Authorization
string
required
Bearer token. See Authentication.
url
string
required
HTTPS endpoint to receive events.
events
string[]
required
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"]
  }'
{
  "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.

Headers

HeaderValue
Content-Typeapplication/json
X-Webhook-Eventinterview.completed
X-Webhook-SignatureHMAC-SHA256 signature of the request body

Payload

event
string
"interview.completed"
timestamp
string
ISO 8601 timestamp of when the event was sent.
data
object
{
  "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:
AttemptDelay
1st retry10 seconds
2nd retry1 minute
3rd retry5 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.
Node.js
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
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");
});