Queek API docs

Webhooks

Order events pushed to your server, signed with Standard Webhooks.

Queek pushes order events to an HTTPS endpoint you own, so you can stop polling. Every delivery is signed with Standard Webhooks — the same scheme a dozen platforms use, so any off-the-shelf verification library works.

Subscriptions are created by the merchant, on the merchant side: the dashboard's own API (/api/v1/biz/vendor/webhooks, vendor-authenticated) registers a URL, the topics it wants and returns the signing secret. The secret is shown once, at the call that mints it. The storefront API documented on this site neither creates nor lists them.

Topics

TopicFires when
orders/createAn order was placed, on any channel.
orders/paidAn order moved to paid.
orders/updatedAn order changed — status, fulfilment stage or any merchant action.
orders/fulfilledAn order was completed or delivered.
orders/cancelledAn order was cancelled or rejected.

orders/updated fires alongside the specific topic, so subscribe to it only if you want every transition. Orders are the whole v1 catalogue: products, inventory and customers are not emitted yet.

The headers on a delivery

HeaderExampleNotes
webhook-id9f0f7a0e-…Unique per event. Dedupe on this — a retry reuses it.
webhook-timestamp1614265330Unix seconds. Reject anything more than 300s from now.
webhook-signaturev1,g0hM9S…=One or more space-delimited signatures (see rotation).
X-Queek-Topicorders/paidThe topic that fired.
X-Queek-Api-Versionv1The payload contract version.

The signed content is {webhook-id}.{webhook-timestamp}.{raw body}. The MAC is HMAC-SHA256, and the key is the decoded bytes of the secret's base64 half — the part after whsec_ — not the printable string. That single detail is the most common integration bug.

Verify before you trust

Sign against the raw request body, byte for byte. Any framework that re-serialises JSON for you will break verification, which is exactly what it is there to do.

verify.js
import crypto from 'node:crypto';

export function verifyQueekWebhook(secret, headers, rawBody) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatureHeader = headers['webhook-signature'];
  if (!id || !timestamp || !signatureHeader) return false;

  // Replay window: 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected =
    'v1,' +
    crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64');
  const expectedBuffer = Buffer.from(expected);

  // Several signatures while a secret rotation is open — any one may match.
  return signatureHeader
    .split(' ')
    .some(
      (candidate) =>
        candidate.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(candidate), expectedBuffer),
    );
}

With Express, keep the raw bytes: express.raw({ type: 'application/json' }) on the webhook route, and parse only after the signature checks out.

Check your implementation against this vector

Secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw, id msg_p5jXN8AQM9LWM0D4loKWxJek, timestamp 1614265330, body {"test": 2432232314} must produce v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=. It is the Standard Webhooks published vector, and Queek's own test suite pins the same one.

The payload

One JSON object per event — the order, in the shape below. id is the short integer id you can quote to a human or a model; the UUID rides along as uid. Money is a decimal string, never a JSON number, because a binary float loses kobo.

FieldTypeNotes
idintegerThe order's short id.
uidstringThe order's UUID.
order_numberstringWhat the merchant and customer see.
status, payment_status, payment_methodstringTolerate values you do not know.
fulfillment_statusstringfulfilled or unfulfilled.
cancelled, cancel_reasonboolean, string | null
channel, platform, delivery_methodstringWhere the order came from.
currencystringISO code, uppercase.
subtotal, discount_total, shipping_total, tax_total, totalstringDecimal strings.
customerobject | nullid, name, email, phone.
shipping_addressobject | nulladdress, latitude, longitude.
line_items[]arrayid, product_id, product_uid, title, variant_title, quantity, unit_price, total.
notestring | nullThe merchant's note.
metafieldsobjectTyped fields the merchant defined.
metadataobjectThe opaque values your integration wrote — your own ERP id comes back to you here.
created_at, updated_at, fulfilled_atISO 8601

Internal ledger facts — transaction ids, settlement, commission, platform fees, delivery PINs — are never in a webhook payload.

Delivery, retries and failure

Acknowledge with any 2xx within five seconds. Queue the work; do not do it inside the request.

Attempt12345678
After5s30s2m10m30m1h2h

Eight attempts span roughly 3h42m. After the last one the delivery is dead and only a replay moves it. An endpoint that has been failing continuously for 24 hours is disabled automatically and the merchant is notified; one success clears the streak.

Every attempt is recorded — status, response code, truncated response body, duration — and the merchant can inspect or replay a delivery from the dashboard API for 7 days. A replay is sent as a new event id, so your dedupe does not swallow it.

Rotating a secret

Rotation mints a new secret and keeps the old one valid for 24 hours. During that window the webhook-signature header carries both signatures, space delimited, and a receiver accepts the message if either verifies — which is why the snippets above loop over the candidates instead of comparing once. Roll your configuration any time inside the window with zero dropped events.

Before you point it at production

  • HTTPS only. Plain http:// is accepted outside production so you can hit your own machine while building.
  • Queek refuses endpoints that resolve to loopback, private, link-local or metadata addresses, and never follows redirects — a 302 to an internal address is inert.
  • The payload contains customer personal data (name, email, phone, address). The merchant is its controller; treat it accordingly on your side.

On this page