Integrations · Example

Pass an ID in, get it back with the answers

A customer has an unfinished request open on your website. They talk it through with your avatar, and the details land in their request, not in a new one.

The idea

Your website passes the ID of the customer's open request as a dynamic variable. The avatar never needs to see it. When the conversation ends, a data handoff sends what the avatar collected to your webhook, with that same ID, and your server updates the right request.

StepWhereWho
1. Pass the request IDYour websiteYour developer
2. Add the ID to the handoff's webhook messageConsole → your agent → Data handoffsYou
3. Receive, check, update the requestYour serverYour developer

1. Your website passes the ID

On the page where the customer's request is open, next to the widget snippet. Use an ID nobody could guess (not 1, 2, 3…), because a visitor can see and edit anything in their own browser.

<!-- The page where signed-in customer John has service request r_8Qm2xL7vT3 open -->
<script src="https://talk.selviaai.com/embed.js" data-embed-key="emb_…" async></script>
<script>
  window.SelviaAI = window.SelviaAI || { q: [], identify: function (v) { this.q.push(v); } };
  SelviaAI.identify({ name: "John", request_id: "r_8Qm2xL7vT3" });
</script>

You don't mention request_id in your Dynamic Instructions, so the avatar never sees or says it. It only travels back through the webhook.

2. The handoff sends it back

In the console, set up a data handoff for the agent that collects the details you need (here: vehicle, problem, preferred time) and sends them to your webhook in JSON. Tell the handoff assistant to include the ID, for example “…and include the request_id my website passes”, or add it to the webhook message yourself with {{visitor.request_id}}:

Webhook message

{
  "request_id": "{{visitor.request_id}}",
  "vehicle": "{{vehicle}}",
  "problem": "{{problem}}",
  "preferred_time": "{{preferred_time}}"
}

{{visitor.…}} is filled with what your website passed; the other values are what the avatar collected. The message arrives shortly after the call ends (usually within a minute), not during it.

3. Your server gets the ID back

What arrives

POST /selvia-webhook HTTP/1.1
Content-Type: application/json; charset=utf-8
X-Selvia-Timestamp: 1790025248
X-Selvia-Signature: sha256=5f0c…e91a
X-Selvia-Delivery: 5127

{
  "request_id": "r_8Qm2xL7vT3",
  "vehicle": "2019 Honda Civic",
  "problem": "Brakes squeal when stopping",
  "preferred_time": "Thursday morning"
}

Handle it

Check the signature, skip repeats, then update the request the ID points to:

Node.js (Express)

const crypto = require("crypto");
const express = require("express");
const app = express();

app.post("/selvia-webhook", express.raw({ type: "*/*" }), async (req, res) => {
  // 1. Check the request came from Selvia AI (Webhooks → Verify the signature).
  const ts = req.get("X-Selvia-Timestamp") || "";
  const sig = req.get("X-Selvia-Signature") || "";
  const body = req.body.toString("utf8");
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET)   // whsec_… from the console
    .update(ts + "." + body)
    .digest("hex");
  const fresh = /^\d+$/.test(ts) && Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  const valid = sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!fresh || !valid) return res.status(401).end();

  // 2. A delivery can arrive twice: skip one you've already handled.
  const delivery = req.get("X-Selvia-Delivery");
  if (await db.deliveries.exists(delivery)) return res.status(200).end();   // your own code
  await db.deliveries.add(delivery);

  // 3. Your ID came back: fill in that customer's request.
  const data = JSON.parse(body);
  const request = await db.requests.find(data.request_id);                 // your own code
  if (request && request.status === "draft") {
    await db.requests.update(data.request_id, {
      vehicle: data.vehicle,
      problem: data.problem,
      preferredTime: data.preferred_time,
    });
  } else {
    console.warn("No open draft for", data.request_id);  // log it, but still answer 200
  }
  res.status(200).end();
});

app.listen(3000);

Python (Flask)

import hashlib, hmac, json, os, time
from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"].encode()   # whsec_… from the console

@app.post("/selvia-webhook")
def selvia_webhook():
    # 1. Check the request came from Selvia AI (Webhooks → Verify the signature).
    ts = request.headers.get("X-Selvia-Timestamp", "")
    sig = request.headers.get("X-Selvia-Signature", "")
    body = request.get_data(as_text=True)
    expected = "sha256=" + hmac.new(SECRET, f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(sig, expected):
        abort(401)

    # 2. A delivery can arrive twice: skip one you've already handled.
    delivery = request.headers["X-Selvia-Delivery"]
    if db.delivery_seen(delivery):                   # your own code
        return "", 200
    db.remember_delivery(delivery)

    # 3. Your ID came back: fill in that customer's request.
    data = json.loads(body)
    draft = db.find_request(data["request_id"])      # your own code
    if draft and draft.status == "draft":
        db.update_request(data["request_id"],
                          vehicle=data["vehicle"],
                          problem=data["problem"],
                          preferred_time=data["preferred_time"])
    else:
        app.logger.warning("No open draft for %s", data["request_id"])  # still answer 200
    return "", 200
  • Answer 200 even when the request isn't found or is already submitted. Log it and move on. Any other 4xx marks your webhook as needing attention.
  • If your page shows the request, refresh it after the call (or poll for a minute) to show the new details.

If the ID comes back empty

CauseFix
The page didn't pass it, or passed it after the call startedCall identify() before the visitor starts the call. Details are fixed when the call starts.
The names don't matchThe name in {{visitor.request_id}} must be the one your page passes (upper/lower case doesn't matter).
The detail is marked "Only when signed"Unsigned, it counts as missing. Sign it on your server: see Signed values.
The visitor never spokeA conversation where the visitor said nothing sends no handoff.
Want the ID tamper-proof? Sign it on your server instead of passing it plainly, and mark it Only when signed. Then a visitor can't swap in someone else's request ID. See Signed values.