وبهوک
وقتی وضعیت سفارشی در حسابداری نمو عوض شود، نمو به سایت شما خبر میدهد — امضاشده و با تلاش دوباره.
بیشتر وقتها پاسخ همان درخواستی که فرستادید کافی است. اما گاهی وضعیت سفارش در نمو عوض میشود: سفارشی needs_review بود، حسابدار موجودی را اصلاح کرد و «تلاش دوباره» زد، و حالا paid است. وبهوک این تغییر را به سایت شما خبر میدهد.
افزونهٔ ووکامرس این کار را خودش انجام میدهد؛ این صفحه برای سایتهای اختصاصی است.
ثبت نشانی
PUT/api/v1/webhook/
دسترسی لازم: orders.write
curl -X PUT https://panel.nemosystem.ir/api/v1/webhook/ \
-H "Authorization: Bearer $NEMO_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://shop.example.com/nemo/webhook" }'
{ "url": "https://shop.example.com/nemo/webhook", "secret": "whsec_…" }
secretکلید امضای همین نشانی است و فقط همینجا برمیگردد. آن را کنار کلید API روی سرورتان نگه دارید.- ثبت دوبارهٔ همان نشانی یک
secretتازه میسازد و قبلی باطل میشود — راه چرخاندن کلید امضا همین است. - نشانی باید
httpsباشد و به یک نشانی عمومی اینترنت اشاره کند؛ نشانیهای شبکهٔ داخلی (مثل10.x،192.168.x،localhost) پذیرفته نمیشوند. - هر سازمان حداکثر ۱۰ نشانی دارد. فهرست نشانیها و آخرین ارسال هر کدام در پنل، صفحهٔ اتصال فروشگاه، دیده میشود و از همانجا قابل حذف است.
حذف
DELETE/api/v1/webhook/?url={نشانی}
پاسخ 204. نشانی را میتوانید در بدنه ({"url": "…"}) هم بفرستید.
آنچه دریافت میکنید
نمو یک POST با بدنهٔ JSON به نشانی شما میفرستد:
POST /nemo/webhook HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: nemo-webhooks/1.0
X-Nemo-Event: order.updated
X-Nemo-Delivery: 5b0e7a4c-2f1e-4f3a-9a51-0d6c1c9f2b11
X-Nemo-Signature: t=1790841600,v1=4f9c…e21a
{
"event": "order.updated",
"data": {
"source": "site:shop.example.com",
"external_id": "10452",
"status": "paid",
"status_display": "پرداختشده",
"total": "6250000",
"invoice_number": 84,
"invoice_status": "posted",
"receipt_number": 37,
"return_number": null,
"error": null
},
"delivery": "5b0e7a4c-2f1e-4f3a-9a51-0d6c1c9f2b11"
}
data دقیقاً همان شکل پاسخ ثبت سفارش را دارد. اگر چند فروشگاه را با sourceهای مختلف وصل کردهاید، با data.source بفهمید سفارش مال کدام است و بقیه را نادیده بگیرید.
پاسخ شما
- هر پاسخ
2xxیعنی رسید. بدنهٔ پاسخ مهم نیست. - ظرف ۱۰ ثانیه پاسخ بدهید؛ کار سنگین را به پسزمینه بسپارید.
- تغییر مسیر (
3xx) دنبال نمیشود و ناموفق حساب میشود. - اگر پاسخ
2xxنرسید، نمو تا ۴ بار دیگر — ۱، ۵، ۲۵ و ۱۲۵ دقیقه بعد — دوباره میفرستد. - ممکن است یک رویداد بیش از یک بار برسد (مثلاً اگر پاسخ شما در راه گم شود).
X-Nemo-Deliveryبرای هر رویداد ثابت است؛ اگر قبلاً پردازشش کردهاید، فقط200برگردانید. بهروزرسانی باdata.statusهم خودبهخود تکرارپذیر است.
بررسی امضا
هر درخواستی را که امضایش درست نیست رد کنید (401) — نشانی وبهوک عمومی است و هر کسی میتواند به آن چیزی بفرستد.
سرآیند X-Nemo-Signature شکل t=<زمان یونیکس>,v1=<امضا> دارد. امضا HMAC-SHA256 با secret روی رشتهٔ <t>.<بدنهٔ خام درخواست> است، بهصورت hex. سه قاعده:
- روی بدنهٔ خام امضا را حساب کنید، نه JSON بازسازیشده — یک فاصله یا ترتیب دیگر امضا را خراب میکند.
- با تابع مقایسهٔ زمانثابت مقایسه کنید (
hash_equals،timingSafeEqual،compare_digest). - اگر
tبیش از ۵ دقیقه با ساعت سرور شما فاصله دارد، رد کنید تا درخواست ضبطشده دوباره پخش نشود. ساعت سرور را با NTP درست نگه دارید.
PHP
<?php
$secret = getenv('NEMO_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_NEMO_SIGNATURE'] ?? '';
if (!preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $m)
|| abs(time() - (int) $m[1]) > 300
|| !hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, $secret), $m[2])) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
$order = $event['data'];
// بهروزرسانی سفارش $order['external_id'] با وضعیت $order['status']
http_response_code(200);
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.NEMO_WEBHOOK_SECRET;
// express.raw: the signature is over the raw bytes, not re-serialized JSON.
app.post("/nemo/webhook", express.raw({ type: "application/json" }), (req, res) => {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(req.get("X-Nemo-Signature") || "");
if (!match || Math.abs(Date.now() / 1000 - Number(match[1])) > 300) return res.sendStatus(401);
const expected = crypto.createHmac("sha256", secret)
.update(`${match[1]}.`).update(req.body).digest();
const given = Buffer.from(match[2], "hex");
if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) return res.sendStatus(401);
const { data } = JSON.parse(req.body.toString("utf8"));
// update order data.external_id to data.status
res.sendStatus(200);
});
Python (Django)
import hashlib, hmac, json, os, re, time
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
SECRET = os.environ["NEMO_WEBHOOK_SECRET"].encode()
SIGNATURE = re.compile(r"^t=(\d+),v1=([a-f0-9]{64})$")
@csrf_exempt
@require_POST
def nemo_webhook(request):
match = SIGNATURE.match(request.headers.get("X-Nemo-Signature", ""))
if not match or abs(time.time() - int(match[1])) > 300:
return HttpResponse(status=401)
expected = hmac.new(SECRET, match[1].encode() + b"." + request.body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, match[2]):
return HttpResponse(status=401)
data = json.loads(request.body)["data"]
# update order data["external_id"] to data["status"]
return HttpResponse(status=200)
رویدادها
| رویداد | کی |
|---|---|
order.updated | وضعیت سفارشی در خود نمو عوض شد — مثلاً حسابدار سفارش «نیازمند بررسی» را با «تلاش دوباره» قطعی کرد و پرداختش ثبت شد. |
تغییری که در پاسخ درخواست خود شما برگشته (ثبت سفارش، پرداخت، برگشت) دوباره با وبهوک فرستاده نمیشود.