مستندات وبهوک برای توسعهدهندگان
اگر میخواهید سرویس خودتان نتیجهی تولید محتوای Textoma را مستقیم و لحظهای دریافت کند، این صفحه هر چیزی که برای ساخت یک endpoint امن و قابلاعتماد لازم دارید را توضیح میدهد: رویدادها، ساختار داده، نحوهی تایید امضا، سیاست تلاش مجدد و نمونه کد آماده به چند زبان.
۱رویدادها (Events)
هر پروفایل وبهوک برای یک یا چند تا از این رویدادها فعال میشود:
| رویداد | زمان ارسال |
|---|---|
job.started | همان لحظهای که پردازش سفارش (job) شروع میشود |
job.completed | وقتی مقاله با موفقیت تولید و آماده شد |
job.failed | وقتی تولید مقاله به هر دلیلی شکست بخورد |
پیشفرض روی job.completed و job.failed فعال است؛ job.started اختیاری است.
۲ساختار داده (Payload)
بدنهی دقیقی که ارسال میشود را خود کاربر Textoma میتواند با یک قالب سفارشی (template) تغییر دهد. آنچه زیر میبینید قالب پیشفرض و فیلدهای استانداردی است که همیشه در دسترساند؛ اگر کاربر قالب خودش را تعریف کرده باشد، ممکن است اسم/ساختار فیلدها فرق کند — اما دادههای زیرین (job, batch, article, meta) همیشه همینها هستند.
{
"event": "job.completed",
"job": {
"id": 9001,
"title": "راهنمای شروع بازاریابی محتوایی",
"status": "completed"
},
"batch": {
"id": 501,
"category": "بازاریابی و تبلیغات"
},
"article": {
"title": "راهنمای شروع بازاریابی محتوایی",
"summary": "خلاصهی مقاله...",
"content": "<h1>...</h1><p>محتوای کامل HTML مقاله...</p>",
"tags": ["بازاریابی محتوایی", "تولید محتوا", "استراتژی دیجیتال"],
"sections": [
{ "heading": "بازاریابی محتوایی چیست؟", "content": "..." },
{ "heading": "گامهای اجرای یک استراتژی موفق", "content": "..." }
]
},
"sent_at": "2026-07-17T10:15:00+03:30"
}
اگر رویداد job.failed باشد، job.status برابر "failed" میشود و یک فیلد job.error هم با متن خطا اضافه میشود؛ در این حالت معمولاً article خالی میماند.
فیلدهای در دسترس (صرفنظر از قالب)
| فیلد | توضیح |
|---|---|
job.id | شناسهی سفارش (job) |
job.title | عنوان مقاله |
job.status | processing / completed / failed |
job.error | فقط در حالت شکست پر میشود |
batch.id | شناسهی دسته (batch) که این job به آن تعلق دارد |
batch.category | عنوان دستهبندی محتوا |
article.title / summary / content | محتوای نهایی (content بهصورت HTML خام) |
article.tags | آرایهای از رشتهها |
article.toc / sections | آرایهای از بخشها، هرکدام heading و content |
article.faq | آرایهای از {question, answer} |
meta.generated_at | زمان تولید payload، فرمت RFC3339 |
meta.platform | همیشه "textoma" |
۳هدرها
| هدر | مقدار |
|---|---|
Content-Type | پیشفرض application/json (قابل تغییر در پنل) |
X-Textoma-Signature | امضای HMAC-SHA256 بدنهی خام درخواست، بهصورت hex |
هیچ هدر Authorization/Bearer دیگری ارسال نمیشود — تایید اصالت درخواست فقط از طریق همین هدر امضا انجام میشود.
۴تایید امضا (Signature Verification)
هنگام ساخت پروفایل وبهوک، یک Secret به شما داده
میشود. Textoma برای هر درخواست، امضای زیر را حساب و در هدر
X-Textoma-Signature قرار میدهد:
hex( HMAC_SHA256( secret, raw_request_body ) )
hmac.compare_digest، hash_equals، crypto.timingSafeEqual یا hmac.Equal.
هیچوقت امضا را با == ساده مقایسه نکنید — این کار سرور شما را در برابر timing attack آسیبپذیر میکند.
۵تلاش مجدد (Retry) و پاسخ مورد انتظار
- سرور شما باید در بازهی حدود ۱۵ ثانیه یک پاسخ با کد وضعیت ۲xx برگرداند. هر چیز دیگری (۴xx، ۵xx، تایماوت، اتصال ناموفق) شکست محسوب میشود.
- در صورت شکست، حداکثر ۶ تلاش کل (۱ تلاش اول + ۵ تلاش مجدد) با فاصلهی ۱، ۵، ۱۵، ۶۰ دقیقه انجام میشود (تلاشهای بعدی هر ۶۰ دقیقه تکرار میشوند).
- اگر همهی تلاشها شکست بخورد، دیگر تلاش مجددی انجام نمیشود.
- پردازش سنگین (ذخیره در دیتابیس، انتشار در CMS و...) را async انجام دهید و سریع ۲۰۰ برگردانید.
- چون retry ممکن است باعث دریافت یک رویداد مشابه بیش از یکبار شود، پردازش شما بهتر است idempotent باشد (بر اساس
job.id+eventچک کنید که قبلاً پردازش نشده باشد).
۶نمونه کد دریافت و تایید امضا
هر چهار نمونه دقیقاً یک کار را انجام میدهند: بدنهی خام را میخوانند، امضا را با ثابتزمان تایید میکنند، سریع ۲۰۰ برمیگردانند و بعد رویداد را پردازش میکنند.
Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.TEXTOMA_WEBHOOK_SECRET;
// باید بدنهی خام (raw) رو نگه داریم، نه فقط JSON پارسشده
app.use(express.raw({ type: '*/*' }));
app.post('/webhooks/textoma', (req, res) => {
const signature = req.header('X-Textoma-Signature') || '';
const rawBody = req.body; // Buffer
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
const isValid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!isValid) {
return res.status(401).json({ error: 'invalid signature' });
}
const payload = JSON.parse(rawBody.toString('utf8'));
// فوراً ۲۰۰ برگردونید، پردازش سنگین رو async انجام بدید
res.status(200).json({ received: true });
switch (payload.event) {
case 'job.completed':
console.log('article ready:', payload.article.title);
break;
case 'job.failed':
console.error('job failed:', payload.job.error);
break;
case 'job.started':
console.log('job started:', payload.job.id);
break;
}
});
app.listen(3000);
PHP
<?php
$secret = getenv('TEXTOMA_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TEXTOMA_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'invalid signature']);
exit;
}
$payload = json_decode($rawBody, true);
http_response_code(200);
echo json_encode(['received' => true]);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
switch ($payload['event']) {
case 'job.completed':
error_log('article ready: ' . $payload['article']['title']);
break;
case 'job.failed':
error_log('job failed: ' . ($payload['job']['error'] ?? ''));
break;
case 'job.started':
error_log('job started: ' . $payload['job']['id']);
break;
}
Python (Flask)
import hashlib
import hmac
import json
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["TEXTOMA_WEBHOOK_SECRET"]
@app.post("/webhooks/textoma")
def textoma_webhook():
raw_body = request.get_data() # بدنهی خام، قبل از هر پارسی
signature = request.headers.get("X-Textoma-Signature", "")
expected = hmac.new(
WEBHOOK_SECRET.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
return jsonify({"error": "invalid signature"}), 401
payload = json.loads(raw_body)
event = payload.get("event")
if event == "job.completed":
print("article ready:", payload["article"]["title"])
elif event == "job.failed":
print("job failed:", payload["job"].get("error"))
elif event == "job.started":
print("job started:", payload["job"]["id"])
return jsonify({"received": True}), 200
if __name__ == "__main__":
app.run(port=3000)
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
)
var webhookSecret = os.Getenv("TEXTOMA_WEBHOOK_SECRET")
type webhookPayload struct {
Event string `json:"event"`
Job struct {
ID int `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
Error string `json:"error"`
} `json:"job"`
Article struct {
Title string `json:"title"`
Content string `json:"content"`
} `json:"article"`
}
func sign(secret string, body []byte) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
return hex.EncodeToString(mac.Sum(nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
signature := r.Header.Get("X-Textoma-Signature")
expected := sign(webhookSecret, rawBody)
if !hmac.Equal([]byte(expected), []byte(signature)) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var payload webhookPayload
if err := json.Unmarshal(rawBody, &payload); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"received": true}`))
switch payload.Event {
case "job.completed":
log.Println("article ready:", payload.Article.Title)
case "job.failed":
log.Println("job failed:", payload.Job.Error)
case "job.started":
log.Println("job started:", payload.Job.ID)
}
}
func main() {
http.HandleFunc("/webhooks/textoma", handler)
log.Fatal(http.ListenAndServe(":3000", nil))
}
وردپرس (افزونهی آماده)
اگر سایت شما وردپرسی است، نیازی به نوشتن کد نیست — افزونهی رسمی Textoma Webhook Receiver همین دریافت و تایید امضا را خودش انجام میدهد و از هر job.completed یک نوشته میسازد.
فعالسازی سریع:
- در پیشخوان وردپرس: افزونهها > افزودن > بارگذاری افزونه، فایل بالا را آپلود و فعال کنید.
- مسیر تنظیمات > Textoma Webhook را باز کنید و فیلدها (نوع پست، وضعیت انتشار، نویسنده، دستهبندی و غیره) را تنظیم کنید.
- آدرس وبهوکی که همان صفحه نشان میدهد (چیزی شبیه
https://yourdomain.com/wp-json/textoma-webhook/v1/receive) را کپی کنید. - در پنل Textoma، هنگام ساخت پروفایل وبهوک، همان آدرس را در فیلد URL بگذارید و یک Secret تعیین کنید؛ همان Secret را عیناً در فیلد Webhook Secret وردپرس هم وارد کنید.
- یک تست ارسال کنید و در نوشتههای وردپرس بررسی کنید که پست ساخته شده باشد.
اگر Permalink سایت روی «ساده» (Plain) باشد، مسیرهای REST API ممکن است کار نکنند — از تنظیمات > پیوندهای یکتا یک ساختار غیر از Plain انتخاب کنید.
۷چکلیست پیادهسازی
- endpoint شما بدنهی خام (raw bytes) را قبل از پارس JSON، برای محاسبهی امضا نگه میدارد.
- امضا با تابع امن (constant-time) مقایسه میشود، نه
==ساده. - در صورت نامعتبر بودن امضا، درخواست رد میشود (۴۰۱).
- پاسخ ۲xx در کمتر از چند ثانیه برگردانده میشود؛ پردازش سنگین async انجام میشود.
- پردازش رویدادها idempotent است (تکرار احتمالی بهخاطر retry مشکلی ایجاد نمیکند).
- هر سه رویداد (
job.started,job.completed,job.failed) هندل میشوند، یا حداقل نادیده گرفتن رویدادهای ناشناس باعث خطا نمیشود.