Notifications
Wino sends a POST request with a JSON body to your URL:
{
"kind": "Variant",
"source": {
"shopId": "20258224-05f3-49a7-9299-fab4acb89ee5",
"deviceId": "b1fd5f30-a879-40a0-9a86-46804c16ed12"
},
"data": {
"id": "4e584c64-a377-4689-aa01-7fbd32f12a44",
"…": "the properties of the variant"
},
"clientState": "<a secret of yours>"
}
| Field | Description |
|---|---|
kind | The kind of data that changed: one of the settings names. It doesn't say what happened (created, updated, archived): fetch the record to see its current state |
source.shopId | The shop where the change happened |
source.deviceId | The device the change is reported from. For changes made through the API, it's the shop's Web device (slug W), not your device: it doesn't tell you who made the change |
data | The record after the change. Its properties depend on kind and use Wino's internal field names, not always the GraphQL ones. Its dates are timestamps in milliseconds (GraphQL returns ISO dates). data.id is always there |
clientState | The value you set on the subscription |
Other properties may appear: ignore the ones you don't use.
Receive notifications
- Answer fast with a
2xxstatus. Wino waits 10 seconds at most. Save the notification and process it afterwards (a queue, a background job). - Check
clientStateagainst the secret you set, and ignore notifications that don't match. - Use HTTPS, and give the final URL. If your URL redirects (for example from
httptohttps), the notification arrives as aGETwithout body. - Fetch the current data with the GraphQL API from
data.id, rather than relying only ondata: it's the safest way to get related records too. - Expect duplicates and any order. When a delivery fails (an error status, or no answer within 10 seconds), Wino sends the notification again to every subscriber of the shop, and keeps trying for a while: you can receive it again even after answering
2xx. If your URL keeps failing, Wino contacts you by email. Process the same notification twice without harm, for example by comparingdata.updatedAt(in milliseconds) with the last one you processed. A stock movement never changes: skip aStockActivitywhosedata.idyou already have. - Don't write back what you already have. You're also notified of the changes your integration makes. Before writing a change back to Wino, compare it with your own data, and do nothing if it's the same: that's what prevents loops (you update the stock, get notified, update it again…). For stock movements (
StockActivity),data.deviceIdis the device the movement was recorded with: your device ID for the movements you create.
In these receivers, WINO_CLIENT_STATE holds the clientState of your subscription. They refuse to work without it: otherwise a request with no clientState would pass the check. queue stands for your own queue or job system.
- JavaScript
- PHP
- Python
import express from "express";
const clientState = process.env.WINO_CLIENT_STATE;
if (!clientState) throw new Error("Set WINO_CLIENT_STATE before starting");
const app = express();
app.use(express.json({ limit: "5mb" })); // the default 100 kB is too small for some notifications
app.post("/wino-webhooks", (req, res) => {
const notification = req.body;
if (notification.clientState !== clientState) {
return res.sendStatus(401);
}
queue.push(notification); // your queue or job system: process it later, outside the request
res.sendStatus(200);
});
app.listen(3000);
<?php
$clientState = getenv('WINO_CLIENT_STATE');
if (!$clientState) {
http_response_code(500); // WINO_CLIENT_STATE isn't set
exit;
}
$notification = json_decode(file_get_contents('php://input'), true);
if (($notification['clientState'] ?? null) !== $clientState) {
http_response_code(401);
exit;
}
// One file per notification, processed later by a script
$file = '/var/wino/queue/' . bin2hex(random_bytes(16)) . '.json';
if (file_put_contents($file, json_encode($notification)) === false) {
http_response_code(500); // not saved: Wino will send it again
exit;
}
http_response_code(200);
import os
from flask import Flask, request
CLIENT_STATE = os.environ.get("WINO_CLIENT_STATE")
if not CLIENT_STATE:
raise RuntimeError("Set WINO_CLIENT_STATE before starting")
app = Flask(__name__)
@app.post("/wino-webhooks")
def wino_webhooks():
notification = request.get_json()
if notification.get("clientState") != CLIENT_STATE:
return "", 401
queue.append(notification) # your queue or job system: process it later, outside the request
return "", 200