CloPOS DataStream (Clopos)
Written by AI, not reviewed by a human. Please apply the written-by-ai-not-reviewed label. The contract below comes from Clopos's published OpenAPI specification — no live Clopos brand has been exercised.
Integrates Clopos as a NexSigns DataStream source.
Source name |
|
Vendor API | Clopos Open API v2, host |
Docs | developer.clopos.com — with an OpenAPI document at |
Status | CODE COMPLETE, UNVERIFIED LIVE |
Configuration
{
"DataSource": "CloPOS",
"Version": "1.0",
"ClientID": "<Replace Clopos Client ID>",
"ClientSecret": "<Replace Clopos Client Secret>",
"Brand": "<Replace Clopos Brand>",
"IntegratorID": "<Replace Clopos Integrator ID>",
"VenueID": "",
"ProductTypes": "DISH,GOODS",
"Currency": "",
"UseInventory": false,
"IncludeHiddenItems": false,
"IncludeUnavailableItems": true
}Field | Meaning |
|---|---|
| Issued by Clopos. The secret is never logged; the ID appears only as |
| The top-level tenant — a chain, a cloud-kitchen operator. A short string such as |
| Identifies Kuusoft as a partner, not the customer. Requested from Clopos once. See the warning below. |
| Overrides the venue baked into the token, via the optional |
| Comma-separated server-side filter. Default |
| Display only. Clopos exposes no currency code on any read resource; nothing in the price path reads this. |
| Pulls the stop list. Off by default; see Availability. |
|
|
A helper endpoint lists the brand's venues:
GET /api/nexsigns/DataStreams/GetCloPOSAvailableVenueList?DataStreamGUID=...Four credentials, and the test/production trap
A test integrator can only call a non-production brand. Clopos flags every integrator ID as is_test: true or false and enforces the pairing on both /v2/auth and every authenticated route, rejecting a mismatch with "Integrator is in test mode. But brand is not in test mode". Building against a sandbox brand needs a test integrator ID; going live needs a production one — a separate request to Clopos. The refresh log passes Clopos's own message through verbatim because it is more useful than anything we could write.
Authentication is POST /open-api/v2/auth with all four values, returning a JWT that encodes brand, venue, integrator and environment.
The JWT goes in an x-token header, not Authorization. Clopos rejects a Bearer Authorization header outright. This is the most common way to mis-implement the API.
Tokens last one hour and are cached in DataStream.TempIntegratorData, scoped by a fingerprint of the client ID plus the brand so rotating credentials cannot silently reuse an old token. Caching is not just efficiency: /v2/auth is rate-limited to 60 requests/minute per client IP and Clopos explicitly asks integrators to reuse a token until it is near expiry.
Rate limits
Three tiers, each on a one-minute sliding window with standard RateLimit-* response headers:
60/min per client IP on the auth endpoint (brute-force guard).
600 failed requests/min per client IP across authenticated routes — only responses ≥ 400 count, so legitimate traffic never touches it.
300/min per
integrator_id:brandpair — the one to plan against. It is shared by every stream on that brand and every token minted for it; minting more tokens does not buy more quota, and distributing across workers does not multiply it.
The integration honours RateLimit-Reset as well as retry-after on a 429, backing off twice before deferring to the next cycle.
API contract
Call | Purpose |
|---|---|
| Credentials → JWT. |
| Validates |
|
|
| With |
| Optional. |
Envelope is { success, data[], total } — and success is checked as well as the HTTP status, because storing an error envelope as the catalog would resolve every binding to {E2}. Paging is page/limit (products cap at 100, categories at 999); the end condition is arithmetic against total, with a short page as the backstop.
Clopos's filter syntax is positional tuples, not the usual field=value: filters[0][0] names the column and filters[0][1] carries the value(s). Easy to get wrong and easy to mistake for a bug when reading the request.
Why the ProductTypes filter matters
A real brand's INGREDIENT and PREPARATION rows vastly outnumber its sellable products. Filtering to DISH,GOODS server-side is the difference between two pages and thirty — and thirty pages would blow the 25-second sync budget.
Prices are decimal major units. No division anywhere.
How the catalog is normalized
{
"source": "CloPOS", "schema_version": 1,
"brand": "...", "venue_id": "...", "venue_name": "...", "currency": "",
"categories": [ { "id", "name", "parent_id", "parent_name", "sort_order" } ],
"items": [ { "id", "name", "description", "category_ids": [], "image",
"thumbnail", "barcode", "product_type", "cooking_time",
"sold_by_weight", "unit_weight", "sold_out",
"variations": [ { "id", "name", "barcode", "price_amount",
"cost_price", "sold_out", "is_default" } ] } ]
}Sizes are "modifications"
Clopos calls a size/variant a modification: a child product row whose parent_id points at the base product, carrying its own price and its own barcodes. A modification's price is its OWN FULL PRICE, not an upcharge — so the sync stores it verbatim and adds nothing to it.
This is the opposite of LightspeedPOS and Eats365POS, whose modifier prices are upcharges. Getting it backwards would silently double every sized item.
A product with no modifications gets one synthetic unnamed variation carrying its own price, and the picker hides the Variation row.
Variation labels
A modification's own name is usually blank and its full_name carries "Pizza (Large)". The sync prefers an explicit name, then falls back to trimming the parent's name off the front and stripping one balanced bracket pair — so the dropdown reads "Large" rather than repeating the item name on every row. Anything that does not match that pattern is returned unchanged; a wrong guess would be worse than a verbose label.
Categories
Clopos returns a nested tree (children[] recursively) which the sync flattens into a list carrying parent_id / parent_name, so the picker labels a subcategory "Parent / Child". Unlike Loyverse, Clopos does carry an operator-assigned position, so that arrangement is honoured — with a null position sorting to the end, because no ordinal means "no opinion", not "first".
Inactive and hidden categories are dropped along with their whole subtree: a child of a hidden parent is not something a menu board should surface even if the child itself is visible.
Images and barcodes
Images come from media[].urls (original / extra_large / thumb); the first entry wins. Barcodes read the codes[] array first and fall back to the deprecated scalar barcode field, which keeps this working against a brand on an older deployment.
Availability
UseInventory is off by default, for a specific reason: an unfiltered stop list reports every product that has ANY limitation on it, which is not the same thing as "sold out right now" for a brand that uses stock limits as a soft warning. The pull is also non-fatal — a board renders fine without availability, so a stop-list failure logs a warning and the sync continues with everything reading available.
Data fields available in the Smart Menu Editor
Name, Variation Name, Price, Description, Image URL, Thumbnail URL, Barcode, Cost Price, Cooking Time, Product Type, Inventory Status, Product ID, Venue Name, Currency.
No calories — Clopos has no nutrition concept. A calories binding resolves to a blank, not an error token.
Multi-venue and multi-brand designs
A Clopos product id is an auto-increment key inside one brand's database. Within a brand the same catalog serves every venue, so the id always hits and the fallback never fires. The picker records barcode and name for the cross-brand case; there is no SKU field in Clopos, which is why that chain is one link shorter than CloverPOS's.
Troubleshooting
Symptom | Likely cause |
|---|---|
"Integrator is in test mode. But brand is not in test mode" | A test |
Authentication fails with all four values present | Check the |
HTTP 401 on every route after a successful auth | The token is being sent as |
HTTP 429 | The 300/min per integrator:brand budget is shared with every other stream on that brand. Reduce stream count or raise the refresh interval. |
Sized items are priced roughly double | Something has started adding the parent price to the modification price. Modifications carry their own full price — see the warning above. |
Board shows ingredients and prep items |
|
Variation dropdown reads "Pizza (Large)" | The parent-name trim did not match — the operator's |
Known gaps
modificator_groups(true modifiers — extras, sauces) are fetched but not projected; onlymodificationsfold into sizes.Price lists (
/price-lists) are not applied, so a venue using a non-default price list will show base prices. This is the most likely source of a "wrong prices" report.Recipes, packages, taxes and stations are not projected.
No webhook — hourly polling only. Clopos lists webhooks as roadmap, not shipped.
Verification status
Done: builds clean (0 errors); the editor partial parsed with the real Razor engine (0 parser errors) and verified pure ASCII; the contract taken from Clopos's own OpenAPI document and its per-page Markdown rather than the rendered docs.
Not done: no live Clopos call; no offline assertion harness; no end-to-end walkthrough. Onboarding is blocked until Kuusoft has an integrator_id from Clopos — that request should go out early, since a test one is needed even to build against a sandbox brand.