Live inventory checks

Let Localoy ask your own system whether an item is available before it accepts a booking.

With inventory checks switched on, Localoy asks your system before it accepts a booking for a linked item — so a table, ticket or session that sold out in your own system cannot be double-booked on Localoy.

When Localoy calls you#

Localoy calls your inventory endpoint when all three hold:

  1. A customer is booking an event ticket, an activity or a dining reservation.
  2. That bookable is linked to one of your catalogue items through localoyRef. See Link an item to a Localoy listing.
  3. Your production inventory endpoint is saved and enabled in the Partner Portal.

The call is made before the booking is written, and the customer waits for your answer.

Inventory check
Inventory checkCustomerLocaloyYour inventory endpoint1. Book sku-4471 × 22. GET ?external_id=sku-4471&quantity=23. Verify signature, count stockIf available4. 200 { available: true }5. Booking createdIf sold out6. 200 { available: false, remaining: 1 }7. Refused: only 1 leftIf no valid answer within 1.5 s8. Refused: try again in a moment
  1. Customer → Localoy: Book sku-4471 × 2
  2. Localoy → Your inventory endpoint: GET ?external_id=sku-4471&quantity=2
  3. Your inventory endpoint → Your inventory endpoint: Verify signature, count stock
  4. — If available —
  5. Your inventory endpoint → Localoy: 200 { available: true }
  6. Localoy → Customer: Booking created
  7. — If sold out —
  8. Your inventory endpoint → Localoy: 200 { available: false, remaining: 1 }
  9. Localoy → Customer: Refused: only 1 left
  10. — If no valid answer within 1.5 s —
  11. Localoy → Customer: Refused: try again in a moment

Set up the endpoint#

In the Partner Portal, open Open Network → Inventory and save an endpoint URL for each environment. It must be https and reachable from the internet. Each environment has its own signing secret, starting invsec_, which you can reveal and rotate there. Rotation takes effect immediately.

The request#

Request from Localoy
GET /localoy/inventory?external_id=sku-4471&quantity=2&at=2026-09-30T13%3A30%3A00.000Z&module=dining HTTP/1.1
Host: pos.example.com
Accept: application/json
User-Agent: Localoy-Inventory/1.0
X-Localoy-Signature: t=1790410530,v1=5b1f0c…
X-Localoy-Request-Id: 9b2d6f1e-3c7a-4e8b-a5d0-1f2e3c4b5a69
Query parameterAlways sentMeaning
external_idYesThe externalId of your catalogue item.
quantityYesHow many are being booked — seats, people or units.
atNoWhen the booking is for, in ISO 8601 UTC. Sent only when the booking has a date or time.
moduleYesevent, activity or dining.

Query parameters already in your endpoint URL are kept.

Verify the signature#

Inventory checks are signed like webhooks, with one difference: there is no body, so the signed string is the path and query exactly as received:

Signed string
{t}.{path}?{query}          e.g.  1790410530./localoy/inventory?external_id=sku-4471&quantity=2&…

v1 is the hex HMAC-SHA256 of that string, keyed with your full invsec_… secret. Use the raw request target — before any URL decoding — because the query is signed in its encoded form (%3A, not :).

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

app.get("/localoy/inventory", (req, res) => {
  const header = req.get("X-Localoy-Signature") ?? "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(401);

  // req.originalUrl is the path and query exactly as received
  const expected = createHmac("sha256", process.env.LOCALOY_INVENTORY_SECRET)
    .update(`${t}.${req.originalUrl}`)
    .digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  if (given.length !== 32 || !timingSafeEqual(given, Buffer.from(expected, "hex"))) {
    return res.sendStatus(401);
  }

  const left = stockFor(req.query.external_id, req.query.at);
  const wanted = Number(req.query.quantity);
  res.status(200).json({ available: left >= wanted, remaining: left });
});

Your answer#

Answer 200 OK with a JSON object:

Response · 200
{ "available": true, "remaining": 12 }
FieldTypeRequiredMeaning
availablebooleanYesWhether the requested quantity can be booked.
remaininginteger or nullNoHow many are left. Shown to the customer when you say false.
  • Answer "sold out" as 200 with "available": false — not as an error status.
  • Any status other than 200 — including 201, 204 and redirects — counts as a failed check.
  • The body must be valid JSON of at most 64 KB. Other fields are ignored.
  • Answer within 1.5 seconds.

What happens next#

Your answerThe booking
available: trueContinues to Localoy's own checks and is created.
available: falseIs refused. The customer is told the item is sold out, or how many are left.
No valid answer — timeout, error status, bad JSONIs refused, and the customer is asked to try again.

If your endpoint fails 5 times in a row, Localoy stops calling it for 60 seconds, and bookings of linked items are refused during that time without a call. The next booking after the pause tries your endpoint again.

An unreachable endpoint blocks bookings

Because a failed check refuses the booking, keep the endpoint fast and highly available. If you need to take it down, disable it in the portal first — bookings then go ahead without a check.

Monitor your checks#

The Partner Portal lists your last 50 checks — real bookings and tests — with the answer and how long it took. Use the Test button to send one real, signed check to either environment's endpoint.