Give a catalogue item a localoyRef naming the Localoy ticket type, activity item or restaurant it stands for. GET /bookables lists the ids.
Inventory checker
When your own system holds the real stock — seats, slots, tables — Localoy can ask it before taking a booking, so a customer is never sold something you no longer have.
Save your endpoint's URL under Inventory Checker on the Self-Managed page and switch the live one on.
Before every booking of a linked item, Localoy calls your endpoint and books only if you say yes.
Both switches are needed: an item that is not linked, or a live endpoint that is off, means Localoy books without asking — exactly as before. Linking is part of the item write (see The catalog item), so a sync that already sends your items can send the link too.
GET https://yoursite.com/localoy/inventory?external_id=TICKET-VIP&quantity=2&at=2026-10-02T13%3A30%3A00.000Z&module=event
X-Localoy-Signature: t=1791552600,v1=5b1f0c…
X-Localoy-Request-Id: 1d0e7a52-6c2b-4f0e-9d7a-3b9a0c4e8f21
User-Agent: Localoy-Inventory/1.0| Query parameter | Meaning |
|---|---|
external_id | Your item's externalId — the item linked to what is being booked. |
quantity | How many the customer is asking for. A whole number, 1 or more. |
at | The date or time asked for, as an ISO-8601 UTC timestamp, when the booking has one. Absent otherwise. |
module | event, activity or dining — which kind of booking is asking. |
{ "available": true, "remaining": 12 }available is required and must be a boolean; remaining is optional, and when present must be a whole number of 0 or more. Anything else — a different status, a body that is not that JSON, more than 64 KB, a redirect — is not an answer.
Verify the signature — over the path and query
It is the webhook scheme with the path and query in place of a body: v1 = HMAC-SHA256(secret, t + "." + path?query), keyed by this endpoint's invsec_… secret. The question lives in the query string, so signing it is what stops someone replaying a signature to ask about a different item or quantity. Check t is recent, and compare in constant time.
const express = require("express");
const crypto = require("node:crypto");
const app = express();
const SECRET = process.env.LOCALOY_INVENTORY_SECRET; // invsec_…
app.get("/localoy/inventory", (req, res) => {
const header = req.get("X-Localoy-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) {
return res.status(400).json({ error: "stale" });
}
// Signed: "<t>." + the path AND query exactly as requested.
const expected = crypto
.createHmac("sha256", SECRET)
.update(timestamp + "." + req.originalUrl)
.digest();
const provided = Buffer.from(parts.v1 ?? "", "hex");
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) {
return res.status(401).json({ error: "bad signature" });
}
const left = stockFor(req.query.external_id, req.query.at);
const wanted = Number(req.query.quantity);
res.json({ available: left >= wanted, remaining: left });
});
app.listen(8080);When your system does not answer
You have 1.5 seconds by default — a customer is waiting. A timeout, an error or an unreadable answer is decided by Localoy's policy, which the Inventory Checker panel states: by default the booking is refused, so an outage on your side cannot oversell. After five failures in a row Localoy stops asking for a minute and applies the same policy without a call, so a dead endpoint does not slow every booking down. A "available": false always refuses the booking — it is an answer, not a failure.
Test an endpoint with Send test check on the panel: it makes one real, signed request whether or not the endpoint is switched on, and books nothing. Every check — from bookings and from tests — is listed under Inventory Checks, and failures appear on Integration Management.