مستندات توسعهدهنده چرتکه
اتصال نرمافزارها، سامانه حسابداری و CRM به دادههای استخراجشدهی چرتکه
آشنایی با دو قابلیت اصلی
چرتکه فاکتورها و پروندههای خسارت را به داده ساختاریافته تبدیل میکند. بخش «تنظیمات توسعهدهنده» در /app/settings دو راه برای اتصال دادههای سازمان شما به سایر سیستمها فراهم میکند:
۱. توکن API (بیرونکشیدن داده)
نرمافزار شما در هر زمان میتواند لیست اسناد و آمار مصرف را با یک درخواست HTTP دریافت کند؛ مناسب داشبوردها، گزارشهای دورهای و همگامسازی.
۲. وبهوک (جایگزاری رویداد)
به محض ذخیره یک فاکتور جدید، چرتکه بلافاصله آن را به آدرس شما POST میکند؛ مناسب ثبت خودکار در نرمافزار حسابداری و CRM.
اتصال به نرمافزارهای حسابداری و CRM ایرانی
چرتکه با ترکیب «توکن API + وبهوک» میتواند داده فاکتور را مستقیم به نرمافزار حسابداری یا CRM سازمان شما منتقل کند. آدرس پایه، روش احراز هویت و بدنه درخواست ثبت فاکتور در هر نرمافزار متفاوت است؛ از فهرست زیر راهنمای نرمافزار خودتان را دنبال کنید:
GET /api/accounting/export خروجی Excel/CSV بگیرید یا از GET /api/v1/documents استفاده کنید.buyerErpCode یا customerRef که در آنها میبینید، کدهای داخلی نرمافزار مقصد هستند — نه فیلدی که چرتکه میفرستد. چرتکه contactId خودش را میفرستد و تبدیل آن به کد طرفحساب در هلو یا سپیدار، وظیفهٔ شماست: یا با GET /api/v1/accounting/contacts جدول نگاشت بسازید و کنارش نگه دارید، یا در چرتکه برای هر طرفحساب یک کد یکتا ثبت کنید و همان را مبنای تطبیق بگذارید. بدون این نگاشت، اسکریپت شما هر بار یک طرفحساب تکراری در نرمافزار مقصد میسازد.هلو (Holoo) — وبسرویس REST روی شبکه داخلی
هلو یک وبسرویس REST را روی همان سرور نصب نرمافزار (پیشفرض پورت 8080) فعال میکند. ابتدا با Login توکن Bearer میگیرید و بقیه متدها را با آن صدا میزنید:
| متد | Endpoint | توضیح |
|---|---|---|
| POST | {Address}/Login | احراز هویت؛ نتیجه Login.State و Login.Token (Bearer). |
| POST | {Address}/Invoice/Invoice | ثبت فاکتور؛ Type: 1 فروش، 2 برگشت فروش، 3 خرید، 4 برگشت خرید. |
| POST | {Address}/Invoice/PreInvoice | ثبت پیشفاکتور (نوعها مثل Invoice). |
| POST | {Address}/Invoice/Order | ثبت سفارش (نوعها مثل Invoice). |
| GET | {Address}/Invoice | دریافت فاکتورها با فیلتر. |
| POST | {Address}/Customer | ثبت مشتری. |
| GET | {Address}/Customer | دریافت مشتریها. |
| POST | {Address}/Product | ثبت کالا/خدمت. |
| GET | {Address}/Product | دریافت کالاها. |
| POST | {Address}/webhook/subscribe | وبهوک هلو برای گزارش رویدادها (tables: customer / product / mainGroup / sideGroup / resInvoice). |
# آدرس پایه هلو: http://[IP]:8080/TncHoloo/api (نام دیتابیس هلو را از پیکربندی بگیرید)
# ۱) لاگین — رمز عبور را Base64 کنید
curl -X POST "http://[IP-Server-Hello]:8080/TncHoloo/api/Login" \
-H "Content-Type: application/json" \
-d '{"userinfo":{"username":"admin","userpass":"<Base64 رمز>","dbname":"Holoo1"}}'
# پاسخ موفق:
# {"Login":{"State":"True","Token":"Bearer <token>","ErrorCode":0,"Error":""}}
# ۲) برای همه متدها لازم است:
# Authorization: Bearer <token>
# Content-Type: application/json
// وبهوک چرتکه ← درج فاکتور در هلو (Node.js / Next.js)
export async function POST(req) {
// ۱) تأیید امضای X-Chortke-Signature — کد بخش «وبهوک» بالا
const payload = await req.json();
if (payload.event !== "document.created") return Response.json({ ok: true });
const d = payload.data;
const holo = "http://[IP-Server-Hello]:8080/TncHoloo/api";
const token = "<Token-از-لاگین>"; // با لاگین گرفته و کش کنید
const body = {
invoiceinfo: [{
Type: "3", // 1=فروش 2=برگشت از فروش 3=خرید 4=برگشت از خرید
customererpcode: d.buyerErpCode, // ErpCode طرفحساب ثبتشده در هلو
date: "2026-09-16", // میلادی YYYY-MM-DD (تاریخ شمسی را تبدیل کنید)
time: "10:30",
Cash: Number(d.finalAmount),
Discount: 0,
detailinfo: {
ProductErpCode: d.itemErpCode, // ErpCode کالای ثبتشده در هلو
few: Number(d.quantity ?? 1),
price: Number(d.finalAmount),
levy: 0,
scot: 0,
},
}],
};
await fetch(`${holo}/Invoice/Invoice`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify(body),
});
return Response.json({ ok: true });
}
POST http://[IP]:8080/TncHoloo/api/webhook/subscribe
Content-Type: application/json
{"subscriptions":[{"url":"https://server.com/hook","tablename":"product","apikey":"<کلید ثابت>"}]}
# tablename مجاز: customer | product | mainGroup | sideGroup | resInvoice
# اعلان دریافتی: {"Dbname":"Holoo1","Table":"product","operation":"create|update|delete","changedfields":"[...]"}
سپیدار (Sepidar) — وبسرویس محلی
سپیدار سیستم یک وبسرویس محلی (پیشفرض http://localhost:7373) فراهم میکند. پس از ثبت دستگاه (کلید RSA) و لاگین (JWT)، فاکتورها را پست میکنید:
| متد | Endpoint | توضیح |
|---|---|---|
| POST | /api/Devices/Register | ثبت یکبار دستگاه؛ پاسخ شامل کلید عمومی (XML/RSA) برای رمزنگاری مقادیر. |
| POST | /api/users/login | لاگین؛ Credentials: UserName + PasswordHash (MD5). |
| POST | /api/invoices/ | ثبت فاکتور فروش با GUID و Items. |
| GET | /api/invoices/:id | دریافت جزئیات فاکتور. |
# آدرس پایه سپیدار: http://localhost:7373 (از تنظیمات «حالت سرویسدهنده» قابل تغییر است)
# ۱) ثبت دستگاه (یکبار) — پاسخ شامل کلید عمومی (قالب XML / RSA) برای رمزنگاری است
curl -X POST "http://[IP-Sepidar]:7373/api/Devices/Register" \
-H "Content-Type: application/json" \
-d '{"DeviceName":"Chortke"}'
# ۲) لاگین
# برای هر درخواست یک GUID جدید بهعنوان ArbitraryCode بسازید:
# EncArbitraryCode = Base64( RSA-PKCS1-v1_5(ArbitraryCode, کلید عمومی) )
curl -X POST "http://[IP-Sepidar]:7373/api/users/login" \
-H "Authorization: Bearer <JWT>" \
-H "GenerationVersion: 1" \
-H "IntegrationID: <شناسه-دستگاه-Registered>" \
-H "ArbitraryCode: <GUID-جدید>" \
-H "EncArbitraryCode: <Base64-RSA>" \
-H "Content-Type: application/json" \
-d '{"UserName":"<نام کاربری>","PasswordHash":"<MD5-رمز>"}'
# ۳) برای همه متدها: Authorization: Bearer JWT + GenerationVersion + IntegrationID + ArbitraryCode + EncArbitraryCode
// وبهوک چرتکه ← ثبت فاکتور در سپیدار (Node.js / Next.js)
// توابع Register/Login و encryptRSA را طبق مستندات رسمی سپیدار پیاده کنید.
export async function POST(req) {
// ۱) تأیید امضای X-Chortke-Signature — کد بخش «وبهوک» بالا
const payload = await req.json();
if (payload.event !== "document.created") return Response.json({ ok: true });
const d = payload.data;
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer <JWT-از-لاگین>",
GenerationVersion: "1",
IntegrationID: "<IntegrationID-از-Register>",
ArbitraryCode: crypto.randomUUID(),
EncArbitraryCode: encryptRSA(crypto.randomUUID()), // RSA PKCS#1 v1.5 + Base64
};
const body = {
GUID: crypto.randomUUID(), // جلوگیری از ثبت تکراری (Idempotency)
CustomerRef: d.customerRef, // شناسه مشتری ثبتشده در سپیدار
SaleTypeRef: d.saleTypeRef ?? 1,
DiscountOnCustomer: 0,
Price: Number(d.finalAmount), // مبلغ ناخالص
Discount: 0,
Tax: 0,
Duty: 0,
Addition: 0,
Items: [{
ItemRef: d.itemRef, // شناسه کالا در سپیدار
StockRef: 0,
Quantity: Number(d.quantity ?? 1),
SecondaryQuantity: 0,
Fee: Number(d.finalAmount),
Description: d.invoiceNumber,
}],
};
await fetch("http://[IP-Sepidar]:7373/api/invoices/", {
method: "POST",
headers,
body: JSON.stringify(body),
});
return Response.json({ ok: true });
}
GenerationVersion، IntegrationID،ArbitraryCode و EncArbitraryCode نیاز دارند وArbitraryCode باید برای هر درخواست جدید ساخته شود. بدون لایسنس ماژول وبسرویس، سرور پاسخ نمیدهد.دیدار (Didar) — CRM ابری
دیدار یک CRM ابری با API عمومی است؛ کلید API را از تنظیمات توسعهدهنده دیدار بگیرید و در کوئری بفرستید:
| متد | Endpoint | توضیح |
|---|---|---|
| POST | /api/AccountingInvoice/Create | ایجاد فاکتور حسابداری؛ نیازمند SellerInfo.BizDomainId و Items. |
| POST | /api/AccountingInvoice/List | فهرست فاکتورهای حسابداری. |
| POST | /api/AccountingInvoice/Delete | حذف فاکتور. |
| GET | /api/Product/GetProducts | دریافت کالاها برای یافتن ProductId. |
| GET | /api/Product/GetProductsByCode | دریافت کالا با کد. |
// وبهوک چرتکه ← ایجاد فاکتور در دیدار (Node.js / Next.js)
const API_KEY = "<کلید-API-دیدار>";
export async function POST(req) {
// تأیید امضا — کد بخش «وبهوک» بالا
const payload = await req.json();
if (payload.event !== "document.created") return Response.json({ ok: true });
const d = payload.data;
const body = {
Items: [{
ProductId: d.productId, // از Search Products / Get Products By Code
Title: d.title ?? "فاکتور چرتکه",
Quantity: Number(d.quantity ?? 1),
Unit: d.unit ?? "عدد",
UnitPrice: Number(d.finalAmount),
Discount: 0,
DiscountPercent: 0,
DiscountType: "Amount",
TaxPercent: d.taxPercent ?? 0,
}],
SellerInfo: {
SellerName: "<نام فروشنده>",
BizDomainId: "<BizDomainId فروشنده>", // الزامی
},
BuyerInfo: {
BuyerType: "Company",
BuyerName: d.buyer,
BuyerEconomicCode: d.buyerEconomicCode ?? "",
},
IssueDate: "2026-09-16T10:30:00.000Z",
};
await fetch(`https://app.didar.me/api/AccountingInvoice/Create?apikey=${API_KEY}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return Response.json({ ok: true });
}
حسابفا (Hesabfa) — حسابداری ابری
حسابفا یک حسابداری ابری با وبسرویس عمومی (همه متدها POST) است. اعتبارها (apiKey + loginToken یا userId+password) در بدنه درخواست ارسال میشوند و پاسخ همیشه در قالب{ Success, ErrorCode, ErrorMessage, Result } است:
| متد | Endpoint | توضیح |
|---|---|---|
| POST | /v1/accounting/GetSetting | تنظیمات حساب (از جمله TokenKey / LoginToken). |
| POST | /v1/invoice/save | ثبت/ویرایش فاکتور. |
| POST | /v1/invoice/get | دریافت فاکتور با id. |
| POST | /v1/Setting/Hook | هوک تغییرات (Hook) برای دریافت رویدادها. |
// وبهوک چرتکه ← ثبت فاکتور در حسابفا (Node.js / Next.js)
export async function POST(req) {
// تأیید امضا — کد بخش «وبهوک» بالا
const payload = await req.json();
if (payload.event !== "document.created") return Response.json({ ok: true });
const d = payload.data;
const res = await fetch("https://api.hesabfa.com/v1/invoice/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
apiKey: "<apiKey>", // تنظیمات حسابفا (بخش API)
loginToken: "<loginToken>", // یا userId + password
invoice: {
number: d.invoiceNumber ?? "",
date: "2026-09-16 10:30:00", // میلادی
dueDate: "2026-09-16 10:30:00",
contactCode: d.buyerContactCode, // کد شخص در حسابفا
contactTitle: d.buyer,
invoiceType: 1, // نوع فاکتور از جدول TypesTable (۰=فروش، ۱=خرید، …)
status: 2,
invoiceItems: [{
itemCode: d.itemCode,
description: d.title ?? "فاکتور چرتکه",
unit: "عدد",
quantity: Number(d.quantity ?? 1),
unitPrice: Number(d.finalAmount),
discount: 0,
tax: 0,
}],
},
}),
});
const data = await res.json();
if (!data.Success) console.error(data.ErrorCode, data.ErrorMessage);
return Response.json({ ok: true });
}
سایر نرمافزارهای حسابداری
برخی نرمافزارهای دیگر نیز API / وبسرویس عمومی دارند (یا مستندات آن را باز کردهاند)؛ اما برای بیشتر آنها اتصال نیازمند هماهنگی با فروشنده نرمافزار است. بهروزترین وضعیت را همیشه از مستندات رسمی هر نرمافزار بررسی کنید:
| نرمافزار | نوع | مسیر اتصال |
|---|---|---|
| محک | حسابداری ابری | از سایت محک دریافت کنید. |
| تدبیر | ERP دسکتاپ | از سایت تدبیر دریافت کنید. |
| پیوست، رافع، دشت، اوراش، پارمیس، پارسیان | متغیر | با فروشنده نرمافزار هماهنگ کنید. |
GET /api/v1/documents را با خروجی Excel/CSV ترکیب کنید و با ابزارهای داخلی همان نرمافزار وارد کنید.توکن API
برای ساخت توکن به /app/settings بروید و در بخش «توکنهای API» نامی برای کلید تعریف کنید (مثلاً «سامانه مالی»). توکن به این شکل ساخته میشود:
chtk_abc123def456_4f3d…(48 کاراکتر hex)نکات امنیتی مهم:
- اصل توکن فقط یکبار نمایش داده میشود؛ در سیستم چرتکه فقط هش SHA-256 آن ذخیره میشود. بلافاصله آن را کپی و نزد خود نگه دارید.
- هر توکن فقط به سازمان شما دسترسی دارد؛ با توکن نمیتوان به سازمانهای دیگر دسترسی گرفت.
- اگر توکن افشا شد یا دیگر لازم نیست، بلافاصله در صفحه تنظیمات آن را لغو (Revoke) کنید.
در هر درخواست، توکن را در هدر استاندارد بفرستید:
Authorization: Bearer chtk_abc123def456_<48-char-secret>Endpointهای عمومی (v1)
دریافت اسناد — GET /api/v1/documents
لیست اسناد استخراجشده سازمان را با صفحهبندی و فیلتر برمیگرداند.
| پارامتر | نوع | پیشفرض | توضیح |
|---|---|---|---|
page | number | 1 | شماره صفحه نتایج (حداقل ۱). |
limit | number | 20 | تعداد نتایج در هر صفحه؛ بین ۱ تا ۱۰۰. |
search | string | — | جستوجوی متنی. در اسناد: شماره فاکتور، فروشنده و خریدار. در حسابداری: نام یا کد همان موجودیت. |
fromJ | string (شمسی) | — | تاریخ شمسی شروع بازه (مثلاً 1404/01/01). |
toJ | string (شمسی) | — | تاریخ شمسی پایان بازه. |
seller | string | — | فیلتر فروشنده با مطابقت دقیق. فقط در مسیرهای اسناد. |
status | string | — | وضعیت سند. در اسناد مقدارِ متنِ آزادِ فارسی است (پیشفرض «آماده ثبت»)، نه یک enum لاتین. |
curl -G "https://chortkey.ir/api/v1/documents" \
-H "Authorization: Bearer chtk_abc123def456_<48-char-secret>" \
--data-urlencode "page=1" \
--data-urlencode "limit=20" \
--data-urlencode "search=فاکتور شماره";
// Node.js یا Next.js (Route Handler / Server Action)
const TOKEN = "chtk_abc123def456_<48-char-secret>";
const res = await fetch("https://chortkey.ir/api/v1/documents?page=1&limit=20&search=فاکتور", {
headers: { Authorization: "Bearer " + TOKEN },
});
const result = await res.json();
console.log(result.data.items);
# Python 3 (با نصب: pip install requests)
import requests
TOKEN = "chtk_abc123def456_<48-char-secret>"
res = requests.get(
"https://chortkey.ir/api/v1/documents",
headers={"Authorization": "Bearer " + TOKEN},
params={"page": 1, "limit": 20},
)
data = res.json()
print(data["data"]["items"])
<?php
// PHP یا افزونه WordPress
$TOKEN = "chtk_abc123def456_<48-char-secret>";
$args = array(
"timeout" => 15,
"headers" => array("Authorization" => "Bearer " . $TOKEN),
);
$response = wp_remote_get(
"https://chortkey.ir/api/v1/documents?page=1&limit=20",
$args
);
$body = json_decode(wp_remote_retrieve_body($response), true);
print_r($body["data"]["items"]);
آمار مصرف — GET /api/v1/usage
یکی از پارامترها ندارد و آمار کلی سازمان را برمیگرداند:
curl "https://chortkey.ir/api/v1/usage" \
-H "Authorization: Bearer chtk_abc123def456_<48-char-secret>";
{
"success": true,
"data": {
"totalDocuments": 42,
"currentPeriodDocuments": 7,
"byStatus": [
{ "status": "draft", "count": 20 },
{ "status": "approved", "count": 15 },
{ "status": "rejected", "count": 7 }
],
"byThreeWayMatch": [
{ "threeWayMatch": "تأیید شد", "count": 30 },
{ "threeWayMatch": "-", "count": 12 }
]
},
"meta": {
"organizationId": "org-xxxx",
"apiVersion": "v1"
}
}currentPeriodDocuments تعداد اسناد دوره جاری (شمسی) را نشان میدهد و برای کنترل سهمیه و بودجه دوره کاربرد دارد. فیلد periodIsJalali همیشه true است تا مصرفکننده مجبور نباشد حدس بزند دوره بر مبنای تقویم ایرانی است یا میلادی.
دادههای حسابداری — GET /api/v1/accounting/<resource>
خواندن دادههای حسابداری با همان توکن. این مسیر فقط خواندنی است؛ نوشتن (ثبت فاکتور، سند، رسید) از مسیر نشست و با مجوزِ داخلی انجام میشود و عمداً از API عمومی باز نشده تا یک توکنِ لو رفته نتواند در دفتر کل بنویسد.
۴۰۳ میگیرد، حتی اگر توکنش معتبر باشد.| متد | Endpoint | توضیح |
|---|---|---|
| GET | /api/v1/accounting/invoices | فهرست فاکتورها؛ فیلترهای docType، status، contactId، search و include=items برای گرفتن ردیفهای کالا. صفحهبندی دارد. |
| GET | /api/v1/accounting/contacts | فهرست طرفحسابها؛ فیلترهای type و search. |
| GET | /api/v1/accounting/items | فهرست کالا و خدمات؛ فیلترهای type و search. |
| GET | /api/v1/accounting/ledger | فهرست اسناد دوبل بههمراه ردیفها؛ فیلترهای docType، fromJ، toJ، search. |
| GET | /api/v1/accounting/trial-balance | تراز آزمایشی کامل، بدون صفحهبندی (ترازِ ناقص بیمعناست). شامل balanced. |
| GET | /api/v1/accounting/profit-loss | سود و زیان بازه؛ فیلترهای اختیاری fromJ و toJ. |
| پارامتر | نوع | پیشفرض | توضیح |
|---|---|---|---|
page | number | 1 | شماره صفحه نتایج (حداقل ۱). |
limit | number | 20 | تعداد نتایج در هر صفحه؛ بین ۱ تا ۱۰۰. |
search | string | — | جستوجوی متنی. در اسناد: شماره فاکتور، فروشنده و خریدار. در حسابداری: نام یا کد همان موجودیت. |
fromJ | string (شمسی) | — | تاریخ شمسی شروع بازه (مثلاً 1404/01/01). |
toJ | string (شمسی) | — | تاریخ شمسی پایان بازه. |
status | string | — | وضعیت. در مسیر invoices فقط دو مقدار معتبر دارد: draft و final. |
docType | string | — | نوع سند: sale، sale_return، sale_quote، purchase، purchase_return، purchase_quote. |
contactId | string | — | شناسه طرف حساب. همیشه در محدودهٔ همان سازمانِ توکن بررسی میشود. |
include | string | — | فقط در invoices: با مقدار items، ردیفهای کالا هم برمیگردد. پیشفرض خالی است تا پاسخ سبک بماند. |
# فاکتورهای قطعی فروشِ یک ماه
curl "https://chortkey.ir/api/v1/accounting/invoices?docType=sale&status=final&fromJ=1404/01/01&toJ=1404/01/31&limit=100" \
-H "Authorization: Bearer chtk_abc123def456_<48-char-secret>"
# همان، ولی با ردیفهای کالا — برای ساخت فاکتور در هلو/سپیدار/حسابفا لازم است.
# بدون این پارامتر، پاسخ فقط سرِ فاکتور است و items خالی میماند.
curl "https://chortkey.ir/api/v1/accounting/invoices?docType=sale&status=final&limit=100&include=items" \
-H "Authorization: Bearer chtk_abc123def456_<48-char-secret>"
# تراز آزمایشی (بدون صفحهبندی — همهٔ سطرها برمیگردد)
curl "https://chortkey.ir/api/v1/accounting/trial-balance" \
-H "Authorization: Bearer chtk_abc123def456_<48-char-secret>"
# پاسخ هر دو: { "success": true, "data": ..., "meta": { "organizationId": "<uuid>", ... } }
پاسخ همهٔ این مسیرها همان پوشش { success, data, meta } را دارد و meta.organizationId همیشه یک UUID است.
GET /api/accounting/export را با نشست ببینید: ۱۳ موجودیت بهصورت CSV یا XLSX و بهشکلی که دوباره قابل ورود به چرتکه است. دکمهٔ «دانلود فایل قالب» هم در مسیر /app/accounting/data همان ستونها را میدهد. توجه: /api/v1/documents فقط JSON برمیگرداند و خروجی جدولی ندارد.وبهوکها (Webhook)
وبهوک یک «قلاب» است: به محض رخدادن رویداد، چرتکه یک درخواست HTTP را با محتوای JSON به آدرس شما میفرستد و دیگر نیازی به چککردن دورهای نیست. این روش برای ثبت خودکار فاکتور در نرمافزار حسابداری، ارسال به ربات تلگرام، داشبورد زنده و CRM مناسب است.
رویدادهای موجود
| رویداد | یعنی چه |
|---|---|
document.created | سند جدید (ذخیره فاکتور) |
invoice.created | فاکتور پیشنویس ثبت شد |
invoice.finalized | فاکتور قطعی شد (آمادهٔ ارسال به نرمافزار حسابداری) |
payment.received | دریافت وجه |
payment.paid | پرداخت وجه |
check.passed | چک پاس شد |
check.cleared | چک وصول شد |
check.returned | چک برگشت خورد |
journal.posted | سند دوبل دستی ثبت شد |
contact.created | طرف حساب با ماندهٔ افتتاحیه ثبت شد |
invoice.finalized همان چیزی است که لازم دارید: این رویداد فقط وقتی میآید که فاکتور واقعاً قطعی شده و ردیفهای کالا با جزئیات کامل داخل data.items آمدهاند. رویداد invoice.created فقط پیشنویس است و ممکن است بعداً تغییر کند، پس مبنای همگامسازی نیست.نمونه بدنِ درخواست ارسالی به آدرس شما:
{
"event": "document.created",
"data": {
"id": "uuid",
"invoiceNumber": "1234",
"seller": "شرکت الف",
"buyer": "شرکت ب",
"issueDateJ": "1404/01/15",
"finalAmount": 12500000,
"threeWayMatch": "-"
}, "sentAt": "2026-09-16T10:30:00.000Z"
}و برای invoice.finalized، فیلد data چنین است (items آرایهٔ ردیفهاست و همیشه میآید، حتی اگر خالی باشد):
{
"event": "invoice.finalized",
"data": {
"id": "uuid",
"docType": "sale",
"number": "1404-12",
"issueDateJ": "1404/01/15",
"dueDateJ": "1404/02/15",
"contactId": "uuid",
"currency": "ریال",
"subtotal": 11363636,
"discount": 0,
"vat": 1136364,
"finalAmount": 12500000,
"paidAmount": 0,
"items": [
{
"description": "خدمات پشتیبانی",
"itemId": "uuid",
"quantity": 1,
"unitPrice": 11363636,
"discount": 0,
"total": 11363636,
"taxRate": 10,
"vat": 1136364
}
]
},
"sentAt": "2026-09-16T10:30:00.000Z"
}تأیید امضای درخواست (الزامی)
هر درخواست وبهوک با هدر امضا میشود:
X-Chortke-Signature: t=1770000000,v1=<64-char-hmac-sha256>t زمان ارسال (ثانیه Unix) و v1 مقدار HMAC-SHA256 بدنه درخواست با «کلید محرمانه» وبهوک است. کلید محرمانه هنگام ساخت وبهوک فقط یکبار نمایش داده میشود. همیشه پیش از پردازش، امضا را بررسی کنید؛ این کار از ارسالهای جعلی و درخواستهای بازپخش جلوگیری میکند.
// تأیید امضای وبهوک در Node.js / Next.js
import { createHmac, timingSafeEqual } from "crypto";
export function verifyWebhook(rawBody, signatureHeader, secret) {
const parts = signatureHeader.split(",");
const timestamp = String(parts.find((p) => p.startsWith("t="))?.slice(2) ?? "");
const signature = parts.find((p) => p.startsWith("v1="))?.slice(3) ?? "";
// ضد بازپخش: بازه زمانی ۵ دقیقه
const now = Math.floor(Date.now() / 1000);
if (now - Number(timestamp) > 300) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signature, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
// در Route Handler:
export async function POST(req) {
const rawBody = await req.text();
const header = req.headers.get("x-chortke-signature") ?? "";
const ok = verifyWebhook(rawBody, header, "YOUR_WEBHOOK_SECRET");
if (!ok) return new Response("invalid signature", { status: 401 });
const payload = JSON.parse(rawBody);
// حالا payload.event === "document.created"
return new Response("ok", { status: 200 });
}
# دریافتکننده وبهوک با FastAPI / Flask
import hashlib
import hmac
import time
from fastapi import FastAPI, Request, Header
app = FastAPI()
WEBHOOK_SECRET = "YOUR_WEBHOOK_SECRET"
@app.post("/webhook")
async def webhook(request: Request, x_chortke_signature: str = Header("")):
raw_body = (await request.body()).decode()
parts = dict(p.split("=", 1) for p in x_chortke_signature.split(","))
ts = int(parts.get("t", "0"))
signature = parts.get("v1", "")
if int(time.time()) - ts > 300:
return {"error": "expired"}
expected = hmac.new(
WEBHOOK_SECRET.encode(),
raw_body.encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
return {"error": "invalid"}, 401
payload = await request.json()
# payload["event"] == "document.created"
return {"ok": True}
<?php
// دریافتکننده وبهوک در PHP / WordPress
define("WEBHOOK_SECRET", "YOUR_WEBHOOK_SECRET");
$rawBody = file_get_contents("php://input");
$signatureHeader = $_SERVER["HTTP_X_CHORTKE_SIGNATURE"] ?? "";
$secret = constant("WEBHOOK_SECRET");
$parts = [];
foreach (explode(",", $signatureHeader) as $item) {
[$k, $v] = explode("=", $item, 2);
$parts[$k] = $v;
}
if (time() - (int)($parts["t"] ?? 0) > 300) {
http_response_code(401);
exit;
}
$expected = hash_hmac("sha256", $rawBody, $secret);
if (!hash_equals($expected, $parts["v1"] ?? "")) {
http_response_code(401);
exit;
}
$payload = json_decode($rawBody, true);
// $payload["event"] === "document.created"
http_response_code(200);
echo "ok";
نکات فنی
- حداکثر زمان پاسخدهی شما ۱۰ ثانیه است. آدرس مقصد باید
httpیاhttpsباشد، ولی چون محتوای وبهوک شامل دادههای مالی است، HTTPS را اکیداً توصیه میکنیم: روی HTTP، امضا و محتوا در هر مسیری که بین شما و سرور جابهجا میشود قابل خواندن و دستکاری است. - در حال حاضر تلاش مجدد (Retry) خودکار انجام نمیشود؛ سرور شما باید سریع و پایدار باشد.
- برای قطع موقت، وبهوک را از صفحه تنظیمات غیرفعال کنید؛ بدون حذف آدرس.
- اگر ارسال به یک وبهوک با خطا مواجه شود، بقیه وبهوکها و مسیر اصلی پردازش سند آسیبی نمیبینند.
بهترین روشهای امنیتی
- توکن و کلید محرمانه وبهوک را در متغیرهای محیطی (Env) نگه دارید؛ هرگز در کد یا صفحه وب آشکار نکنید.
- برای هر نرمافزار، یک توکن جداگانه بسازید تا در صورت نیاز فقط همانها را لغو کنید.
- همیشه پیش از پردازش وبهوک، امضای
X-Chortke-Signatureرا باtimingSafeEqual/hash_equalsبررسی و بازه زمانیtرا محدود کنید (مثال: ۵ دقیقه). - آدرس وبهوک را ترجیحاً فقط HTTPS ثبت کنید و اطمینان حاصل کنید سرور شما پاسخ سریع (زیر ۱۰ ثانیه) میدهد.
- دسترسی ساخت توکن را فقط به مدیر/مالک سازمان محدود نگه دارید.
محدودیتهایی که باید بدانید
| محدودیت | جزئیات |
|---|---|
| توکن، همهی کار را میکند | توکن سطحدسترسی (scope) ندارد: یا همهٔ مسیرهای مجازِ آن سازمان را میبیند یا هیچکدام را. اگر برای یک نرمافزار توکن جدا میسازید، دلیلش فقط امکان لغو جداگانه است، نه محدودکردن دسترسی. |
| سقف نرخ بر اساس IP است، نه توکن | محدودیت نرخ روی آیپی هر درخواست اعمال میشود (پیشفرض ۱۲۰ درخواست در دقیقه، قابل تنظیم توسط مدیر). اگر چند سرویس پشت یک IP مشترک اجرا شوند، سهمیهٔ همهٔ آنها یکی است و ممکن است به ۴۲۹ بخورید. |
| نیازمند پلن متوسط یا بزرگ | همهٔ مسیرهای توسعهدهنده — توکن، وبهوک و /api/v1 — در پلنهای پایینتر با ۴۰۳ بستهاند. |
| تلاش مجدد انجام نمیشود | وبهوک یکبار فرستاده میشود و در صورت خطا فقط لاگ میشود؛ نه صف دارد، نه retry. اگر همگامسازی برایتان حیاتی است، در سرور مقصد وضعیت هر دریافت را نگه دارید و در صورت نیاز از /api/v1/accounting دوباره بگیرید. |
| امضا را سرور ما کنترل نمیکند | پارامتر t فقط یک راهنماست؛ ما بازپخش (replay) را مسدود نمیکنیم. بررسی پنجرهٔ زمانی و ذخیرهٔ شناسهٔ رویداد برای جلوگیری از پردازش دوباره، بر عهدهٔ سرور شماست. |
عیبیابی رایج
| مشکل | علت و راهحل |
|---|---|
| پاسخ HTTP 401 «توکن نامعتبر یا لغو شده» | توکن اشتباه، دارای کاراکتر فضای خالی، یا قبلاً لغو شده است. توکن جدید بسازید و دقیقاً در هدر Authorization قرار دهید. |
| وبهوک ارسال میشود اما دیتا ثبت نمیشود | امضای هدر را بررسی کنید؛ متداولترین علت، خطا در کپی کلید محرمانه یا اشتباه در HMAC است. |
| وبهوک پاسخ نمیگیرد (Timeout) | سرور شما باید زیر ۱۰ ثانیه پاسخ دهد؛ وبهوک را موقتاً غیرفعال کنید و سرور را بررسی کنید. |
| تاریخ ازJ / تاJ کار نمیکند | این پارامترها به شمسی (Jalali) هستند مثل 1404/01/01؛ نه MILADI. |
| چرا اصل توکن دوباره نمایش داده نمیشود؟ | برای امنیت، فقط در لحظه ساخت نمایش داده میشود. اگر گم شد، توکن قبلی را لغو و توکن جدید بسازید. |
آماده اتصال هستید؟ ابتدا در تنظیمات توسعهدهنده توکن یا وبهوک بسازید و در صورت نیاز به کمک با مرکز پشتیبانی در تماس باشید.