۱. منشی هوشمند چطور به سامانهٔ شما وصل میشود؟
منشی هوشمند نقطهفون تلفن مشتریان را جواب میدهد و با صدای طبیعی و همزمان صحبت میکند. هرجا برای پاسخ به اطلاعات لحظهای سامانهٔ شما نیاز داشته باشد (وضعیت سفارش، تیکت، رزرو، موجودی، نوبت خالی و…) یا بخواهد کاری ثبت کند (سفارش تلفنی، رزرو، لغو)، یک ابزار را صدا میزند: یک درخواست HTTPS امضاشده به نشانیای که شما تعیین کردهاید.
- در پنل، بخش «منشی هوشمند» ← «اتصال به سامانهها»، ابزار را با نام، کاربرد، ورودیها و نشانی وبهوک تعریف میکنید.
- وسط مکالمه، منشی بر اساس «کاربرد» تصمیم میگیرد ابزار را صدا بزند و ورودیها را از مشتری میپرسد.
- نقطهفون درخواست را با کلید امضای منشی امضا کرده و به سرور شما میفرستد.
- سرور شما در کمتر از چند ثانیه JSON برمیگرداند و منشی نتیجه را به زبان ساده برای مشتری میگوید.
درخواستها از سرورهای نقطهفون در ایران ارسال میشوند؛ لازم نیست سرور شما از خارج در دسترس باشد. اگر دسترسی را با IP محدود کردهاید، با پشتیبانی هماهنگ کنید.
۲. تعریف ابزار
| فیلد | توضیح |
|---|---|
name | نام فنی یکتا با حروف کوچک انگلیسی، عدد و _ (مثلاً order_status). در هدر X-Noghte-Tool و بدنه فرستاده میشود. |
| عنوان | نامی که در پنل و گزارشها میبینید. |
| کاربرد | مهمترین بخش: به زبان ساده بنویسید چه زمانی استفاده شود، چه ورودیهایی لازم است و چه برمیگرداند. منشی فقط از روی همین متن تصمیم میگیرد. |
| ورودیها | هر ورودی نام انگلیسی، نوع (string، number، integer، boolean، date بهصورت YYYY-MM-DD، array)، توضیح، الزامی بودن و در صورت نیاز فهرست مقادیر مجاز دارد. |
| روش | POST (پیشنهادی؛ بدنهٔ JSON) یا GET (ورودیها در query string). |
| هدرهای اختصاصی | مثلاً Authorization برای احراز هویت سامانهٔ شما. رمزنگاریشده نگهداری میشوند و در پنل نمایش داده نمیشوند. |
| پیش از اجرا تأیید بگیرد | برای ابزارهایی که چیزی ثبت، پرداخت یا لغو میکنند: منشی جزئیات را برای مشتری تکرار میکند و فقط با «بله»ی صریح ابزار را صدا میزند. |
| جملهٔ انتظار | جملهای که منشی هنگام صدا زدن ابزار میگوید («یک لحظه بررسی میکنم») تا سکوت پیش نیاید. |
| حداکثر زمان پاسخ | ۱ تا ۱۵ ثانیه (پیشفرض ۶). پس از آن منشی عذرخواهی کرده و تماس برگشتی پیشنهاد میدهد. |
۳. درخواستی که دریافت میکنید
POST /api/noghte/order-status HTTP/1.1
Host: shop.example.com
Content-Type: application/json
User-Agent: NoghtePhone-Agent/1.0
X-Noghte-Event: tool.call
X-Noghte-Tool: order_status
X-Noghte-Agent: 9b1f0c7e-1d0b-4a8e-9a43-2a5f0e7c1d11
X-Noghte-Conversation: 5e0b7c55-8f1e-4a5c-b3c6-0e3c9f1f2a77
X-Noghte-Caller: 09121234567
X-Noghte-Signature: t=1792138211,v1=3f6c0a…e91b
{
"event": "tool.call",
"tool": "order_status",
"arguments": { "order_number": "100245" },
"agent": { "id": "9b1f0c7e-1d0b-4a8e-9a43-2a5f0e7c1d11", "name": "سارا" },
"conversation": {
"id": "5e0b7c55-8f1e-4a5c-b3c6-0e3c9f1f2a77",
"channel": "phone",
"caller": "09121234567",
"collected": { "name": "مریم احمدی" }
},
"test": false,
"sent_at": "2026-10-16T09:30:11+00:00"
}| فیلد | توضیح |
|---|---|
arguments | ورودیهایی که منشی از مشتری گرفته؛ مطابق تعریف ابزار. |
conversation.id | شناسهٔ یکتای مکالمه؛ برای ربط دادن چند درخواست یک تماس و جلوگیری از ثبت تکراری (idempotency) استفاده کنید. |
conversation.caller | شمارهٔ تماسگیرنده (۰۹…) — اغلب برای یافتن مشتری کافی است. |
conversation.collected | اطلاعاتی که منشی تا این لحظه از مشتری ثبت کرده (نام، شهر و…). |
test | در دکمهٔ «آزمایش» پنل true است؛ در این حالت چیزی را واقعاً ثبت نکنید. |
در روش GET بدنهای فرستاده نمیشود: ورودیها به query string نشانی شما اضافه میشوند (?order_number=100245) و شناسهٔ مکالمه و شمارهٔ تماسگیرنده در هدرهای X-Noghte-Conversation و X-Noghte-Caller میآید.
۴. بررسی امضا
هر درخواست هدر X-Noghte-Signature: t=…,v1=… دارد. t زمان ارسال (Unix) و v1 برابر HMAC-SHA256 (hex) رشتهٔ {t}.{payload} با کلید امضای منشی (whsec_…) است. payload در POST بدنهٔ خام درخواست و در GET کل query string است، دقیقاً همانطور که دریافت شده.
- کلید را در پنل ← منشی ← «برنامهنویسان» ببینید یا عوض کنید.
- درخواستهای با اختلاف زمانی بیش از ۵ دقیقه را رد کنید تا جلوی تکرار درخواست گرفته شود.
- از مقایسهٔ زمانثابت (hash_equals / timingSafeEqual / compare_digest) استفاده کنید.
<?php
// Laravel: Route::post('/api/noghte/order-status', OrderStatusController::class);
public function __invoke(Request $request)
{
$secret = config('services.noghte.webhook_secret'); // whsec_…
$raw = $request->isMethod('GET') ? $request->server('QUERY_STRING') : $request->getContent();
parse_str(str_replace(',', '&', (string) $request->header('X-Noghte-Signature')), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '').'.'.$raw, $secret);
abort_unless(hash_equals($expected, $sig['v1'] ?? '') && abs(time() - (int) ($sig['t'] ?? 0)) < 300, 401);
$order = Order::where('number', $request->input('arguments.order_number'))->first();
if (! $order) {
return ['result' => 'سفارشی با این شماره پیدا نشد.'];
}
return ['result' => ['status' => $order->status_label, 'eta' => $order->eta?->format('Y-m-d')]];
}// Express — the raw body is needed for the signature
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/api/noghte/order-status", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Noghte-Signature") ?? "";
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", process.env.NOGHTE_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!fresh || !v1 || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return res.sendStatus(401);
const { arguments: args } = JSON.parse(req.body);
res.json({ result: { status: "ارسال شده", eta: "فردا" } });
});# FastAPI
import hashlib, hmac, os, time
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
SECRET = os.environ["NOGHTE_WEBHOOK_SECRET"].encode()
@app.post("/api/noghte/order-status")
async def order_status(request: Request, x_noghte_signature: str = Header("")):
raw = await request.body()
sig = dict(part.split("=", 1) for part in x_noghte_signature.split(",") if "=" in part)
expected = hmac.new(SECRET, f"{sig.get('t', '')}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig.get("v1", "")) or abs(time.time() - int(sig.get("t", 0))) > 300:
raise HTTPException(401)
args = (await request.json())["arguments"]
return {"result": {"status": "ارسال شده", "eta": "فردا"}}func verify(r *http.Request, body []byte, secret string) bool {
parts := map[string]string{}
for _, p := range strings.Split(r.Header.Get("X-Noghte-Signature"), ",") {
if k, v, ok := strings.Cut(p, "="); ok {
parts[k] = v
}
}
t, _ := strconv.ParseInt(parts["t"], 10, 64)
if math.Abs(float64(time.Now().Unix()-t)) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(parts["t"] + "."))
mac.Write(body) // for GET: []byte(r.URL.RawQuery)
return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(parts["v1"]))
}۵. پاسخ شما
با کد ۲۰۰ و JSON پاسخ دهید. سادهترین شکل فقط کلید result دارد؛ منشی آن را میخواند و به زبان خودش برای مشتری بیان میکند:
HTTP/1.1 200 OK
Content-Type: application/json
{
"result": {
"status": "ارسال شده",
"carrier": "پست پیشتاز",
"tracking_code": "2401234567",
"eta": "پنجشنبه"
}
}کلیدهای اختیاری برای کنترل بیشتر:
{
"result": { "balance": 0 },
"say": "حساب شما تسویه است و بدهی ندارید.",
"end_call": false,
"transfer_to": "accounting"
}| کلید | اثر |
|---|---|
result | هر مقدار JSON (متن، شیء یا آرایه). اگر نباشد، کل پاسخ (بهجز کلیدهای زیر) به منشی داده میشود. |
say | جملهای که منشی عیناً و بلافاصله به مشتری میگوید (مثلاً کد پیگیری). |
end_call | true: منشی پس از جملهٔ آخر تماس را مؤدبانه پایان میدهد. |
transfer_to | کلید یکی از بخشهای تعریفشده در «انتقال» (مثلاً sales)؛ پس از جملهٔ آخر منشی، تماس به آن بخش وصل میشود. کلید نامعتبر نادیده گرفته میشود. |
خطا: اگر کاری ممکن نیست (سفارش پیدا نشد)، بهتر است با ۲۰۰ و توضیح در result پاسخ دهید تا منشی همان را بگوید. پاسخهای ۴xx/۵xx با {"error": "…"} هم پذیرفته میشوند؛ منشی عذرخواهی کرده و راه دیگری (تماس برگشتی) پیشنهاد میدهد.
مکالمه زنده است: هرچه سریعتر پاسخ دهید، مکالمه طبیعیتر است. پاسخ زیر ۱ ثانیه عالی و تا ۳ ثانیه قابل قبول است. نتیجه را کوتاه نگه دارید (ترجیحاً زیر ۲ کیلوبایت) و فقط آنچه مشتری لازم دارد برگردانید.
۶. رویداد پایان مکالمه
اگر در «برنامهنویسان» نشانی دریافت رویداد تعیین کنید، پس از هر تماس یک POST امضاشده (با همان روش امضا) با متن کامل مکالمه، اطلاعات ثبتشده و نتیجه دریافت میکنید. همین اطلاعات با رویداد ai_agent.conversation_ended به وبهوکهای عمومی بخش «اتصال و API» هم فرستاده میشود.
POST /api/noghte/events
X-Noghte-Event: conversation.ended
X-Noghte-Signature: t=1792138460,v1=…
{
"event": "conversation.ended",
"agent": { "id": "9b1f0c7e-…", "name": "سارا" },
"conversation": {
"id": "5e0b7c55-…",
"caller": "09121234567",
"started_at": "2026-10-16T09:29:40+00:00",
"ended_at": "2026-10-16T09:31:12+00:00",
"seconds": 92,
"outcome": "transfer",
"transfer": { "department": "sales", "label": "فروش", "reason": "خرید عمده" },
"collected": { "name": "مریم احمدی", "city": "شیراز" },
"transcript": [
{ "role": "agent", "text": "سلام، فروشگاه نمونه، سارا هستم. بفرمایید." },
{ "role": "caller", "text": "سلام، برای خرید عمده تماس گرفتم." }
],
"tool_calls": [{ "name": "save_caller_info", "arguments": { "name": "مریم احمدی" }, "ok": true }]
}
}| outcome | معنی |
|---|---|
completed | مکالمه عادی تمام شد. |
transfer | تماس به یکی از بخشها منتقل شد (جزئیات در transfer). |
caller_hangup | تماسگیرنده قطع کرد. |
no_response | تماسگیرنده صحبتی نکرد. |
time_limit | به سقف مدت مکالمه رسید. |
error | خطای فنی. |
۷. تواناییهای داخلی (بدون نیاز به وبسرویس)
- شناسایی تماسگیرنده در CRM نقطهفون و سلام با نام او
- ثبت نام و اطلاعات مشتری در CRM (همان «اطلاعاتی که از تماسگیرنده گرفته میشود»)
- جستجو در منابع دانش (وبسایت، PDF، متن و پرسشهای متداول شما)
- اعلام باز یا بسته بودن بر اساس ساعات کاری و تعطیلات رسمی
- انتقال تماس به داخلی، گروه زنگ، صف کالسنتر یا شمارهٔ بیرونی
- ثبت درخواست تماس برگشتی و گرفتن پیام برای همکاران (با اعلان)
- پایان مؤدبانهٔ تماس
۸. نمونهها بر اساس صنف
order_statusمشتری وضعیت سفارشش را میپرسد. شمارهٔ سفارش یا موبایل سفارشدهنده را بگیرید.
{ "order_number": "100245" }{ "result": { "status": "تحویل پست شده", "tracking_code": "2401234567", "eta": "پنجشنبه" } }💡 اگر سفارش با شمارهٔ تماسگیرنده ثبت شده، از conversation.caller هم برای جستجو استفاده کنید تا لازم نباشد مشتری شماره سفارش را بخواند.
ticket_statusمشتری دربارهٔ درخواست قبلیاش میپرسد.
{ "ticket_id": "T-5521" }{ "result": { "status": "در حال بررسی توسط واحد فنی", "updated": "امروز ساعت ۱۰", "next_step": "تا پایان روز تماس میگیریم" } }check_reservationمهمان با کد ووچر یا نام سرپرست، رزروش را بررسی میکند.
{ "voucher": "HT-88412", "guest_name": "رضایی" }{ "result": { "found": true, "room": "دوتخته رو به دریا", "check_in": "2026-11-02", "nights": 3, "breakfast": true, "paid": true } }check_availability → create_reservationابتدا اتاق خالی و قیمت استعلام میشود؛ پس از تأیید صریح مشتری، رزرو ثبت میشود (گزینهٔ «پیش از اجرا تأیید بگیرد» را روشن کنید).
{ "check_in": "2026-11-02", "check_out": "2026-11-05", "adults": 2 }{ "result": [{ "room_id": "DBL-SEA", "title": "دوتخته رو به دریا", "price_per_night": 4200000 }, { "room_id": "SUITE", "title": "سوئیت", "price_per_night": 7900000 }] }💡 برای ثبت رزرو، پاسخ را با کد پیگیری برگردانید؛ منشی آن را برای مشتری تکرار میکند. لینک پرداخت را با پیامک از سامانهٔ خودتان بفرستید.
place_orderمشتری غذا سفارش میدهد؛ منشی اقلام، تعداد، نشانی و روش پرداخت را میپرسد و پیش از ثبت، سفارش را بلند تکرار میکند.
{ "items": ["پیتزا مخصوص", "نوشابه"], "address": "شیراز، معالیآباد، کوچه ۱۲", "payment": "cash_on_delivery" }{ "result": { "order_id": "R-3391", "total": 680000, "ready_in_minutes": 40 }, "say": "سفارش شما با کد ۳۳۹۱ ثبت شد و حدود چهل دقیقهٔ دیگر میرسد." }search_flights → book_flightمبدأ، مقصد، تاریخ و تعداد مسافر پرسیده و نتایج خوانده میشود؛ رزرو قطعی با تأیید مشتری.
{ "origin": "THR", "destination": "MHD", "date": "2026-11-10", "adults": 2 }{ "result": [{ "flight": "IR 452", "departs": "07:30", "price": 3150000, "seats": 4 }] }💡 فقط ۳ تا ۵ گزینهٔ مناسب برگردانید؛ خواندن فهرست طولانی پشت تلفن برای مشتری خستهکننده است.
available_slots → book_appointmentنوبتهای خالی برای خدمت یا پزشک موردنظر اعلام و پس از انتخاب مشتری ثبت میشود.
{ "service": "جرمگیری", "date": "2026-10-20" }{ "result": { "slots": ["10:00", "11:30", "16:00"] } }برای بیشتر صنفها، هنگام ساخت منشی قالب آمادهٔ این ابزارها (با ورودیها و توضیحات) پیشنهاد میشود؛ فقط نشانی وبسرویس را وارد کنید.
۹. توصیهها
- یکتایی: برای ابزارهای ثبت (سفارش، رزرو) با
conversation.idو ورودیها از ثبت تکراری جلوگیری کنید؛ ممکن است مشتری درخواست را تکرار کند. - حریم خصوصی: فقط اطلاعات لازم را برگردانید. منشی هرگز شمارهٔ کارت، CVV2، رمز یا کد یکبارمصرف نمیپرسد؛ پرداخت را با لینک پیامکی از سامانهٔ خودتان انجام دهید.
- متن خوانا: مقادیر را همانطور که باید گفته شوند برگردانید (مثلاً «پنجشنبه ۲۴ مهر» بهجای timestamp). مبالغ را با واحد مشخص (تومان) بفرستید.
- آزمایش: با دکمهٔ «آزمایش» کنار هر ابزار، درخواست واقعی با
test: trueبفرستید و پاسخ را ببینید؛ سپس در «گفتوگوی آزمایشی» یا با شمارهٔ آزمایشی منشی، مکالمهٔ کامل را امتحان کنید. - HTTPS: نشانی وبسرویس باید HTTPS با گواهی معتبر باشد.
۱۰. محدودیتها
| مورد | مقدار |
|---|---|
| ابزار برای هر منشی | ۳۰ |
| ورودی برای هر ابزار | ۲۵ |
| هدر اختصاصی برای هر ابزار | ۱۰ |
| حداکثر زمان پاسخ | ۱۵ ثانیه |
| منبع دانش برای هر منشی | ۵۰ (هر فایل تا ۲۰ مگابایت؛ هر وبسایت تا ۶۰ صفحه) |