Webhook להחלטות
מענה אוטומטי להתייעצויות באמצע שיחה.
איך עובדות התייעצויות
כשהסוכנת הקולית נתקלת באמצע שיחה במשהו שדורש את אישורכם, למשל מחיר מעל max_price_ils או שעה אחרת מזו שביקשתם, היא פותחת התייעצות: שאלה אחת, הקשר קצר ואפשרויות (approve, decline, ask_user). לכל שיחה יש לכל היותר התייעצות פתוחה אחת בכל רגע.
אפשר לענות בכמה דרכים, והתשובה הראשונה שמגיעה לפני המועד האחרון קובעת:
- הסוכן המחובר ב־MCP, עם הכלי
respond_to_consultation. - הקוד שלכם, עם
POST /v1/consultations/:id/response. - ה־webhook שלכם, אוטומטית, כמו שמתואר בעמוד הזה.
- אתם, ממסך השיחה באתר.
אם לא הגיעה תשובה בזמן, הסוכנת ממשיכה בלי להתחייב, וההחלטה מחכה לכם.
איך ה־webhook עובד
כשהסוכנת צריכה החלטה באמצע שיחה, הסוכן שלכם יכול לענות לבד, בלי שמישהו ילחץ.
- באמצע שיחה הסוכנת נתקלת במשהו שדורש את אישורכם, למשל מחיר מעל התקרה. היא אומרת לצד השני ״רגע, אני בודקת מול הלקוח״.
- OneLi שולחת מיד בקשת POST חתומה לכתובת שהגדרתם.
- השרת שלכם עונה בגוף התשובה:
approve,declineאוask_user. - הסוכנת ממשיכה את השיחה לפי התשובה. אם לא עניתם בזמן, היא ממשיכה בלי התחייבות ומחזירה את ההחלטה אליכם.
אפשר לענות גם מאוחר יותר, ממסך השיחה באתר או מהסוכן המחובר (Claude). התשובה הראשונה שמגיעה לפני המועד האחרון קובעת.
הפעלה
- בדף חיבור הסוכן, בכרטיס החלטות אוטומטיות, מדביקים כתובת https ציבורית ולוחצים הפעלה.
- מופיע סוד שמתחיל ב־
whsec_. הוא מוצג פעם אחת בלבד. שמרו אותו כמשתנה סביבה בשרת שלכם, למשלONELI_WEBHOOK_SECRET. - עדכון הכתובת יוצר סוד חדש, והסוד הקודם מפסיק לעבוד. ״ביטול״ מכבה את הערוץ.
אפשר גם להגדיר מהקוד, עם מפתח API:
curl -X PUT https://api.oneli.io/v1/decision-webhook \
-H "Authorization: Bearer $ONELI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-agent.example.com/oneli" }'
# → { "url": "https://your-agent.example.com/oneli", "secret": "whsec_…" } (the secret is shown once)BAD_WEBHOOK_URL). הפניות (redirect) לא נעקבות.מה נשלח אליכם
POST https://your-agent.example.com/oneli
content-type: application/json
x-oneli-timestamp: 1792586400
x-oneli-signature: sha256=5f1c…
user-agent: OneLi-Webhook/1
{
"type": "consultation.requested",
"consultation": {
"id": "con_3d48e1a72832d769",
"task_id": "task_44be8f8d43da4748",
"question": "האם לאשר החלפת שמן ב-450 ש״ח?",
"options": ["approve", "decline", "ask_user"],
"context_summary": "יש תור מחר ב-17:30. המחיר לא כולל מע״מ.",
"deadline_at": "2026-10-21T12:00:09.073Z"
},
"task": {
"goal": "לברר מחיר להחלפת שמן ולקבוע תור",
"to_number": "972501234567",
"constraints": { "max_price_ils": 300 }
}
}consultation.question: ההחלטה הנדרשת, במשפט אחד.consultation.context_summary: העובדות שהסוכנת אספה עד כה.consultation.options: התשובות המותרות בשאלה הזאת.consultation.deadline_at: עד מתי התשובה עוד משנה את השיחה.task: המטרה, המספר והמגבלות שהגדרתם בשיחה.
כותרות: x-oneli-timestamp (שניות יוניקס), x-oneli-signature ו־user-agent: OneLi-Webhook/1.
אימות החתימה
כל בקשה חתומה ב־HMAC-SHA256 עם הסוד שלכם, על המחרוזת {timestamp}.{raw body}, בפורמט sha256=<hex>. בדקו את החתימה על הבייטים המקוריים של הגוף, לפני שאתם מפענחים JSON, ודחו בקשות שהחותמת שלהן ישנה מ־5 דקות.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const SECRET = process.env.ONELI_WEBHOOK_SECRET; // whsec_… (shown once when you turn it on)
const app = express();
// Keep the raw body: the signature is over the exact bytes OneLi sent.
app.post("/oneli", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("x-oneli-timestamp") ?? "";
const sig = req.get("x-oneli-signature") ?? "";
const expected = "sha256=" + createHmac("sha256", SECRET).update(`${ts}.${req.body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300; // reject replays older than 5 minutes
const ok = fresh && sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.status(401).end();
const { consultation, task } = JSON.parse(req.body);
// Your rules (or your own AI) decide here. Answer fast: the person on the line is waiting.
const price = Number((consultation.question.match(/\d+/) ?? [])[0]);
const limit = task.constraints?.max_price_ils;
const decision = limit && price && price <= limit * 1.5 ? "approve" : "ask_user";
res.json({ decision, reason: decision === "approve" ? "בתוך הטווח" : "צריך את אישורך" });
});
app.listen(3000);import hmac, hashlib, json, os, time
from flask import Flask, request, abort, jsonify
SECRET = os.environ["ONELI_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/oneli")
def oneli():
ts = request.headers.get("x-oneli-timestamp", "")
sig = request.headers.get("x-oneli-signature", "")
body = request.get_data() # raw bytes
expected = "sha256=" + hmac.new(SECRET, f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
if abs(time.time() - int(ts or 0)) > 300 or not hmac.compare_digest(sig, expected):
abort(401)
data = json.loads(body)
return jsonify(decision="ask_user", reason="דורש אישור אישי")איך עונים
HTTP/1.1 200 OK
content-type: application/json
{ "decision": "approve", "reason": "מתאים, אפשר לקבוע" }approve: הסוכנת מאשרת וממשיכה, למשל קובעת את התור.decline: הסוכנת אומרת שזה לא מתאים כרגע, בלי לחשוף תקציב.ask_user: לא מתחייבים. הסוכנת אוספת פרטים, וההחלטה מחכה לכם באתר.reason(לא חובה, עד 500 תווים) נשמר ביומן השיחה. הסוכנת לא מקריאה אותו לצד השני.
כל תשובה אחרת, סטטוס שאינו 2xx או גוף שאינו JSON נחשבים כ״אין תשובה״.
זמנים ותקלות
- לכל התייעצות יש חלון של שניות ספורות, עד
deadline_at. הצד השני מחכה על הקו, אז ענו תוך שנייה או שתיים. - OneLi מחכה לכל היותר 10 שניות לתשובה, ופחות אם המועד האחרון קרוב (חצי שנייה לפניו).
- אין ניסיון חוזר. אם השרת לא ענה בזמן, הסוכנת ממשיכה בלי התחייבות, וההחלטה מופיעה אצלכם באתר.
- בכרטיס ״החלטות אוטומטיות״ מופיעים זמן המשלוח האחרון והסטטוס שלו, לבדיקת תקלות. אותו מידע ב־
GET /v1/decision-webhook(last_status, last_delivery_at).
בדיקה
- הפעילו את הערוץ עם כתובת ציבורית. לפיתוח מקומי אפשר להשתמש ב־ngrok או ב־Cloudflare Tunnel.
- פתחו שיחה עם ״מחיר מקסימלי לאישור״ נמוך (
max_price_ils), כדי שהסוכנת תצטרך להתייעץ. - בדף השיחה תראו את השאלה, את התשובה של השרת שלכם ומאיפה היא הגיעה. באירועים,
consultation.answeredיופיע עםanswered_via: "webhook".