پرش به محتویات

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

مستند تعاملی روی /docs استقرار خودت است؛ استقرار مرجع روی kasbbook.nyxon.tech/docs است. این صفحه دربارهٔ قراردادهایی است که شمای OpenAPI نمی‌تواند بیانشان کند.

مسیر پایه: /api/v1. بررسی سلامت بیرون از آن است، روی /healthz و /readyz.

قراردادها

پول رشته است

{ "converted_amount": "12500000.1500", "original_currency": "IRR" }

هرگز عدد JSON نیست. JSON یک نوع عددی دارد و آن هم اعشاری ممیز شناور است؛ 12500000.15 از این رفت‌وبرگشت سالم بیرون نمی‌آید. سمت خودت با یک نوع دهدهی تجزیه‌اش کن — Decimal در پایتون، BigDecimal در جاوا، یک کتابخانهٔ decimal در جاوااسکریپت. آن را به Number تبدیل نکن.

تاریخ‌ها ISO‌اند، دوره‌ها جلالی

occurred_on برابر 2026-08-25 است. اما دورهٔ گزارش 1403-05 است — مرداد ۱۴۰۳ — که پیش از اجرای کوئری به بازهٔ میلادی تبدیل می‌شود.

دوره یعنی
1403-05 یک ماه جلالی
1403 یک سال جلالی
week همین هفته
(نبودنش) همه‌چیز

خطاها

{ "detail": "این دفتر پیدا نشد" }

خطاهای دامنه وضعیت خودشان را دارند: ۴۰۴ پیدا نشد، ۴۰۳ رد شد، ۴۲۲ نامعتبر، ۴۰۹ تعارض، ۴۰۱ احراز نشده، ۴۲۹ محدودیت نرخ (با هدر Retry-After به ثانیه). مبلغ ۱۰ به توان ۲۴ یا بیشتر ۴۲۲ است. خطاهای اعتبارسنجی از Pydantic می‌آیند، با شکل خودش و نام فیلد مقصر.

استثناهای مدیریت‌نشده همیشه {"detail": "something went wrong on our side"} با وضعیت ۵۰۰‌اند. خطای واقعی در لاگ است، هرگز در پاسخ.

«پیدا نشد» یعنی «مال تو نیست»

درخواست دفتری که عضوش نیستی ۴۰۴ برمی‌گرداند، نه ۴۰۳. باطل‌کردن کلید API دیگری هم ۴۰۴ می‌دهد. این عمدی است: ۴۰۳ تأیید می‌کرد که آن شناسه وجود دارد.

گرفتن کلید بدون خارج‌شدن از تلگرام

حساب ← 🔌 API یکی صادر می‌کند. یک بار نشان داده می‌شود، پشت یک لمس پنهان، و دیگر هرگز: فقط خلاصهٔ SHA-256 ذخیره می‌شود، و همین است که یک دیتابیس درزکرده را بی‌ارزش می‌کند. صدور کلید تازه کلید قبلی را باطل می‌کند، پس هیچ‌وقت کلید دومی نیست که کسی یادش نباشد ساخته.

احراز هویت

یا توکن حامل، یا کلید API. هر دو به یک حساب می‌رسند، اما مدیریت خود حساب فقط با توکن حامل یک آدم ممکن است — کلیدها، نمایه، راه تماس، رمز، نشست‌ها، بستن حساب و اتصال پیام‌رسان به کلید API پاسخ ۴۰۳ می‌دهند.

curl -H "Authorization: Bearer $ACCESS_TOKEN" ...
curl -H "X-API-Key: kb_..." ...

ثبت‌نام و ورود

curl -X POST /api/v1/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"display_name":"عماد","email":"you@example.com","password":"at-least-8"}'
{
  "access_token": "eyJ...",
  "refresh_token": "xK9...",
  "token_type": "Bearer",
  "expires_in": 1800
}

ایمیل یا تلفن لازم است — حسابی که هیچ راهی برای رسیدن به صاحبش نداشته باشد قابل بازیابی نیست.

ماندن در حساب

curl -X POST /api/v1/auth/refresh -d '{"refresh_token":"xK9..."}'

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

خود حساب

PATCH /auth/me نام نمایشی، منطقهٔ زمانی، زبان
PUT /auth/me/contact ایمیل یا تلفنی که حساب با آن پیدا و بازیابی می‌شود
PUT /auth/me/password تعیین یا تغییرش — همهٔ نشست‌ها را می‌بندد، از جمله نشست خودت

تغییر رمز همه‌جا را بیرون می‌اندازد — هم توکن‌های تازه‌سازی و هم توکن‌های دسترسی صادرشده، پس همان حاملی که با آن درخواست دادی هم از کار می‌افتد. کسی که از ترس درز اطلاعات این کار را می‌کند دقیقاً همین را انتظار دارد. بعدش دوباره وارد شو.

کلید API دست‌نخورده می‌ماند: مال یک برنامه است، و کار شبانه نباید ساعت سه بامداد بایستد چون آدمی رمزش را عوض کرده.

نشانی‌ای که حساب دیگری از قبل دارد با ۴۲۲ رد می‌شود — پیامی که تأیید نمی‌کند آن نشانی ثبت شده است.

GET /auth/me/deletion-preview بستن حساب چه چیزی را نابود می‌کند و چه چیزی جلویش را می‌گیرد
DELETE /auth/me می‌بنددش — برگشت‌ناپذیر، بدون مهلت

DELETE /auth/me وقتی حساب رمز دارد رمز می‌خواهد؛ حسابی که رمز ندارد فقط با توکن حامل اثبات می‌شود. اگر دفتر مشترکی مانع باشد ۴۲۲ برمی‌گرداند. شناسه‌ای هم نمی‌گیرد: یک توکن فقط می‌تواند حساب خودش را ببندد.

POST /auth/logout این نشست را می‌بندد
POST /auth/logout-everywhere همه را می‌بندد
GET /auth/sessions این حساب کجاها وارد است

کلید برای برنامه‌ها

curl -X POST /api/v1/auth/api-keys -H "Authorization: Bearer $T" \
  -d '{"name":"nightly export","expires_in_days":365}'

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

دفترها و تراکنش‌ها

GET /books دفترهایی که این حساب می‌بیند
POST /books {"name":"مغازه","type":"business","currency":"IRR"}
GET /books/{id}/members · POST · DELETE /{user_id} عضویت
GET /books/{id}/transactions ?since=&until=&page=&per_page=، تازه‌ترین اول
POST /books/{id}/transactions پایین‌تر
GET/DELETE /books/{id}/transactions/{tx} یک تراکنش
GET /books/{id}/currencies · PUT /{code} ارزهایی که این دفتر می‌تواند نگه دارد
GET /books/{id}/wallet موجودی به تفکیک ارز
GET/POST /books/{id}/conversions جابه‌جایی ارزش بین دو ارز دفتر
curl -X POST /api/v1/books/$BOOK/transactions \
  -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
  -d '{"flow":"income","category":"فروش","amount":"250000","occurred_on":"2026-08-25"}'

تراکنش has_receipt و receipt_kind و receipt_file_name را همراه دارد. خودِ شناسهٔ فایل برنمی‌گردد: مبهم است و محدود به همان پیام‌رسانی که فایل را نگه داشته، پس اینجا معنایی ندارد و دادنش فقط دامنهٔ چیزی را که یک درز افشا می‌کند بزرگ‌تر می‌کند. پیوست‌کردن رسید جریان ربات است — هنوز نقطهٔ پایانی برای آپلود وجود ندارد.

scope اختیاری است و از نوع دفتر مقدار می‌گیرد. currency پیش‌فرض ارز خود دفتر است، و ارز دیگر باید از ارزهای فعال‌شدهٔ دفتر باشد و conversion_rate همراه داشته باشد — آن نرخ روی تراکنش منجمد می‌شود و هرگز دوباره اعمال نمی‌شود. هزینه به ارزی که دفتر به‌اندازهٔ کافی ندارد رد می‌شود؛ ارز پایه مستثناست.

POST /books هم currency می‌گیرد، و ندادنش حالا یعنی همان پیش‌فرضی که ربات استفاده می‌کند. قبلاً یعنی IRR بود، و به همین دلیل دفتری که در ربات ساخته شده بود هر تراکنشی را که اینجا ثبت می‌شد رد می‌کرد.

افزودن عضو نیازمند آن است که آن شخص از قبل حساب داشته باشد؛ با ایمیل یا تلفن پیدا می‌شود.

گزارش‌ها

GET /books/{id}/reports/summary ?period=1403-05 ← درآمد، هزینه، خالص، تعداد
GET /books/{id}/reports/by-category ?flow=expense&period= ← جمع‌ها و سهم‌ها
GET /books/{id}/reports/export.csv همان دوره به شکل فایل، با تاریخ جلالی؛ سلول متنی که با = + - @ شروع شود یک ' اول می‌گیرد
GET /books/{id}/reports/day ?date=2026-03-19 ← همان تفکیک لیست روزانه: کاری، شخصی، قسط، پس‌انداز

بودجه، بدهی، وام

GET/PUT /books/{id}/budgets سقف برای هر دسته؛ PUT چون هر دسته یکی دارد
DELETE /books/{id}/budgets/{budget}
GET/POST /books/{id}/debts ?include_settled=true برای دیدن بسته‌شده‌ها
POST /books/{id}/debts/{debt}/settle می‌بندد؛ با {"record_transaction": true} پول را هم ثبت می‌کند (درآمد اگر طلب توست، هزینه اگر بدهی). پیش‌فرض خاموش است: اگر فروش یا خرید اصلی قبلاً ثبت شده، ثبت پرداخت هم آن را دوبار می‌شمارد. تسویهٔ دوباره ۴۲۲ است.
GET/POST /books/{id}/loans مبلغ و تعداد قسط، نه جمع کل
POST /books/{id}/loans/{loan}/pay یک قسط و هزینه‌اش را ثبت می‌کند

وام با هزینهٔ یک قسط و تعدادش توصیف می‌شود، چون وام برای آدم همین‌طور توصیف می‌شود: «دوازده‌تا چهار میلیونی».

حقوق و خزانه

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

GET/POST /books/{id}/periods فهرست، یا بازکردن یکی
PATCH /books/{id}/periods/{p} تغییر نام یا بازه؛ فیش‌ها دوباره حساب می‌شوند و پرداخت‌ها می‌مانند
DELETE /books/{id}/periods/{p} فقط تا وقتی فیشی صادر نکرده
GET /books/{id}/periods/{p}/distribution درآمد − هزینه − خزانه = قابل‌توزیع (هرگز زیر صفر؛ shortfall زیان ماهِ زیان‌ده است)
POST /books/{id}/periods/{p}/status/{status} فقط گذارهای مستندشده
POST /books/{id}/periods/{p}/close یک‌جا تا قفل؛ ۴۲۲ بدون هیچ جابه‌جایی اگر فیشی نیست، فیش‌ها کهنه‌اند یا کسی طلب دارد
GET/PUT /books/{id}/shares چه کسی چه می‌برد؛ PUT قاعدهٔ قبلی را تاریخ‌بندی می‌کند
DELETE /books/{id}/shares/{user} پرداخت به این عضو را از امروز متوقف کن؛ دوره‌های گذشته قاعده را نگه می‌دارند
GET/PUT /books/{id}/periods/{p}/performance ساعت، روز، امتیاز — برای مبناهای سنجشی
GET/POST /books/{id}/periods/{p}/adjustments پاداش و کسر، علامت‌دار
POST …/adjustments/{a}/approve ثبت و تأیید دو کار جدا هستند
POST /books/{id}/periods/{p}/calculate فیش‌ها، با تمام ورودی‌ها منجمد روی خودشان؛ دفعهٔ بعد سر جایشان به‌روز می‌شوند و پرداخت‌ها می‌مانند
DELETE /books/{id}/periods/{p}/payslips باطل‌کردن محاسبه — بعد از اولین پرداخت رد می‌شود
GET /books/{id}/periods/{p}/payslips فیش همه، یا فقط خودت، بسته به دسترسی
POST /books/{id}/payslips/{slip}/payments مرحله‌ای؛ قسطی‌بودن قاعده است؛ با هر ارزی که دفتر دارد
DELETE /books/{id}/payslips/{slip}/payments/{pay} پس‌گرفتن پرداخت تا وقتی دوره باز است
GET/POST/DELETE /books/{id}/funds صندوق‌های خزانه
GET/POST/DELETE /books/{id}/funds/{f}/rules چه چیزی تغذیه‌شان می‌کند

وقتی هیچ عضوی سهم ندارد، calculate ۴۲۲ برمی‌گرداند، نه فهرست خالی. اجرایی که چیزی تولید نمی‌کند چون اصلاً از سؤالش جواب نگرفته‌ایم، نتیجهٔ خالی نیست.

پرداخت به‌طور پیش‌فرض به ارز خود فیش است. ارز دیگر باید در دفتر فعال باشد و دفتر به اندازهٔ کافی از آن داشته باشد، و باید conversion_rate به ارز فیش را همراه داشته باشد؛ فیش آن را با همان نرخ حساب می‌کند، و هر پرداخت در پاسخ نرخ خودش را دارد تا فهرست با paid جور دربیاید. نرخی که با ارز خود فیش فرستاده شود نادیده گرفته می‌شود، همان‌طور که در تراکنش. reference حداکثر ۸۰ حرف است.

curl -X POST /api/v1/books/$BOOK/payslips/$SLIP/payments \
  -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
  -d '{"amount":"50","currency":"USDT","conversion_rate":"229867"}'

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

وب‌هوک

POST /api/v1/webhooks/{provider}/{secret}

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

فقط وقتی در دسترس است که این پروسه برای سرو وب‌هوک پیکربندی شده باشد. وگرنه همهٔ مسیرها ۴۰۴ می‌دهند. برای اینکه بدانی آن راز داخل مسیر چه کار می‌کند امنیت را ببین.

سلامت

GET /healthz بدون دست‌زدن به چیزی جواب می‌دهد؛ بی‌محابا صدایش بزن
GET /readyz یک کوئری اجرا می‌کند؛ وقتی دیتابیس دور از دسترس است ۵۰۳

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