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.
| Step | Where | Who |
|---|---|---|
| 1. Pass the request ID | Your website | Your developer |
| 2. Add the ID to the handoff's webhook message | Console → your agent → Data handoffs | You |
| 3. Receive, check, update the request | Your server | Your 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
200even 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
| Cause | Fix |
|---|---|
| The page didn't pass it, or passed it after the call started | Call identify() before the visitor starts the call. Details are fixed when the call starts. |
| The names don't match | The 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 spoke | A conversation where the visitor said nothing sends no handoff. |