Developer docs contents

Webhooks

Get told when prices reach the shelf, when one is refused, and when labels need a product.

Register an https endpoint on the POS API page of your dashboard and choose its events. Each delivery is a POST of a JSON event, signed with the endpoint’s secret, which is shown once when you create it.

EventSent when
products.deliveredPrices reached the label platform and every label showing them has redrawn. Per store, up to 1,000 products per event.
products.rejectedThe label platform refused products, with its reason for each.
labels.needs_productDeleting products moved their labels to the “No product” screen.
batch.failedA batch could not be applied at all.
pingYou pressed Send test in the dashboard.
{
  "id": "evt_3f1c2b9d0a4e4b6f8c7d5e2a1b0c9d8e",
  "type": "products.delivered",
  "created": "2026-10-30T09:00:03.412Z",
  "apiVersion": "1",
  "data": {
    "storeId": "22222222-2222-2222-2222-222222222222",
    "posLocationId": "STORE-014",
    "products": [
      { "barcode": "4011", "priceCents": 199, "isPromo": false, "syncedAt": "2026-10-30T09:00:02.981Z" }
    ]
  }
}

Verify the signature

Every delivery has Eslana-Signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of the timestamp, a full stop and the raw request body, keyed with your endpoint’s secret. Compute it over the body exactly as received, before parsing it, compare in constant time, and reject timestamps more than five minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

// Express: app.post("/eslana", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verify(req.body.toString("utf8"), req.get("Eslana-Signature"), process.env.ESLANA_WEBHOOK_SECRET)) return res.sendStatus(400);
//   res.sendStatus(204); // then handle JSON.parse(req.body) in the background
// });

Answer quickly, then do the work

Answer any 2xx within 10 seconds, then process the event in the background. Anything else, including a redirect or a timeout, counts as a failure.

Retries, duplicates and order

A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and then every 12 hours, for three days. An endpoint that has failed every delivery for three days is turned off and you are notified in the dashboard; turn it back on there once it is fixed.

Deliveries can repeat and can arrive out of order. Deduplicate on the event id (also in the Eslana-Event-Id header), and use the times inside the event rather than the order of arrival.

Endpoints we will not send to

Endpoints must be https and resolve to a public address. Private, loopback and link-local addresses are refused when the endpoint is saved and again on every delivery.