سبزحساب پورتال توسعه‌دهنده

رابط برنامه‌نویسی سبزحساب

یک REST نسخه‌دار که دفتر حسابداری شما را به هر سیستم دیگری وصل می‌کند: فروشگاه اینترنتی، مارکت‌پلیس، باشگاه مشتریان، یا هر چیزی که خودتان نوشته‌اید. قرارداد کامل است، واحد پول ریال است و تاریخ هم شمسی می‌پذیرد.

این قرارداد منتشرشده است، ولی هنوز زنده نیست. سبزحساب در مرحله آماده‌سازی برای عرضه است و api.sabzhesab.com در فاز کانکتور باز می‌شود. این صفحه را عمدا پیش از نوشتن کد منتشر کرده‌ایم تا اگر جایی از قرارداد با کار شما نمی‌خواند، همین حالا بگویید — بعد از انتشار، تغییر دادنش برای شما هزینه دارد.

از کجا شروع کنید

کل این صفحه یک قرارداد را توضیح می‌دهد، و همان قرارداد به شکل ماشین‌خوان هم منتشر می‌شود: OpenAPI 3.1 روی /v1/openapi.json. هر سه SDK رسمی از همان فایل تولید می‌شوند، پس سند و کد نمی‌توانند از هم جدا بیفتند.

آموزش گام‌به‌گام — وصل کردن یک فروشگاه

سناریو ساده است: فروشگاه اینترنتی دارید و می‌خواهید هر سفارش تأییدشده خودش در سبزحساب فاکتور و سند بخورد، موجودی انبار به فروشگاه برگردد، و اگر چکی برگشت خورد خبردار شوید.

  1. توکن بگیرید

    در پنل بروید به تنظیمات ← رابط برنامه‌نویسی ← توکن تازه. اختیارات لازم همین سناریو اینهاست: sales:write items:read inventory:read contacts:write webhooks:manage

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

    🔴

    توکن فقط یک بار نشان داده می‌شود. ما خودمان هم بعدش نمی‌توانیم بخوانیمش، چون هش argon2id ذخیره می‌شود. اگر گمش کردید باید باطل کنید و تازه بسازید — این یک محدودیت نیست، تنها شکل درست نگه داشتن کلیدی است که به دفتر مالی شما وصل است.

  2. اتصال را بسنجید

    اولین تماس هر اتصالی باید /v1/me باشد. با یک درخواست، اختیارات توکن، طرح اشتراک، سقف‌ها و سال مالی باز را یک‌جا می‌گیرید.

    bash
    curl -s https://api.sabzhesab.com/v1/me \
      -H "Authorization: Bearer shk_live_7Qx9..."
    json
    {
      "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 می‌گیرد. این را همین گام اول ببینید، نه وسط شب و وسط صد سفارش.

  3. کالاها را نگاشت کنید

    فروشگاه شما SKU دارد، سبزحساب کد کالا. برای اینکه مجبور نباشید نقشه شناسه‌ها را جایی نگه دارید، هر کالا یک external_id می‌گیرد — و از آن به بعد با همان صدایش می‌زنید.

    http
    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" }
      ]
    }

    بعدش موجودی را یک‌جا بخوانید — نه کالا به کالا. هر فروشگاهی این را هر چند دقیقه می‌پرسد و یک درخواست به‌جای سیصد درخواست، تفاوت بین ماندن و خوردن به سقف نرخ است:

    http
    GET /v1/items/stock?external_ids=SKU-4410,SKU-4411&warehouse_id=3
  4. سفارش را فاکتور کنید

    این گام قلب اتصال است. سه چیز در آن هست که کار را ساده می‌کند: طرف حساب را لازم نیست از قبل بسازید، تاریخ را می‌توانید شمسی بفرستید، و شماره سفارش خودتان نقش Idempotency-Key را بازی می‌کند.

    http
    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" }
      ]
    }
    json · 201 Created
    {
      "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 بزنید. اصلاح فاکتور ثبت‌شده فقط با سند جدید انجام می‌شود: برگشت از فروش، یا سند برگشتی.

  5. وبهوک را ثبت کنید

    تا اینجا شما به سبزحساب حرف زدید. این گام برعکسش است: سبزحساب به شما خبر می‌دهد. بدون این، مجبورید هر چند دقیقه بپرسید «چیزی عوض شد؟» — و همان چیزی است که سقف نرخ را می‌خورد.

    http
    POST /v1/webhooks
    
    {
      "url": "https://shop.example.ir/hooks/sabzhesab",
      "events": ["sales_invoice.paid", "cheque.bounced", "stock.low"]
    }

    پاسخ یک secret دارد که فقط همان یک بار می‌آید. با آن امضای هر پیام را بسنجید:

    php
    $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 نیست.

json
{ "amount": "125000.0000", "currency": "IRR" }

چرا رشته و نه عدد: JSON.parse("125000.0001") در جاوااسکریپت یک double می‌دهد، و double در حسابداری ریال ایران — جایی که مبالغ به‌سادگی از دو به توان پنجاه‌وسه عبور می‌کنند — ریال گم می‌کند. یک ریال گم‌شده در یک سند، یک سند ناتراز است.

تومان در API وجود ندارد. تومان لایه نمایش است؛ اگر رابط شما تومان نشان می‌دهد، تقسیم بر ۱۰ کار خودتان است. ما هیچ‌جا حدس نمی‌زنیم که عدد فرستاده‌شده ریال بود یا تومان.

تاریخ

ذخیره و تبادل: میلادی ISO 8601. نمایش شمسی کار سمت شماست.

json
{ "date": "2026-10-03", "created_at": "2026-10-03T14:22:11+03:30" }

ولی هر مسیری که تاریخ می‌گیرد، شمسی را هم می‌پذیرد — به شرط اینکه صریح علامت بخورد:

json
{ "date": "1405-07-11", "date_calendar": "jalali" }
ℹ

بدون date_calendar، تاریخ میلادی خوانده می‌شود. ما تقویم را از شکل رشته حدس نمی‌زنیم: 1405-07-11 و 2026-10-03 هر دو تاریخ معتبرند و اشتباه گرفتنشان ۶۲۱ سال خطا می‌دهد — خطایی که در سال مالی، فورا سند را به سالی می‌فرستد که وجود ندارد.

شناسه بیرونی

هر منبع یک میدان external_id دارد: رشته آزاد، یکتا در دامنه خودش. شناسه سفارش فروشگاه، شناسه کاربر، SKU — هر چیزی که در سیستم خودتان کلید است، اینجا می‌نشیند.

http
GET /v1/sales-invoices?external_id=SHOP-884512

نتیجه‌اش این است که لازم نیست جدول نگاشت شناسه‌ها را خودتان نگه دارید. هر جا contact_id یا item_id می‌پذیریم، یک شی با external_id هم می‌پذیریم.

تضمین یک‌بار

🔴

هر POSTی که سند می‌سازد باید هدر Idempotency-Key داشته باشد. بدون آن، 400 idempotency_key_required می‌گیرید.

http
Idempotency-Key: ord-SHOP-884512
وضعیتچه می‌شود
کلید تازه سند ساخته می‌شود، 201
همان کلید، همان بدنه همان پاسخ اول با هدر Idempotent-Replay: true — سند دوم ساخته نمی‌شود
همان کلید، بدنه متفاوت 409 idempotency_key_reused
بدون کلید 400 idempotency_key_required

کلید ۲۴ ساعت نگه داشته می‌شود. این سخت‌گیری عمدی است: مشکل شماره یک هر کانکتوری در دنیا این است که یک سفارش دو بار سند می‌خورد — و سند دوم را کسی تا پایان سال مالی نمی‌بیند.

✓

ساده‌ترین کلید درست، شماره سفارش خودتان است. اگر سیستم شما همان سفارش را دو بار بفرستد — چون تایم‌اوت خورد، چون صف دوباره تلاش کرد، چون کاربر دو بار کلیک کرد — همان کلید می‌آید و ما سند دوم نمی‌زنیم.

صفحه‌بندی و صافی

مکان‌نما، نه skip:

http
GET /v1/sales-invoices?limit=50&cursor=eyJpZCI6MTg4MiwiZCI6IjIwMjYtMTAtMDEifQ
json
{
  "data": [ ... ],
  "meta": {
    "limit": 50,
    "has_more": true,
    "next_cursor": "eyJpZCI6MTkzMiwiZCI6IjIwMjYtMDktMjgifQ"
  }
}

limit پیش‌فرض ۵۰، سقف ۲۰۰.

🔴

چرا مکان‌نما و چرا skip را حتی به‌عنوان گزینه اضافه نمی‌کنیم: روی دفتری که همین حالا فاکتور می‌خورد، skip=100 بین دو درخواست جابه‌جا می‌شود و ردیف‌ها یا جا می‌افتند یا دو بار می‌آیند. مکان‌نما به محل اشاره می‌کند نه به شماره ردیف، پس درج همزمان آن را خراب نمی‌کند.

صافی

http
?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 ستون فقرات هر همگام‌سازی است: هر کانکتوری باید بتواند بپرسد «از آخرین بار چه عوض شد» — و این تنها راهی است که اتصال شما بدون خواندن کل دفتر، تازه بماند.

خطاها

json · 409 Conflict
{
  "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)

راه پیش‌فرض، و برای اتصال خودتان به دفتر خودتان همین درست است. در پنل: تنظیمات ← رابط برنامه‌نویسی ← توکن تازه.

http
Authorization: Bearer shk_live_7Qx9...

پیشوند معنا دارد:

shk_live_دفتر واقعی — سند واقعی می‌زند
shk_test_جعبه‌شن — هر شب پاک می‌شود

اینکه از روی خود رشته بشود فهمید کدام است، یک اشتباه رایج را می‌بندد: توکن تولید که اشتباهی در فایل تست جا مانده.

هر توکن اینها را دارد: عنوان، فهرست اختیارات، تاریخ انقضا (پیش‌فرض یک سال)، و فهرست IP مجاز (اختیاری).

OAuth2

اگر افزونه‌ای می‌نویسید که کاربرهای دیگر نصبش می‌کنند — افزونه ووکامرس، یک اپلیکیشن موبایل، یک سرویس واسط — این راه درست است. کاربر توکنش را به شما نمی‌دهد و هر وقت بخواهد دسترسی را قطع می‌کند.

authorization_code با PKCE:

http
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درخواست در دقیقهانبوه در دقیقه
آزمایشی✓۱۲۰۱۰
پایه———
رشد———
چندکاناله✓۶۰۰۶۰
سازمانی✓۱۲۰۰۱۲۰
لایسنس اختصاصی✓روی سرور خودتان — بی‌سقف

هدرها روی هر پاسخ:

http
RateLimit-Limit: 600
RateLimit-Remaining: 584
RateLimit-Reset: 37
Retry-After: 37          # فقط روی ۴۲۹

گزارش‌های سنگین (ترازآزمایشی، ترازنامه، سود و زیان) سقف جداگانه دارند: ۱۰ در دقیقه. در عوض تعهدشان این است که ترازآزمایشی روی صد هزار سند زیر دو ثانیه برگردد.

جعبه‌شن

https://sandbox.sabzhesab.com/v1 — یک کسب‌وکار آزمایشی با کدینگ ایرانی و داده نمونه. هر شب از نو ساخته می‌شود.

  • بدون سقف نرخ سخت — حلقه بزنید و خطا بسازید.
  • استعلام هزینه ندارد و داده ثابت برمی‌گرداند.
  • ارسال به مؤدیان شبیه‌سازی می‌شود و به سامانه واقعی نمی‌رود.
  • وبهوک‌ها واقعا فرستاده می‌شوند، پس امضا را همین‌جا بسنجید.

مسیرها — شناخت و فراداده

قاعده کلی هر منبع: GET فهرست · POST ساخت · GET /{id} یک رکورد · PATCH اصلاح · DELETE حذف. استثناها در جدول‌ها علامت خورده‌اند.

مسیرفعلاختیارتوضیح
/v1/meGET—کسب‌وکار، توکن، طرح، سقف، سال مالی باز
/v1/businessGETPATCHsettingsنام، کد اقتصادی، نشانی، سربرگ
/v1/planGET—امکانات و سقف‌های طرح
/v1/usersGETusers:readکاربران و نقش‌هایشان
/v1/openapi.jsonGET—قرارداد ماشین‌خوان
/v1/healthGET—سلامت سرویس — بدون احراز هویت

طرف حساب

مسیرفعلاختیار
/v1/contactsGETPOSTcontacts
/v1/contacts/{id}GETPATCHDELETEcontacts
/v1/contacts/batchPOSTcontacts:write
/v1/contacts/{id}/statementGETcontacts:read
/v1/contacts/{id}/balanceGETcontacts:read
/v1/contacts/{id}/share-linkPOSTcontacts:read
/v1/contact-groupsGETPOSTcontacts
/v1/contact-fieldsGETPOSTcontacts

میدان‌ها

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/itemsGETPOSTitems
/v1/items/{id}GETPATCHDELETEitems
/v1/items/batchPOSTitems:write
/v1/items/by-barcode/{barcode}GETitems:read
/v1/items/{id}/variantsGETPOSTitems
/v1/items/{id}/price-listsGETPUTitems
/v1/items/stockGETinventory:read
/v1/items/{id}/stockGETinventory:read
/v1/item-groupsGETPOSTitems
/v1/unitsGETitems:read
/v1/labelsGETPOSTitems

میدان‌ها

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-invoicesGETPOSTsales
/v1/sales-invoices/{id}GETPATCHDELETEsales
/v1/sales-invoices/{id}/postPOSTsales:write
/v1/sales-invoices/{id}/paymentPOSTtreasury:write
/v1/sales-invoices/{id}/share-linkPOSTsales:read
/v1/sales-invoices/{id}/pdfGETsales:read
/v1/sales-invoices/{id}/journal-entryGETaccounting:read
/v1/sales-invoices/{id}/moadianGETPOSTsales
/v1/quotationsGETPOSTsales
/v1/quotations/{id}GETPATCHDELETEsales
/v1/quotations/{id}/convertPOSTsales:write
/v1/sales-returnsGETPOSTsales
/v1/sales-returns/{id}GETDELETEsales
/v1/installmentsGETPOSTsales
/v1/sales-agentsGETsales:read
/v1/desk/salesPOSTsales:write

نمونه کامل ساخت فاکتور، همراه توضیح هر میدان، در آموزش گام‌به‌گام گام چهارم است.

🔴

اصلاح فاکتور ثبت‌شده با PATCH ممکن نیست. سند ثبت‌شده تغییرناپذیر است و این را تریگر دیتابیس تضمین می‌کند، نه کد اپلیکیشن — پس هیچ مسیری نمی‌تواند دورش بزند.

برای اصلاح، سند جدید بزنید: POST /v1/sales-returns برای برگشت از فروش، یا POST /v1/accounting/journal-entries/{id}/reverse برای سند برگشتی.

خرید

مسیرفعلاختیار
/v1/purchase-invoicesGETPOSTpurchase
/v1/purchase-invoices/{id}GETPATCHDELETEpurchase
/v1/purchase-invoices/{id}/postPOSTpurchase:write
/v1/purchase-invoices/{id}/paymentPOSTtreasury:write
/v1/purchase-returnsGETPOSTpurchase
/v1/purchase-returns/{id}GETDELETEpurchase

انبار

مسیرفعلاختیار
/v1/warehousesGETPOSTinventory
/v1/stock-documentsGETPOSTinventory
/v1/stock-documents/{id}GETDELETEinventory
/v1/stock-transfersGETPOSTinventory
/v1/stock-balanceGETinventory:read
/v1/stock-ledgerGETinventory:read
/v1/stocktakesGETPOSTinventory
/v1/stocktakes/{id}/countPOSTinventory:write
/v1/stocktakes/{id}/finishPOSTinventory:write
/v1/picking-listsGETPOSTinventory
/v1/picking-lists/{id}/pickPOSTinventory:write
/v1/shelvesGETPOSTinventory
/v1/serialsGETinventory:read
/v1/production-ordersGETPOSTinventory

/v1/stock-ledger کاردکس است: هر ورود و خروج با بهای تمام‌شده. اینجا جایی است که بهایابی موجودی منفی دیده می‌شود — اگر کالایی منفی رفته و بعد رسید خورده، سند اصلاح بهای تمام‌شده در همین دفتر ظاهر می‌شود.

خزانه

مسیرفعلاختیار
/v1/receiptsGETPOSTtreasury
/v1/receipts/{id}GETDELETEtreasury
/v1/paymentsGETPOSTtreasury
/v1/payments/{id}GETDELETEtreasury
/v1/expensesGETPOSTtreasury
/v1/money-transfersGETPOSTtreasury
/v1/money-placesGETPOSTtreasury
/v1/chequesGETPOSTtreasury
/v1/cheques/{id}/actPOSTtreasury:write
/v1/payable-chequesGETPOSTtreasury
/v1/bank-statementsPOSTtreasury:write
/v1/bank-statements/{id}/matchPOSTtreasury:write
✓

money-places همان صندوق، بانک و تنخواه است — در سبزحساب یک مفهوم با سه نوع، نه سه مسیر جدا. یک میدان kind و یک شناسه، تمام. رقبا برای همین کار سه پارامتر موازی می‌گیرند و شما باید حدس بزنید کدام را پر کنید.

POST /v1/cheques/{id}/act رویدادهای چرخه چک را می‌گیرد: deposit · clear · bounce · transfer · return · cancel.

حسابداری

مسیرفعلاختیار
/v1/accounting/journal-entriesGETPOSTaccounting
/v1/accounting/journal-entries/{id}GETDELETEaccounting
/v1/accounting/journal-entries/{id}/postPOSTaccounting:write
/v1/accounting/journal-entries/{id}/reversePOSTaccounting:write
/v1/accounting/accountsGETPOSTaccounting
/v1/accounting/accounts/{id}GETPATCHaccounting
/v1/accounting/dimensionsGETPOSTaccounting
/v1/accounting/dimension-typesGETaccounting:read
/v1/accounting/fiscal-yearsGETPOSTaccounting
/v1/accounting/fiscal-years/{id}/closePOSTaccounting:write
/v1/accounting/fiscal-years/{id}/reopenPOSTaccounting:write
/v1/accounting/currenciesGETPOSTaccounting
/v1/accounting/opening-entryGETPUTaccounting

کدینگ چهارسطحی

کدینگ ایرانی چهار سطح دارد: گروه، کل، معین، تفصیلی. سند فقط روی سطح آخر می‌نشیند.

json · GET /v1/accounting/accounts?level=detail
{
  "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.

سند دستی

json · POST /v1/accounting/journal-entries
{
  "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-balancefrom to level dimension—
/v1/reports/balance-sheetas_of compare_to—
/v1/reports/income-statementfrom to compare_to—
/v1/reports/journalfrom to cursor—
/v1/reports/general-ledgeraccount_id from to—
/v1/reports/subsidiary-ledgeraccount_id from to—
/v1/reports/receivablesas_of contact_id—
/v1/reports/payablesas_of contact_id—
/v1/reports/agingas_of buckets—
/v1/reports/inventoryas_of warehouse_id—
/v1/reports/inventory-valuationas_of warehouse_id—
/v1/reports/cash-flowfrom to—
/v1/reports/sales-by-itemfrom to group_by—
/v1/reports/sales-by-contactfrom to—
/v1/reports/profit-by-channelfrom toگزارش کانال
/v1/reports/vatseason yearمالیات فصلی
/v1/reports/account-turnoveraccount_id from to—

مالیات و مؤدیان

مسیرفعلاختیار
/v1/moadian/invoicesGETsales:read
/v1/moadian/invoices/{id}/sendPOSTsales:write
/v1/moadian/invoices/{id}/statusGETsales:read
/v1/moadian/invoices/{id}/cancelPOSTsales:write
/v1/moadian/fiscal-infoGETsettings:read
/v1/moadian/item-codesGETitems:read
ℹ

ارسال به مؤدیان 202 می‌دهد، نه 200 — کار به صف می‌رود و نتیجه‌اش با وبهوک moadian.invoice.accepted یا moadian.invoice.rejected برمی‌گردد.

منتظر ماندن روی پاسخ همگام، در سامانه‌ای که خودش کند و ناپایدار است، یعنی بستن اتصال شما. پس ما منتظر نمی‌مانیم و شما هم نباید.

حقوق و دستمزد

مسیرفعلاختیار
/v1/employeesGETPOSTsettings
/v1/employees/{id}GETPATCHsettings
/v1/payrollsGETPOSTsettings
/v1/payrolls/{id}GETsettings:read
/v1/payrolls/{id}/payslipsGETsettings:read
/v1/payrolls/{id}/insurance-fileGETsettings:read

insurance-file فایل لیست بیمه تأمین اجتماعی را با فرمت خود سازمان می‌دهد — آماده بارگذاری، بدون تبدیل دستی.

استعلام پولی

🔴

هر تماس به این مسیرها پول خرج می‌کند. اختیار inquiry:run را فقط به توکنی بدهید که واقعا لازمش دارد، و قبل از هر حلقه‌ای اعتبار باقی‌مانده را بپرسید.

مسیرفعلاختیار
/v1/inquiries/creditGETinquiry:read
/v1/inquiries/national-codePOSTinquiry:run
/v1/inquiries/mobile-matchPOSTinquiry:run
/v1/inquiries/ibanPOSTinquiry:run
/v1/inquiries/cardPOSTinquiry:run
/v1/inquiries/card-to-ibanPOSTinquiry:run
/v1/inquiries/postal-codePOSTinquiry:run
/v1/inquiries/economic-codePOSTinquiry:run
/v1/inquiries/sayad-chequePOSTinquiry:run

هر پاسخ cost و credit_remaining دارد، پس هزینه را همان‌جا می‌بینید. GET /v1/inquiries/credit اعتبار را بدون خرج کردن می‌دهد.

پیوست و بایگانی

مسیرفعلاختیار
/v1/attachmentsPOSTدامنه سند مقصد
/v1/attachments/{id}GETDELETEدامنه سند مقصد

multipart/form-data با attachable_type و attachable_id. سقف ۲۰ مگابایت.

ℹ

اختیار لازم از سند مقصد می‌آید، نه از یک اختیار جدا: پیوست یک فاکتور خرید، purchase:write می‌خواهد. پیوستی که اختیار خودش داشته باشد، راهی می‌شود برای چسباندن فایل به سندی که اجازه دیدنش را ندارید.

کانکتور و مارکت‌پلیس

مسیرفعلاختیار
/v1/connectorsGETconnectors:manage
/v1/connectors/{id}/syncPOSTconnectors:manage
/v1/connectors/{id}/eventsGETconnectors:manage
/v1/item-mappingsGETPOSTDELETEconnectors:manage
/v1/external-ordersPOSTsales:write
/v1/settlementsGETPOSTconnectors:manage
/v1/settlements/{id}/reconcilePOSTconnectors:manage
/v1/settlements/{id}/variancesGETconnectors:manage

دو راه برای وصل کردن یک فروشگاه

راهبرای کیچه می‌کند
POST /v1/external-orders فروشگاهی که نمی‌خواهد منطق حسابداری بداند سفارش خام را می‌فرستید؛ سبزحساب خودش طرف حساب، فاکتور، سند و حواله انبار را می‌سازد
POST /v1/sales-invoices کسی که می‌خواهد خودش کنترل کند شما تعیین می‌کنید چه حسابی، چه انباری، چه کانالی، پیش‌نویس یا قطعی

/v1/settlements تسویه مارکت‌پلیس را می‌گیرد، و variances مغایرت را نشان می‌دهد. مبلغ واریزی مارکت‌پلیس هیچ‌وقت با جمع فاکتورها برابر نیست — کمیسیون، جریمه و مرجوعی وسط است — و پیدا کردن همان اختلاف، کاری است که دستی روزها وقت می‌برد.

وبهوک

مسیرفعلاختیار
/v1/webhooksGETPOSTwebhooks:manage
/v1/webhooks/{id}GETPATCHDELETEwebhooks:manage
/v1/webhooks/{id}/testPOSTwebhooks:manage
/v1/webhooks/{id}/deliveriesGETwebhooks:manage
/v1/webhooks/deliveries/{id}/retryPOSTwebhooks:manage

هدرهای هر پیام

http
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 است و بدون دسترسی خواندنی نیست — و همین خودش یک تفاوت است: قراردادی که نمی‌توانید قبل از خرید بخوانید، قرارداد نیست.

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