Sviluppatori

Webhook

Inviamo un evento in POST al tuo server appena un ordine avanza o un’inserzione cambia, così non devi mai interrogare l’API. Firmato, ritentato per circa un giorno, e uguale su ogni route dell’API.
In questa pagina
  1. 1Configura un endpoint
  2. 2L’evento
  3. 3Eventi
  4. 4Firme
  5. 5Verifica una consegna
  6. 6Nuovi tentativi e ordine

Configura un endpoint

  1. Aggiungi al tuo server una route https che legga il corpo grezzo e risponda 2xx entro 10 secondi. Fai il lavoro dopo aver risposto.
  2. Crea l’endpoint con l’API, o chiedi al nostro team. Salva il segreto whsec_ che restituisce: viene mostrato una sola volta.
  3. Verifica ogni richiesta (qui sotto), poi deduplica sull’id dell’evento.
  4. Inviati un evento di test per ogni tipo che gestisci.

L’evento

Ogni evento ha lo stesso involucro. data contiene i campi propri del tipo.

idstring
Inizia con evt_. Uguale a ogni nuovo tentativo, e nell’intestazione webhook-id.
typestring
Uno degli eventi qui sotto.
api_versionstring
La versione in cui è scritto data: quella del tuo endpoint.
created_atstring
Quando è successo, che può essere prima che tu lo riceva.
testboolean
true per gli eventi di test.
dataobject
ID con cui recuperare l’oggetto completo, e cosa è cambiato.
Una consegna
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
  }
}

Eventi

order.created

Un ordine ci è arrivato, dal tuo negozio Shopify o dall’API. Pagato, o 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

Un ordine è stato pagato dal saldo. Scatta anche per gli ordini che aspettavano una ricarica.

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

Abbiamo pagato il venditore. Le righe sono with_seller finché non arrivano nel nostro magazzino.

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

Gli articoli sono nel nostro magazzino, controllati, con le loro foto. Segue l’imballaggio.

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 pacco è partito con tracciamento; il costo di spedizione è definitivo. Anche il negozio da cui è arrivato l’ordine ha il tracciamento.

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 venditore ha cambiato un prezzo, e cosa ha fatto la tua regola di sincronizzazione: prezzo ricalcolato, pausa o notifica.

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

Il venditore ha esaurito una variante e il tuo scaffale non ne ha: la portiamo a 0 sui tuoi canali.

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

Il nostro controllo ha trovato un problema con un articolo: colore o taglia sbagliati, danno, un pezzo mancante. Scegli entro respond_by; senza risposta lo tratteniamo e ti scriviamo.

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

Firme

Le consegne sono firmate secondo Standard Webhooks, quindi una libreria che forse usi già le verifica. Ogni consegna ha tre intestazioni:

IntestazioneCosa contiene
webhook-idL’ID dell’evento.
webhook-timestampQuando abbiamo inviato questo tentativo, in secondi Unix.
webhook-signaturev1, poi una virgola e l’HMAC-SHA256 in base64 di id.timestamp.body con il tuo segreto. Durante una rotazione, due, separate da uno spazio.

La chiave è il tuo segreto dopo whsec_, decodificato da base64. Firma il corpo grezzo esattamente come è arrivato; rifiuta un timestamp sfasato di più di cinque minuti, così una richiesta intercettata non può essere ripetuta.

Verifica una consegna

Con la libreria standardwebhooks, o in poche righe tue:

// 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
})

Nuovi tentativi e ordine

Tutto ciò che non è un 2xx entro 10 secondi è un errore, e riproviamo, ogni volta con lo stesso webhook-id e nuovi timestamp e firma:

TentativoDopo quello precedente
1Subito
25 secondi
35 minuti
430 minuti
52 ore
65 ore
710 ore
810 ore, l’ultima
  • Dopo l’ultimo tentativo la consegna è fallita. Inviala di nuovo da l’elenco delle consegne.
  • Un endpoint le cui consegne sono fallite tutte per cinque giorni viene disattivato, dopo un’email. Riattivarlo invia ciò che ha perso negli ultimi tre giorni.
  • Gli eventi possono arrivare due volte e fuori ordine. Deduplica su id e, quando l’ordine conta, recupera l’oggetto e fidati del suo stato invece che di quello dell’evento.