Desenvolvedores

Webhooks

Enviamos um evento via POST ao seu servidor no momento em que um pedido avança ou um anúncio muda, para você nunca precisar fazer consultas periódicas. Assinado, com novas tentativas por cerca de um dia, e igual em todas as rotas da API.
Nesta página
  1. 1Configurar um endpoint
  2. 2O evento
  3. 3Eventos
  4. 4Assinaturas
  5. 5Verificar uma entrega
  6. 6Novas tentativas e ordem

Configurar um endpoint

  1. Adicione ao seu servidor uma rota https que leia o corpo bruto e responda 2xx em até 10 segundos. Faça o processamento depois de responder.
  2. Crie o endpoint com a API, ou peça à nossa equipe. Guarde o segredo whsec_ que ele retorna: ele aparece uma única vez.
  3. Verifique cada requisição (abaixo) e depois elimine duplicados pelo id do evento.
  4. Envie para você mesmo um evento de teste de cada tipo que você trata.

O evento

Todo evento tem o mesmo envelope. data contém os campos próprios do tipo.

idstring
Começa com evt_. É o mesmo em cada nova tentativa, e no cabeçalho webhook-id.
typestring
Um dos eventos abaixo.
api_versionstring
A versão em que data é escrito: a do seu endpoint.
created_atstring
Quando aconteceu, o que pode ser antes de você receber.
testboolean
true para eventos de teste.
dataobject
Ids para buscar o objeto completo, e o que mudou.
Uma 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

Um pedido chegou até nós, da sua loja Shopify ou pela API. Pago, 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

Um pedido foi pago com o seu saldo. Também é disparado para pedidos que esperavam uma 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 o vendedor. As linhas ficam em with_seller até chegarem ao nosso armazém.

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

Os itens estão no nosso armazém, verificados, com as fotos. A embalagem vem a seguir.

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

Um pacote saiu com rastreio; a cobrança do frete é definitiva. A loja de onde o pedido veio também recebe o rastreio.

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

Um vendedor mudou um preço, e o que a sua regra de sincronização fez: reajustou, pausou ou notificou.

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

O vendedor ficou sem uma variante e a sua prateleira não tem nenhuma: colocamos em 0 nos seus canais.

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

A nossa verificação encontrou um problema em um item: cor ou tamanho errado, dano, uma peça faltando. Escolha até respond_by; sem resposta, retemos o item e escrevemos para você.

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

Assinaturas

As entregas são assinadas no padrão Standard Webhooks, então uma biblioteca que você talvez já use as valida. Três cabeçalhos acompanham cada entrega:

CabeçalhoO que contém
webhook-idO id do evento.
webhook-timestampQuando enviamos esta tentativa, em segundos Unix.
webhook-signaturev1, depois uma vírgula e o HMAC-SHA256 em base64 de id.timestamp.body com o seu segredo. Durante uma rotação, duas delas, separadas por um espaço.

A chave é o seu segredo depois de whsec_, decodificado em base64. Assine o corpo bruto exatamente como chegou; recuse um timestamp com mais de cinco minutos de diferença, para que uma requisição capturada não possa ser reproduzida.

Verificar uma entrega

Com a biblioteca standardwebhooks, ou em algumas linhas próprias:

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

Novas tentativas e ordem

Qualquer resposta que não seja 2xx em até 10 segundos é uma falha, e tentamos de novo, sempre com o mesmo webhook-id e um novo timestamp e assinatura:

TentativaDepois da anterior
1Na hora
25 segundos
35 minutos
430 minutos
52 horas
65 horas
710 horas
810 horas, a última
  • Depois da última tentativa, a entrega fica como falha. Envie de novo pela lista de entregas.
  • Um endpoint cujas entregas falharam todas por cinco dias é desativado, depois de um e-mail para você. Ao reativá-lo, enviamos o que ele perdeu nos últimos três dias.
  • Os eventos podem chegar duas vezes e fora de ordem. Elimine duplicados pelo id e, quando a ordem importar, busque o objeto e confie no status dele, não no do evento.