همهٔ صفحه‌های راهنما

API گفت‌وگو (آزمایشی)

سیستم خودتان، مثل اپ یا سایت یا پنل فروش، با کلید API گفت‌وگو شروع می‌کند، پیام مشتری را می‌فرستد و جواب دستیار و همکاران را می‌گیرد.

این API آزمایشی است. ممکن است شکل درخواست‌ها و جواب‌ها عوض شود و هر تغییری همین‌جا نوشته می‌شود. هدر Goya24-Api-Status در هر جوابش همین را می‌گوید، با مقدار experimental.

پیام‌رسان برای سایت است. API برای وقتی است که صفحهٔ گفت‌وگو را خودتان ساخته‌اید، مثلاً بخش پشتیبانی اپ موبایل یا پنل مشتری‌ها، و می‌خواهید همان دستیار و همان تیم جواب بدهند. گفت‌وگوها در صندوق ورودی با برچسب «API» دیده می‌شوند و همکارانتان مثل بقیهٔ گفت‌وگوها جوابشان را می‌دهند.

کلید

  1. 1در «تنظیمات → API» یک کلید بسازید و اسمی بگذارید که بدانید مال کدام سیستم است. فقط مالک و ادمین می‌توانند.
  2. 2کلید فقط همان یک بار نشان داده می‌شود. همان لحظه کپی‌اش کنید و در سرورتان نگه دارید.
  3. 3هر درخواست را با هدر Authorization بفرستید. مقدارش کلمهٔ Bearer است، یک فاصله، و بعد کلید.

کلید فقط مال سرور است. آن را در کد مرورگر یا داخل اپ موبایل نگذارید، چون هر کسی آن را داشته باشد گفت‌وگوهای API شما را می‌خواند و از طرف مشتری‌ها پیام می‌فرستد. اگر لو رفت، همان‌جا «ابطال» را بزنید و کلید تازه بسازید.

شروع گفت‌وگو

مشتری را با شناسهٔ خود سیستمتان معرفی کنید، همان شناسه‌ای که سایتتان برای شناساندن کاربر لاگین‌شده به پیام‌رسان می‌دهد. یک شناسه همیشه یک مشتری است، و مشتری‌ای که در سایت لاگین کرده و از اپ هم پیام می‌دهد یک نفر حساب می‌شود.

curl https://goya24.com/api/v1/conversations \
  -H "Authorization: Bearer g24_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-10247-1" \
  -d '{"contact": {"id": "u_1024", "name": "سارا", "phone": "09123456789"},
       "text": "سفارش ۱۰۲۴۷ کی می‌رسه؟"}'

جواب ۲۰۱ است، با گفت‌وگو، پیام مشتری و جواب دستیار:

{
  "conversation": {
    "id": "6f1c0b9e-…",
    "state": "ai",
    "channel": "api",
    "subject": null,
    "createdAt": "2026-11-04T09:12:03.418+00:00",
    "lastMessageAt": "2026-11-04T09:12:04.902+00:00",
    "contact": { "id": "u_1024", "name": "سارا", "email": null, "phone": "+989123456789" }
  },
  "message": { "id": "…", "author": "customer", "authorName": null, "text": "سفارش ۱۰۲۴۷ کی می‌رسه؟", "files": [], "createdAt": "…" },
  "reply":   { "id": "…", "author": "assistant", "authorName": "آوا", "text": "…", "files": [], "createdAt": "…" }
}
  • contact.id لازم است. name، email و phone اختیاری‌اند و اگر بفرستید جای اطلاعات قبلی مشتری می‌نشینند. موبایل به یک شکل ذخیره می‌شود (+989123456789) و شماره‌ای که خوانده نشود خطای invalid_phone می‌گیرد.
  • دستیار این مشتری را «تأییدشده» می‌شناسد، مثل کاربری که سایت با امضا معرفی کرده. پس ابزارهایی مثل پیگیری سفارش شناسه‌اش را می‌گیرند.
  • text اختیاری است. بدون آن گفت‌وگو باز می‌شود و هنوز جوابی داده نمی‌شود. حداکثر ۸۰۰۰ نویسه.
  • agentId اختیاری است. بدون آن اولین دستیار فعال فضای کاری جواب می‌دهد.
  • state یکی از این‌هاست: ai یعنی دستیار جواب می‌دهد، escalated یعنی به تیم سپرده شده و هنوز کسی برش نداشته، assigned یعنی همکاری برداشته، closed یعنی بسته شده.

پیام بعدی مشتری

curl https://goya24.com/api/v1/conversations/6f1c0b9e-…/messages \
  -H "Authorization: Bearer g24_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-10247-2" \
  -d '{"text": "ارسال به شیراز هم دارید؟"}'
  • جواب ۲۰۱ است با conversation، message و reply.
  • وقتی گفت‌وگو دست تیم است، reply خالی (null) است. جواب همکار با وب‌هوک می‌رسد، یا وقتی گفت‌وگو را دوباره بخوانید.
  • اگر گفت‌وگو بسته شده باشد، یا دوباره باز می‌شود یا گفت‌وگوی تازه‌ای شروع می‌شود. همیشه conversation.id جواب را نگه دارید، نه شناسه‌ای که فرستاده بودید.
  • پیام فقط به گفت‌وگوهایی می‌رود که با API شروع شده‌اند.

جواب همان‌طور که نوشته می‌شود

اگر در بدنه stream را true بگذارید، یا هدر Accept را با مقدار text/event-stream بفرستید، جواب دستیار تکه‌تکه و همان‌طور که نوشته می‌شود می‌رسد (server-sent events). وضعیت این جواب ۲۰۰ است، نه ۲۰۱. آخرین تکه done است و همان بدنه‌ای را دارد که جواب یک‌جا داشت. اگر وسط کار خطایی پیش بیاید، تکهٔ error می‌آید و جریان تمام می‌شود.

event: delta
data: {"text": "سفارش شما "}

event: delta
data: {"text": "امروز از انبار خارج شد."}

event: done
data: {"conversation": {…}, "message": {…}, "reply": {…}}

خواندن گفت‌وگو و فهرست

# یک گفت‌وگو با همهٔ پیام‌هایش، از قدیمی به جدید
curl https://goya24.com/api/v1/conversations/6f1c0b9e-… \
  -H "Authorization: Bearer g24_..."

# فهرست گفت‌وگوهای API، از جدید به قدیمی، ۲۰ تا در هر صفحه
curl "https://goya24.com/api/v1/conversations?limit=20&contact=u_1024" \
  -H "Authorization: Bearer g24_..."

# صفحهٔ بعد: nextCursor جواب قبلی
curl "https://goya24.com/api/v1/conversations?limit=20&before=2026-11-04T09:12:03.418%2B00:00" \
  -H "Authorization: Bearer g24_..."
  • نویسندهٔ هر پیام یکی از این‌هاست: customer، assistant یا teammate. authorName اسم دستیار یا همکار است.
  • یادداشت داخلی همکاران و پیام‌های سیستمی هیچ‌وقت در جواب نیستند. پیامی که همکار حذف کرده با متن خالی می‌آید.
  • فایلی که همکار فرستاده در files می‌آید، با اسم و لینکی که بازش می‌کند.
  • کلید فقط گفت‌وگوهایی را می‌بیند که با API شروع شده‌اند. گفت‌وگوهای پیام‌رسان و ایمیل و تلگرام برایش وجود ندارند.
  • اگر صفحه پر باشد، nextCursor دارد. آن را به‌عنوان before بفرستید تا صفحهٔ بعد بیاید. null یعنی صفحهٔ دیگری نیست.

تکرار درخواست، بدون پیام تکراری

اگر درخواستی جواب نگرفت و دوباره فرستادید، ممکن است بار اول رسیده باشد و پیام مشتری دو بار ثبت شود. برای همین، هر دو درخواست POST هدر Idempotency-Key می‌گیرند. برای هر پیام یک مقدار تازه بسازید (مثلاً یک UUID) و در تکرارها همان را بفرستید.

  • تکرار با همان کلید و همان بدنه، تا ۲۴ ساعت، همان جواب اول را می‌گیرد و چیزی دوباره ثبت نمی‌شود. چنین جوابی هدر Idempotent-Replayed را با مقدار true دارد.
  • همان کلید با بدنهٔ دیگر خطای ۴۲۲ (idempotency_key_reused) می‌گیرد.
  • اگر درخواست اول هنوز در حال اجرا باشد، تکرار خطای ۴۰۹ (idempotency_in_progress) می‌گیرد. کمی بعد دوباره بفرستید.
  • جوابی که خطا بوده نگه داشته نمی‌شود، پس تکرار دوباره اجرا می‌شود.

وب‌هوک: جواب همکاران

جواب دستیار در همان درخواست خودتان برمی‌گردد. اما وقتی گفت‌وگو به تیم سپرده شد، جواب همکار بعداً نوشته می‌شود. برای اینکه منتظر نمانید، یک آدرس بدهید تا جواب‌ها با امضا به آن فرستاده شوند.

  1. 1در «تنظیمات → API»، بخش وب‌هوک، آدرس سرورتان را بگذارید. آدرس باید با https:// شروع شود و از اینترنت در دسترس باشد.
  2. 2با اولین ذخیره یک کلید امضا (whsec_…) ساخته می‌شود که فقط همان یک بار نشان داده می‌شود. آن را در سرورتان نگه دارید.
  3. 3«ارسال آزمایشی» را بزنید. یک رویداد ping فرستاده می‌شود و همان‌جا می‌بینید رسید یا نه.
POST https://example.com/goya24/webhook
Content-Type: application/json
Goya24-Event: message.created
Goya24-Delivery: b2a4d1c7-…
Goya24-Signature: t=1793351211,v1=5f0c9e…

{
  "id": "b2a4d1c7-…",
  "type": "message.created",
  "createdAt": "2026-11-04T09:20:11.002+00:00",
  "data": {
    "conversation": { "id": "6f1c0b9e-…", "state": "assigned", "channel": "api", "contact": { "id": "u_1024", … }, … },
    "message": { "id": "…", "author": "teammate", "authorName": "رضا", "text": "امروز ارسال می‌شود.", "files": [], "createdAt": "…" }
  }
}
  • رویدادها: message.created برای جواب همکار در گفت‌وگوی API، و ping برای ارسال آزمایشی.
  • سرورتان باید در کمتر از ۱۰ ثانیه جوابی بین ۲۰۰ تا ۲۹۹ بدهد. اول جواب بدهید و بعد کار را انجام دهید.
  • هر جواب دیگری، ریدایرکت، یا نرسیدن جواب در ۱۰ ثانیه یعنی نرسید. همان رویداد با همان id بعد از ۳۰ ثانیه، ۲ دقیقه، ۱۰ دقیقه، ۱ ساعت و ۶ ساعت دوباره فرستاده می‌شود. بعد از ششمین بار «نرسید» می‌ماند.
  • ممکن است یک رویداد دو بار برسد. id را نگه دارید و رویداد تکراری را کنار بگذارید.
  • وقتی سرورتان رویداد را گرفت، زیر پیام همکار در صندوق ورودی تیک دوم می‌خورد.
  • ارسال‌های ۳۰ روز اخیر و دلیل نرسیدن‌ها در همان بخش تنظیمات دیده می‌شود.

بررسی امضا

هر درخواست هدر Goya24-Signature دارد. مقدارش دو بخش دارد: t که زمان ارسال است و v1 که امضاست. امضا HMAC-SHA256 زمان و یک نقطه و بدنه است، با کلید امضا. امضا را روی متن خام درخواست حساب کنید، نه روی JSON خوانده و دوباره ساخته‌شده. درخواستی را که امضایش جور نیست یا زمانش بیش از پنج دقیقه با حالا فاصله دارد رد کنید.

<?php
$body = file_get_contents('php://input');   // متن خام، نه JSON خوانده‌شده
parse_str(str_replace(',', '&', $_SERVER['HTTP_GOYA24_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, GOYA24_WEBHOOK_SECRET);

if (abs(time() - (int) ($sig['t'] ?? 0)) > 300 || !hash_equals($expected, $sig['v1'] ?? '')) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
http_response_code(200);
// بعد از جواب دادن: اگر $event['id'] را قبلاً دیده‌اید، کنارش بگذارید
import crypto from "node:crypto";
import express from "express";

const app = express();

// express.raw: امضا روی متن خام است، نه روی JSON خوانده‌شده
app.post("/goya24/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = Object.fromEntries((req.get("Goya24-Signature") ?? "").split(",").map((p) => p.split("=")));
  const expected = crypto
    .createHmac("sha256", process.env.GOYA24_WEBHOOK_SECRET)
    .update(`${sig.t}.`)
    .update(req.body)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
  const valid =
    typeof sig.v1 === "string" &&
    sig.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig.v1), Buffer.from(expected));
  if (!fresh || !valid) return res.sendStatus(400);

  res.sendStatus(200);              // اول جواب، بعد کار
  const event = JSON.parse(req.body.toString("utf8"));
  // ...
});
import hashlib
import hmac
import json
import time


def goya24_event(body: bytes, signature: str, secret: str) -> dict:
    """body: متن خام درخواست. signature: هدر Goya24-Signature."""
    sig = dict(part.split("=", 1) for part in signature.split(",") if "=" in part)
    expected = hmac.new(secret.encode(), f"{sig.get('t', '')}.".encode() + body, hashlib.sha256).hexdigest()
    if abs(time.time() - int(sig.get("t", 0))) > 300 or not hmac.compare_digest(expected, sig.get("v1", "")):
        raise ValueError("not from goya24")
    return json.loads(body)

«کلید امضای تازه» در تنظیمات، کلید قبلی را همان لحظه از کار می‌اندازد. اول کلید تازه را در سرورتان بگذارید، بعد دکمه را بزنید و کلید را جایگزین کنید.

خطاها

هر خطا JSON است. روی error تصمیم بگیرید. message برای آدم‌هاست و ممکن است عوض شود.

{ "error": "invalid_api_key", "message": "…" }
  • کلید نیست، اشتباه است یا باطل شده. کد invalid_api_key، وضعیت ۴۰۱.
  • شناسهٔ مشتری خالی است. کد missing_fields، وضعیت ۴۰۰.
  • شمارهٔ موبایل خوانده نمی‌شود. کد invalid_phone، وضعیت ۴۰۰.
  • دستیاری که خواسته‌اید فعال نیست یا مال این فضای کاری نیست. کد unknown_agent، وضعیت ۴۰۰.
  • متن پیام خالی است. کد empty_message، وضعیت ۴۰۰.
  • مقدار هدر Idempotency-Key بیشتر از ۲۵۵ نویسه است یا نویسهٔ غیرمجاز دارد. کد invalid_idempotency_key، وضعیت ۴۰۰.
  • این مشتری در فضای کاری مسدود شده. کد contact_blocked، وضعیت ۴۰۳.
  • چنین گفت‌وگویی در گفت‌وگوهای API این فضای کاری نیست. کد not_found، وضعیت ۴۰۴.
  • تکرار درخواست با همان Idempotency-Key. کد idempotency_in_progress با وضعیت ۴۰۹ و کد idempotency_key_reused با وضعیت ۴۲۲. بخش «تکرار درخواست» را ببینید.
  • بدنه با شکل خواسته‌شده جور نیست. کد validation، وضعیت ۴۲۲.
  • بیش از ۱۲۰ درخواست در دقیقه با یک کلید. کد rate_limited، وضعیت ۴۲۹. هدر Retry-After می‌گوید چند ثانیه صبر کنید.
  • API موقتاً خاموش است. کد api_disabled، وضعیت ۵۰۳.

چیزهایی که هنوز نیست

  • فرستادن فایل یا عکس از طرف مشتری. فعلاً فقط متن.
  • کارت‌هایی که دستیار می‌سازد، مثل محصول و سفارش و فرم، در API نمی‌آیند. فقط متن جواب می‌آید.
  • بستن، واگذاری یا برچسب زدن گفت‌وگو از API. این‌ها کار صندوق ورودی است.
  • رویداد وب‌هوک برای ویرایش یا حذف پیام همکار. با خواندن دوبارهٔ گفت‌وگو متن تازه را می‌گیرید.
API گفت‌وگو (آزمایشی) — راهنما · گویا۲۴