القائمة

الويب هوك

ترسل واميا أحداث JSON موقّعة إلى النقاط التي يسجلها مفتاح عبر apiRegisterWebhook (حتى 10 لكل مفتاح؛ يسردها apiMyWebhooks ويحذف apiDeleteWebhook واحدة). اشترك في أنواع مفردة، أو في عائلة بـ <family>.*، أو في كل شيء بـ *.

كتالوج الأحداث

العائلات وأنواعها. connect.test مسموح دائمًا وهو ما يرسله apiTestWebhook.

الكتالوج المباشر من الواجهة

order.*10

  • order.placed
  • order.confirmed
  • order.preparing
  • order.shipped
  • order.delivered
  • order.completed
  • order.cancelled
  • order.status_changed
  • order.slice_status_changed
  • order.refunded

fulfilment.*8

  • fulfilment.slice_prepared
  • fulfilment.slice_ready
  • fulfilment.slice_handed_over
  • fulfilment.slice_in_transit
  • fulfilment.slice_delivered
  • fulfilment.delivery_failed
  • fulfilment.slice_returned_to_origin
  • fulfilment.return_received

rma.*6

  • rma.created
  • rma.approved
  • rma.rejected
  • rma.received
  • rma.completed
  • rma.seller_contested

listing.*1

  • listing.state_changed

inventory.*3

  • inventory.position_changed
  • inventory.back_in_stock
  • inventory.out_of_stock

payout.*6

  • payout.scheduled
  • payout.approved
  • payout.sent
  • payout.settled
  • payout.bounced
  • payout.cancelled

penalty.*2

  • penalty.applied
  • penalty.reversed

connect.*4

  • connect.batch_held
  • connect.batch_confirmed
  • connect.batch_discarded
  • connect.anomaly_detected

inbound.*9

  • inbound.declared
  • inbound.scheduled
  • inbound.rescheduled
  • inbound.slot_closed
  • inbound.arrived
  • inbound.received
  • inbound.discrepancy
  • inbound.no_show
  • inbound.cancelled

withdrawal.*8

  • withdrawal.requested
  • withdrawal.decided
  • withdrawal.scheduled
  • withdrawal.slot_closed
  • withdrawal.ready
  • withdrawal.handed_over
  • withdrawal.no_show
  • withdrawal.cancelled

return.*2

  • return.arrived
  • return.processed

دلالات التسليم

مرة واحدة على الأقل، بالترتيب لكل مفتاح

أجب بأي 2xx خلال 10 ثوانٍ؛ ونفّذ العمل لاحقًا. أزل التكرار بـ id: تعيد المحاولات استخدامه. تُحاول تسليمات النقطة الواحدة بالترتيب لكن قد تتجاوز إعادة محاولة حدثًا أحدث — قارن occurredAt قبل الكتابة فوق الحالة المحلية.

إعادة المحاولة والتعطيل التلقائي

يُعاد إرسال التسليم الفاشل حتى 8 مرات بتراجع أسي — 30 ثانية ثم ×3 كل مرة بسقف 8 ساعات مع عشوائية — ويُحمل رقم المحاولة في Wamia-Attempt. بعد 72 ساعة من الفشل المستمر تُعطَّل النقطة ويُبلَّغ المتجر. تُحفظ التسليمات 30 يومًا: يعيد المتجر إرسال ما فات من التكاملات › الويب هوك بعد تعافي النقطة.

الاختبار

يرسل apiTestWebhook حدث connect.test موقّعًا ويعيد رمز الحالة وأول بايتات أجاب بها مستقبِلك، فيُكتشف الرابط الخاطئ من شاشة إعدادات الإضافة.

ترويسات التسليم

HTTP
POST /wamia/webhook HTTP/1.1
Content-Type: application/json
User-Agent: wamia-webhooks/1
Wamia-Signature: t=1759655553,v1=5f1c0b9e…
Wamia-Delivery-Id: 1b6f0d2a-…
Wamia-Event: listing.state_changed
Wamia-Attempt: 1

الحمولة

JSON
{
  "id": "evt_01J9ZK8Q2M3N4P5R6S7T8V9W0X",
  "type": "listing.state_changed",
  "occurredAt": "2026-10-05T09:12:33.120Z",
  "organizationId": "7c1f9f1e-5b4e-4d8f-9a6b-2f3e4d5c6b7a",
  "connectionId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "data": { "sku": "TSHIRT-RED-M", "state": "live", "previousState": "in_review" }
}

التحقق من التوقيع

يُعرض السر مرة واحدة عند إنشاء النقطة (whsec_…). احسب HMAC-SHA256 على <t>.<raw body> بسلسلة السر الكاملة كمفتاح، وقارن بزمن ثابت مع v1، وارفض الطوابع الزمنية الأقدم من 5 دقائق. استخدم جسم الطلب الخام — إعادة تسلسل JSON تغيّر البايتات.

TypeScript (Node)

TypeScript (Node)
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 300;

export function verifyWamiaSignature(secret: string, header: string, rawBody: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=') as [string, string]));
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express: keep the raw body — JSON re-serialisation breaks the signature.
app.post('/wamia/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifyWamiaSignature(process.env.WAMIA_WEBHOOK_SECRET!, req.header('Wamia-Signature') ?? '', raw)) {
    return res.status(401).end();
  }
  const event = JSON.parse(raw) as { id: string; type: string; occurredAt: string; data: Record<string, unknown> };
  // Deduplicate on event.id, then enqueue; answer 2xx within 10 seconds.
  res.status(202).end();
});

PHP

PHP
<?php
const WAMIA_TOLERANCE_SECONDS = 300;

function wamia_verify_signature(string $secret, string $header, string $rawBody): bool
{
    $parts = [];
    foreach (explode(',', $header) as $pair) {
        [$key, $value] = array_pad(explode('=', $pair, 2), 2, '');
        $parts[$key] = $value;
    }
    if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) {
        return false;
    }
    if (abs(time() - (int) $parts['t']) > WAMIA_TOLERANCE_SECONDS) {
        return false;
    }
    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1']);
}

// WordPress REST route, WooCommerce plugin: register_rest_route('wamia/v1', '/webhook', ...)
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_WAMIA_SIGNATURE'] ?? '';
if (!wamia_verify_signature(getenv('WAMIA_WEBHOOK_SECRET'), $header, $raw)) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// Deduplicate on $event['id'], queue the job (Action Scheduler), answer 202 at once.
http_response_code(202);

Python

Python
import hmac, hashlib, time

TOLERANCE_SECONDS = 300

def verify_wamia_signature(secret: str, header: str, raw_body: bytes) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(",") if "=" in kv)
    try:
        timestamp = int(parts["t"])
        given = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
        return False
    message = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, given)

# Odoo controller: @http.route('/wamia/webhook', type='http', auth='public', methods=['POST'], csrf=False)
def wamia_webhook(self, **kwargs):
    raw = request.httprequest.get_data()
    header = request.httprequest.headers.get("Wamia-Signature", "")
    if not verify_wamia_signature(secret, header, raw):
        return Response(status=401)
    event = json.loads(raw)
    # Deduplicate on event["id"], enqueue with queue_job, answer 202 at once.
    return Response(status=202)

عناوين المصدر

تخرج التسليمات من عناوين NAT ثابتة مدرجة في Wamia Shop تحت التكاملات › الويب هوك. أدرجها إن تطلب جدارك الناري ذلك، لكن اعتمد على التوقيع لا على العنوان.