SpotOnPOS DataStream (SpotOn Restaurant)

SpotOnPOS DataStream (SpotOn Restaurant)

Written by AI, not reviewed by a human. Please apply the written-by-ai-not-reviewed label. The contract below comes from SpotOn's published documentation — no live SpotOn location has been exercised.

Integrates SpotOn Restaurant as a NexSigns DataStream source.

Source name

SpotOnPOS

Vendor API

SpotOn Restaurant POS Export API

Docs

developers.spoton.com/restaurant/docs

Status

CODE COMPLETE, UNVERIFIED LIVE

Read this before selling it

SpotOn is the flattest of the fourteen POS sources by a wide margin. A menu item has a name, one price and a report category — and nothing else.

No images. No descriptions. No sizes or variants. No calories.

A SpotOn-driven menu board will be text and prices. That is a property of SpotOn's export API, not a gap in this integration, and it should be said to a customer before a design is promised.

The /modifiers and /modifier-options routes do exist — but neither carries a price, and neither carries any link back to the menu items it applies to. So there is genuinely nothing to fold in, and joining them would not produce a size dropdown. This is not an omission to be "fixed" later without SpotOn extending the API.

Configuration

{ "DataSource": "SpotOnPOS", "Version": "1.0", "ApiKey": "<Replace SpotOn API Key>", "LocationID": "<Replace SpotOn Location ID>", "Environment": "Production", "IncludeOpenItems": false, "IncludeUnavailableItems": true }

Field

Meaning

Field

Meaning

ApiKey

Sent as a single x-api-key header. No bearer prefix, no token exchange, no expiry. Still a full-access credential for every location it has been granted, so it is treated exactly like a bearer token for logging — never written out in full.

LocationID

The store. See the permission note below.

Environment

Productionrestaurantapi.spoton.com, QArestaurantapi-qa.spoton.com. Both use the /posexport/v1 path prefix. An unrecognised value falls back to Production with a warning in the refresh log, because getting it wrong is not harmless — the QA host carries a different (usually empty) menu and a production key is rejected there.

IncludeOpenItems

An "open item" is a placeholder the operator names and prices at the till ("Misc Food"). It has no price to show and no name worth showing, so it is dropped by default rather than rendering a blank row.

IncludeUnavailableItems

Applied after normalization. false drops 86'd items from the picker entirely.

A connection-check endpoint reports the location a stream is pointed at:

GET /api/nexsigns/DataStreams/GetSpotOnPOSLocationInfo?DataStreamGUID=...

Unlike the other five sources there is no "list my locations" call — every SpotOn route is location-centric and the key is granted per location — so this is a connection check rather than a picker feed.

API keys are granted per location

SpotOn grants an API key per location. A key that works everywhere else answers HTTP 403 for a location it has not been granted, and only SpotOn can grant it. So onboarding a second store is a support request to SpotOn, not a configuration change on our side.

Because that distinction matters operationally, 403 gets its own error message separate from 401 ("the key is wrong") and 404 ("the location does not exist").

API contract

Call

Purpose

Call

Purpose

GET /posexport/v1/locations/{locationId}

Proves the key works and is granted this location; supplies the name and organization ID. Answers a single JSON object.

GET /posexport/v1/locations/{locationId}/report-categories

The board's categories. Hierarchical via parentId.

GET /posexport/v1/locations/{locationId}/menu-items

The menu items.

The collection routes answer a BARE JSON ARRAY — no envelope object at all. That is the single biggest shape difference from every other source in this family, all of which wrap their data. The integration also accepts a data[]/items[] wrapper purely so a future enveloped version does not silently normalize to an empty menu.

There is no pagination on any reference route. SpotOn documents none, and its own best-practice guidance is to "retrieve all entities daily" — each call returns the location's whole collection in one response. So the sync is three plain GETs bounded only by wall clock and a per-request timeout.

Prices are decimal STRINGS

SpotOn documents standardPriceAmount as "a decimal number string representing US dollars" — so $1.40 arrives as the four characters "1.4". It is parsed once at sync time into a real JSON number, so the field resolver and the picker JS both read a number and can never disagree about how to read it. There is no divide-by-100 anywhere.

How the menu is normalized

{ "source": "SpotOnPOS", "schema_version": 1, "location_id": "...", "location_name": "...", "organization_id": "...", "environment": "Production", "currency": "USD", "categories": [ { "id", "name", "parent_id", "parent_name", "sort_order" } ], "items": [ { "id", "name", "plu", "category_ids": [], "open_item", "sold_out", "variations": [ { "id", "name", "plu", "pricing_type", "price_amount", "sold_out", "is_default" } ] } ] }

One synthetic variation per item

Every item gets exactly one unnamed variation carrying its price, and the picker hides the Variation row (it appears only below two entries). That is deliberate: the single-variation shape is what lets the Smart Menu Editor keep the same one-item-plus-size-dropdown UX across all fourteen sources — the same treatment a standalone CloverPOS item gets.

The variation's id deliberately equals the item id. There is no separate variant identity in SpotOn's export to borrow, and inventing one would put a value in a saved binding that nothing on the SpotOn side could ever be matched back to.

Categories

Report categories form a tree through parentId, flattened into a list carrying parent_name so the picker labels a subcategory "Parent / Child" — which also keeps two same-named children under different parents distinguishable. They carry no ordinal, so ordering is parent name then own name, which conveniently also groups a parent's children together.

Open items

An open item is projected with pricing_type: "VARIABLE" and a null amount, so if IncludeOpenItems is switched on the price binding renders blank rather than an error token — the same treatment as Clover's VARIABLE and OLO's OPEN.

Availability

Availability needs no opt-in flag here, unlike LoyversePOS, CloPOS, LightspeedPOS and Eats365POS. SpotOn exports an explicit available boolean on every menu item, set by the operator when they 86 something — and an explicit operator flag is safe to trust unconditionally. That is CloverPOS's treatment of its own available field, for the same reason. Absent information still means available.

Data fields available in the Smart Menu Editor

Name, Price, PLU, Inventory Status, Item ID, Location Name, Currency.

That is the complete list — seven fields, the shortest of the fourteen sources, and a direct consequence of what the export carries. Image, description and calorie bindings carried over from another POS source resolve to a blank rather than an error token, so a cross-source design degrades quietly.

Multi-location designs

Every SpotOn location exports its own menu item ids, so a design authored against location A would resolve to nothing against location B on the id alone. The picker records the item's plu and name into the binding's fallback key: PLU leads because it is the operator-authored product lookup code and the value a chain is most likely to keep identical across its stores; name is the last resort because a rename breaks it. There is no SKU or barcode in the export to sit between them.

Troubleshooting

Symptom

Likely cause

Symptom

Likely cause

HTTP 401

The key is wrong, or Environment does not match where it was issued.

HTTP 403

The key is valid but has not been granted this location. Contact SpotOn — this cannot be fixed on our side.

HTTP 404

Wrong LocationID, or Environment points at the stack the location does not live on.

"response was neither a JSON array nor an object carrying data[]/items[]"

SpotOn has changed the response envelope. Worth reporting — the integration deliberately accepts both shapes.

Board has no images or descriptions

Expected. SpotOn's export does not carry them — see the panel at the top.

Prices all resolve {E2}

standardPriceAmount is arriving in a format that will not parse as a decimal. Report it; the integration deliberately reports a broken price rather than silently rendering zero.

Some items missing from the picker

They are open items (dropped by default) or 86'd with IncludeUnavailableItems off.

Known gaps

  • Everything in the panel at the top — no images, descriptions, sizes or calories.

  • Modifiers and modifier options are not projected, because they carry no price and no item link.

  • No webhook — hourly polling only, so a 86'd item takes up to an hour to disappear from a board. SpotOn's own guidance is daily reference-data refresh, so an hour is already more current than they suggest.

  • Currency is hard-coded to USD. SpotOn Restaurant is a US product and the export documents its amounts in US dollars, so this is a statement of fact rather than a guess — but it is projected as a field so a future multi-currency export has somewhere obvious to land.

Verification status

Done: builds clean (0 errors); the editor partial parsed with the real Razor engine (0 parser errors) and verified pure ASCII.

Not done: no live SpotOn call; no offline assertion harness; no end-to-end walkthrough.

Because the shape is so simple, SpotOn is one of the quicker sources to validate — the whole contract is three GETs and nine fields. The one thing genuinely worth confirming on a live location is the decimal-string price format, since everything else is structurally trivial.