حسابداری نمو راهنمای توسعه‌دهندگان

وب‌هوک

وقتی وضعیت سفارشی در حسابداری نمو عوض شود، نمو به سایت شما خبر می‌دهد — امضاشده و با تلاش دوباره.

بیشتر وقت‌ها پاسخ همان درخواستی که فرستادید کافی است. اما گاهی وضعیت سفارش در نمو عوض می‌شود: سفارشی 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_…" }

حذف

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 بفهمید سفارش مال کدام است و بقیه را نادیده بگیرید.

پاسخ شما

بررسی امضا

هر درخواستی را که امضایش درست نیست رد کنید (401) — نشانی وب‌هوک عمومی است و هر کسی می‌تواند به آن چیزی بفرستد.

سرآیند X-Nemo-Signature شکل t=<زمان یونیکس>,v1=<امضا> دارد. امضا HMAC-SHA256 با secret روی رشتهٔ <t>.<بدنهٔ خام درخواست> است، به‌صورت hex. سه قاعده:

  1. روی بدنهٔ خام امضا را حساب کنید، نه JSON بازسازی‌شده — یک فاصله یا ترتیب دیگر امضا را خراب می‌کند.
  2. با تابع مقایسهٔ زمان‌ثابت مقایسه کنید (hash_equals، timingSafeEqual، compare_digest).
  3. اگر 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وضعیت سفارشی در خود نمو عوض شد — مثلاً حسابدار سفارش «نیازمند بررسی» را با «تلاش دوباره» قطعی کرد و پرداختش ثبت شد.

تغییری که در پاسخ درخواست خود شما برگشته (ثبت سفارش، پرداخت، برگشت) دوباره با وب‌هوک فرستاده نمی‌شود.