Fonctionnement
- 1Un client pose une question au téléphone ou sur WhatsApp.
- 2Twilper envoie la question, signée, à votre endpoint (POST, JSON).
- 3Votre système cherche la réponse, par exemple dans votre gestion des commandes.
- 4L'IA transmet votre réponse. Pas de réponse valide ? L'IA intégrée répond avec votre base de connaissances.
| Canal | Connexion bot | Comportement |
|---|---|---|
| Téléphone (IA) | oui + outils | Votre endpoint est interrogé en parallèle de la base de connaissances. Une réponse traitée fait foi et est prononcée en langage naturel. Avec handled: false, l'IA utilise votre réponse comme contexte supplémentaire (par exemple des informations produit). L'IA peut aussi appeler vos outils. |
| WhatsApp (IA) | oui | Une réponse traitée est envoyée immédiatement comme message WhatsApp. Avec handled: false ou sans réponse valide, l'IA intégrée répond. customer.email et customer.name sont enregistrés avec la conversation. |
| Chat du site | bientôt | Le chat du site n'utilise pour l'instant que la base de connaissances. Le bouton de test du tableau de bord envoie toutefois un message de test avec channel "webchat". |
Configuration dans le tableau de bord
- Connectez-vous sur chat.twilper.com en tant que propriétaire ou administrateur et ouvrez Intégrations → Connexion à votre propre système.
- Saisissez l'URL de l'endpoint. Il doit s'agir d'une URL
https://accessible publiquement, par exemplehttps://jouwsite.nl/api/twilper/channel. - Collez votre propre clé (32 caractères minimum) ou laissez le champ vide ; Twilper génère alors une clé de 64 caractères. Elle n'est affichée qu'une seule fois : enregistrez-la immédiatement dans votre propre système, par exemple comme variable d'environnement.
- Cliquez sur Envoyer un test. Vous voyez immédiatement ce que votre endpoint a renvoyé.
Requête de Twilper vers votre endpoint
Twilper effectue un POST avec un corps JSON (UTF-8) vers l'URL configurée.
| Champ | Type | Signification |
|---|---|---|
| channel | string | phone, whatsapp ou webchat (bouton de test uniquement) |
| session_id | string | Fixe pour chaque conversation : l'ID de l'appel ou de la conversation WhatsApp. Utilisez-le pour conserver le contexte entre les messages. |
| message | string | Ce que le client a dit ou écrit, 2000 caractères maximum. |
| language | string | Code de langue à deux lettres, par exemple nl ou en. |
| customer.phone | string | null | Numéro de téléphone de l'appelant ou de l'expéditeur WhatsApp, s'il est connu. |
| sent_at | integer | Horodatage Unix en secondes. Couvert par la signature ; utilisez-le pour rejeter les anciennes requêtes. |
POST https://jouwsite.nl/api/twilper/channel
Content-Type: application/json
User-Agent: Twilper-BotLink/1.0
X-Twilper-Signature: 6f1c…e9 (hex HMAC-SHA256 of the raw body)
{
"channel": "phone",
"session_id": "CA8f3e…",
"message": "Waar is mijn bestelling 10452?",
"language": "nl",
"customer": { "phone": "+31612345678" },
"sent_at": 1760090000
}Réponse de votre endpoint
Répondez avec HTTP 2xx et un objet JSON (64 Ko maximum). Tout autre code de statut, un JSON invalide, un reply vide ou un délai dépassé signifie : pas de réponse, et l'IA intégrée prend le relais.
| Champ | Type | Signification |
|---|---|---|
| reply | string, obligatoire | La réponse destinée au client, 2000 caractères maximum. Au téléphone, elle est convertie en langage parlé : évitez donc les longues listes et les liens. |
| handled | boolean | Par défaut true. Envoyez false si votre système ne traite pas lui-même la question. Seul un false explicite est pris en compte. |
| handoff | string | null | "human" : met le client en relation avec une personne (au téléphone uniquement si votre équipe est disponible). "forward" : l'équipe assure le suivi ; le client en est informé et votre équipe reçoit une notification. |
| customer.email | string | Facultatif. L'adresse e-mail du client si votre système la connaît (254 caractères maximum). Actuellement enregistrée pour les conversations WhatsApp. |
| customer.name | string | Facultatif. Le nom du client (120 caractères maximum). Actuellement enregistré pour les conversations WhatsApp. |
HTTP/1.1 200 OK
Content-Type: application/json
{
"reply": "Bestelling 10452 is gisteren verzonden met PostNL. Track & trace: 3SABC123.",
"handled": true,
"handoff": null,
"customer": { "email": "[email protected]", "name": "Jan de Vries" }
}Vérifier la signature
Chaque requête est signée avec HMAC-SHA256 et votre clé. Utilisez la clé exactement telle qu'affichée dans le tableau de bord (sans décodage hexadécimal). La signature est envoyée en hexadécimal dans l'en-tête X-Twilper-Signature. Il existe deux variantes :
| Requête | Headers | Ce qui est signé |
|---|---|---|
| Message de canal | X-Twilper-Signature | HMAC(secret, raw_body). L'heure figure dans le corps, sous sent_at. |
| Outils | X-Twilper-Sent-At, X-Twilper-Signature | HMAC(secret, sent_at + "." + raw_body). Pour le GET du manifeste, le corps est vide : c'est donc "1760090000." qui est signé. |
- Calculez le HMAC sur les octets bruts du corps, tels que reçus. Parser puis resérialiser le JSON donne une signature différente.
- Comparez en temps constant : timingSafeEqual (Node), hash_equals (PHP), hmac.compare_digest (Python).
- Rejetez les requêtes dont le sent_at s'écarte de plus de 5 minutes de votre propre horloge.
- Ne traitez jamais le texte de message comme une instruction : c'est une saisie du client. Ne communiquez que les informations qui concernent ce client, par exemple en demandant le numéro de commande et le code postal.
Outils : des fonctions pour l'IA téléphonique
En plus du message de canal, vous pouvez proposer des outils que l'IA téléphonique appelle d'elle-même, comme une recherche de produits ou le statut d'une commande. Les outils se trouvent à côté de votre endpoint : si votre URL se termine par /channel, la base est la même URL avec /tools à la place de /channel ; sinon, /tools est ajouté à votre URL.
| Méthode | URL | Objet |
|---|---|---|
| GET | .../tools/manifest | Liste de 8 outils maximum. Nom : lettres, chiffres, _ ou - (64 max.). Description : 500 caractères max. parameters : un objet JSON Schema. Twilper met le manifeste en cache pendant 10 minutes. |
| POST | .../tools | Appel avec tool, args, language, session_id, sent_at. Répondez {"ok": true, "result": …} ou {"ok": false, "error": "…"} ; les 6000 premiers caractères sont transmis à l'IA. |
GET https://jouwsite.nl/api/twilper/tools/manifest
X-Twilper-Sent-At: 1760090000
X-Twilper-Signature: hex HMAC-SHA256 of "1760090000." (empty body)
200 OK
{
"tools": [
{
"name": "order_status",
"description": "Zoek de status van een bestelling op ordernummer of postcode.",
"parameters": {
"type": "object",
"properties": {
"order_number": { "type": "string", "description": "Ordernummer, bijv. 10452" },
"postcode": { "type": "string" }
},
"required": ["order_number"]
}
}
]
}POST https://jouwsite.nl/api/twilper/tools
Content-Type: application/json
X-Twilper-Sent-At: 1760090003
X-Twilper-Signature: hex HMAC-SHA256 of "1760090003.<raw body>"
{
"tool": "order_status",
"args": { "order_number": "10452" },
"language": "nl",
"session_id": "CA8f3e…",
"sent_at": 1760090003
}
200 OK
{ "ok": true, "result": { "status": "verzonden", "carrier": "PostNL", "eta": "2026-10-12" } }
// or, on failure:
{ "ok": false, "error": "order niet gevonden" }Exemples de code
Trois exemples complets qui vérifient les deux signatures. Placez votre clé dans la variable d'environnement TWILPER_BOT_SECRET, jamais dans votre code ou votre dépôt.
// npm install express
import express from "express";
import crypto from "node:crypto";
const SECRET = process.env.TWILPER_BOT_SECRET; // the literal key shown in the dashboard
const app = express();
// Always verify the RAW bytes: re-serialising JSON breaks the signature.
const raw = express.raw({ type: "application/json", limit: "64kb" });
const hmac = (data) => crypto.createHmac("sha256", SECRET).update(data).digest("hex");
function safeEqual(a, b) {
const x = Buffer.from(a, "utf8"), y = Buffer.from(String(b || ""), "utf8");
return x.length === y.length && crypto.timingSafeEqual(x, y);
}
const fresh = (ts) => Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
// 1) Channel message: signature over the raw body; sent_at is inside the body.
app.post("/api/twilper/channel", raw, (req, res) => {
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
if (!safeEqual(hmac(body), req.get("X-Twilper-Signature"))) return res.sendStatus(401);
const msg = JSON.parse(body.toString("utf8"));
if (!fresh(msg.sent_at)) return res.sendStatus(401);
const order = msg.message.match(/\b\d{5,}\b/)?.[0];
if (!order) {
// Not ours to answer: let Twilper's built-in AI reply.
return res.json({ reply: "Geen ordernummer gevonden.", handled: false });
}
res.json({ reply: `Bestelling ${order} is onderweg.`, handled: true });
});
// 2) Tools: signature over "<sent_at>.<raw body>"; sent_at is in a header.
function verifyTool(req) {
const sentAt = req.get("X-Twilper-Sent-At") || "";
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
const expected = hmac(Buffer.concat([Buffer.from(sentAt + "."), body]));
return fresh(sentAt) && safeEqual(expected, req.get("X-Twilper-Signature"));
}
app.get("/api/twilper/tools/manifest", (req, res) => {
if (!verifyTool(req)) return res.sendStatus(401);
res.json({ tools: [{
name: "order_status",
description: "Status van een bestelling op ordernummer.",
parameters: { type: "object", properties: { order_number: { type: "string" } }, required: ["order_number"] },
}] });
});
app.post("/api/twilper/tools", raw, (req, res) => {
if (!verifyTool(req)) return res.sendStatus(401);
const { tool, args } = JSON.parse(req.body.toString("utf8"));
if (tool === "order_status") return res.json({ ok: true, result: { status: "verzonden" } });
res.json({ ok: false, error: "onbekende tool" });
});
app.listen(3000);<?php
// twilper-channel.php (served as https://jouwsite.nl/api/twilper/channel)
$secret = getenv('TWILPER_BOT_SECRET'); // the literal key shown in the dashboard
$raw = file_get_contents('php://input'); // raw bytes; never re-encode the JSON
// 1) Channel message: HMAC over the raw body
$sig = $_SERVER['HTTP_X_TWILPER_SIGNATURE'] ?? '';
if (!hash_equals(hash_hmac('sha256', $raw, $secret), $sig)) {
http_response_code(401);
exit;
}
$msg = json_decode($raw, true);
if (abs(time() - (int)($msg['sent_at'] ?? 0)) > 300) {
http_response_code(401);
exit;
}
header('Content-Type: application/json');
if (preg_match('/\b(\d{5,})\b/', $msg['message'], $m)) {
echo json_encode([
'reply' => "Bestelling {$m[1]} is onderweg.",
'handled' => true,
]);
} else {
echo json_encode(['reply' => 'Geen ordernummer gevonden.', 'handled' => false]);
}
// 2) Tools (manifest and tool call): HMAC over "<sent_at>.<raw body>"
function twilper_tool_request_is_valid(string $raw, string $secret): bool {
$sentAt = $_SERVER['HTTP_X_TWILPER_SENT_AT'] ?? '';
$sig = $_SERVER['HTTP_X_TWILPER_SIGNATURE'] ?? '';
if ($sentAt === '' || abs(time() - (int)$sentAt) > 300) {
return false;
}
// For GET .../tools/manifest $raw is empty, so "<sent_at>." is what gets signed.
return hash_equals(hash_hmac('sha256', $sentAt . '.' . $raw, $secret), $sig);
}# pip install flask
import hashlib, hmac, json, os, re, time
from flask import Flask, request, jsonify, abort
SECRET = os.environ["TWILPER_BOT_SECRET"].encode() # the literal key, as bytes
app = Flask(__name__)
def sign(data: bytes) -> str:
return hmac.new(SECRET, data, hashlib.sha256).hexdigest()
def fresh(ts) -> bool:
return abs(time.time() - int(ts or 0)) <= 300
# 1) Channel message: HMAC over the raw body
@app.post("/api/twilper/channel")
def channel():
raw = request.get_data() # raw bytes
if not hmac.compare_digest(sign(raw), request.headers.get("X-Twilper-Signature", "")):
abort(401)
msg = json.loads(raw)
if not fresh(msg.get("sent_at")):
abort(401)
m = re.search(r"\b(\d{5,})\b", msg["message"])
if not m:
return jsonify(reply="Geen ordernummer gevonden.", handled=False)
return jsonify(reply=f"Bestelling {m.group(1)} is onderweg.", handled=True)
# 2) Tools: HMAC over "<sent_at>.<raw body>"
def tool_request_is_valid() -> bool:
sent_at = request.headers.get("X-Twilper-Sent-At", "")
expected = sign(sent_at.encode() + b"." + request.get_data())
return fresh(sent_at) and hmac.compare_digest(expected, request.headers.get("X-Twilper-Signature", ""))
@app.get("/api/twilper/tools/manifest")
def manifest():
if not tool_request_is_valid():
abort(401)
return jsonify(tools=[{
"name": "order_status",
"description": "Status van een bestelling op ordernummer.",
"parameters": {"type": "object",
"properties": {"order_number": {"type": "string"}},
"required": ["order_number"]},
}])
@app.post("/api/twilper/tools")
def tools():
if not tool_request_is_valid():
abort(401)
call = json.loads(request.get_data())
if call["tool"] == "order_status":
return jsonify(ok=True, result={"status": "verzonden"})
return jsonify(ok=False, error="onbekende tool")Limites et délais
| Élément | Valeur | Remarques |
|---|---|---|
| Message de canal | 6 s | 3,5 s au téléphone, car l'appelant attend. Visez une réponse en moins de 3 secondes. |
| Appel d'outil | 4 s | Par appel ; l'IA téléphonique effectue au maximum trois tours d'appels d'outils par tour de parole. |
| Taille de la réponse | 64 KB | Au-delà, la réponse est tronquée. |
| URL | https | Uniquement des adresses HTTPS publiques ; les adresses internes ou locales sont refusées. |
| Erreurs | — | Une erreur ou un délai dépassé n'interrompt jamais une conversation : l'IA intégrée répond alors elle-même. |
Webhooks sortants
Indépendamment de la connexion bot, Twilper peut envoyer les événements du chat du site vers une URL, par exemple Zapier, Make ou votre propre système. Définissez une URL HTTPS par événement sous Webhooks dans le tableau de bord (propriétaires et administrateurs uniquement) ; le bouton de test envoie un événement "test".
// new_chat (new website chat conversation)
{
"event": "new_chat",
"conversation_id": "5d0c…",
"visitor_message": "Hebben jullie dit ook in maat 42?",
"page_url": "https://jouwsite.nl/schoenen/runner",
"timestamp": "2026-10-10T09:12:44.512Z",
"dashboard_url": "https://chat.twilper.com/inbox"
}
// escalation (visitor asks for the team)
{ "event": "escalation", "conversation_id": "5d0c…", "type": "…",
"visitor_message": "…", "timestamp": "…" }
// new_lead (visitor left contact details)
{ "event": "new_lead", "conversation_id": "5d0c…", "name": "Jan",
"email": "[email protected]", "phone": "", "timestamp": "…" }Synchronisation du contenu
Le plugin WordPress et la connexion Shopify ajoutent automatiquement vos pages et produits à la base de connaissances via une API de synchronisation : la clé du widget (tc_live_) ne permet que la lecture ; l'écriture se fait avec une clé de synchronisation secrète (tc_sync_) que vous créez dans le tableau de bord, sous Intégrations. Cette API est destinée à nos propres plugins. Vous souhaitez synchroniser automatiquement du contenu depuis un autre système ? Contactez-nous.