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 webhookConsole → 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({ client_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:

Webhook message

{
  "vehicle": "{{vehicle}}",
  "problem": "{{problem}}",
  "preferred_time": "{{preferred_time}}"
}

Then, on that webhook, click Add dynamic variable and type request_id, exactly as your page passes it. (Starting a new handoff, + Add dynamic variables on the empty card takes you straight there.) It's added to the message with what your website passed; the other values are what the avatar collected. The handoff assistant can't add it for you, because it can't see what your website sends. 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

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

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 you added with Add dynamic variable must be the one your page passes (upper/lower case doesn't matter).
The visitor never spokeA conversation where the visitor said nothing sends no handoff.
Want the ID tamper-proof? Put it in a token your server signs instead of passing it plainly, and turn on Require a verified visitor. A signed value always wins over an unsigned one, so a visitor can't swap in someone else's request ID. See Signed values.