Developers

Webhooks

We POST'en een event naar je server zodra een bestelling beweegt of een listing verandert, dus je hoeft nooit te pollen. Ondertekend, ongeveer een dag lang herhaald, en hetzelfde op elke route van de API.
Op deze pagina
  1. 1Een endpoint inrichten
  2. 2Het event
  3. 3Events
  4. 4Handtekeningen
  5. 5Een aflevering verifiëren
  6. 6Herhalingen en volgorde

Een endpoint inrichten

  1. Voeg een https-route toe aan je server die de ruwe body leest en binnen 10 seconden 2xx antwoordt. Doe het werk na het antwoord.
  2. Maak het endpoint aan met de API, of vraag het ons team. Bewaar het whsec_-secret uit het antwoord: het wordt één keer getoond.
  3. Verifieer elk verzoek (hieronder) en ontdubbel daarna op de event-id.
  4. Stuur jezelf een testevent van elk type dat je verwerkt.

Het event

Elk event heeft dezelfde envelop. data bevat de eigen velden van het type.

idstring
Begint met evt_. Hetzelfde bij elke herhaling, en in de webhook-id-header.
typestring
Een van de events hieronder.
api_versionstring
De versie waarin data is geschreven: die van je endpoint.
created_atstring
Wanneer het gebeurde, wat eerder kan zijn dan je het ontvangt.
testboolean
true voor testevents.
dataobject
Id's om het volledige object mee op te halen, en wat er veranderde.
Een aflevering
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
  }
}

Events

order.created

Er is een bestelling bij ons binnengekomen, uit je Shopify-winkel of via de API. Betaald, of 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

Een bestelling is betaald uit je saldo. Wordt ook verstuurd voor bestellingen die op een opwaardering wachtten.

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

We hebben de verkoper betaald. De regels zijn with_seller tot ze in ons magazijn aankomen.

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

De artikelen zijn in ons magazijn, gecontroleerd, met hun foto's. Daarna volgt het inpakken.

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

Een pakket is vertrokken met tracking; de verzendkosten zijn definitief. De winkel waar de bestelling vandaan kwam, heeft de tracking ook.

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

Een verkoper heeft een prijs gewijzigd, en wat je syncregel daarmee deed: prijs aangepast, gepauzeerd of gemeld.

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

De verkoper is door een variant heen en je schap heeft er geen: we zetten hem op je kanalen op 0.

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

Onze controle vond een probleem met een artikel: verkeerde kleur of maat, schade, een ontbrekend onderdeel. Kies vóór respond_by; zonder antwoord houden we het vast en schrijven we je.

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

Handtekeningen

Afleveringen worden ondertekend volgens Standard Webhooks, dus een library die je misschien al gebruikt, verifieert ze. Bij elke aflevering horen drie headers:

HeaderWat het bevat
webhook-idDe event-id.
webhook-timestampWanneer we deze poging verstuurden, in Unix-seconden.
webhook-signaturev1, dan een komma en de base64-HMAC-SHA256 van id.timestamp.body met je secret. Tijdens een rotatie twee ervan, gescheiden door een spatie.

De sleutel is je secret na whsec_, base64-gedecodeerd. Onderteken de ruwe body precies zoals hij binnenkwam; weiger een tijdstempel die meer dan vijf minuten afwijkt, zodat een onderschept verzoek niet opnieuw kan worden afgespeeld.

Een aflevering verifiëren

Met de library standardwebhooks, of in een paar eigen regels:

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

Herhalingen en volgorde

Alles behalve een 2xx binnen 10 seconden is een mislukking, en we proberen het opnieuw, elke keer met dezelfde webhook-id en een nieuwe tijdstempel en handtekening:

PogingNa de vorige
1Meteen
25 seconden
35 minuten
430 minuten
52 uur
65 uur
710 uur
810 uur, de laatste
  • Na de laatste poging is de aflevering mislukt. Stuur hem opnieuw vanuit de lijst met afleveringen.
  • Een endpoint waarvan vijf dagen lang elke aflevering mislukte, wordt uitgeschakeld nadat we je hebben gemaild. Schakel je hem weer in, dan sturen we wat hij de afgelopen drie dagen heeft gemist.
  • Events kunnen dubbel en in de verkeerde volgorde aankomen. Ontdubbel op id, en als de volgorde telt, haal het object op en vertrouw op de status daarvan in plaats van die van het event.