Menu

Webhooks

Wamia envoie des événements JSON signés aux points qu’une clé enregistre avec apiRegisterWebhook (10 par clé au plus ; apiMyWebhooks les liste, apiDeleteWebhook en retire un). Abonnez-vous à des types, à une famille avec <famille>.*, ou à tout avec *.

Catalogue d’événements

Les familles et leurs types. connect.test est toujours autorisé ; c’est ce qu’envoie apiTestWebhook.

Catalogue en direct depuis l’API

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

Sémantique de livraison

Au moins une fois, dans l’ordre par clé

Répondez 2xx en moins de 10 secondes ; faites le travail ensuite. Dédoublonnez sur id : les relances le réutilisent. Les livraisons d’un point sont tentées dans l’ordre, mais une relance peut dépasser un événement plus récent — comparez occurredAt avant d’écraser l’état local.

Relances et désactivation automatique

Une livraison en échec est relancée jusqu’à 8 fois avec recul exponentiel — 30 secondes, puis ×3 à chaque fois, plafonné à 8 heures, avec aléa — et le numéro de tentative voyage dans Wamia-Attempt. Après 72 heures d’échec continu le point est désactivé et la boutique prévenue. Les livraisons sont conservées 30 jours : la boutique rejoue celles manquées depuis Intégrations › Webhooks une fois le point rétabli.

Test

apiTestWebhook envoie un événement connect.test signé et renvoie le code de statut et les premiers octets répondus par votre récepteur : une URL mal configurée est repérée depuis l’écran de réglages du plugin.

En-têtes d’une livraison

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

Contenu

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" }
}

Vérifier la signature

Le secret est affiché une fois à la création du point (whsec_…). Calculez HMAC-SHA256 sur <t>.<corps brut> avec la chaîne complète du secret comme clé, comparez en temps constant avec v1, et refusez les horodatages de plus de 5 minutes. Utilisez le corps brut de la requête — un JSON resérialisé change les octets.

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)

Adresses sources

Les livraisons partent d’adresses NAT fixes listées dans Wamia Shop, Intégrations › Webhooks. Autorisez-les si votre pare-feu l’exige, mais fiez-vous à la signature, pas à l’adresse.