API نسخهٔ ۱
سایتت را به استودیوی محتوا وصل کن
تیم محتوای استودیو هر روز مقاله مینویسد، تصویر میسازد و صفحههای فعلی سایتت را بهتر میکند. این API راهی است که هر سایت یا پلتفرمی، با هر زبان و فریمورکی، این کارها را بگیرد و خودش منتشر کند. اگر سایتت وردپرس است، به کد نیاز نداری: افزونهٔ ما را نصب کن و با یک کلیک وصل شو.
- نشانی پایه
{{origin}}/api/v1- قالب
- JSON روی HTTPS
- احراز هویت
- توکن Bearer برای هر پروژه
- مشخصات
- OpenAPI 3.1
مدل کار در یک نگاه
- استودیو مقاله را آماده میکند و یک «تحویل» (delivery) در صف پروژه میگذارد.
- وبهوک امضاشده به سایتت خبر میدهد که کاری منتظر است. اگر وبهوک نداری، هر چند دقیقه صف را بخوان.
- سایتت تحویل را میگیرد، مقاله را به روش خودش منتشر میکند و نشانی زندهاش را به استودیو برمیگرداند (ack).
- سایتت فهرست صفحهها و دیدگاههایش را هم میفرستد تا تیم سایت موجود را بشناسد و بهترش کند.
شروع سریع
در پنج قدم یک اتصال کامل میسازی. نمونهها را با زبان دلخواهت ببین؛ انتخابت در همهٔ صفحه حفظ میشود.
-
توکن بساز
در استودیو به تنظیمات پروژه ← اتصال سایت و API برو، یک توکن با «دسترسی کامل» بساز و همان لحظه کپیاش کن. توکن فقط یک بار نمایش داده میشود و این شکل را دارد:
cst_3f9a1c2e_9xQ2kq…(۴۳ نویسه) -
اتصال را آزمایش کن
curl {{origin}}/api/v1/me \ -H "Authorization: Bearer $STUDIO_TOKEN"const BASE = "{{origin}}/api/v1"; const TOKEN = process.env.STUDIO_TOKEN; async function api(method, path, body) { const res = await fetch(BASE + path, { method, headers: { authorization: `Bearer ${TOKEN}`, ...(body ? { "content-type": "application/json" } : {}), }, body: body ? JSON.stringify(body) : undefined, }); const data = await res.json(); if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`); return data; } console.log(await api("GET", "/me"));function studio_api(string $method, string $path, ?array $body = null): array { $ch = curl_init('{{origin}}/api/v1' . $path); $headers = ['Authorization: Bearer ' . getenv('STUDIO_TOKEN')]; if ($body !== null) { $headers[] = 'Content-Type: application/json'; curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE)); } curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, ]); $data = json_decode((string) curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if ($status >= 400) throw new RuntimeException($data['error']['code'] ?? 'error'); return $data; } print_r(studio_api('GET', '/me'));import os, requests BASE = "{{origin}}/api/v1" SESSION = requests.Session() SESSION.headers["Authorization"] = f"Bearer {os.environ['STUDIO_TOKEN']}" def api(method, path, body=None): res = SESSION.request(method, BASE + path, json=body, timeout=20) data = res.json() if not res.ok: raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}") return data print(api("GET", "/me")) -
سایتت را معرفی کن
با این درخواست، این توکن «اتصال انتشار» پروژه میشود و در پنل مدیر «وصل است» نشان داده میشود.
curl -X PUT {{origin}}/api/v1/site \ -H "Authorization: Bearer $STUDIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{"platform":"custom","siteUrl":"https://shop.example","publishMode":"publish"}'await api("PUT", "/site", { platform: "custom", siteUrl: "https://shop.example", publishMode: "publish" });studio_api('PUT', '/site', ['platform' => 'custom', 'siteUrl' => 'https://shop.example', 'publishMode' => 'publish']);api("PUT", "/site", {"platform": "custom", "siteUrl": "https://shop.example", "publishMode": "publish"}) -
کارهای منتظر را بگیر و منتشر کن
این حلقه را هر چند دقیقه (یا وقتی وبهوک رسید) اجرا کن.
myCmsجای کد انتشار خود توست.# کارهای منتظر curl {{origin}}/api/v1/deliveries -H "Authorization: Bearer $STUDIO_TOKEN" # گرفتن یکی (۱۵ دقیقه برای تو رزرو میشود) curl {{origin}}/api/v1/deliveries/dlv_8c1d… -H "Authorization: Bearer $STUDIO_TOKEN" # اعلام نتیجه curl -X POST {{origin}}/api/v1/deliveries/dlv_8c1d…/ack \ -H "Authorization: Bearer $STUDIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{"remoteId":"812","status":"publish","link":"https://shop.example/blog/shoe-wax/"}'const { data } = await api("GET", "/deliveries"); for (const item of data) { const delivery = await api("GET", `/deliveries/${item.id}`); if (delivery.type !== "post.publish") continue; const post = delivery.payload; try { const saved = await myCms.upsertBySlug(post.slug, { title: post.title, html: post.contentHtml, excerpt: post.excerpt, }); await api("POST", `/deliveries/${item.id}/ack`, { remoteId: String(saved.id), status: "publish", link: saved.url, }); } catch (error) { await api("POST", `/deliveries/${item.id}/fail`, { code: "publish_failed", message: String(error.message).slice(0, 500), retryable: true, }); } }$waiting = studio_api('GET', '/deliveries'); foreach ($waiting['data'] as $item) { $delivery = studio_api('GET', '/deliveries/' . $item['id']); if ($delivery['type'] !== 'post.publish') continue; $post = $delivery['payload']; try { $saved = my_cms_upsert_by_slug($post['slug'], $post['title'], $post['contentHtml'], $post['excerpt']); studio_api('POST', '/deliveries/' . $item['id'] . '/ack', [ 'remoteId' => (string) $saved['id'], 'status' => 'publish', 'link' => $saved['url'], ]); } catch (Throwable $e) { studio_api('POST', '/deliveries/' . $item['id'] . '/fail', [ 'code' => 'publish_failed', 'message' => mb_substr($e->getMessage(), 0, 500), 'retryable' => true, ]); } }for item in api("GET", "/deliveries")["data"]: delivery = api("GET", f"/deliveries/{item['id']}") if delivery["type"] != "post.publish": continue post = delivery["payload"] try: saved = my_cms.upsert_by_slug(post["slug"], post["title"], post["contentHtml"], post["excerpt"]) api("POST", f"/deliveries/{item['id']}/ack", {"remoteId": str(saved.id), "status": "publish", "link": saved.url}) except Exception as error: api("POST", f"/deliveries/{item['id']}/fail", {"code": "publish_failed", "message": str(error)[:500], "retryable": True}) -
وبهوک بگذار (پیشنهادی)
تا مقالهها همان لحظه منتشر شوند، نه در نوبت بعدی خواندن صف. راهنمای وبهوک را ببین.
احراز هویت و توکن
هر درخواست (بهجز چند مسیر عمومی) باید توکن پروژه را در هدر Authorization بفرستد:
Authorization: Bearer cst_3f9a1c2e_9xQ2kq…
- هر توکن فقط به یک پروژه دسترسی دارد. برای هر سایت توکن جدا بساز.
- توکن فقط یک بار نمایش داده میشود. استودیو فقط هش SHA-256 آن را نگه میدارد و دیگر نمیتواند نشانش بدهد.
- هرگز در نشانی (query string) نفرست. توکنی که در URL بیاید پذیرفته نمیشود، چون در لاگها و تاریخچه میماند.
- اعتبار: ۳۰ روز، ۹۰ روز، یک سال یا بدون انقضا. توکن اتصال یککلیکی انقضا ندارد تا انتشار قطع نشود.
- ابطال فوری: هر توکن را هر وقت بخواهی از تنظیمات پروژه باطل کن. «قطع اتصال سایت» توکن آن سایت را هم باطل میکند. سایتی که خودش قطع میشود، با
DELETE /meتوکن خودش را باطل میکند. - هر پروژه حداکثر ۱۰ توکن فعال دارد.
| وضعیت | کد | یعنی |
|---|---|---|
| 401 | missing_token | هدر Authorization نیامده یا Bearer نیست. |
| 401 | invalid_token | توکن وجود ندارد یا نادرست است. |
| 401 | token_revoked | توکن باطل شده است. |
| 401 | token_expired | اعتبار توکن تمام شده است. |
| 403 | insufficient_scope | توکن این دسترسی را ندارد. details.required دسترسی لازم را میگوید. |
| 429 | too_many_failures | از این IP تلاش ناموفق زیادی آمده؛ ۱۰ دقیقه صبر کن. |
دسترسیها (scopes)
هنگام ساخت توکن، فقط دسترسیهایی را بده که لازم است. «دسترسی کامل» همهٔ اینهاست و برای انتشار روی سایت لازم است.
| scope | اجازه | مسیرها |
|---|---|---|
project:read | خواندن مشخصات پروژه و وضعیت اتصال | GET /site |
posts:read | خواندن مقالهها، تصویر شاخص و تصویرهای متن | /posts، /figures |
deliveries:read | دیدن و گرفتن کارهای انتشار | GET /deliveries |
deliveries:write | اعلام انجام یا شکست کار | POST /deliveries/{id}/ack|fail |
site:write | معرفی سایت و فرستادن صفحهها، دیدگاهها و دستهها | PUT /site… |
webhooks:manage | تنظیم، آزمایش و چرخش راز وبهوک | /webhook… |
GET /me با هر توکنی کار میکند و بهترین راه آزمایش اتصال است.
خطاها
هر خطا یک شکل دارد. code ثابت و انگلیسی است و برای برنامهات؛ message فارسی است و برای آدمها. requestId را هنگام گزارش مشکل بفرست (در هدر X-Request-Id هم هست).
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "site_mismatch",
"message": "link باید روی دامنهٔ سایت این پروژه باشد.",
"requestId": "req_5c0e8b2f1a9d4e77",
"details": { "field": "link" }
}
}
| وضعیت | کدهای رایج | چه کنی |
|---|---|---|
| 400 | invalid_json، invalid_query، invalid_cursor، invalid_grant | درخواست را اصلاح کن؛ تکرارش فایده ندارد. |
| 401 / 403 | بخش احراز هویت | توکن یا دسترسیاش را درست کن. |
| 404 | not_found، delivery_not_found | نشانی یا شناسه را بررسی کن. |
| 405 | method_not_allowed | هدر Allow روشهای مجاز را میگوید. |
| 409 | delivery_closed، webhook_missing | وضعیت را دوباره بخوان. |
| 409 | connection_exists | این پروژه با توکن دیگری به سایت وصل است. فقط همان توکن اتصال را تغییر میدهد؛ مدیر اول از تنظیمات قطعش کند. |
| 410 | delivery_gone | محتوای کار حذف شده و کار لغو شد؛ از آن بگذر. |
| 413 / 415 | payload_too_large، unsupported_media_type | بدنه را کوچکتر کن و Content-Type: application/json بفرست. |
| 422 | invalid_field، site_mismatch، invalid_webhook_url، too_many_items | details.field فیلد مشکلدار را میگوید. |
| 429 | rate_limited | به اندازهٔ Retry-After ثانیه صبر کن. |
| 5xx | server_error | با فاصلهٔ فزاینده دوباره امتحان کن. |
محدودیتها
- ۱۲۰ درخواست در دقیقه برای هر توکن. هر پاسخ هدرهای
RateLimit-Limit،RateLimit-RemainingوRateLimit-Reset(ثانیه تا شروع دقیقهٔ بعد) را دارد. پاسخ 429 هدرRetry-Afterهم دارد. - بدنهٔ درخواست: حداکثر ۱ مگابایت؛ برای
PUT /site/contentوPUT /site/taxonomiesحداکثر ۴ مگابایت. - دستهها: هر
PUT /site/contentحداکثر ۱۰۰ آیتم و هرPUT /site/commentsحداکثر ۲۰۰ دیدگاه. استودیو تا ۵۰۰۰ صفحه و ۲۰۰۰ دیدگاه آخر را نگه میدارد. - تلاش ناموفق: ۲۰ احراز هویت ناموفق از یک IP در ۱۰ دقیقه، آن IP را ۱۰ دقیقه مسدود میکند.
صفحهبندی و تکرار امن
صفحهبندی با cursor
فهرستها { "data": [...], "nextCursor": "..." } برمیگردانند. تا وقتی nextCursor خالی نیست، آن را در ?cursor= بفرست. limit بین ۱ تا ۱۰۰ است (پیشفرض ۲۰).
Idempotency-Key
روی هر POST و PUT میتوانی هدر Idempotency-Key (۸ تا ۱۰۰ نویسه) بفرستی. اگر شبکه قطع شد و همان درخواست را با همان کلید تکرار کردی، استودیو تا ۲۴ ساعت همان پاسخ اول را برمیگرداند (با هدر Idempotent-Replayed: true) و کار را دوباره انجام نمیدهد. ack خودش هم تکرارپذیر است: ack دوباره برای یک تحویل، duplicate: true برمیگرداند.
چرخهٔ تحویل
هر کاری که سایت باید انجام دهد یک «تحویل» است. محتوای تحویل هنگام گرفتنش ساخته میشود، پس همیشه آخرین نسخهٔ متن را میگیری.
- pending
- deliveredlease 15 min
- acked
fail با retryable: true آن را با فاصلهٔ فزاینده (۱، ۴، ۱۶، ۶۴ دقیقه…) تا ۵ بار به صف برمیگرداند؛ بعد از آن یا با retryable: false وضعیت failed میشود و مدیر در پنل میبیند.| نوع | کار سایت | ack |
|---|---|---|
post.publish | مقالهٔ تازه را بساز (یا اگر remoteId دارد، بهروز کن) با تصویر شاخص و تصویرهای متن. | remoteId، status، link، coverSet |
post.cover | تصویر شاخص مقالهای را که داری عوض کن. | coverSet: true |
content.update | صفحهٔ موجودی را که مدیر تأیید کرده بازنویسی کن. | remoteId، link، hash |
content.restore | نسخهٔ قبلی همان صفحه را برگردان. | remoteId، hash |
نوع ناشناختهای دیدی؟ با fail و کد unsupported_type و retryable: false ردش کن تا صف گیر نکند.
انتشار مقاله
محتوای یک تحویل post.publish:
{
"id": "dlv_8c1d0a4f2b7e9c3d5a61",
"type": "post.publish",
"status": "delivered",
"leaseUntil": "2026-10-22T09:15:00.000Z",
"payload": {
"slug": "shoe-wax-guide",
"title": "راهنمای واکس زدن کفش چرم",
"excerpt": "واکس مناسب، برس نرم و ده دقیقه وقت؛ همهٔ آنچه برای براق کردن کفش لازم داری.",
"keyword": "واکس کفش",
"contentHtml": "<h2>ابزار لازم</h2><p>…</p><figure><img src=\"{{origin}}/api/v1/figures/3fa2c9e1b07d\" alt=\"…\"></figure>",
"seo": { "title": "", "description": "واکس مناسب، برس نرم و…", "focusKeyword": "واکس کفش" },
"cover": { "url": "{{origin}}/api/v1/posts/shoe-wax-guide/cover", "mime": "image/png", "alt": "راهنمای واکس زدن کفش چرم", "version": 1761124500000 },
"figures": [{ "key": "3fa2c9e1b07d", "url": "{{origin}}/api/v1/figures/3fa2c9e1b07d", "mime": "image/png", "alt": "…" }],
"remoteId": null
}
}
قاعدهها
- عنوان، H1 صفحه است.
contentHtmlبا<h2>شروع میشود و H1 ندارد؛ عنوان را خودت H1 صفحه کن. - تصویرها را خودت میزبانی کن. نشانی
cover.urlو هرfigures[].urlفقط با همان توکن باز میشود. هر کدام را دانلود کن، در رسانههای سایت بگذار و نشانیاش را درcontentHtmlجایگزین کن (نشانیها عیناً در HTML آمدهاند). - سئو:
seo.descriptionتوضیح متا وseo.focusKeywordکلمهٔ کلیدی است.seo.titleبرای مقالهٔ تازه خالی است تا قالب عنوان سایت دست نخورد. - تکراری نساز. اول با
slugدنبال مقاله بگرد؛ اگر تحویلی دوباره آمد (مثلاً ack قبلی نرسید)، همان را بهروز کن. - HTML را پاکسازی کن. محتوا را با ابزار امن پلتفرمت (مثل
wp_kses_postدر وردپرس) ذخیره کن.
پیشنویس و زمانبندی
اگر سایتت مقالهها را پیشنویس نگه میدارد یا زمانبندی میکند، ack را با status: "draft" یا "future" و بدون link بفرست. وقتی بعداً منتشر شد، همان مقاله را در PUT /site/content با status: "publish" و link بفرست؛ استودیو آن را منتشرشده حساب میکند و پیگیری سئو را شروع میکند.
تغییر صفحههای موجود
تیم برای صفحههای فعلی سایت (حتی آنهایی که خودش ننوشته) پیشنهاد بهبود میدهد. وقتی مدیر تأیید کرد، یک تحویل content.update میرسد:
{
"remoteId": "41",
"type": "page",
"url": "https://shop.example/about/",
"expectedHash": "9b0c…e4",
"title": "دربارهٔ ما",
"excerpt": "…",
"contentHtml": "…",
"seo": { "title": "دربارهٔ ما", "description": "…", "focusKeyword": "" },
"figures": []
}
- هش فعلی صفحه را حساب کن (فرمول در همگامسازی).
- اگر با
expectedHashبرابر نیست، صفحه بعد از پیشنهاد عوض شده: چیزی را تغییر نده و باcode: "snapshot_mismatch"fail کن. پیشنهاد «منقضی» میشود و تیم نسخهٔ تازه میسازد. - اگر برابر است، عنوان، خلاصه، متن و سئو را بنویس. رسانههایی که صفحه داشت و بازنویسی حذفشان کرده بود، از قبل به انتهای
contentHtmlبرگردانده شدهاند. - ack را با
hashصفحه بعد از ذخیره بفرست. با همین هش، مدیر میتواند تغییر را بعداً برگرداند (content.restore).
همگامسازی سایت
تیم فقط وقتی میتواند سایت موجودت را بهتر کند که آن را بشناسد. بعد از اتصال، همهٔ نوشتهها و برگههای منتشرشده را در دستههای ۱۰۰تایی بفرست، و از آن به بعد هر صفحه را هر وقت تغییر کرد. در همگامسازی کامل، اولین دسته را با "reset": true بفرست تا صفحههایی که دیگر وجود ندارند از فهرست استودیو هم پاک شوند.
curl -X PUT {{origin}}/api/v1/site/content \
-H "Authorization: Bearer $STUDIO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"items":[{"remoteId":"41","type":"page","status":"publish",
"link":"https://shop.example/about/","title":"دربارهٔ ما",
"excerpt":"","contentHtml":"<p>…</p>","modified":"2026-10-01T10:00:00Z"},
{"remoteId":"17","deleted":true}]}'
await api("PUT", "/site/content", {
items: pages.map((page) => ({
remoteId: String(page.id),
type: page.type, // "post" | "page" | …
status: page.status, // "publish" | "draft" | …
link: page.url,
title: page.title,
excerpt: page.excerpt,
contentHtml: page.rawHtml, // HTML ذخیرهشده، نه رندرشده
modified: page.updatedAt,
})),
});
$items = array_map(fn ($page) => [
'remoteId' => (string) $page['id'],
'type' => $page['type'],
'status' => $page['status'],
'link' => $page['url'],
'title' => $page['title'],
'excerpt' => $page['excerpt'],
'contentHtml' => $page['raw_html'],
'modified' => $page['updated_at'],
], $pages);
studio_api('PUT', '/site/content', ['items' => $items]);
api("PUT", "/site/content", {"items": [{
"remoteId": str(page.id),
"type": page.type,
"status": page.status,
"link": page.url,
"title": page.title,
"excerpt": page.excerpt,
"contentHtml": page.raw_html,
"modified": page.updated_at.isoformat(),
} for page in pages]})
هش محتوا
استودیو برای هر صفحه این هش را حساب میکند و تغییرها را فقط روی همان نسخه اعمال میکند. ورودیها مقدارهای خام ذخیرهشده هستند، با بایت NUL (\0) به هم چسبیده:
hash = sha256_hex(title + "\0" + excerpt + "\0" + contentHtml)
printf '%s\0%s\0%s' "$TITLE" "$EXCERPT" "$HTML" | sha256sum
import { createHash } from "node:crypto";
const hash = createHash("sha256").update([title, excerpt, html].join("\0")).digest("hex");
$hash = hash('sha256', $title . "\0" . $excerpt . "\0" . $html);
import hashlib
digest = hashlib.sha256("\0".join([title, excerpt, html]).encode("utf-8")).hexdigest()
دیدگاهها و دستهها
PUT /site/comments دیدگاههای تأییدشده را میگیرد تا تیم بداند خوانندهها چه میپرسند. فقط نام نمایشی و متن را بفرست؛ ایمیل و IP نفرست (اگر بیاید هم دور ریخته میشود). PUT /site/taxonomies فهرست کامل دستهها و برچسبها را جایگزین میکند.
PUT /site/comments
{ "items": [{ "remoteId": "c-901", "postRemoteId": "812", "postLink": "https://shop.example/blog/shoe-wax/",
"author": "مینا", "content": "برای کفش جیر هم جواب میدهد؟", "date": "2026-10-21T18:02:00Z", "status": "approved" }] }
PUT /site/taxonomies
{ "categories": [{ "remoteId": "3", "name": "راهنما", "slug": "guides", "count": 24 }],
"tags": [{ "remoteId": "11", "name": "چرم", "slug": "leather", "count": 9 }] }
وبهوک
وبهوک فقط خبر میدهد، محتوا ندارد. تو باید بعد از دریافتش تحویل را با توکنت بگیری؛ پس حتی اگر کسی وبهوک جعلی بفرستد، به محتوایی نمیرسد. باز هم امضا را همیشه بررسی کن.
تنظیم
PUT /webhook
{ "url": "https://shop.example/hooks/content-studio" }
→ { "url": "https://shop.example/hooks/content-studio", "secret": "whsec_Qm9…" }
نشانی باید https و روی همان دامنهٔ سایت پروژه باشد. راز فقط همین یک بار برمیگردد؛ اگر گمش کردی، POST /webhook/rotate راز تازه میدهد. POST /webhook/test یک رویداد ping میفرستد.
رویدادها
| رویداد | کی | چه کنی |
|---|---|---|
delivery.created | کاری در صف آمد. | GET /deliveries/{data.deliveryId} |
ping | آزمایش از پنل یا API. | فقط 200 برگردان. |
connection.revoked | مدیر اتصال را قطع یا توکن را باطل کرد. | توکن و راز را پاک کن. |
POST /hooks/content-studio
Content-Type: application/json
User-Agent: ContentStudio-Webhook/1.0.0
X-Studio-Event: delivery.created
X-Studio-Event-Id: evt_6b1f0c2d9e8a7f5c4b3a2918
X-Studio-Delivery: dlv_8c1d0a4f2b7e9c3d5a61
X-Studio-Signature: t=1761124500,v1=5f2b…c9
{"id":"evt_6b1f…","event":"delivery.created","projectId":"…","createdAt":"2026-10-22T09:15:00.000Z",
"data":{"deliveryId":"dlv_8c1d0a4f2b7e9c3d5a61","type":"post.publish"}}
بررسی امضا
امضا HMAC-SHA256 رشتهٔ <t>.<بدنهٔ خام> با کل راز (همراه whsec_) است. بدنه را پیش از parse کردن بخوان، زمان را با فاصلهٔ حداکثر ۳۰۰ ثانیه بپذیر و مقایسه را زمانثابت انجام بده.
# امضای مورد انتظار را دستی بساز
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyStudio(secret, header, rawBody) {
const parts = Object.fromEntries(String(header).split(",").map((part) => part.trim().split("=")));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 || "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}
// Express: app.post("/hooks/content-studio", express.raw({ type: "application/json" }), (req, res) => {
// if (!verifyStudio(SECRET, req.get("x-studio-signature"), req.body.toString("utf8"))) return res.sendStatus(401);
// res.sendStatus(202); // اول جواب بده، بعد کار کن
// queue.push(JSON.parse(req.body).data.deliveryId);
// });
function studio_verify(string $secret, string $header, string $rawBody): bool {
$parts = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
$parts[$key] = $value;
}
$t = (int) ($parts['t'] ?? 0);
if ($t === 0 || abs(time() - $t) > 300) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, (string) ($parts['v1'] ?? ''));
}
$raw = file_get_contents('php://input');
if (!studio_verify(getenv('STUDIO_WEBHOOK_SECRET'), $_SERVER['HTTP_X_STUDIO_SIGNATURE'] ?? '', $raw)) {
http_response_code(401);
exit;
}
http_response_code(202);
import hashlib, hmac, time
def verify_studio(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
try:
t = int(parts.get("t", "0"))
except ValueError:
return False
if not t or abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
تحویل مطمئن
- ظرف ۱۰ ثانیه با 2xx جواب بده و کار را بعد از جواب انجام بده.
- اگر پاسخ 2xx نبود یا نرسید، استودیو بعد از ۱ دقیقه، ۵ دقیقه، ۳۰ دقیقه، ۲ ساعت و ۶ ساعت دوباره میفرستد. شناسهٔ
X-Studio-Event-Idدر تکرارها ثابت است؛ با آن تکراریها را نادیده بگیر. - وبهوک جایگزین خواندن صف نیست: هر چند دقیقه
GET /deliveriesرا هم بخوان تا هیچ کاری جا نماند. - وبهوک به ریدایرکت عمل نمیکند و به نشانیهای خصوصی (localhost، شبکهٔ داخلی) فرستاده نمیشود.
اتصال یککلیکی (برای سازندگان پلتفرم)
اگر برای پلتفرمی افزونه یا اپ میسازی، کاربرانت لازم نیست توکن کپی کنند. از جریان استاندارد OAuth 2 «authorization code» با PKCE استفاده کن؛ افزونهٔ وردپرس ما هم همین کار را میکند.
-
verifier و challenge بساز
VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n' | cut -c1-64) CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n')import { createHash, randomBytes } from "node:crypto"; const verifier = randomBytes(32).toString("base64url"); // ۴۳ نویسه const challenge = createHash("sha256").update(verifier).digest("base64url"); const state = randomBytes(16).toString("hex"); // در نشست کاربر نگه دار$b64url = fn (string $bin) => rtrim(strtr(base64_encode($bin), '+/', '-_'), '='); $verifier = $b64url(random_bytes(32)); $challenge = $b64url(hash('sha256', $verifier, true)); $state = bin2hex(random_bytes(16)); // در نشست کاربر نگه دارimport base64, hashlib, secrets verifier = secrets.token_urlsafe(32) challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode() state = secrets.token_hex(16) # در نشست کاربر نگه دار -
کاربر را به صفحهٔ تأیید بفرست
{{origin}}/connect ?site=https://shop.example &redirect_uri=https://shop.example/admin/studio/callback &state=STATE &code_challenge=CHALLENGE &code_challenge_method=S256 &platform=myplatform &site_name=فروشگاه کفشredirect_uriباید https و روی همان دامنهٔsiteباشد. کاربر وارد استودیو میشود، پروژهٔ همدامنه را انتخاب و تأیید میکند. -
کد را با توکن عوض کن
استودیو کاربر را به
redirect_uri?code=…&state=…برمیگرداند (یاerror=access_deniedاگر انصراف داد).stateرا با نسخهٔ نشست مقایسه کن، بعد از سرور:POST {{origin}}/api/v1/connect/token Content-Type: application/json { "code": "…", "code_verifier": "VERIFIER", "redirect_uri": "https://shop.example/admin/studio/callback", "webhook_url": "https://shop.example/hooks/content-studio" } → { "access_token": "cst_…", "token_type": "Bearer", "scope": "project:read posts:read deliveries:read deliveries:write site:write webhooks:manage", "project": { "id": "…", "name": "فروشگاه کفش", "siteUrl": "https://shop.example/" }, "webhook": { "url": "https://shop.example/hooks/content-studio", "secret": "whsec_…" } }کد یکبارمصرف است و ۵ دقیقه اعتبار دارد. اتصال تازه، توکن اتصال قبلی همان پروژه را باطل میکند. توکن و راز وبهوک را رمزنگاریشده ذخیره کن.
مرجع endpointها
جزئیات کامل هر فیلد در مشخصات OpenAPI است و میتوانی آن را در Postman، Insomnia یا ابزار ساخت SDK بارگذاری کنی.
| روش | مسیر | دسترسی | کار |
|---|---|---|---|
| GET | /api/v1 | عمومی | نام و نسخهٔ API |
| GET | /api/v1/openapi.json | عمومی | مشخصات OpenAPI |
| GET | /api/v1/plugins/wordpress/latest | عمومی | آخرین نسخهٔ افزونهٔ وردپرس |
| POST | /api/v1/connect/token | PKCE | گرفتن توکن با کد اتصال |
| GET | /api/v1/me | هر توکن | آزمایش توکن، پروژه و اتصال |
| DELETE | /api/v1/me | هر توکن | باطل کردن همین توکن (قطع اتصال از سمت سایت) |
| GET | /api/v1/posts | posts:read | فهرست مقالهها (status، limit، cursor) |
| GET | /api/v1/posts/{slug} | posts:read | یک مقاله با HTML کامل |
| GET | /api/v1/posts/{slug}/cover | posts:read | تصویر شاخص (باینری) |
| GET | /api/v1/figures/{key} | posts:read | تصویر داخل متن (باینری) |
| GET | /api/v1/deliveries | deliveries:read | کارهای منتظر |
| GET | /api/v1/deliveries/{id} | deliveries:read | گرفتن و رزرو یک کار |
| POST | /api/v1/deliveries/{id}/ack | deliveries:write | اعلام انجام |
| POST | /api/v1/deliveries/{id}/fail | deliveries:write | اعلام شکست |
| GET | /api/v1/site | project:read | وضعیت اتصال و همگامسازی |
| PUT | /api/v1/site | site:write | معرفی سایت و اتصال انتشار |
| PUT | /api/v1/site/content | site:write | فرستادن صفحهها |
| PUT | /api/v1/site/comments | site:write | فرستادن دیدگاهها |
| PUT | /api/v1/site/taxonomies | site:write | دستهها و برچسبها |
| GET | /api/v1/webhook | webhooks:manage | وضعیت وبهوک |
| PUT | /api/v1/webhook | webhooks:manage | تنظیم نشانی |
| DELETE | /api/v1/webhook | webhooks:manage | حذف وبهوک |
| POST | /api/v1/webhook/rotate | webhooks:manage | راز تازه |
| POST | /api/v1/webhook/test | webhooks:manage | فرستادن ping |
افزونهٔ وردپرس
برای سایتهای وردپرسی همهٔ این کارها آماده است و به هیچ کدی نیاز نداری.
- افزونه را دانلود کن و در افزونهها ← افزودن ← بارگذاری افزونه نصب و فعال کن.
- به تنظیمات ← استودیوی محتوا برو و اتصال با یک کلیک را بزن.
- در استودیو پروژه را تأیید کن؛ به وردپرس برمیگردی و تمام.
- انتشار خودکار مقاله با تصویر شاخص و تصویرهای متن در کتابخانهٔ رسانهٔ خود سایت
- حالت انتشار: مستقیم، پیشنویس، یا زمانبندی با تأخیر یا ساعت ثابت
- نویسنده و دستهٔ پیشفرض، و افزودن کلمهٔ کلیدی بهعنوان برچسب
- توضیح متا، کلمهٔ کلیدی و عنوان سئو در Yoast SEO و Rank Math
- همگامسازی نوشتهها، برگهها، دیدگاهها و دستهها
- اعمال امن تغییرهای تأییدشده با بررسی هش و حفظ رسانهها
- وبهوک امضاشده و سرکشی پشتیبان هر ۵ دقیقه
- صفحهٔ وضعیت، لاگ رویدادها و آزمونهای «سلامت سایت»
- رمزنگاری توکن روی سرور خودت و بهروزرسانی خودکار
امنیت
- توکن را مثل رمز عبور نگه دار: رمزنگاریشده در پایگاه داده یا متغیر محیطی، هرگز در کد سمت مرورگر، مخزن git یا لاگ.
- کمترین دسترسی: برای ابزاری که فقط میخواند، توکن «فقط خواندن» بساز.
- انقضا و چرخش: برای توکنهای دستی انقضا بگذار و پیش از تمام شدنش توکن تازه بساز.
- امضای وبهوک را همیشه بررسی کن و فاصلهٔ زمانی بیش از ۵ دقیقه را رد کن.
- HTML را پاکسازی کن پیش از ذخیره، با ابزار امن پلتفرم خودت.
- نشانیهای تصویر را فقط با همین توکن و فقط از نشانیهای همین استودیو دانلود کن.
- نشت؟ همان لحظه توکن را از تنظیمات پروژه باطل کن؛ «رویدادهای اخیر API» در همانجا نشان میدهد چه کسی و کی از آن استفاده کرده.
- مشکل امنیتی پیدا کردی؟ با
requestIdبه پشتیبانی خبر بده؛ عمومی منتشرش نکن.
نسخهها
نسخهٔ API در هدر X-Api-Version هر پاسخ میآید. تغییرهای ناسازگار فقط در /api/v2 میآیند؛ در v1 فقط فیلد و endpoint تازه اضافه میشود، پس فیلدهای ناشناخته را نادیده بگیر.
- 1.0.0 مهر ۱۴۰۵
- نسخهٔ نخست: توکنهای پروژه، تحویلها، همگامسازی سایت، وبهوک امضاشده، اتصال یککلیکی و افزونهٔ وردپرس.