Desarrolladores

Webhooks

Enviamos un evento por POST a tu servidor en cuanto un pedido avanza o cambia una publicación, para que nunca tengas que consultar periódicamente. Firmado, reintentado durante aproximadamente un día, e igual en todas las rutas de la API.
En esta página
  1. 1Configurar un endpoint
  2. 2El evento
  3. 3Eventos
  4. 4Firmas
  5. 5Verificar una entrega
  6. 6Reintentos y orden

Configurar un endpoint

  1. Agrega a tu servidor una ruta https que lea el cuerpo sin procesar y responda 2xx en menos de 10 segundos. Haz el trabajo después de responder.
  2. Crea el endpoint con la API, o pídeselo a nuestro equipo. Guarda el secreto whsec_ que devuelve: se muestra una sola vez.
  3. Verifica cada solicitud (ver abajo) y luego elimina duplicados por el id del evento.
  4. Envíate un evento de prueba de cada tipo que gestionas.

El evento

Todos los eventos tienen el mismo sobre. data contiene los campos propios del tipo.

idstring
Empieza por evt_. Es el mismo en cada reintento, y en el encabezado webhook-id.
typestring
Uno de los eventos siguientes.
api_versionstring
La versión en que se escribe data: la de tu endpoint.
created_atstring
Cuándo ocurrió, que puede ser antes de que lo recibas.
testboolean
true para eventos de prueba.
dataobject
Ids con los que obtener el objeto completo, y qué cambió.
Una entrega
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
  }
}

Eventos

order.created

Nos llegó un pedido, de tu tienda de Shopify o de la API. Pagado, 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

Se pagó un pedido desde tu saldo. También se dispara para pedidos que esperaban una recarga.

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

Pagamos al vendedor. Las líneas están en with_seller hasta que llegan a nuestro almacén.

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

Los artículos están en nuestro almacén, revisados, con sus fotos. Lo siguiente es el empaque.

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 paquete salió con seguimiento; el cargo de envío es definitivo. La tienda de la que vino el pedido también tiene el seguimiento.

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 vendedor cambió un precio, y lo que hizo tu regla de sincronización: recalcular el precio, pausar o notificar.

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

El vendedor se quedó sin una variante y tu estante no tiene ninguna: la ponemos a 0 en tus canales.

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

Nuestra revisión encontró un problema con un artículo: color o talla incorrectos, daños, una pieza faltante. Elige antes de respond_by; si no respondes, lo retenemos y te escribimos.

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

Firmas

Las entregas se firman según Standard Webhooks, así que una biblioteca que quizá ya usas las verifica. Cada entrega lleva tres encabezados:

EncabezadoQué contiene
webhook-idEl id del evento.
webhook-timestampCuándo enviamos este intento, en segundos Unix.
webhook-signaturev1, luego una coma y el HMAC-SHA256 en base64 de id.timestamp.body con tu secreto. Durante una rotación, dos de ellos, separados por un espacio.

La clave es tu secreto tras whsec_, decodificado en base64. Firma el cuerpo sin procesar exactamente como llegó; rechaza una marca de tiempo desfasada más de cinco minutos, para que no se pueda repetir una solicitud capturada.

Verificar una entrega

Con la biblioteca standardwebhooks, o en unas pocas líneas propias:

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

Reintentos y orden

Cualquier cosa que no sea 2xx en 10 segundos es un fallo, y lo reintentamos, cada vez con el mismo webhook-id y una nueva marca de tiempo y firma:

IntentoDespués del anterior
1De inmediato
25 segundos
35 minutos
430 minutos
52 horas
65 horas
710 horas
810 horas, el último
  • Tras el último intento, la entrega queda fallida. Vuelve a enviarla desde la lista de entregas.
  • Un endpoint cuyas entregas fallaron todas durante cinco días se desactiva, tras enviarte un correo. Al activarlo de nuevo se envía lo que se perdió en los últimos tres días.
  • Los eventos pueden llegar dos veces y desordenados. Elimina duplicados por id y, cuando el orden importe, obtén el objeto y fíate de su estado en lugar del del evento.