Développeurs

Webhooks

Nous envoyons un événement en POST à votre serveur dès qu’une commande avance ou qu’une annonce change : vous n’avez jamais à interroger l’API. Signé, réessayé pendant environ une journée, et identique sur toutes les routes de l’API.
Sur cette page
  1. 1Configurer un endpoint
  2. 2L’événement
  3. 3Événements
  4. 4Signatures
  5. 5Vérifier une livraison
  6. 6Nouvelles tentatives et ordre

Configurer un endpoint

  1. Ajoutez à votre serveur une route https qui lit le corps brut et répond 2xx en moins de 10 secondes. Faites le traitement après la réponse.
  2. Créez l’endpoint avec l’API, ou demandez à notre équipe. Enregistrez le secret whsec_ qu’il renvoie : il n’est affiché qu’une fois.
  3. Vérifiez chaque requête (ci-dessous), puis dédoublonnez sur l’id de l’événement.
  4. Envoyez-vous un événement de test de chaque type que vous traitez.

L’événement

Chaque événement a la même enveloppe. data contient les champs propres au type.

idstring
Commence par evt_. Identique à chaque nouvelle tentative, et dans l’en-tête webhook-id.
typestring
L’un des événements ci-dessous.
api_versionstring
La version dans laquelle data est écrit : celle de votre endpoint.
created_atstring
Quand cela s’est produit, ce qui peut précéder sa réception.
testboolean
true pour les événements de test.
dataobject
Les identifiants pour récupérer l’objet complet, et ce qui a changé.
Une livraison
POST /hooks/ninjabuy HTTP/1.1
Host: acme-goods.com
Content-Type: application/json
User-Agent: NinjaBuy-Webhooks/1.0
webhook-id: evt_4m2x
webhook-timestamp: 1793685600
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{
  "id": "evt_4m2x",
  "type": "order.shipped",
  "api_version": "2026-11",
  "created_at": "2026-11-03T06:00:00Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "shipment": "shp_5r8w",
    "carrier": "YunExpress",
    "tracking_number": "YT2630812345678",
    "tracking_url": "https://t.17track.net/en#nums=YT2630812345678",
    "shipping_charged": { "amount": "134.52", "currency": "CNY" },
    "pushed_to_channel": true
  }
}

Événements

order.created

Une commande nous est parvenue, de votre boutique Shopify ou de l’API. Payée, ou awaiting_payment.

order.created
{
  "id": "evt_1a7c",
  "type": "order.created",
  "api_version": "2026-11",
  "created_at": "2026-11-01T09:14:02Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "source": "shopify",
    "status": "buying",
    "payment_status": "paid",
    "lines": [
      {
        "line": "lin_9a2c",
        "variant": "var_41hz",
        "sku": "5187209554",
        "quantity": 1
      }
    ]
  }
}

order.paid

Une commande a été payée depuis votre solde. Déclenché aussi pour les commandes qui attendaient une recharge.

order.paid
{
  "id": "evt_1a9f",
  "type": "order.paid",
  "api_version": "2026-11",
  "created_at": "2026-11-01T11:02:40Z",
  "test": false,
  "data": {
    "order": "ord_3n8a",
    "external_id": "1013",
    "amount": { "amount": "57.00", "currency": "CNY" },
    "paid_from": "balance",
    "was_awaiting_payment": true
  }
}

order.bought

Nous avons payé le vendeur. Les lignes sont en with_seller jusqu’à leur arrivée dans notre entrepôt.

order.bought
{
  "id": "evt_2c4e",
  "type": "order.bought",
  "api_version": "2026-11",
  "created_at": "2026-11-01T13:30:05Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "lines": [
      {
        "line": "lin_9a2c",
        "sku": "5187209554",
        "quantity": 1,
        "status": "with_seller"
      }
    ]
  }
}

order.checked

Les articles sont dans notre entrepôt, vérifiés, avec leurs photos. L’emballage suit.

order.checked
{
  "id": "evt_9w1c",
  "type": "order.checked",
  "api_version": "2026-11",
  "created_at": "2026-11-02T03:20:11Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "fulfillment_order": "fo_7k2m",
    "lines": [
      {
        "line": "lin_9a2c",
        "qc": "passed",
        "photos": [
          "https://media.ninjabuy.com/qc/ord_3n7q/1.webp",
          "https://media.ninjabuy.com/qc/ord_3n7q/2.webp"
        ]
      }
    ]
  }
}

order.shipped

Un colis est parti avec un suivi ; les frais d’expédition sont définitifs. La boutique d’origine de la commande a aussi le suivi.

order.shipped
{
  "id": "evt_4m2x",
  "type": "order.shipped",
  "api_version": "2026-11",
  "created_at": "2026-11-03T06:00:00Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "shipment": "shp_5r8w",
    "carrier": "YunExpress",
    "tracking_number": "YT2630812345678",
    "tracking_url": "https://t.17track.net/en#nums=YT2630812345678",
    "shipping_charged": { "amount": "134.52", "currency": "CNY" },
    "pushed_to_channel": true
  }
}

product.cost_changed

Un vendeur a changé un prix, et ce que votre règle de synchronisation a fait : prix recalculé, mise en pause ou notification.

product.cost_changed
{
  "id": "evt_8q0d",
  "type": "product.cost_changed",
  "api_version": "2026-11",
  "created_at": "2026-11-04T02:10:00Z",
  "test": false,
  "data": {
    "product": "prd_1x0a",
    "variant": "var_2b7k",
    "sku": "4410923871",
    "cost": {
      "from": { "amount": "42.00", "currency": "CNY" },
      "to": { "amount": "48.00", "currency": "CNY" }
    },
    "action": "repriced",
    "price": {
      "from": { "amount": "18.99", "currency": "USD" },
      "to": { "amount": "20.99", "currency": "USD" }
    },
    "rule": "rule_default"
  }
}

product.out_of_stock

Le vendeur est en rupture sur une variante et votre étagère n’en a pas : nous la mettons à 0 sur vos canaux.

product.out_of_stock
{
  "id": "evt_2b9n",
  "type": "product.out_of_stock",
  "api_version": "2026-11",
  "created_at": "2026-11-04T02:11:30Z",
  "test": false,
  "data": {
    "product": "prd_8f2k",
    "variant": "var_41j0",
    "sku": "5187209555",
    "warehouse_on_hand": 0,
    "channels_updated": [
      {
        "channel": "shopify",
        "shop": "acme-goods.myshopify.com",
        "available": 0
      }
    ]
  }
}

qc.issue

Notre contrôle a trouvé un problème sur un article : mauvaise couleur ou taille, dommage, pièce manquante. Choisissez avant respond_by ; sans réponse, nous le gardons et vous écrivons.

qc.issue
{
  "id": "evt_6c3h",
  "type": "qc.issue",
  "api_version": "2026-11",
  "created_at": "2026-11-02T03:20:11Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "fulfillment_order": "fo_7k2m",
    "line": "lin_9a2c",
    "sku": "5187209554",
    "issue": "wrong_color",
    "note": "Received grey, ordered black.",
    "photos": ["https://media.ninjabuy.com/qc/ord_3n7q/3.webp"],
    "choices": ["ship_anyway", "exchange", "refund"],
    "respond_by": "2026-11-05T03:20:11Z"
  }
}

Signatures

Les livraisons sont signées selon Standard Webhooks : une bibliothèque que vous utilisez peut-être déjà les vérifie. Trois en-têtes accompagnent chaque livraison :

En-têteCe qu’il contient
webhook-idL’identifiant de l’événement.
webhook-timestampL’envoi de cette tentative, en secondes Unix.
webhook-signaturev1, puis une virgule et le HMAC-SHA256 en base64 de id.timestamp.body avec votre secret. Pendant un renouvellement, deux, séparés par une espace.

La clé est votre secret après whsec_, décodé en base64. Signez le corps brut tel qu’il est arrivé ; refusez un horodatage décalé de plus de cinq minutes, pour qu’une requête interceptée ne puisse pas être rejouée.

Vérifier une livraison

Avec la bibliothèque standardwebhooks, ou en quelques lignes de votre cru :

// npm install standardwebhooks express
import express from "express"
import { Webhook } from "standardwebhooks"

const webhook = new Webhook(process.env.NINJABUY_WEBHOOK_SECRET) // whsec_...
const app = express()

// The raw body: a parsed and re-serialised body no longer matches.
app.post("/hooks/ninjabuy", express.raw({ type: "application/json" }), (req, res) => {
  let event
  try {
    event = webhook.verify(req.body.toString("utf8"), req.headers)
  } catch {
    return res.sendStatus(400)
  }
  res.sendStatus(204) // answer first, then do the work
  queue.push(event) // deduplicate on event.id: a retry repeats it
})

Nouvelles tentatives et ordre

Tout ce qui n’est pas un 2xx en moins de 10 secondes est un échec, et nous réessayons, chaque fois avec le même webhook-id et un nouvel horodatage et une nouvelle signature :

TentativeAprès la précédente
1Immédiatement
25 secondes
35 minutes
430 minutes
52 heures
65 heures
710 heures
810 heures, la dernière
  • Après la dernière tentative, la livraison est en échec. Renvoyez-la depuis la liste des livraisons.
  • Un endpoint dont toutes les livraisons ont échoué pendant cinq jours est désactivé, après un e-mail. Le réactiver envoie ce qu’il a manqué les trois derniers jours.
  • Les événements peuvent arriver deux fois et dans le désordre. Dédoublonnez sur id et, quand l’ordre compte, récupérez l’objet et fiez-vous à son statut plutôt qu’à celui de l’événement.