Otomi API — راهنمای برنامهنویس (نسخه ۱)
برای سایت یا اپی که ووکامرس نیست. اگر سایت شما وردپرس + ووکامرس است، افزونه را نصب کنید؛ اگر PrestaShop است، ماژول آماده را (در اپ: افزودن فروشگاه ← پرستاشاپ). این راهنما برای بقیه است.
آدرس API:
https://my.otomi.app/api(تا راهاندازی سرور اصلی). شرح ماشینی همهی درخواستها:openapi.json(OpenAPI 3.1، از همان Schemaهایی که سرور با آنها بررسی میکند).
۱. ده دقیقه تا اولین سفارش
- مدیر فروشگاه در اپ اتومی: تنظیمات ← اتصال به فروشگاه ← افزودن فروشگاه ← سایت یا اپ دیگر ← مشخصات ← ساخت کلید ← کلید آزمایشی و کلید را به شما میدهد.
- این را اجرا کنید:
curl https://my.otomi.app/api/v1/orders \
-H "Authorization: Bearer osk_test_XXXXXXXX" \
-H "Content-Type: application/json" \
-d '{"id":"1023","status":"paid","total":"150000","customer":{"mobile":"09121234567"}}'
-
پاسخ:
{"ok":true,"mode":"test","events":["order.created"]}— یعنی با کلید اصلی «سفارش جدید» ثبت میشد. همین درخواست در اپ، زیر درخواستهای اخیر دیده میشود. -
وقتی همهچیز درست بود، کلید اصلی (
osk_live_…) را جایگزین کنید و آدرس سایت را ثبت کنید (بخش ۴) تا اطلاعات قبلی خودکار بیاید.
کلید آزمایشی همهچیز را بررسی میکند ولی هیچچیز ذخیره نمیکند و هیچ پیامی نمیفرستد.
۲. تنها قانونی که باید یادتان بماند
هر بار چیزی ساخته شد یا تغییر کرد، کل آن را بفرستید.
سفارش (ثبت، پرداخت، ارسال، لغو، استرداد)، مشتری (ثبتنام، ورود)، محصول (قیمت، موجودی، دسته)، اشتراک (خرید، تمدید، لغو، پرداخت ناموفق) و نظرِ منتشرشده. لازم نیست بگویید «چه اتفاقی افتاد»: اتومی با نسخهی قبلی مقایسه میکند و خودش میفهمد. دوباره فرستادن هیچ ضرری ندارد (پیام تکراری نمیرود).
۳. درخواستها
همه با Authorization: Bearer <کلید> و بدنهی JSON.
| درخواست | کار |
|---|---|
GET /v1/ping |
کلید درست است؟ نام و ارز فروشگاه را برمیگرداند |
POST /v1/orders |
یک سفارش (ساخت یا بهروزرسانی؛ id تکراری = بهروزرسانی) |
POST /v1/customers |
یک مشتری |
POST /v1/products |
یک محصول |
DELETE /v1/products/{id} |
محصولی که از سایت حذف شد |
POST /v1/reviews |
نظری که روی سایت منتشر شد |
POST /v1/subscriptions |
یک اشتراک |
PUT /v1/hook |
آدرس سایت: اتومی خودش اطلاعات قبلی و هر ساعت تغییرات را میگیرد (بخش ۴) |
POST /v1/import |
فقط اگر آدرس سایت ندارید: دادهی قدیمی، تا ۵۰۰ رکورد، بدون پیام |
سفارش
| فیلد | اجباری | توضیح | مثال |
|---|---|---|---|
id |
✔ |
شناسهی سفارش در سیستم شما: حرف، رقم، _ .
-، تا ۶۴
|
"SF-1023" |
status |
✔ |
یکی از: pending_payment paid processing
shipped delivered completed
cancelled refunded
|
"paid" |
total |
✔ | مبلغ به ارزی که هنگام اتصال انتخاب شد (تومان یا ریال)؛ عدد یا متن | "150000" |
customer |
✔ | دستکم mobile یا email (جدول مشتری) |
|
paid |
اگر نفرستید از status حساب میشود |
true |
|
refunded |
جمع مبلغ مسترد تا الان | "50000" |
|
discountTotal |
جمع تخفیف این سفارش | "20000" |
|
coupons |
کدهای تخفیف استفادهشده | ["YALDA"] |
|
items |
[{ "productId": "55", "quantity": 1 }]
— همان id محصول در /products
|
||
paymentMethod |
یک کلمه، بیفاصله؛ در اپ برای شرط «روش پرداخت» فهرست میشود | "zarinpal" |
|
shippingMethod |
مثل بالا؛ با آن گزینههای ارسال در اپ روشن میشود | "post" |
|
source |
خریدار از کجا آمد (utm_source) | "instagram" |
|
currency |
اگر بفرستید باید ارز فروشگاه باشد: IRT یا IRR |
"IRT" |
|
createdAt، paidAt، updatedAt |
ISO 8601 با منطقهی زمانی. updatedAt = آخرین تغییر در سیستم شما
|
"2026-10-24T10:15:00+03:30" |
|
subscription |
سفارشی که اشتراک فروخته:
{ "plan": "monthly", "periodEnd": "…"
}
|
مشتری (داخل سفارش و در /customers)
| فیلد | توضیح |
|---|---|
id |
شناسهی کاربر در سیستم شما (در سفارش مهمان نفرستید؛ در
/customers اجباری)
|
mobile |
هر شکلی: 09121234567، +989121234567، با ارقام فارسی
|
email، firstName، lastName،
city، province
|
|
registeredAt |
فقط در /customers: زمان ثبتنام |
lastLoginAt |
فقط در /customers: آخرین ورود. هر بار دیرتر شود، اتوماسیون «ورود
مشتری» اجرا میشود
|
loginCount |
فقط در /customers: تعداد ورودها تا الان (۱ = خود ثبتنام) |
source |
فقط در /customers: حساب از کجا ساخته شد (utm_source) |
محصول (/products)
| فیلد | اجباری | توضیح | مثال |
|---|---|---|---|
id |
✔ | شناسهی محصول در سیستم شما | "font-12" |
name |
✔ | "فونت ایرانسنس" |
|
url |
صفحهی محصول؛ باید روی دامنهی خود سایت باشد (پیامها فقط به سایت شما لینک میدهند) | ||
price |
قیمت بدون حراج | 450000 |
|
salePrice |
قیمت حراج، وقتی حراج است | 390000 |
|
stock |
موجودی؛ اگر موجودی نگه نمیدارید نفرستید. با آن «هشدار موجودی کم» و گزارش انبار روشن میشود | 3 |
|
active |
false = پنهان یا پیشنویس |
true |
|
categories |
[{ "id": "fonts", "name": "فونتها",
"url": "https://…/c/fonts" }]
— url برای لینک به دسته
|
||
attributes |
[{ "name": "رنگ", "value": "قرمز"
}]
|
||
updatedAt |
آخرین تغییر |
تغییر قیمت یا موجودی محصولی که قبلاً فرستادهاید خودش رویداد میشود. حذف:
DELETE /v1/products/font-12.
نظر (/reviews)
وقتی نظری روی سایت منتشر شد بفرستید. فقط نظرِ کمتر از یک روز اتوماسیون «ثبت نظر» را اجرا میکند.
| فیلد | اجباری | توضیح |
|---|---|---|
id |
✔ | شناسهی نظر |
productId |
✔ | همان id محصول |
rating |
۱ تا ۵؛ بدون ستاره نفرستید | |
customer |
✔ |
id، mobile یا email — پیام به نویسنده فقط
اگر مشتری شناختهشده باشد
|
createdAt |
اشتراک (/subscriptions)
برای اپ یا سایتی که اشتراک میفروشد. اتومی از روی آن «یادآوری تمدید»، «پرداخت تمدید ناموفق»، «تشکر از تمدید» و «برگشت بعد از تمام شدن» را میفرستد.
| فیلد | اجباری | توضیح | مثال |
|---|---|---|---|
id |
✔ | شناسهی اشتراک | "sub_77" |
customer |
✔ | مثل مشتری سفارش | |
plan |
✔ | نام یا شناسهی پلن | "یکساله" |
status |
✔ |
trialing active past_due
cancelled expired
|
"active" |
currentPeriodEnd |
✔ | تا کی پرداخت شده | "2027-10-24T10:00:00+03:30" |
startedAt، autoRenew، price،
currency، updatedAt
|
-
تمدید =
currentPeriodEndدیرتر شد. تمام شدن: اگر پایان گذشت و تمدیدی نرسید، اتومی خودش یک روز بعد آن را تمامشده حساب میکند. -
راه سادهتر: اگر «اشتراک» جدا ندارید، داخل سفارش
subscriptionبفرستید؛ سفارش بعدیِ همان پلن تمدید حساب میشود.
۴. آدرس سایت: همهچیز خودکار (پیشنهادی)
یک آدرس روی سایت خودتان بسازید و یک بار ثبت کنید. از آن به بعد:
- اطلاعات قبلی (مشتریها، محصولات، سفارشها، اشتراکها) را اتومی خودش صفحهبهصفحه میگیرد — بهعنوان تاریخچه، بدون هیچ پیامی به مشتریهای قدیمی.
- هر ساعت هر چه عوض شده را میپرسد؛ اگر درخواستی از سایت گم شده باشد (قطعی شبکه، خطا)، همینجا پیدا میشود و اتوماسیونش اجرا میشود. چیزی که قبلاً رسیده بود تکرار نمیشود.
- اگر بخواهید، همین آدرس کد تخفیف اختصاصی هم میسازد (بخش ۵).
ثبت
سایت یک کلید (ok_ + ۲۲ نویسهی URL-safe) و یک
راز (۳۲ بایت تصادفی، base64url = ۴۳ نویسه) میسازد، اول نزد خودش ذخیره
میکند، بعد با کلید اصلی ثبت میکند:
curl -X PUT https://my.otomi.app/api/v1/hook \
-H "Authorization: Bearer osk_live_..." -H "Content-Type: application/json" \
-d '{"url":"https://shop.example/otomi","keyId":"ok_…","secret":"…43 chars…"}'
-
urlباید HTTPS و روی دامنهی سایتی باشد که در اپ ثبت شده (یا زیردامنه). -
اتومی همان لحظه
healthو یکchangesامضاشده میفرستد؛ فقط اگر درست جواب دهد ذخیره میکند. خطاها:hook_url_not_allowed،hook_unreachable،hook_bad_signature،hook_bad_reply. - با کلید آزمایشی امتحان میشود ولی ذخیره نمیشود. با ثبت، مدیر اعلان میگیرد و در اپ ردیف «دریافت خودکار» وضعیت را نشان میدهد. برای عوض کردن راز، دوباره بفرستید.
-
برای کد تخفیف:
"coupons": { "hourlyLimit": 600, "percentCap": true }را هم بفرستید.
امضا (هر دو طرف)
هر درخواست اتومی POST با بدنهی
{"command": "...", "params": {...}} و چهار
هدر است: X-Otomi-Key، X-Otomi-Timestamp (ثانیهی یونیکس)،
X-Otomi-Nonce، X-Otomi-Signature.
canonical = "OTOMI-HMAC-SHA256\nPOST\n/otomi/v1/command\n" + timestamp + "\n" + nonce + "\n" + hex(sha256(body))
signature = base64url(HMAC-SHA256(راز ۳۲ بایتی, canonical))
مسیر امضاشده همیشه /otomi/v1/command است، هر آدرسی که داشته باشید. بررسی
کنید: کلید همان است، اختلاف زمان حداکثر ۵ دقیقه، امضا با مقایسهی زمانثابت،
nonce را ۱۰ دقیقه نگه دارید و تکراری را رد کنید. هر خطا =
401 خالی.
پاسخ شما 200 و JSON است و خودش امضا میشود: همان چهار هدر، با
RESPONSE بهجای متد و nonce درخواست بهجای مسیر —
"OTOMI-HMAC-SHA256\nRESPONSE\n" + nonce درخواست + "\n" +
timestamp شما + "\n" + nonce تازهی شما (۱۶ تا ۶۴ نویسه) + "\n" +
hex(sha256(بدنهی پاسخ)). نمونهی کامل در بخش ۸.
فرمان changes
params:
{ "kind": "customers" | "products" | "orders"
| "subscriptions", "since": "2026-10-24T10:00:00Z",
"limit": 100 }
پاسخ: { "items": [ … ], "hasMore": true } — رکوردهای آن
نوع که updatedAtشان برابر یا بعد از
since است، قدیمیترین اول، حداکثر limit تا،
دقیقاً به همان شکل درخواستهای بخش ۳، هرکدام با updatedAt. اولین بار since = 1970-01-01T00:00:00.000Z است (همهچیز).
رکوردی که سرور رد کند (مثلاً لینک خارج از سایت) کنار گذاشته میشود و بقیه ادامه دارد.
health: هر شیء JSON، مثلاً {"ok":true}.
۵. کد تخفیف (اختیاری)
پیامها کد تخفیف اختصاصی دارند اگر فروشگاه در اپ یکی از دو راه را روشن کند. کد ثابت برای همه هیچکدام را لازم ندارد.
راه ۱ — بانک کد (بدون برنامهنویسی): مدیر فایل Excel/CSV کدهای
یکبارمصرف را در اپ بارگذاری میکند. شما فقط کدهای استفادهشده را در
coupons سفارش بفرستید.
راه ۲ — ساخت در سایت (مثل ووکامرس): آدرس سایت با
coupons ثبت شود و این فرمانها را هم جواب دهد. قیمتها به ارز فروشگاه و
رشتهاند؛ expiresAt = زمان ISO. هر کد باید منقضی شود، با تخفیفهای دیگر جمع
نشود، و کلیدش (sendKey / key) را نگه دارد تا
درخواست تکراری همان کد قبلی را برگرداند.
| فرمان | params | پاسخ |
|---|---|---|
create_coupon |
code، sendKey (۶۴ hex)،
type
(percent/fixed/free_shipping)،
amount، maxAmount?، minPurchase?،
excludeSaleItems، expiresAt
|
{"ok":true,"couponId":123} یا
{"ok":false,"reason":"..."}
|
create_coupons |
coupons: [{code, key}] (حداکثر ۱۰۰) + همان تخفیف |
{"ok":true,"made":[{code,couponId}],"taken":[code]}
|
create_shared_coupon |
code، key، automationId، تخفیف،
totalLimit?، expiresAt
|
مثل create_coupon؛ هر مشتری یک بار |
extend_coupon |
code، key، expiresAt |
{"ok":true} — فقط دیرتر |
revoke_coupon |
code |
{"ok":true} — فقط اگر استفاده نشده |
coupon_stats |
— |
{"used":0,"unused":0,"expired":0,"purge":null}
|
purge_used_coupons |
— |
{"ok":true,"total":N} — در پسزمینه و دستهای
|
دلیلهای رد: invalid، limit_percent،
limit_amount، limit_hourly، code_taken.
سقفهای خودتان (درصد، مبلغ، تعداد در ساعت) را همیشه در سایت نگه دارید:
اگر کلیدی لو برود، تخفیف بیحد ساخته نمیشود.
۶. پاسخها و تکرار
| کد | معنی | دوباره بفرستم؟ |
|---|---|---|
200 |
رسید. events میگوید چه چیزی ثبت شد ([] = چیز تازهای
نبود)
|
— |
400 |
داده ایراد دارد. issues[0].path میگوید کجا |
نه، اول درستش کنید |
401 |
کلید اشتباه یا باطلشده | نه |
403 |
از مرورگر فرستادهاید | نه |
429 |
بیش از ۲۰ در ثانیه | بله، بعد از Retry-After |
5xx یا قطع شبکه |
بله، با فاصلهی بیشتر هر بار |
{ "code": "validation", "message": "invalid input", "requestId": "…",
"issues": [{ "path": ["customer"], "code": "mobile_or_email_required" }] }
فیلدی که در جدولها نیست رد میشود (unrecognized_keys) — عمداً، تا دادهی
شخصی اضافه به ما نرسد.
۷. امنیت
- کلید را فقط در سرور نگه دارید (متغیر محیطی)، نه در اپ موبایل، نه در جاوااسکریپت سایت، نه در گیت. درخواستی که از مرورگر بیاید رد میشود.
- کلید لو رفت؟ در اپ کلید تازه بسازید و قبلی را باطل کنید (فوری).
- ارسال را در صف/Job انجام دهید تا با قطع شبکه چیزی گم نشود (با آدرس سایت، هر چه جا بماند هم ساعتی پیدا میشود).
۸. نمونهکد
همهی نمونهها یک سفارش میفرستند و برای 429، 5xx و قطع شبکه
تا ۵ بار با فاصلهی ۱، ۲، ۴، ۸ ثانیه دوباره تلاش میکنند. برای مشتری، محصول و… فقط مسیر
عوض میشود.
PHP
function otomi_send(string $path, array $body): array {
for ($try = 0; $try < 5; $try++) {
$ch = curl_init('https://my.otomi.app/api/v1/' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('OTOMI_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$res = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($res !== false && $status === 200) return json_decode($res, true);
if ($res !== false && $status < 500 && $status !== 429) {
throw new InvalidArgumentException("otomi $status: $res"); // داده را درست کنید
}
sleep(2 ** $try);
}
throw new RuntimeException('otomi unreachable'); // صف شما بعداً دوباره میفرستد
}
otomi_send('orders', ['id' => '1023', 'status' => 'paid', 'total' => '150000',
'customer' => ['mobile' => '09121234567']]);
Laravel
use Illuminate\Support\Facades\Http;
class SendToOtomi implements \Illuminate\Contracts\Queue\ShouldQueue
{
use \Illuminate\Bus\Queueable, \Illuminate\Queue\InteractsWithQueue;
public int $tries = 5;
public array $backoff = [1, 2, 4, 8];
public function __construct(private string $path, private array $body) {}
public function handle(): void
{
$res = Http::withToken(config('services.otomi.key'))
->timeout(10)
->post('https://my.otomi.app/api/v1/' . $this->path, $this->body);
if ($res->status() === 429 || $res->serverError()) {
$this->release(max(1, (int) $res->header('Retry-After')));
return;
}
if ($res->clientError()) {
$this->fail(new \RuntimeException('otomi: ' . $res->body())); // داده را درست کنید
}
}
}
// در Observer سفارش: هر بار ساخته یا ذخیره شد
SendToOtomi::dispatch('orders', [
'id' => (string) $order->id, 'status' => $order->status, 'total' => (string) $order->total,
'customer' => ['id' => (string) $order->user_id, 'mobile' => $order->mobile],
'updatedAt' => $order->updated_at->toIso8601String(),
]);
Node.js
async function otomiSend(path, body) {
for (let attempt = 0; attempt < 5; attempt++) {
try {
const res = await fetch(`https://my.otomi.app/api/v1/${path}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OTOMI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(10_000),
});
if (res.ok) return res.json();
if (res.status !== 429 && res.status < 500) {
throw Object.assign(new Error(await res.text()), { final: true }); // داده را درست کنید
}
} catch (err) {
if (err.final) throw err;
}
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
}
throw new Error('otomi unreachable'); // صف شما بعداً دوباره میفرستد
}
await otomiSend('orders', {
id: '1023', status: 'paid', total: '150000', customer: { mobile: '09121234567' },
});
Python / Django
import os, time, requests
def otomi_send(path: str, body: dict) -> dict:
for attempt in range(5):
try:
res = requests.post(
f"https://my.otomi.app/api/v1/{path}",
json=body,
headers={"Authorization": f"Bearer {os.environ['OTOMI_API_KEY']}"},
timeout=10,
)
if res.status_code == 200:
return res.json()
if res.status_code != 429 and res.status_code < 500:
raise ValueError(f"otomi {res.status_code}: {res.text}") # داده را درست کنید
except requests.RequestException:
pass
time.sleep(2 ** attempt)
raise RuntimeError("otomi unreachable") # Celery/RQ دوباره اجرا میکند
# Django: در signal بعد از ذخیرهی سفارش (بهتر: در یک task صف)
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Order)
def order_saved(sender, instance, **kwargs):
otomi_send("orders", {
"id": str(instance.pk), "status": instance.status, "total": str(instance.total),
"customer": {"id": str(instance.user_id), "mobile": instance.mobile},
"updatedAt": instance.updated_at.isoformat(),
})
C# / .NET
using System.Net;
using System.Net.Http.Json;
public sealed class OtomiClient(HttpClient http)
{
// http.BaseAddress = new("https://my.otomi.app/api/v1/");
// http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("OTOMI_API_KEY"));
public async Task<string> SendAsync(string path, object body, CancellationToken ct = default)
{
for (var attempt = 0; attempt < 5; attempt++)
{
try
{
using var res = await http.PostAsJsonAsync(path, body, ct);
if (res.IsSuccessStatusCode) return await res.Content.ReadAsStringAsync(ct);
if (res.StatusCode != HttpStatusCode.TooManyRequests && (int)res.StatusCode < 500)
throw new InvalidOperationException(await res.Content.ReadAsStringAsync(ct)); // داده را درست کنید
}
catch (HttpRequestException) { }
catch (TaskCanceledException) when (!ct.IsCancellationRequested) { }
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), ct);
}
throw new InvalidOperationException("otomi unreachable"); // صف شما دوباره میفرستد
}
}
// await otomi.SendAsync("orders", new { id = "1023", status = "paid", total = "150000",
// customer = new { mobile = "09121234567" } });
Go
package otomi
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
var client = &http.Client{Timeout: 10 * time.Second}
// Send posts one record (path: "orders", "customers", "products", …) with retries.
func Send(path string, body any) ([]byte, error) {
payload, err := json.Marshal(body)
if err != nil {
return nil, err
}
for attempt := 0; attempt < 5; attempt++ {
req, _ := http.NewRequest("POST", "https://my.otomi.app/api/v1/"+path, bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("OTOMI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
out, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode == 200 {
return out, nil
}
if res.StatusCode != 429 && res.StatusCode < 500 {
return nil, fmt.Errorf("otomi %d: %s", res.StatusCode, out) // داده را درست کنید
}
}
time.Sleep(time.Duration(1<<attempt) * time.Second)
}
return nil, fmt.Errorf("otomi unreachable") // صف شما دوباره میفرستد
}
آدرس سایت — PHP
یک فایل؛ $store را به جدولهای خودتان وصل کنید.
OTOMI_HOOK_KEY و OTOMI_HOOK_SECRET همان کلید و رازی است که با
PUT /v1/hook ثبت کردید.
<?php // https://shop.example/otomi
$key = getenv('OTOMI_HOOK_KEY');
$secret = sodium_base642bin(getenv('OTOMI_HOOK_SECRET'), SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
$b64 = fn ($raw) => rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
$sign = fn ($method, $path, $ts, $nonce, $body) => $b64(hash_hmac('sha256',
"OTOMI-HMAC-SHA256\n$method\n$path\n$ts\n$nonce\n" . hash('sha256', $body), $secret, true));
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_OTOMI_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_X_OTOMI_NONCE'] ?? '';
$ok = ($_SERVER['HTTP_X_OTOMI_KEY'] ?? '') === $key
&& abs(time() - (int) $ts) <= 300
&& hash_equals($sign('POST', '/otomi/v1/command', $ts, $nonce, $body), $_SERVER['HTTP_X_OTOMI_SIGNATURE'] ?? '')
&& nonce_is_new($nonce); // در جدول یا Redis، ۱۰ دقیقه نگه دارید
if (!$ok) { http_response_code(401); exit; }
$cmd = json_decode($body, true);
$p = $cmd['params'] ?? [];
$reply = match ($cmd['command'] ?? '') {
'health' => ['ok' => true],
// رکوردهای تغییرکرده از $p['since']، قدیمیترین اول، $p['limit'] + 1 تا برای hasMore
'changes' => (function () use ($p, $store) {
$rows = $store->changedSince($p['kind'], $p['since'], $p['limit'] + 1); // به شکل بخش ۳ + updatedAt
return ['items' => array_slice($rows, 0, $p['limit']), 'hasMore' => count($rows) > $p['limit']];
})(),
default => null,
};
if ($reply === null) { http_response_code(400); exit; }
$out = json_encode($reply, JSON_UNESCAPED_UNICODE);
$myTs = (string) time();
$myNonce = $b64(random_bytes(18));
header('Content-Type: application/json');
header("X-Otomi-Key: $key");
header("X-Otomi-Timestamp: $myTs");
header("X-Otomi-Nonce: $myNonce");
header('X-Otomi-Signature: ' . $sign('RESPONSE', $nonce, $myTs, $myNonce, $out));
echo $out;
نمونهی کاملتر با کد تخفیف:
plugin/prestashop/otomiconnector/classes/OtomiSigner.php و
controllers/front/coupons.php.
۹. چکلیست پیش از کلید اصلی
- کلید در متغیر محیطی سرور است
- ارز درست است (تومان یا ریال، همان که در اپ انتخاب شد)
- ارسال در صف با تکرار است
- سفارش جدید، پرداخت، لغو و استرداد را با کلید آزمایشی امتحان کردهاید
- شماره موبایل مشتری فرستاده میشود
-
محصولات (با
stockاگر موجودی دارید) و نظرهای منتشرشده فرستاده میشوند -
آدرس سایت ثبت شده و در اپ ردیف «دریافت خودکار» سبز است (یا دادهی قدیمی را یکبار با
/importفرستادهاید — نه با/orders، که برای سفارشهای تازه پیام میفرستد)