اسناد PDF محصولات خود را از سایت یا ربات خودتان، به نام مشتریتان واترمارک کنید و لینک دانلود تحویل بگیرید. پرداخت فقط به ازای هر درخواست، از موجودی کیف پول فروشندگی شما.
برای ساخت کلید API و مشاهده مصرف، با حساب فروشنده وارد شوید.
هر دو گزینهٔ زیر رایگان هستند و به همان /api/v1 میروند که کد شما استفاده میکند —
ولی خروجیشان فرق دارد:
کلید، محصول، نام، موبایل و کد ملی بررسی میشوند و میگوییم درخواستتان درست است یا نه.
خروجی: فقط پاسخ — سندی ساخته نمیشود
همان "test": true در کد شما. بینهایت بار.
یک سند واقعی از محصول خودتان، به نام «کاربر نمونه»، تا خروجی و سرعت واترمارک را ببینید.
خروجی: یک فایل PDF واقعی برای دانلود
حداکثر ۵ بار در ساعت.
—
—
کلید شما فقط در همین صفحه و در حافظهٔ مرورگر شما استفاده میشود و جایی ذخیره نمیشود.
کلاینت تکفایلی برای زبان دلخواهتان — کپی یا دانلود کنید، bd_live_XXXX را با کلید خودتان
و PRODUCT_ID را با شناسهٔ محصول (تب «محصولات») جایگزین کنید.
هر سه SDK از حالت آزمایشی (test)، Idempotency-Key و انتظار برای آمادهشدن سند پشتیبانی میکنند.
"test": true، هم یک سند نمونهٔ واقعی از محصول خودتان.پرداخت به ازای هر درخواست — بدون اشتراک ماهانه. مبالغ به تومان:
| عملیات | شرح | هزینه هر درخواست |
|---|---|---|
| در حال دریافت تعرفهها… | ||
درخواستهای ناموفق (خطای سرور) بهصورت خودکار برگشت داده میشوند. لینک دانلود تا ۷۲ ساعت معتبر است.
وقتی کلید و شناسهٔ محصول را داشتید، کل اتصال همین است: یک درخواست برای هر مشتری،
و لینک دانلودی که به او میدهید. با "test": true همین درخواست رایگان اعتبارسنجی میشود.
curl -X POST "https://buydocs.ir/api/v1/products/PRODUCT_ID/stamp" \
-H "Authorization: Bearer bd_live_XXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-order-1001" \
-d '{
"first_name": "علی",
"last_name": "رضایی",
"phone_number": "09123456789",
"national_code": "0012345679"
}'
پاسخ (201):
{
"document_id": "d3f1c2...",
"status": "processing",
"download_url": "https://buydocs.ir/api/v1/documents/d3f1c2.../download?exp=...&sig=...",
"expires_epoch": 1780000000,
"charged": 25000
}
لینک download_url را به مشتری بدهید؛ تا آمادهشدن سند، پاسخ 202 با هدر
Retry-After برمیگردد و سپس فایل PDF دانلود میشود. برای دانلود نیازی به کلید API نیست — لینک امضاشده و زماندار است.
| مقدار | از کجا میآید | کجا استفاده میشود |
|---|---|---|
bd_live_…کلید API |
تب «کلیدها و مصرف» → ساخت کلید جدید. فقط یک بار نمایش داده میشود؛ ما فقط اثر انگشتش را نگه میداریم. |
هدر Authorization: Bearer … در هر درخواست. |
PRODUCT_IDشناسهٔ محصول |
تب «محصولات» → دکمهٔ «کد اتصال» (کپی شناسه)، یا GET /api/v1/products. |
در آدرس درخواست: /products/PRODUCT_ID/stamp. |
Idempotency-Key |
شمارهٔ سفارش خودتان — هر مقداری که در سیستم شما یکتاست. | هدر اختیاری. اگر همان درخواست را دوباره بفرستید، همان سند برمیگردد و بار دوم هزینهای کسر نمیشود. |
اگر کلیدی را باطل کردید و کلید تازه ساختید، فقط مقدار bd_live_… در کد شما عوض میشود؛
شناسهٔ محصولات و آدرسها ثابت میمانند.
احراز هویت: هدر Authorization: Bearer bd_live_...
| Endpoint | شرح | هزینه |
|---|---|---|
GET /api/v1/pricing | تعرفههای فعلی (بدون احراز هویت) | رایگان |
GET /api/v1/balance | موجودی کیف پول API شما، بههمراه low (هشدار کمبودن موجودی) — با همان کلید API، بدون نیاز به ورود به پنل | رایگان |
GET /api/v1/products | فهرست محصولات آمادهٔ شما — بههمراه kyc_required و requires_birth_date هر محصول | رایگان |
POST /api/v1/products/{id}/stamp | تولید سند واترمارکشده برای مشتری شما | طبق تعرفه |
POST /api/v1/products/{id}/sample | نمونهٔ واقعی از محصول خودتان به نام «کاربر نمونه» — بدون بدنهٔ درخواست، حداکثر ۵ بار در ساعت | رایگان |
GET /api/v1/documents | تاریخچهٔ سندهای شما — با صفحهبندی و فیلتر status؛ برای تطبیق با سفارشهای خودتان. سطرهای این فهرست لینک دانلود ندارند (لینک فقط در GET /documents/{id} ساخته میشود) ولی idempotency_key و product_id دارند | رایگان |
GET /api/v1/documents/{id} | وضعیت سند | رایگان |
GET /api/v1/documents/{id}/download | دانلود با لینک امضاشده (بدون کلید) | رایگان |
GET /api/v1/openapi.json | مشخصات ماشینخوان (OpenAPI 3.1) — در Postman یا Insomnia وارد کنید یا با آن کلاینت تولید کنید (بدون احراز هویت) | رایگان |
هر دو فهرست (/products و /documents) صفحهبندی دارند:
پارامترهای page و per_page، و در پاسخ page، per_page، total و total_pages.
پیشفرض per_page برای محصولات ۱۰۰ و برای سندها ۵۰ است و حداکثر ۲۰۰ — مقدار بزرگتر خطای 422 میگیرد.
هزینهٔ هر محصول: در /products فیلد price «قیمت فروش همان محصول در سایت» است — یعنی چیزی که خریدار میپردازد، نه هزینهٔ شما.
هزینهٔ خودتان برای هر سند در api_cost میآید (با ریز آن در api_cost_breakdown) و شامل احراز هویتهای الزامی همان محصول هم هست:
محصولی که هم شاهکار و هم زحل را الزامی کرده باشد ۴۵٬۰۰۰ تومان برایتان هزینه دارد، نه ۲۵٬۰۰۰.
تطبیق حساب: /documents پارامتر since را میپذیرد —
?since=2026-08-17T12:21:17Z یا فقط ?since=2026-08-17 — و فقط سندهای ساختهشده از آن لحظه به بعد را برمیگرداند.
برای همگامسازی افزایشی با سیستم خودتان، آخرین created_utc که دیدهاید را نگه دارید و دفعهٔ بعد همان را بفرستید.
تاریخها: created_at و ready_at رشتهٔ شمسی به وقت تهراناند
و برای کتابخانههای تاریخ غیرایرانی قابل تجزیه نیستند. کنار هرکدام created_utc و ready_utc
به قالب ISO-8601 و ساعت UTC هم میآید (2026-08-17T12:21:17Z) — در کد خود همینها را بخوانید.
فیلدهای شمسی حذف نمیشوند. expires_epoch هم مثل قبل ثانیهٔ یونیکس است.
| فیلد | نوع | توضیح |
|---|---|---|
first_name | الزامی | نام — فقط حروف فارسی |
last_name | الزامی | نام خانوادگی — فارسی، حداقل ۳ حرف |
phone_number | الزامی | موبایل ۱۱ رقمی با 09 (ارقام فارسی هم پذیرفته میشود) |
national_code | شرطی | کد ملی ۱۰ رقمی — اگر برای محصول «الزامی» شده باشد یا محصول احراز هویت داشته باشد، باید ارسال شود؛ در صورت ارسال روی سند درج میشود |
birth_date | شرطی | تاریخ تولد شمسی مثل 1371/01/01 — برای محصولاتی که «احراز هویت زحل» دارند الزامی است |
verify_shahkar | اختیاری | افزودن استعلام شاهکار (تطبیق کد ملی و موبایل) — هزینهٔ جداگانه. اگر محصول خودش این استعلام را الزامی کرده باشد، صرفنظر از این مقدار انجام میشود |
verify_zohal | اختیاری | افزودن استعلام هویت زحل (کد ملی + تاریخ تولد) — هزینهٔ جداگانه. نام رسمی ثبتاحوال را برمیگرداند و همان روی سند درج میشود |
test | اختیاری | حالت آزمایشی: اعتبارسنجی کامل درخواست بدون کسر هزینه، بدون انجام استعلام و بدون تولید سند — پاسخ 200 است ولی ساختارش با سند فرق دارد: document_id و download_url ندارد و بهجایش test، would_charge، would_charge_breakdown و balance_sufficient دارد. در کدتان روی فیلد test شرط بگذارید |
Idempotency-Key (پیشنهادی): شناسهٔ سفارش خودتان. تکرار همان درخواست (مثلاً بعد از قطعی شبکه) سند قبلی را برمیگرداند و هزینهٔ دوباره کسر نمیشود.X-RateLimit-Limit، X-RateLimit-Remaining و X-RateLimit-Window — سهم باقیماندهٔ شما در پنجرهٔ جاری. با اینها میتوانید قبل از خوردن ۴۲۹ سرعت را کم کنید.هر پاسخ خطا علاوه بر متن detail، یک code ثابت و ماشینخوان دارد.
روی code شرط بگذارید، نه روی متن پیام — متن پیام ممکن است بازنویسی شود، code هرگز.
{
"detail": "موجودی کیف پول کافی نیست | Insufficient wallet balance (required: 25000 Toman)",
"code": "insufficient_balance",
"status": 402
}
| HTTP | code | معنی و کار درست |
|---|---|---|
| 401 | missing_api_keyinvalid_api_key | کلید ارسال نشده، یا نامعتبر/باطل است — کلید را بررسی کنید (قابل تکرار نیست) |
| 403 | account_disabledapi_access_disabled | حساب یا دسترسی API غیرفعال شده — با پشتیبانی تماس بگیرید |
| 402 | insufficient_balance | کیف پول را شارژ کنید و همان درخواست را با همان Idempotency-Key بفرستید |
| 404 | product_not_founddocument_not_found | شناسه اشتباه است یا متعلق به شما نیست |
| 404 | unknown_endpoint | چنین مسیری در API وجود ندارد — آدرس را با /api/v1/openapi.json مقایسه کنید |
| 405 | method_not_allowed | مسیر درست است ولی متد نه — مثلاً /stamp فقط POST را میپذیرد. متدهای مجاز در فیلد allow پاسخ و هدر Allow آمدهاند. هیچ مبلغی کسر نشده |
| 409 | product_not_readyproduct_no_source | محصول هنوز آمادهسازی میشود (چند دقیقه بعد دوباره)، یا فایل PDF آن تنظیم نشده است — در حالت دوم تا وقتی در پنل فروشندگی فایل را درست نکنید تکرار فایده ندارد |
| 409 | shahkar_mismatchzohal_mismatch | هزینهٔ استعلام کسر شد، هزینهٔ سند نه. پاسخ قطعی است — تکرار فایده ندارد و فقط هزینهٔ استعلام دوباره کم میکند |
| 422 | validation_errornational_code_requiredbirth_date_required | ورودی را اصلاح کنید؛ detail میگوید کدام فیلد. هیچ هزینهای کسر نشده |
| 429 | rate_limitedstamp_rate_limitedsample_rate_limited | به اندازهٔ Retry-After صبر کنید و دوباره بفرستید |
| 502 | shahkar_unavailablezohal_unavailable | سرویس استعلام در دسترس نبود — هیچ مبلغی کسر نشد، با خیال راحت دوباره بفرستید |
| 502 | shahkar_brokenzohal_broken | سرویس استعلام پاسخ نامفهوم داد — هزینهٔ استعلام کسر شد (چون استعلام انجام شده)، هزینهٔ سند نه |
| 503 | queue_unavailablepricing_unavailableserver_overloaded | هیچ مبلغی کسر نشده — به اندازهٔ Retry-After صبر کنید و دوباره بفرستید |
| 202 | document_processing | سند هنوز آماده نیست (روی لینک دانلود) — کمی بعد دوباره |
| 410 | document_unavailablefile_expired | سند ناموفق یا منقضی شده و فایلش حذف شده است |
| 403 | invalid_signature | لینک دانلود دستکاری شده یا مهلتش تمام شده |
اگر برای محصولی در پنل فروشندگی احراز هویت را فعال کرده باشید، روی درخواستهای API هم اجرا میشود و نمیتوانید آن را خاموش کنید. دو نوع مستقل است و ممکن است هر دو با هم لازم باشند:
| نوع | چه چیزی را بررسی میکند | ورودی لازم | اثر روی سند |
|---|---|---|---|
شاهکارshahkar |
کد ملی واقعاً متعلق به همین شمارهٔ موبایل است | national_code |
— |
زحلzohal |
کد ملی + تاریخ تولد به یک شخص واقعی و در قید حیات تعلق دارد | national_code و birth_date |
نام رسمی ثبتاحوال جایگزین نامی میشود که شما فرستادهاید |
برای اینکه بدانید هر محصول چه میخواهد، GET /api/v1/products را بخوانید — هر محصول
kyc_required و requires_birth_date دارد. لازم نیست حدس بزنید.
شاهکار و زحل بابت هر استعلام انجامشده از ما هزینه میگیرند — حتی وقتی جواب «عدم انطباق» باشد. بنابراین:
| حالت | هزینهٔ استعلام | هزینهٔ سند |
|---|---|---|
| احراز هویت موفق → سند تولید میشود | کسر میشود | کسر میشود |
| عدم انطباق، یا پاسخ نامفهوم سرویس | کسر میشود | کسر نمیشود |
سرویس اصلاً در دسترس نبود (*_unavailable) | کسر نمیشود | کسر نمیشود |
یعنی هرگز بابت سندی که تحویل نگرفتهاید پول نمیدهید؛ ولی استعلامی که واقعاً انجام شده هزینه دارد. اگر سرویس استعلام قطع باشد هیچ مبلغی کسر نمیشود و میتوانید بیخطر دوباره بفرستید.
پاسخ خطای احراز هویت خودش میگوید چقدر کسر شده:
HTTP 409
{
"detail": "کد ملی و شماره موبایل مطابقت ندارند | ...",
"code": "shahkar_mismatch",
"status": 409,
"charged": 10000,
"kyc": {
"ran": ["shahkar"],
"stamp_charged": false,
"billed_actions": [{ "action": "shahkar_verify", "price": 10000 }]
}
}
در پاسخ سند هم charged (هزینهٔ سند — همانی که در صورت خطای تولید
برمیگردد)، kyc_charged (هزینهٔ استعلام — هرگز برنمیگردد، چون
استعلام واقعاً انجام شده) و charged_total جدا گزارش میشوند.
💡 با "test": true هیچ استعلامی انجام نمیشود و چیزی کسر نمیشود —
فقط would_charge_breakdown میگوید که اگر واقعی بفرستید چقدر میشود.
برای ساختن و اشکالزدایی اتصال، همیشه از همین حالت استفاده کنید.
سند شما چند ثانیه پس از درخواست آماده میشود. دو راه دارید بفهمید کِی آماده شد:
| روش | چطور کار میکند | مناسب چه کسی |
|---|---|---|
| پرسیدن polling |
هر چند ثانیه GET /api/v1/documents/{id} را صدا میزنید تا status برابر ready شود. |
ربات، اسکریپت، هر جایی که آدرس عمومی ندارید. هیچ تنظیمی لازم ندارد. |
| اعلان خودکار webhook |
یک آدرس https از خودتان ثبت میکنید؛ بهمحض آماده (یا ناموفق) شدن سند، ما به آن آدرس یک POST میزنیم. |
سایت یا سروری که آدرس عمومی دارد. دیگر لازم نیست چیزی را مدام بپرسید. |
میتوانید هر دو را با هم داشته باشید — و اگر اعلان نرسید، پرسیدن همیشه جواب درست را میدهد.
https خودتان را وارد کنید.whsec_…) نمایش داده میشود — فقط همین یک بار. همانجا ذخیرهاش کنید.
2xx بدهید و کار سنگین را بعد از پاسخ انجام دهید.| هدر | معنی |
|---|---|
X-BuyDocs-Event | document.ready یا document.failed |
X-BuyDocs-Delivery | شناسهٔ یکتا برای هر تلاش (نه هر رویداد) — برای لاگ و پیگیری |
X-BuyDocs-Timestamp | زمان ارسال (ثانیهٔ یونیکس) — بخشی از چیزی است که امضا میشود |
X-BuyDocs-Signature | sha256=<hex> — امضای HMAC-SHA256 |
سند آماده شد:
POST https://your-site.example/buydocs-hook
{
"event": "document.ready",
"document_id": "d3f1c2...",
"status": "ready",
"download_url": "https://buydocs.ir/api/v1/documents/.../download?exp=...&sig=...",
"expires_epoch": 1780000000,
"charged": 25000,
"refunded": false,
"error": null
}
سند ناموفق بود (مبلغ سند خودکار برگشته است):
{
"event": "document.failed",
"document_id": "d3f1c2...",
"status": "failed",
"download_url": null,
"expires_epoch": 1780000000,
"charged": 25000,
"refunded": true,
"error": "watermark engine timeout"
}
این بدنه فقط charged (هزینهٔ سند) را دارد. برای تفکیک هزینهٔ استعلام،
GET /api/v1/documents/{id} را بخوانید که kyc_charged و
charged_total هم دارد.
امضا برابر است با HMAC-SHA256 روی رشتهٔ
"<timestamp>.<بدنهٔ خام>" با کلید امضا.
بدنه را قبل از JSON-parse و بدون هیچ تغییری (raw) امضا کنید — یک فاصله یا مرتبسازی دوبارهٔ کلیدها، امضا را خراب میکند.
// PHP
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_BUYDOCS_TIMESTAMP'];
$mine = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $SECRET);
if (!hash_equals($mine, $_SERVER['HTTP_X_BUYDOCS_SIGNATURE'])) { http_response_code(403); exit; }
if (abs(time() - (int)$ts) > 300) { http_response_code(403); exit; } // replay
echo 'ok'; // هر پاسخ 2xx یعنی دریافت شد
# Python (Flask)
raw = request.get_data() # RAW bytes, before any parsing
ts = request.headers.get('X-BuyDocs-Timestamp', '')
mine = 'sha256=' + hmac.new(SECRET.encode(),
ts.encode() + b'.' + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(mine, request.headers.get('X-BuyDocs-Signature', '')):
return '', 403
if abs(time.time() - int(ts or 0)) > 300:
return '', 403
return 'ok', 200
// Node (Express) — express.raw(), NOT express.json(): the raw body is what is signed
app.post('/buydocs-hook', express.raw({ type: '*/*' }), (req, res) => {
const ts = req.get('X-BuyDocs-Timestamp') || '';
const sig = req.get('X-BuyDocs-Signature') || '';
const mine = 'sha256=' + crypto.createHmac('sha256', SECRET)
.update(ts + '.' + req.body.toString('utf8'))
.digest('hex');
if (mine.length !== sig.length ||
!crypto.timingSafeEqual(Buffer.from(mine), Buffer.from(sig))) return res.sendStatus(403);
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(403);
res.sendStatus(200); // answer first, work afterwards
const evt = JSON.parse(req.body.toString('utf8'));
});
https، و فقط پورت 443 یا 8443.127.0.0.1 یا هر IP داخلی حل شود پذیرفته نمیشود — هم موقع ثبت، هم دوباره پیش از هر ارسال بررسی میشود.301/302 بدهد — مثل /hook که به /hook/ میرود، یا دامنهٔ بدون www که به www میرود — تحویل ناموفق حساب میشود. آدرس نهایی را ثبت کنید.نمونهٔ یک آدرس درست — مسیر و کوئریاسترینگ آزاد است؛ آنچه اهمیت دارد پروتکل، دامنه و پورت است:
https://yourshop.ir/webhooks/buydocs
و آدرسهایی که پذیرفته نمیشوند، با همان پیامی که هنگام ثبت میگیرید:
| آدرس | پاسخی که میگیرید |
|---|---|
http://yourshop.ir/hook | آدرس باید با https:// شروع شود |
https://user:pass@yourshop.ir/hook | آدرس نباید نام کاربری یا رمز داشته باشد |
https://yourshop.ir:8080/hook | فقط پورت ۴۴۳ یا ۸۴۴۳ مجاز است |
https://localhost/hook · https://192.168.1.10/hook | آدرس داخلی مجاز نیست |
| دامنهای که فقط روی شبکهٔ داخلی خودتان تعریف شده | نام دامنه قابلحل نیست |
⚠️ یک حالت که در ثبت قبول میشود ولی در ارسال شکست میخورد:
آدرسی که ریدایرکت میدهد — مثلاً https://yourshop.ir/hook که به
https://www.yourshop.ir/hook منتقل میشود. همان آدرس نهایی را ثبت کنید.
همچنین اگر دامنهٔ شما هم رکورد A عمومی دارد و هم AAAA داخلی،
رد میشود: همهٔ آدرسهایی که دامنه به آنها حل میشود باید عمومی باشند.
200 بدهید، بعد کارتان را انجام دهید.document_id کار خود را idempotent کنید (هر تلاش X-BuyDocs-Delivery جداگانه دارد، پس آن را برای این کار بهکار نبرید).در GET /api/v1/documents هر سطر یک callback_status دارد:
callback_status | یعنی | کار بعدی |
|---|---|---|
| sent | سرور شما ۲xx داد | مشکل سمت ماست نه — گیرندهٔ خودتان را بررسی کنید که با پیام چه کرده |
| failed | هر سه تلاش ناموفق بود | ریدایرکت، تایماوت و کد پاسخ را بررسی کنید؛ آدرس را در تب کلیدها دوباره ثبت کنید تا اعتبارسنجی شود |
| خالی | برای کلیدی که این سند را ساخته، آدرسی ثبت نشده بود | روی همان کلیدی که در کدتان استفاده میکنید آدرس را فعال کنید |
اعلان به کلیدی وابسته است که سند را ساخته — اگر کلید را عوض کنید، سندهای در جریانِ کلید قبلی همچنان به آدرس ثبتشدهٔ همان کلید اعلام میشوند.
اعلان یک راحتی است، نه منبع حقیقت. سند حتی وقتی هیچ اعلانی تحویل داده نشود تولید میشود، پرداخت شده است و از
GET /api/v1/documents/{id} قابل دریافت است. برای تطبیق نهایی و حسابداری همیشه همان را ملاک بگیرید.
refunded).