رابط برنامهنویسی سبزحساب
یک REST نسخهدار که دفتر حسابداری شما را به هر سیستم دیگری وصل میکند: فروشگاه اینترنتی، مارکتپلیس، باشگاه مشتریان، یا هر چیزی که خودتان نوشتهاید. قرارداد کامل است، واحد پول ریال است و تاریخ هم شمسی میپذیرد.
api.sabzhesab.com در فاز کانکتور باز میشود. این
صفحه را عمدا پیش از نوشتن کد منتشر کردهایم تا اگر
جایی از قرارداد با کار شما نمیخواند، همین حالا بگویید —
بعد از انتشار، تغییر دادنش برای شما هزینه دارد.
از کجا شروع کنید
کل این صفحه یک قرارداد را توضیح میدهد، و همان قرارداد به شکل
ماشینخوان هم منتشر میشود: OpenAPI 3.1 روی
/v1/openapi.json. هر سه SDK رسمی از همان فایل
تولید میشوند، پس سند و کد نمیتوانند از هم جدا بیفتند.
آموزش گامبهگام — وصل کردن یک فروشگاه
سناریو ساده است: فروشگاه اینترنتی دارید و میخواهید هر سفارش تأییدشده خودش در سبزحساب فاکتور و سند بخورد، موجودی انبار به فروشگاه برگردد، و اگر چکی برگشت خورد خبردار شوید.
-
توکن بگیرید
در پنل بروید به تنظیمات ← رابط برنامهنویسی ← توکن تازه. اختیارات لازم همین سناریو اینهاست: sales:write items:read inventory:read contacts:write webhooks:manage
بیشتر از این نگیرید. توکنی که اختیار حسابداری دارد و لازمش ندارد، فقط دامنه خرابی را بزرگ میکند.
🔴توکن فقط یک بار نشان داده میشود. ما خودمان هم بعدش نمیتوانیم بخوانیمش، چون هش
argon2idذخیره میشود. اگر گمش کردید باید باطل کنید و تازه بسازید — این یک محدودیت نیست، تنها شکل درست نگه داشتن کلیدی است که به دفتر مالی شما وصل است. -
اتصال را بسنجید
اولین تماس هر اتصالی باید
/v1/meباشد. با یک درخواست، اختیارات توکن، طرح اشتراک، سقفها و سال مالی باز را یکجا میگیرید.curl -s https://api.sabzhesab.com/v1/me \ -H "Authorization: Bearer shk_live_7Qx9..."{ "business": { "id": 41, "name": "نادیکالا" }, "token": { "name": "کانکتور فروشگاه", "scopes": ["sales:write", "items:read", "inventory:read"], "expires_at": "2027-09-30", "mode": "live" }, "plan": { "key": "multichannel", "name": "چندکاناله" }, "limits": { "requests_per_minute": 600, "invoices_per_month": 2000, "invoices_used_this_month": 318 }, "fiscal_year": { "id": 7, "label": "۱۴۰۵", "from": "2026-03-21", "to": "2027-03-20", "status": "open" } }اگر
fiscal_yearخالی برگشت، هیچ سال مالی بازی نیست و هر تلاشی برای ثبت سند422 fiscal_year_missingمیگیرد. این را همین گام اول ببینید، نه وسط شب و وسط صد سفارش. -
کالاها را نگاشت کنید
فروشگاه شما SKU دارد، سبزحساب کد کالا. برای اینکه مجبور نباشید نقشه شناسهها را جایی نگه دارید، هر کالا یک
external_idمیگیرد — و از آن به بعد با همان صدایش میزنید.POST /v1/items/batch Authorization: Bearer shk_live_7Qx9... Idempotency-Key: items-sync-1405-07-11 { "items": [ { "external_id": "SKU-4410", "name": "کیف چرم دستی", "kind": "stock", "unit": "عدد", "sell_price": "1850000.0000", "tax_rate": "10.00", "barcode": "6260100441012" } ] }بعدش موجودی را یکجا بخوانید — نه کالا به کالا. هر فروشگاهی این را هر چند دقیقه میپرسد و یک درخواست بهجای سیصد درخواست، تفاوت بین ماندن و خوردن به سقف نرخ است:
GET /v1/items/stock?external_ids=SKU-4410,SKU-4411&warehouse_id=3 -
سفارش را فاکتور کنید
این گام قلب اتصال است. سه چیز در آن هست که کار را ساده میکند: طرف حساب را لازم نیست از قبل بسازید، تاریخ را میتوانید شمسی بفرستید، و شماره سفارش خودتان نقش
Idempotency-Keyرا بازی میکند.POST /v1/sales-invoices Authorization: Bearer shk_live_7Qx9... Idempotency-Key: ord-SHOP-884512 Content-Type: application/json { "external_id": "SHOP-884512", "date": "1405-07-11", "date_calendar": "jalali", "contact": { "external_id": "shop-user-77231", "name": "مریم رضایی", "mobile": "09120000000" }, "channel": "webshop", "warehouse_id": 3, "status": "posted", "lines": [ { "item": { "external_id": "SKU-4410" }, "quantity": "2.0000", "unit_price": "1850000.0000", "discount": "0.0000", "tax_rate": "10.00" } ], "charges": [ { "kind": "freight", "title": "هزینه ارسال", "amount": "450000.0000" } ] }{ "id": 9931, "number": "1405-0442", "external_id": "SHOP-884512", "date": "2026-10-03", "status": "posted", "subtotal": "3700000.0000", "discount_total": "0.0000", "tax_total": "370000.0000", "charges_total": "450000.0000", "total": "4520000.0000", "paid": "0.0000", "due": "4520000.0000", "contact": { "id": 1182, "code": "1001-0344", "name": "مریم رضایی" }, "journal_entry": { "id": 20451, "number": 1288, "status": "posted" }, "moadian": { "status": "not_sent", "tax_id": null }, "links": { "self": "/v1/sales-invoices/9931", "pdf": "/v1/sales-invoices/9931/pdf" } }✓contactرا جایcontact_idفرستادیم. اگر آنexternal_idقبلا دیده نشده باشد طرف حساب ساخته میشود، و اگر دیده شده باشد همان به کار میرود. کانکتور شما نیازی ندارد اول طرف حساب بسازد و شناسهاش را نگه دارد.🔴status: "posted"یعنی سند حسابداری همان لحظه خورد — و از آن لحظه فاکتور تغییرناپذیر است.PATCHرویش409 document_postedمیگیرد.اگر میخواهید بعدا ویرایشش کنید،
"draft"بفرستید و وقتی سفارش قطعی شدPOST /v1/sales-invoices/{id}/postبزنید. اصلاح فاکتور ثبتشده فقط با سند جدید انجام میشود: برگشت از فروش، یا سند برگشتی. -
وبهوک را ثبت کنید
تا اینجا شما به سبزحساب حرف زدید. این گام برعکسش است: سبزحساب به شما خبر میدهد. بدون این، مجبورید هر چند دقیقه بپرسید «چیزی عوض شد؟» — و همان چیزی است که سقف نرخ را میخورد.
POST /v1/webhooks { "url": "https://shop.example.ir/hooks/sabzhesab", "events": ["sales_invoice.paid", "cheque.bounced", "stock.low"] }پاسخ یک
secretدارد که فقط همان یک بار میآید. با آن امضای هر پیام را بسنجید:$raw = file_get_contents('php://input'); $timestamp = $_SERVER['HTTP_SABZHESAB_TIMESTAMP'] ?? ''; $signature = $_SERVER['HTTP_SABZHESAB_SIGNATURE'] ?? ''; $expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$raw, $secret); // مقایسه زمانثابت — هرگز با === مقایسه نکنید if (! hash_equals($expected, $signature)) { http_response_code(401); exit; } // پنجره ۵ دقیقه، تا پیام ضبطشده دوباره پخش نشود if (abs(time() - (int) $timestamp) > 300) { http_response_code(401); exit; } // فوری ۲۰۰ بدهید و کار را به صف خودتان بیندازید http_response_code(200);🔴hash_equalsو نه===. مقایسه رشتهای معمولی در اولین نویسه متفاوت برمیگردد، و از همین اختلاف زمان میشود امضا را حرفبهحرف حدس زد.و مقصد باید در ۵ ثانیه پاسخ بدهد. اگر کار سنگینی دارید، فوری ۲۰۰ بدهید و بعد انجامش دهید — وگرنه ما تلاش را ناموفق میشماریم و دوباره میفرستیم.
همین پنج گام یک اتصال کامل است. اگر نمیخواهید منطق حسابداری را
بدانید، راه کوتاهتری هم هست:
POST /v1/external-orders سفارش خام را میگیرد و
سبزحساب خودش طرف حساب، فاکتور، سند و حواله انبار را میسازد.
نشانی و قالب
| نشانی پایه | https://api.sabzhesab.com/v1 |
|---|---|
| جعبهشن | https://sandbox.sabzhesab.com/v1 |
| قالب | application/json; charset=utf-8 |
| احراز هویت | Authorization: Bearer <token> |
| قرارداد ماشینخوان | /v1/openapi.json OpenAPI 3.1 |
| پروتکل | فقط HTTPS با HSTS. درخواست HTTP با ۴۰۳ رد میشود، نه ریدایرکت |
| زیرساخت | داخل ایران. هیچ وابستگی به سرویس بینالمللی |
مستأجر هرگز پارامتر نیست
توکن به یک کسبوکار بسته است. هیچ مسیری
tenant_id یا business_id نمیگیرد و
هیچ پاسخی آن را برنمیگرداند. اگر دو کسبوکار دارید، دو توکن
میگیرید.
این تصمیم امنیتی است نه سلیقهای: تا وقتی مستأجر از خود توکن خوانده شود، هیچ اشتباه کدی نمیتواند داده دفتر یکی را به دیگری بدهد. لحظهای که کسبوکار پارامتر شود، جداسازی سطحردیف دیتابیس تنها خط دفاع باقیمانده است — و خط دفاع آخر نباید تنها خط دفاع باشد.
پول
همه مبالغ رشتهای با چهار رقم اعشار، واحد ریال.
هیچ مبلغی در این API هرگز number نیست.
{ "amount": "125000.0000", "currency": "IRR" }
چرا رشته و نه عدد: JSON.parse("125000.0001") در
جاوااسکریپت یک double میدهد، و
double در حسابداری ریال ایران — جایی که مبالغ
بهسادگی از دو به توان پنجاهوسه عبور میکنند — ریال گم میکند.
یک ریال گمشده در یک سند، یک سند ناتراز است.
تومان در API وجود ندارد. تومان لایه نمایش است؛ اگر رابط شما تومان نشان میدهد، تقسیم بر ۱۰ کار خودتان است. ما هیچجا حدس نمیزنیم که عدد فرستادهشده ریال بود یا تومان.
تاریخ
ذخیره و تبادل: میلادی ISO 8601. نمایش شمسی کار سمت شماست.
{ "date": "2026-10-03", "created_at": "2026-10-03T14:22:11+03:30" }
ولی هر مسیری که تاریخ میگیرد، شمسی را هم میپذیرد — به شرط اینکه صریح علامت بخورد:
{ "date": "1405-07-11", "date_calendar": "jalali" }
بدون date_calendar، تاریخ میلادی خوانده میشود.
ما تقویم را از شکل رشته حدس نمیزنیم:
1405-07-11 و 2026-10-03 هر دو
تاریخ معتبرند و اشتباه گرفتنشان ۶۲۱ سال خطا میدهد — خطایی
که در سال مالی، فورا سند را به سالی میفرستد که وجود ندارد.
شناسه بیرونی
هر منبع یک میدان external_id دارد: رشته آزاد، یکتا
در دامنه خودش. شناسه سفارش فروشگاه، شناسه کاربر، SKU — هر چیزی
که در سیستم خودتان کلید است، اینجا مینشیند.
GET /v1/sales-invoices?external_id=SHOP-884512
نتیجهاش این است که لازم نیست جدول نگاشت شناسهها را خودتان نگه
دارید. هر جا contact_id یا item_id
میپذیریم، یک شی با external_id هم میپذیریم.
تضمین یکبار
هر POSTی که سند میسازد باید هدر
Idempotency-Key داشته باشد. بدون آن،
400 idempotency_key_required میگیرید.
Idempotency-Key: ord-SHOP-884512
| وضعیت | چه میشود |
|---|---|
| کلید تازه | سند ساخته میشود، 201 |
| همان کلید، همان بدنه | همان پاسخ اول با هدر Idempotent-Replay: true — سند دوم ساخته نمیشود |
| همان کلید، بدنه متفاوت | 409 idempotency_key_reused |
| بدون کلید | 400 idempotency_key_required |
کلید ۲۴ ساعت نگه داشته میشود. این سختگیری عمدی است: مشکل شماره یک هر کانکتوری در دنیا این است که یک سفارش دو بار سند میخورد — و سند دوم را کسی تا پایان سال مالی نمیبیند.
سادهترین کلید درست، شماره سفارش خودتان است. اگر سیستم شما همان سفارش را دو بار بفرستد — چون تایماوت خورد، چون صف دوباره تلاش کرد، چون کاربر دو بار کلیک کرد — همان کلید میآید و ما سند دوم نمیزنیم.
صفحهبندی و صافی
مکاننما، نه skip:
GET /v1/sales-invoices?limit=50&cursor=eyJpZCI6MTg4MiwiZCI6IjIwMjYtMTAtMDEifQ
{
"data": [ ... ],
"meta": {
"limit": 50,
"has_more": true,
"next_cursor": "eyJpZCI6MTkzMiwiZCI6IjIwMjYtMDktMjgifQ"
}
}
limit پیشفرض ۵۰، سقف ۲۰۰.
چرا مکاننما و چرا skip را حتی بهعنوان گزینه
اضافه نمیکنیم: روی دفتری که همین حالا فاکتور میخورد،
skip=100 بین دو درخواست جابهجا میشود و
ردیفها یا جا میافتند یا دو بار میآیند. مکاننما به
محل اشاره میکند نه به شماره ردیف، پس درج
همزمان آن را خراب نمیکند.
صافی
?updated_after=2026-09-01T00:00:00%2B03:30
?status=posted&contact_id=1182
?from=2026-07-01&to=2026-09-30
?sort=-date,number
updated_after ستون فقرات هر همگامسازی است: هر
کانکتوری باید بتواند بپرسد «از آخرین بار چه عوض شد» — و این
تنها راهی است که اتصال شما بدون خواندن کل دفتر، تازه بماند.
خطاها
{
"error": {
"code": "document_posted",
"message": "سند ثبتشده تغییر نمیکند. برای اصلاح، سند برگشتی بزنید.",
"message_en": "A posted document is immutable. Issue a reversing entry instead.",
"field": null,
"doc": "https://sabzhesab.com/docs/api#err-document_posted",
"request_id": "req_01JA8X7KQ3M2"
}
}
هر خطا دوزبانه است، و doc به بخش توضیح همان
خطا در این صفحه لینک میدهد. request_id را در
گزارش به پشتیبانی بفرستید؛ با آن دقیقا همان درخواست را پیدا
میکنیم.
کدهای HTTP
| کد | معنا در این API |
|---|---|
200 | انجام شد |
201 | ساخته شد — Location به منبع تازه اشاره میکند |
202 | در صف — کارهای سنگین مثل ارسال مؤدیان |
400 | بدنه یا پارامتر نامعتبر |
401 | توکن نیست، منقضی است، یا باطل شده |
403 | توکن اختیار لازم را ندارد، یا طرح اشتراک این امکان را ندارد |
404 | منبع در این کسبوکار نیست — هرگز نشان نمیدهد که در کسبوکار دیگری هست |
409 | تضاد وضعیت: سند ثبتشده، سال بسته، کلید تکراری با بدنه متفاوت |
422 | قاعده حسابداری نقض شد — سند ناتراز، موجودی منفی بیمجوز |
429 | سقف نرخ — هدر Retry-After را بخوانید |
500 | خطای ما. request_id را بفرستید |
کدهای خطای دامنه
| کد | معنا |
|---|---|
document_posted | سند ثبتشده تغییرناپذیر است. اصلاح یعنی سند جدید |
document_unbalanced | جمع بدهکار با جمع بستانکار برابر نیست |
fiscal_year_closed | سال مالی بسته است |
fiscal_year_missing | تاریخ سند در هیچ سال مالی بازی نیست |
account_not_leaf | سند فقط روی حساب تفصیلی مینشیند، نه گروه و کل |
dimension_required | این حساب تفصیلی شناور اجباری دارد |
negative_stock_blocked | موجودی منفی در این انبار مجاز نیست |
item_not_stocked | کالای خدماتی حواله انبار نمیخورد |
invoice_paid | فاکتور تسویهشده حذف نمیشود |
moadian_already_sent | این فاکتور قبلا به مؤدیان رفته است |
entitlement_missing | طرح اشتراک این امکان را ندارد |
scope_missing | توکن این اختیار را ندارد |
limit_exceeded | سقف طرح (مثل تعداد فاکتور ماه) پر شده است |
idempotency_key_required | هدر Idempotency-Key نیامده |
idempotency_key_reused | همان کلید با بدنه متفاوت |
توکن شخصی (PAT)
راه پیشفرض، و برای اتصال خودتان به دفتر خودتان همین درست است. در پنل: تنظیمات ← رابط برنامهنویسی ← توکن تازه.
Authorization: Bearer shk_live_7Qx9...
پیشوند معنا دارد:
shk_live_ | دفتر واقعی — سند واقعی میزند |
shk_test_ | جعبهشن — هر شب پاک میشود |
اینکه از روی خود رشته بشود فهمید کدام است، یک اشتباه رایج را میبندد: توکن تولید که اشتباهی در فایل تست جا مانده.
هر توکن اینها را دارد: عنوان، فهرست اختیارات، تاریخ انقضا (پیشفرض یک سال)، و فهرست IP مجاز (اختیاری).
OAuth2
اگر افزونهای مینویسید که کاربرهای دیگر نصبش میکنند — افزونه ووکامرس، یک اپلیکیشن موبایل، یک سرویس واسط — این راه درست است. کاربر توکنش را به شما نمیدهد و هر وقت بخواهد دسترسی را قطع میکند.
authorization_code با PKCE:
GET /v1/oauth/authorize
?client_id=...
&redirect_uri=https://plugin.example.ir/callback
&scope=sales:write items:read
&code_challenge=...&code_challenge_method=S256
&state=...
POST /v1/oauth/token # grant_type=authorization_code
POST /v1/oauth/token # grant_type=refresh_token
POST /v1/oauth/revoke
access_token | یک ساعت |
refresh_token | نود روز، با چرخش در هر استفاده |
اختیارات
اختیارات دقیقا همان دامنههای دسترسی داخل پنلاند. یک مدل مجوز، دو مخاطب — پس API هیچوقت چیزی را اجازه نمیدهد که رابط کاربری نمیدهد.
| اختیار | چه چیزی باز میشود |
|---|---|
contacts:read · contacts:write | طرف حساب، گروه، فیلد سفارشی |
items:read · items:write | کالا و خدمات، گروه، تنوع، بارکد |
sales:read · sales:write | فاکتور فروش، پیشفاکتور، برگشت از فروش، میز فروش |
purchase:read · purchase:write | فاکتور خرید، برگشت از خرید |
inventory:read · inventory:write | رسید و حواله، انتقال، انبارگردانی، جمعآوری |
treasury:read · treasury:write | دریافت، پرداخت، هزینه، چک، صندوق و بانک |
accounting:read · accounting:write | سند حسابداری، کدینگ، تفصیلی شناور، سال مالی |
reports:read | همه گزارشهای مالی |
users:read | کاربران و نقشها |
settings:read · settings:write | تنظیمات کسبوکار، کارمند، حقوق |
inquiry:run پولی | استعلام کد ملی، شبا، کارت، کد پستی، چک صیادی |
webhooks:manage | ساخت و حذف وبهوک |
connectors:manage | کانکتور و تسویه مارکتپلیس |
inquiry:run تنها اختیاری است که با آن میتوان
هزینه به کسبوکار تحمیل کرد. هر استعلام یک تماس پولی
به سرویس بیرونی است. این اختیار جدا از خواندن است و در پنل
هشدار صریح دارد — به توکنی که لازمش ندارد ندهیدش.
سقف نرخ و طرح
API در طرح پایه و رشد نیست. در این دو طرح، هر مسیر
API با 403 entitlement_missing پاسخ میدهد — نه
۴۰۴، تا معلوم باشد مسیر درست است و فقط طرح کافی نیست.
| طرح | API | درخواست در دقیقه | انبوه در دقیقه |
|---|---|---|---|
| آزمایشی | ✓ | ۱۲۰ | ۱۰ |
| پایه | — | — | — |
| رشد | — | — | — |
| چندکاناله | ✓ | ۶۰۰ | ۶۰ |
| سازمانی | ✓ | ۱۲۰۰ | ۱۲۰ |
| لایسنس اختصاصی | ✓ | روی سرور خودتان — بیسقف | |
هدرها روی هر پاسخ:
RateLimit-Limit: 600
RateLimit-Remaining: 584
RateLimit-Reset: 37
Retry-After: 37 # فقط روی ۴۲۹
گزارشهای سنگین (ترازآزمایشی، ترازنامه، سود و زیان) سقف جداگانه دارند: ۱۰ در دقیقه. در عوض تعهدشان این است که ترازآزمایشی روی صد هزار سند زیر دو ثانیه برگردد.
جعبهشن
https://sandbox.sabzhesab.com/v1 — یک کسبوکار آزمایشی با کدینگ ایرانی و داده نمونه. هر شب از نو ساخته میشود.
- بدون سقف نرخ سخت — حلقه بزنید و خطا بسازید.
- استعلام هزینه ندارد و داده ثابت برمیگرداند.
- ارسال به مؤدیان شبیهسازی میشود و به سامانه واقعی نمیرود.
- وبهوکها واقعا فرستاده میشوند، پس امضا را همینجا بسنجید.
مسیرها — شناخت و فراداده
قاعده کلی هر منبع:
GET فهرست ·
POST ساخت ·
GET /{id} یک رکورد ·
PATCH اصلاح ·
DELETE حذف.
استثناها در جدولها علامت خوردهاند.
| مسیر | فعل | اختیار | توضیح |
|---|---|---|---|
| /v1/me | GET | — | کسبوکار، توکن، طرح، سقف، سال مالی باز |
| /v1/business | GETPATCH | settings | نام، کد اقتصادی، نشانی، سربرگ |
| /v1/plan | GET | — | امکانات و سقفهای طرح |
| /v1/users | GET | users:read | کاربران و نقشهایشان |
| /v1/openapi.json | GET | — | قرارداد ماشینخوان |
| /v1/health | GET | — | سلامت سرویس — بدون احراز هویت |
طرف حساب
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/contacts | GETPOST | contacts |
| /v1/contacts/{id} | GETPATCHDELETE | contacts |
| /v1/contacts/batch | POST | contacts:write |
| /v1/contacts/{id}/statement | GET | contacts:read |
| /v1/contacts/{id}/balance | GET | contacts:read |
| /v1/contacts/{id}/share-link | POST | contacts:read |
| /v1/contact-groups | GETPOST | contacts |
| /v1/contact-fields | GETPOST | contacts |
میدانها
id · external_id · code ·
name · kind · is_customer ·
is_vendor · national_code ·
economic_code · registration_number ·
tax_type · phone · mobile ·
email · province · city ·
address · postal_code ·
credit_limit · opening_balance ·
group_id · labels ·
custom_fields · note ·
active · created_at ·
updated_at
code اختیاری است؛ اگر نفرستید خودمان بر اساس
کدینگ کسبوکار میسازیمش. ولی external_id را
شما میسازید و همان است که دو سیستم را به هم میبندد.
کالا و خدمات
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/items | GETPOST | items |
| /v1/items/{id} | GETPATCHDELETE | items |
| /v1/items/batch | POST | items:write |
| /v1/items/by-barcode/{barcode} | GET | items:read |
| /v1/items/{id}/variants | GETPOST | items |
| /v1/items/{id}/price-lists | GETPUT | items |
| /v1/items/stock | GET | inventory:read |
| /v1/items/{id}/stock | GET | inventory:read |
| /v1/item-groups | GETPOST | items |
| /v1/units | GET | items:read |
| /v1/labels | GETPOST | items |
میدانها
id · external_id · code ·
name · kind · barcode ·
unit · sub_unit ·
conversion_factor · buy_price ·
sell_price · price_lists ·
tax_code · tax_rate ·
group_id · track_stock ·
track_serial · reorder_point ·
variants · labels ·
custom_fields · active
tax_code همان شناسه کالای سامانه مؤدیان است. اگر
قرار است فاکتورهایتان به مؤدیان برود، این میدان را خالی
نگذارید — وگرنه ارسال در همان سامانه رد میشود، نه اینجا.
فروش
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/sales-invoices | GETPOST | sales |
| /v1/sales-invoices/{id} | GETPATCHDELETE | sales |
| /v1/sales-invoices/{id}/post | POST | sales:write |
| /v1/sales-invoices/{id}/payment | POST | treasury:write |
| /v1/sales-invoices/{id}/share-link | POST | sales:read |
| /v1/sales-invoices/{id}/pdf | GET | sales:read |
| /v1/sales-invoices/{id}/journal-entry | GET | accounting:read |
| /v1/sales-invoices/{id}/moadian | GETPOST | sales |
| /v1/quotations | GETPOST | sales |
| /v1/quotations/{id} | GETPATCHDELETE | sales |
| /v1/quotations/{id}/convert | POST | sales:write |
| /v1/sales-returns | GETPOST | sales |
| /v1/sales-returns/{id} | GETDELETE | sales |
| /v1/installments | GETPOST | sales |
| /v1/sales-agents | GET | sales:read |
| /v1/desk/sales | POST | sales:write |
نمونه کامل ساخت فاکتور، همراه توضیح هر میدان، در آموزش گامبهگام گام چهارم است.
اصلاح فاکتور ثبتشده با PATCH ممکن نیست.
سند ثبتشده تغییرناپذیر است و این را تریگر دیتابیس تضمین
میکند، نه کد اپلیکیشن — پس هیچ مسیری نمیتواند دورش بزند.
برای اصلاح، سند جدید بزنید:
POST /v1/sales-returns برای برگشت از فروش، یا
POST /v1/accounting/journal-entries/{id}/reverse
برای سند برگشتی.
خرید
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/purchase-invoices | GETPOST | purchase |
| /v1/purchase-invoices/{id} | GETPATCHDELETE | purchase |
| /v1/purchase-invoices/{id}/post | POST | purchase:write |
| /v1/purchase-invoices/{id}/payment | POST | treasury:write |
| /v1/purchase-returns | GETPOST | purchase |
| /v1/purchase-returns/{id} | GETDELETE | purchase |
انبار
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/warehouses | GETPOST | inventory |
| /v1/stock-documents | GETPOST | inventory |
| /v1/stock-documents/{id} | GETDELETE | inventory |
| /v1/stock-transfers | GETPOST | inventory |
| /v1/stock-balance | GET | inventory:read |
| /v1/stock-ledger | GET | inventory:read |
| /v1/stocktakes | GETPOST | inventory |
| /v1/stocktakes/{id}/count | POST | inventory:write |
| /v1/stocktakes/{id}/finish | POST | inventory:write |
| /v1/picking-lists | GETPOST | inventory |
| /v1/picking-lists/{id}/pick | POST | inventory:write |
| /v1/shelves | GETPOST | inventory |
| /v1/serials | GET | inventory:read |
| /v1/production-orders | GETPOST | inventory |
/v1/stock-ledger کاردکس است: هر ورود و خروج با بهای
تمامشده. اینجا جایی است که بهایابی موجودی منفی دیده میشود —
اگر کالایی منفی رفته و بعد رسید خورده، سند اصلاح بهای تمامشده
در همین دفتر ظاهر میشود.
خزانه
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/receipts | GETPOST | treasury |
| /v1/receipts/{id} | GETDELETE | treasury |
| /v1/payments | GETPOST | treasury |
| /v1/payments/{id} | GETDELETE | treasury |
| /v1/expenses | GETPOST | treasury |
| /v1/money-transfers | GETPOST | treasury |
| /v1/money-places | GETPOST | treasury |
| /v1/cheques | GETPOST | treasury |
| /v1/cheques/{id}/act | POST | treasury:write |
| /v1/payable-cheques | GETPOST | treasury |
| /v1/bank-statements | POST | treasury:write |
| /v1/bank-statements/{id}/match | POST | treasury:write |
money-places همان صندوق، بانک و تنخواه است — در
سبزحساب یک مفهوم با سه نوع، نه سه مسیر جدا. یک میدان
kind و یک شناسه، تمام. رقبا برای همین کار سه
پارامتر موازی میگیرند و شما باید حدس بزنید کدام را پر کنید.
POST /v1/cheques/{id}/act رویدادهای چرخه چک را
میگیرد: deposit · clear ·
bounce · transfer ·
return · cancel.
حسابداری
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/accounting/journal-entries | GETPOST | accounting |
| /v1/accounting/journal-entries/{id} | GETDELETE | accounting |
| /v1/accounting/journal-entries/{id}/post | POST | accounting:write |
| /v1/accounting/journal-entries/{id}/reverse | POST | accounting:write |
| /v1/accounting/accounts | GETPOST | accounting |
| /v1/accounting/accounts/{id} | GETPATCH | accounting |
| /v1/accounting/dimensions | GETPOST | accounting |
| /v1/accounting/dimension-types | GET | accounting:read |
| /v1/accounting/fiscal-years | GETPOST | accounting |
| /v1/accounting/fiscal-years/{id}/close | POST | accounting:write |
| /v1/accounting/fiscal-years/{id}/reopen | POST | accounting:write |
| /v1/accounting/currencies | GETPOST | accounting |
| /v1/accounting/opening-entry | GETPUT | accounting |
کدینگ چهارسطحی
کدینگ ایرانی چهار سطح دارد: گروه، کل، معین، تفصیلی. سند فقط روی سطح آخر مینشیند.
{
"data": [
{
"id": 2041,
"code": "1101-01-0007",
"name": "بانک ملت — جاری ۱۲۳",
"level": "detail",
"level_number": 4,
"parent_id": 118,
"path": "1/11/1101/1101-01-0007",
"nature": "debit",
"kind": "asset",
"dimension_types": ["project"],
"postable": true
}
]
}
سند فقط روی postable: true مینشیند. ارسال سند
روی گروه یا کل، 422 account_not_leaf میگیرد.
و اگر حساب dimension_types دارد، هر سطر سند
باید تفصیلی شناور همان نوع را همراه داشته باشد — وگرنه
422 dimension_required.
سند دستی
{
"date": "2026-10-03",
"description": "تسعیر ارز پایان مهر",
"lines": [
{
"account_id": 2041,
"debit": "48500000.0000",
"credit": "0.0000",
"description": "سود تسعیر",
"dimensions": { "project": 14 }
},
{
"account_id": 3310,
"debit": "0.0000",
"credit": "48500000.0000"
}
]
}
سند ناتراز 422 document_unbalanced میگیرد — و
این را تریگر دیتابیس میگوید، نه اعتبارسنجی لاراول.
یعنی حتی اگر روزی مسیری تازه اعتبارسنجی را جا بیندازد، سند
ناتراز ثبت نمیشود.
گزارش
همه گزارشها reports:read
میخواهند، Accept: text/csv هم میپذیرند، و سقف
نرخشان ۱۰ در دقیقه است.
| مسیر | پارامتر | نیازمند |
|---|---|---|
| /v1/reports/trial-balance | from to level dimension | — |
| /v1/reports/balance-sheet | as_of compare_to | — |
| /v1/reports/income-statement | from to compare_to | — |
| /v1/reports/journal | from to cursor | — |
| /v1/reports/general-ledger | account_id from to | — |
| /v1/reports/subsidiary-ledger | account_id from to | — |
| /v1/reports/receivables | as_of contact_id | — |
| /v1/reports/payables | as_of contact_id | — |
| /v1/reports/aging | as_of buckets | — |
| /v1/reports/inventory | as_of warehouse_id | — |
| /v1/reports/inventory-valuation | as_of warehouse_id | — |
| /v1/reports/cash-flow | from to | — |
| /v1/reports/sales-by-item | from to group_by | — |
| /v1/reports/sales-by-contact | from to | — |
| /v1/reports/profit-by-channel | from to | گزارش کانال |
| /v1/reports/vat | season year | مالیات فصلی |
| /v1/reports/account-turnover | account_id from to | — |
مالیات و مؤدیان
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/moadian/invoices | GET | sales:read |
| /v1/moadian/invoices/{id}/send | POST | sales:write |
| /v1/moadian/invoices/{id}/status | GET | sales:read |
| /v1/moadian/invoices/{id}/cancel | POST | sales:write |
| /v1/moadian/fiscal-info | GET | settings:read |
| /v1/moadian/item-codes | GET | items:read |
ارسال به مؤدیان 202 میدهد، نه
200 — کار به صف میرود و نتیجهاش با وبهوک
moadian.invoice.accepted یا
moadian.invoice.rejected برمیگردد.
منتظر ماندن روی پاسخ همگام، در سامانهای که خودش کند و ناپایدار است، یعنی بستن اتصال شما. پس ما منتظر نمیمانیم و شما هم نباید.
حقوق و دستمزد
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/employees | GETPOST | settings |
| /v1/employees/{id} | GETPATCH | settings |
| /v1/payrolls | GETPOST | settings |
| /v1/payrolls/{id} | GET | settings:read |
| /v1/payrolls/{id}/payslips | GET | settings:read |
| /v1/payrolls/{id}/insurance-file | GET | settings:read |
insurance-file فایل لیست بیمه تأمین اجتماعی را با
فرمت خود سازمان میدهد — آماده بارگذاری، بدون تبدیل دستی.
استعلام پولی
هر تماس به این مسیرها پول خرج میکند. اختیار
inquiry:run را فقط به توکنی بدهید که واقعا
لازمش دارد، و قبل از هر حلقهای اعتبار باقیمانده را
بپرسید.
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/inquiries/credit | GET | inquiry:read |
| /v1/inquiries/national-code | POST | inquiry:run |
| /v1/inquiries/mobile-match | POST | inquiry:run |
| /v1/inquiries/iban | POST | inquiry:run |
| /v1/inquiries/card | POST | inquiry:run |
| /v1/inquiries/card-to-iban | POST | inquiry:run |
| /v1/inquiries/postal-code | POST | inquiry:run |
| /v1/inquiries/economic-code | POST | inquiry:run |
| /v1/inquiries/sayad-cheque | POST | inquiry:run |
هر پاسخ cost و credit_remaining دارد،
پس هزینه را همانجا میبینید.
GET /v1/inquiries/credit اعتبار را
بدون خرج کردن میدهد.
پیوست و بایگانی
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/attachments | POST | دامنه سند مقصد |
| /v1/attachments/{id} | GETDELETE | دامنه سند مقصد |
multipart/form-data با attachable_type
و attachable_id. سقف ۲۰ مگابایت.
اختیار لازم از سند مقصد میآید، نه از یک اختیار جدا: پیوست یک فاکتور خرید، purchase:write میخواهد. پیوستی که اختیار خودش داشته باشد، راهی میشود برای چسباندن فایل به سندی که اجازه دیدنش را ندارید.
کانکتور و مارکتپلیس
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/connectors | GET | connectors:manage |
| /v1/connectors/{id}/sync | POST | connectors:manage |
| /v1/connectors/{id}/events | GET | connectors:manage |
| /v1/item-mappings | GETPOSTDELETE | connectors:manage |
| /v1/external-orders | POST | sales:write |
| /v1/settlements | GETPOST | connectors:manage |
| /v1/settlements/{id}/reconcile | POST | connectors:manage |
| /v1/settlements/{id}/variances | GET | connectors:manage |
دو راه برای وصل کردن یک فروشگاه
| راه | برای کی | چه میکند |
|---|---|---|
| POST /v1/external-orders | فروشگاهی که نمیخواهد منطق حسابداری بداند | سفارش خام را میفرستید؛ سبزحساب خودش طرف حساب، فاکتور، سند و حواله انبار را میسازد |
| POST /v1/sales-invoices | کسی که میخواهد خودش کنترل کند | شما تعیین میکنید چه حسابی، چه انباری، چه کانالی، پیشنویس یا قطعی |
/v1/settlements تسویه مارکتپلیس را میگیرد، و
variances مغایرت را نشان میدهد. مبلغ واریزی
مارکتپلیس هیچوقت با جمع فاکتورها برابر نیست — کمیسیون، جریمه
و مرجوعی وسط است — و پیدا کردن همان اختلاف، کاری است که دستی
روزها وقت میبرد.
وبهوک
| مسیر | فعل | اختیار |
|---|---|---|
| /v1/webhooks | GETPOST | webhooks:manage |
| /v1/webhooks/{id} | GETPATCHDELETE | webhooks:manage |
| /v1/webhooks/{id}/test | POST | webhooks:manage |
| /v1/webhooks/{id}/deliveries | GET | webhooks:manage |
| /v1/webhooks/deliveries/{id}/retry | POST | webhooks:manage |
هدرهای هر پیام
SabzHesab-Event: sales_invoice.posted
SabzHesab-Delivery: dlv_01JA8X7KQ3M2
SabzHesab-Timestamp: 1791038531
SabzHesab-Signature: v1=5f4dcc3b5aa765d61d8327deb882cf99
امضا HMAC-SHA256 روی رشته
timestamp + "." + raw_body با
secret است. نمونه کد سنجش امضا در
آموزش گام پنجم است.
تلاش مجدد
روی هر پاسخ غیر 2xx: هشت بار با عقبنشینی نمایی —
۱۰ ثانیه، ۳۰ ثانیه، ۲ دقیقه، ۱۰ دقیقه، ۳۰ دقیقه، ۲ ساعت،
۶ ساعت، ۲۴ ساعت. بعدش وبهوک خودکار غیرفعال میشود و ایمیل
اطلاع میرود.
تحویل حداقل یک بار است، نه دقیقا یک بار. اگر شبکه وسط راه قطع شود، ما نمیدانیم پیام رسید یا نه — پس دوباره میفرستیم.
SabzHesab-Delivery را نگه دارید و تکراری را
بیندازید دور. این همان کاری است که
Idempotency-Key در جهت مخالف میکند.
فهرست رویدادها
| گروه | رویداد |
|---|---|
| فروش | sales_invoice.created sales_invoice.posted sales_invoice.paid sales_invoice.deleted sales_return.posted quotation.accepted |
| خرید | purchase_invoice.created purchase_invoice.posted purchase_invoice.paid purchase_return.posted |
| خزانه | receipt.created payment.created cheque.registered cheque.cleared cheque.bounced expense.created |
| انبار | stock_document.posted stock_transfer.posted stock.low stock.negative stocktake.finished |
| حسابداری | journal_entry.posted journal_entry.reversed fiscal_year.closed |
| طرف حساب | contact.created contact.updated contact.credit_exceeded |
| کالا | item.created item.updated item.price_changed |
| مؤدیان | moadian.invoice.accepted moadian.invoice.rejected |
| کانکتور | connector.sync.finished connector.sync.failed settlement.variance_found |
| اشتراک | subscription.suspended subscription.renewed limit.approaching |
stock.negative عمدا رویداد است، نه خطا. موجودی
منفی در سبزحساب گاهی مجاز است و بهایش بعدا اصلاح میشود —
ولی کسی که انبار را از بیرون میخواند باید بداند کی این
اتفاق افتاد.
نسخهبندی
-
نسخه در مسیر است:
/v1/. هیچ نسخهای در هدر پنهان نمیشود، چون نسخه پنهان یعنی توسعهدهنده نمیداند با کدام قرارداد حرف میزند. - تغییر ناشکننده (میدان تازه، مقدار تازه در فهرستی که مستند شده باز است، مسیر تازه) بدون نسخه جدید میآید. مشتری شما باید میدان ناشناخته را نادیده بگیرد.
- تغییر شکننده (حذف یا تغییر نام میدان، تغییر نوع، سختتر شدن اعتبارسنجی) نسخه تازه میخواهد.
-
نسخه قدیم حداقل ۱۲ ماه پس از اعلام نسخه تازه زنده
میماند، با هدر
Sunsetو تاریخ دقیق.
SDK
| زبان | وضعیت |
|---|---|
| PHP (کامپوزر) | برنامهریزیشده |
| TypeScript و JavaScript | برنامهریزیشده |
| Python | برنامهریزیشده |
| افزونه ووکامرس | برنامهریزیشده |
هر سه SDK از همان openapi.json تولید میشوند — یعنی
سند و کد نمیتوانند از هم جدا بیفتند. افزونه ووکامرس بدون یک خط
کد از پنل وردپرس کار میکند.
مقایسه با نرمافزارهای دیگر
API حسابفا، سپیدار و ابرآیرون را واقعا خواندیم — خط به خط، نه از روی بروشور. این جدول خلاصه آن است، و دلیل هر تصمیمی که بالاتر خواندید.
| سبزحساب | حسابفا | سپیدار | ابرآیرون | |
|---|---|---|---|---|
| سبک | REST با فعل درست | همهچیز POST | REST | نامعلوم |
| قرارداد ماشینخوان | OpenAPI 3.1 | ندارد | OpenAPI 3.0 | Postman |
| نسخه در مسیر | /v1/ | /v1/ | در هدر پنهان | نامعلوم |
| احراز هویت | PAT + OAuth2 با PKCE | apiKey + رمز در بدنه | ثبت دستگاه با RSA + JWT | نامعلوم |
| اختیار دقیق | ۲۲ اختیار، برابر مجوز پنل | توکن همهکاره | کاربر همهکاره | نامعلوم |
| محل اجرا | ابری | ابری | localhost — IP ثابت لازم است | ابری |
| وبهوک | با HMAC، پنجره زمانی و ۸ تلاش | هوک بدون امضا | ندارد — همهچیز Pull | دارد، سندش منتشر نیست |
| تضمین یکبار | اجباری روی هر سند | اختیاری | ندارد | نامعلوم |
| صفحهبندی | مکاننما | take و skip | صفحه شمارهای | نامعلوم |
| پول | رشته decimal، ریال | عدد اعشاری | عدد اعشاری | نامعلوم |
| تاریخ شمسی در ورودی | با علامت صریح | فقط میلادی | فقط میلادی | نامعلوم |
| سقف نرخ | لایهبندی بر اساس طرح | ۲۴۰ یکسان برای همه | سرور خودتان | نامعلوم |
| جعبهشن | جدا، هر شب تازه | ندارد | ندارد | ندارد |
«نامعلوم» یعنی سندش عمومی نیست، نه اینکه ندارد. مستندات ابرآیرون روی Postman است و بدون دسترسی خواندنی نیست — و همین خودش یک تفاوت است: قراردادی که نمیتوانید قبل از خرید بخوانید، قرارداد نیست.
اگر جایی از این جدول را اشتباه خواندهایم، بگویید. ما ترجیح میدهیم جدول را درست کنیم تا اینکه رویش حساب باز کنید و بعد نخواند.