رفتن به محتوای اصلی
حرف یار

برای توسعه دهندگان

مستندات API

از برنامه یا سایت خودتان با دستیار گفتگو کنید، مخاطب و محصول را همگام کنید و رویدادها را با وب هوک بگیرید.

آدرس پایه https://api.harfyar.com/api/v1

مستندات API

با API می توانید از برنامه یا سایت خودتان با دستیار گفتگو کنید، مخاطب و سرنخ بخوانید، محصولات و متن دانش را به روز کنید و مصرف را ببینید. با وب هوک هم هر رویداد (گفتگوی جدید، پیام، سرنخ و ...) لحظه ای به سرور شما خبر داده می شود.

آدرس پایه:

https://api.harfyar.com/api/v1

احراز هویت

در پنل ← توسعه دهندگان یک کلید بسازید و دسترسی های لازم را انتخاب کنید. کلید را فقط سمت سرور نگه دارید (هرگز در کد جاوااسکریپت سایت).

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
دسترسی کاربرد
conversations:read خواندن گفتگوها، پیام ها و فهرست دستیارها
conversations:write ساخت گفتگو و ارسال پیام
contacts:read خواندن مخاطبین و سرنخ ها
contacts:write ثبت یا به روزرسانی مخاطب
products:write ثبت، به روزرسانی و غیرفعال کردن محصول
knowledge:write افزودن متن به دانش دستیار و به روزرسانی داده متغیر
usage:read دیدن مصرف دوره جاری

قواعد کلی

  • همه درخواست ها و پاسخ ها JSON هستند (Content-Type: application/json).
  • محدودیت: ۶۰ درخواست در دقیقه برای هر کلید. هدرهای X-RateLimit-Limit و X-RateLimit-Remaining در هر پاسخ هست؛ اگر بیشتر شد پاسخ 429 با هدر Retry-After (ثانیه) می آید.
  • تکرار امن: برای POST و PUT هدر Idempotency-Key (یک رشته یکتا تا ۱۰۰ کاراکتر) بفرستید. اگر همان درخواست دوباره با همان کلید آمد (مثلا بعد از قطعی شبکه)، همان پاسخ قبلی با هدر Idempotent-Replayed: true برمی گردد و کاری دوبار انجام نمی شود. تا ۲۴ ساعت نگه داشته می شود.
  • شناسه ها: همه شناسه ها UUID هستند.
  • صفحه بندی: فهرست ها با ?after=<شناسه آخرین مورد> و limit (حداکثر ۱۰۰) صفحه بندی می شوند؛ next_cursor در پاسخ همان شناسه آخرین مورد است.
  • مبالغ همیشه به ریال و عدد صحیح هستند.

خطاها

همه خطاها یک شکل دارند:

{
  "error": {
    "code": "validation_failed",
    "message": "The request is not valid.",
    "request_id": "01JABCDXYZ...",
    "details": { "text": ["The text field is required."] }
  }
}
کد وضعیت HTTP معنی
unauthenticated 401 کلید نیست یا اشتباه یا باطل شده است
forbidden_ip 403 کلید برای IP دیگری تنظیم شده
plan_required 403 پلن فعلی API ندارد
workspace_inactive 403 حساب تعلیق شده است
insufficient_scope 403 کلید این دسترسی را ندارد
limit_reached 403 به سقف پلن رسیده اید (محصول، دانش)
not_found 404 پیدا نشد (یا مال حساب دیگری است)
validation_failed 422 ورودی نادرست؛ جزئیات در details
invalid_message 422 پیام خالی یا بیش از ۲۰۰۰ کاراکتر
idempotency_conflict 409 همان Idempotency-Key با درخواست متفاوت
rate_limited 429 درخواست زیاد
server_error 500 خطای ما؛ request_id را برای پشتیبانی بفرستید

دستیارها

GET /bots

{ "data": [ { "id": "9b1c...", "name": "دستیار فروشگاه", "status": "active" } ] }

گفتگو

پاسخ دستیار در پس زمینه ساخته می شود (چند ثانیه). بعد از ارسال پیام، پیام های جدید را با GET .../messages?after= بگیرید یا رویداد وب هوک message.created را گوش دهید.

POST /conversations

فیلد نوع توضیح
bot_id uuid الزامی
visitor.external_id string شناسه کاربر در سیستم شما؛ همان شناسه همیشه همان بازدیدکننده است
visitor.name، visitor.mobile، visitor.email string اختیاری؛ مخاطب ساخته یا به روز می شود
message string اختیاری؛ اولین پیام
{
  "data": {
    "id": "3f2a...",
    "bot_id": "9b1c...",
    "channel": "api",
    "status": "open",
    "subject": null,
    "contact_id": "77aa...",
    "page_url": null,
    "created_at": "2026-10-10T08:30:00+00:00"
  }
}

POST /conversations/{id}/messages

{ "text": "هزینه ارسال به شیراز چقدر است؟", "client_id": "order-42-q1" }

client_id (اختیاری) جلوی ثبت دوباره همان پیام را می گیرد. پاسخ 202:

{ "data": { "id": "c1d2...", "conversation_id": "3f2a...", "from": "visitor", "body": "هزینه ارسال ...", "attachments": 0, "created_at": "..." } }

GET /conversations/{id}/messages?after={message_id}

پیام های بعد از یک پیام (قدیمی به جدید). from یکی از visitor، ai، agent (کارشناس) یا system است.

{ "data": [ { "id": "...", "from": "ai", "body": "ارسال به شیراز ...", "created_at": "..." } ], "next_cursor": "...", "status": "open" }

GET /conversations/{id}

مخاطبین و سرنخ ها

  • GET /contacts?after=&mobile=09121234567
  • POST /contacts با name، mobile (یا external_id)، email: اگر مخاطبی با همان موبایل (یا external_id) باشد به روز می شود (200) وگرنه ساخته می شود (201).
  • GET /leads?after=&stage=new — مرحله ها: new، contacted، qualified، won، lost
{
  "id": "…", "stage": "new", "need_summary": "خرید یخچال ساید بای ساید", "score": 70,
  "bot_id": "…", "conversation_id": "…",
  "contact": { "id": "…", "name": "علی", "mobile": "09121234567", "email": null, "external_id": null, "created_at": "…" },
  "created_at": "…"
}

محصولات

PUT /products/{external_id}

external_id شناسه محصول در سیستم شماست (حروف، عدد، _ . : -).

{
  "bot_id": "9b1c...",
  "title": "دریل شارژی ۱۸ ولت",
  "url": "https://shop.ir/p/12",
  "image_url": "https://shop.ir/i/12.jpg",
  "price_rial": 45000000,
  "sale_price_rial": 42000000,
  "in_stock": true,
  "category": "ابزار برقی",
  "description": "دو باتری، کیف حمل",
  "priority": 0
}

POST /products/batch

تا ۱۰۰ محصول در یک درخواست (همان فیلدهای PUT به اضافه external_id در هر مورد). محصولاتی که به خاطر سقف پلن ذخیره نشدند در skipped می آیند.

{ "bot_id": "9b1c...", "items": [ { "external_id": "SKU-12", "title": "دریل شارژی", "price_rial": 45000000 } ] }
{ "data": { "saved": 1, "skipped": [] } }

DELETE /products/{external_id}?bot_id=...

محصول غیرفعال می شود (دستیار دیگر پیشنهادش نمی دهد).

دانش

POST /knowledge/texts

{ "bot_id": "9b1c...", "title": "شرایط ارسال", "text": "ارسال به تهران یک روزه ...", "external_id": "page-shipping" }

با همان external_id متن قبلی جایگزین می شود. status تا آماده شدن processing است و بعد ready.

DELETE /knowledge/texts/{external_id}?bot_id=...

متن و بردارهای آن از دانش حذف می شود.

داده متغیر

اطلاعات کوتاهی که زیاد عوض می شود (قیمت روز، هزینه ارسال، ساعت کاری امروز، تخفیف این هفته). دستیار این ها را همراه هر پاسخ می بیند، عین مقدار را به مشتری می گوید و اگر با متن های قدیمی تر دانش فرق داشت، همین را درست می داند. هر تغییر مقدار، پاسخ های ذخیره شده قبلی را هم باطل می کند. همان دسترسی knowledge:write لازم است.

  • key: شناسه ثابت شما برای این داده؛ حروف و اعداد لاتین و _ . : - تا ۶۴ کاراکتر (بزرگی و کوچکی حروف فرقی ندارد).
  • سقف ها: حداکثر ۳۰ داده برای هر دستیار؛ عنوان تا ۶۰، مقدار تا ۲۰۰ و توضیح تا ۱۵۰ کاراکتر؛ مجموع همه داده های یک دستیار تا ۳۰۰۰ کاراکتر. اگر بیشتر شود خطای validation_failed برمی گردد.

PUT /knowledge/variables/{key}

ساخت یا به روزرسانی. label فقط برای ساخت لازم است؛ اگر note نفرستید، توضیح قبلی می ماند (با null پاک می شود). ارسال همان مقدار قبلی فقط زمان «آخرین به روزرسانی» را تازه می کند.

{ "bot_id": "9b1c...", "label": "قیمت دلار", "value": "62,500 تومان", "note": "نرخ فروش امروز" }
{ "data": { "key": "usd_rate", "label": "قیمت دلار", "value": "62,500 تومان", "note": "نرخ فروش امروز", "updated_at": "2026-10-20T06:30:00+00:00", "bot_id": "9b1c..." } }

پاسخ 201 برای داده تازه و 200 برای به روزرسانی.

GET /knowledge/variables?bot_id=...

فهرست همه داده های متغیر دستیار (همان شکل بالا، بدون صفحه بندی).

DELETE /knowledge/variables/{key}?bot_id=...

{ "data": { "key": "usd_rate", "deleted": true } }

مصرف

GET /usage

{ "data": { "plan": "growth", "period_start": "...", "period_end": "...", "ai_replies_used": 1240, "ai_replies_allowance": 5000, "addon_remaining": 0 } }

وب هوک

در پنل ← توسعه دهندگان آدرس HTTPS و رویدادها را انتخاب کنید. رمز امضا (whsec_...) فقط یک بار نمایش داده می شود.

رویداد کی
conversation.created گفتگوی جدید (ویجت، API، تلگرام و ...)
message.created هر پیام بازدیدکننده، دستیار یا کارشناس (یادداشت داخلی نه)
handoff.requested گفتگو به کارشناس منتقل شد
lead.created سرنخ جدید
form.submitted فرمی ارسال شد

بدنه درخواست:

{
  "id": "5e0f...",
  "event": "lead.created",
  "created_at": "2026-10-10T08:31:00+00:00",
  "data": { "…": "همان شکل اشیای بالا" }
}

هدرها: X-Webhook-Id (همان id؛ برای جلوگیری از پردازش تکراری)، X-Webhook-Event، X-Webhook-Timestamp (ثانیه)، و:

X-Signature: sha256=HMAC_SHA256(secret, timestamp + "." + body)

در ۱۰ ثانیه با وضعیت 2xx پاسخ دهید؛ وگرنه تا ۶ بار (بعد از ۱ دقیقه، ۵ دقیقه، ۳۰ دقیقه، ۲ ساعت و ۶ ساعت) دوباره فرستاده می شود. بعد از ۲۰ شکست پشت سر هم وب هوک غیرفعال و به شما اطلاع داده می شود. دکمه «ارسال آزمایشی» رویداد ping می فرستد.

بررسی امضا — PHP

$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, getenv('CHAT_WEBHOOK_SECRET'));

if (! hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'] ?? '') || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
// $event['id'] را ذخیره کنید و اگر قبلا دیده اید دوباره پردازش نکنید.
http_response_code(200);

بررسی امضا — Node.js

import crypto from 'node:crypto';
import express from 'express';

const app = express();
app.post('/webhooks/chat', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-Webhook-Timestamp') ?? '';
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.CHAT_WEBHOOK_SECRET)
    .update(timestamp + '.' + req.body).digest('hex');
  const given = req.get('X-Signature') ?? '';
  const valid = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
  if (!valid || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  console.log(event.event, event.data);
  res.sendStatus(200);
});

نمونه کامل — PHP (Laravel HTTP یا Guzzle)

use Illuminate\Support\Facades\Http;

$api = Http::baseUrl('https://api.harfyar.com/api/v1')
    ->withToken(env('CHAT_API_KEY'))
    ->acceptJson();

$conversation = $api->withHeaders(['Idempotency-Key' => 'user-42-start'])
    ->post('conversations', [
        'bot_id' => env('CHAT_BOT_ID'),
        'visitor' => ['external_id' => 'user-42', 'name' => 'علی', 'mobile' => '09121234567'],
        'message' => 'سلام، سفارشم کی می رسد؟',
    ])->throw()->json('data');

// چند ثانیه بعد
$messages = $api->get("conversations/{$conversation['id']}/messages")->throw()->json('data');
foreach ($messages as $m) {
    echo "{$m['from']}: {$m['body']}\n";
}

نمونه کامل — JavaScript (Node.js 18+)

const base = 'https://api.harfyar.com/api/v1';
const headers = { Authorization: `Bearer ${process.env.CHAT_API_KEY}`, 'Content-Type': 'application/json' };

async function call(method, path, body, idempotencyKey) {
  const res = await fetch(base + path, {
    method,
    headers: idempotencyKey ? { ...headers, 'Idempotency-Key': idempotencyKey } : headers,
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message} (${json.error.request_id})`);
  return json;
}

const { data: conversation } = await call('POST', '/conversations', {
  bot_id: process.env.CHAT_BOT_ID,
  visitor: { external_id: 'user-42' },
}, 'user-42-start');

const { data: sent } = await call('POST', `/conversations/${conversation.id}/messages`, { text: 'ساعت کاری شما؟' });

// Poll every 2 seconds until the bot answers.
let after = sent.id;
for (let i = 0; i < 15; i++) {
  await new Promise((r) => setTimeout(r, 2000));
  const page = await call('GET', `/conversations/${conversation.id}/messages?after=${after}`);
  for (const m of page.data) console.log(`${m.from}: ${m.body}`);
  if (page.next_cursor) after = page.next_cursor;
  if (page.data.some((m) => m.from !== 'visitor')) break;
}

نمونه — cURL

curl -X PUT https://api.harfyar.com/api/v1/products/SKU-12 \
  -H "Authorization: Bearer $CHAT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sku-12-2026-10-10" \
  -d '{"bot_id":"9b1c...","title":"دریل شارژی","price_rial":45000000,"in_stock":true}'