For developers

Your software, inside every SIDES restaurant

SIDES LABS is the API of the platform that hospitality businesses run their orders, payments and back office on. Build against it, list what you built in the marketplace, and SIDES customers install it.

POST /v1/oauth/token the first request
curl -X POST https://api.sideslabs.com/v1/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=password' \
  --data-urlencode "username=$CLIENT_ID" \
  --data-urlencode "password=$CLIENT_SECRET" \
  --data-urlencode 'scope=marketplace.apps'

{ "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "marketplace.apps" }

The surface

What the API covers

These are the contract's own groups and its own counts, read from GET /v1/publication/public/api-reference — the same document the server validates its router against at boot. This page keeps no list of endpoints of its own, so there is nothing here that can go stale.

The contract documents 388 operations, at version 1.0.0.

system

5 operations

Health, readiness, the contract itself, and the identity of the calling principal. None of these carry business data.

auth

31 operations

Both authentication planes. Machine clients exchange configured credentials at /v1/oauth/token (plane 1); partner users register, sign in, rotate and sign out under /v1/auth (plane 2).

The two issue the same kind of access token and are checked by the same middleware, and they identify different things: a client id names a program an operator provisioned, a subject names a person. A partner user's scopes are derived from their role and their tier and are never requested; a machine client may down-scope its own token, because it is a program with a purpose.

publication

25 operations

Everything SIDES publishes to partner developers: the API changelog, the developer blog, and the developer-resources area with the API reference and its downloadable artefacts.

The changelog is part of the API contract, not marketing. Every release writes an entry through this API, which is what keeps our own release notes and the public changelog one thing rather than two that drift.

Nothing here reaches a reader without an explicit, recorded transition under labs.admin.publication:write — one editorial capability over all three, not three. Developer resources carry a second, orthogonal question besides: audience decides whether a released resource is for the open internet or for authenticated partners, and the anonymous surfaces gate on both.

subscription

26 operations

Partner tiers and what they grant: the public catalogue the pricing page and the registration form render, and the resolved entitlements of one business partner.

A tier is data, not code — a new tier is a row set — and the subscription row, never the payment provider, is what decides access.

marketplace

81 operations

The marketplace: the apps a partner offers with their version history, and the category tree those apps are sorted into.

Categories are SIDES-internal to write and public to read — two access classes over one set of rows, and a third in between: an internal READ that sees the deactivated categories the public endpoint never returns. A partner chooses a category and cannot invent one, which is a fact about the scope tree rather than a check in a handler: every category write sits under labs.admin, and a principal bound to a business partner can never reach that subtree.

Apps are the other way round: partner-scoped throughout, under marketplace.apps. The partner appears in no path and in no body — it comes from the token — so a request naming somebody else's app is not refused, it is unrepresentable.

The storefront is here, and it is a different set of paths. Everything under /v1/marketplace/public/ — the catalogue, one listing, its media and the category tree — carries the storefront scope rather than an owner's, and shows only what an explicit, recorded transition has put there. Everything else in this tag answers for one owner. The two never share a shape.

partner

45 operations

A business partner's own record: its team, the invitations it has sent, and later its profile and imagery.

Every path here carries the partner's uuid, and every one of them COMPARES it against the token rather than selecting by it. The business partner a query sees comes from the authenticated principal; a uuid naming somebody else is 403, which is what makes reaching into another partner's team unrepresentable rather than merely forbidden.

statistics

6 operations

How many SIDES customers use a partner's apps, and the state of the import that answers that question.

The numbers are a CACHE with a ledger beside it, and the ledger is what makes the cache honest. Platform usage is read through the SIDES API and never by querying the platform's own schema, so what SIDES LABS holds is a rollup of what that API last said. Every endpoint here therefore answers when it last said it: GET /v1/statistics/import-status is that answer on its own, and the partner-facing reads carry the same freshness from the same computation.

Which SIDES API, at which granularity, and whether it is pulled or pushed is an OPEN question. The assumption in force is a scheduled pull of completed days into a rollup, and the default configuration imports nothing at all and says so — source: DISABLED — because a platform with no statistics is supportable and one with invented statistics is not.

Both endpoints in this tag are SIDES-internal. They are about the JOB; a partner's own numbers are a different access class over the same rows.

admin

125 operations

SIDES-internal administration across every partner: the business partners themselves, and the platform overview an administrator opens on.

Everything in this tag is a cross-partner read or write, and every operation in it is audited — reads included. That is what the tag IS: each operation writes one audit_log record, in the same transaction as the answer, and an answer whose record cannot be written is refused rather than given untraced.

/v1/admin/… is not /v1/partner/… with a wider filter. Every path in the partner tag carries a partner uuid and COMPARES it against the token; nothing there can name a partner the caller is not. The uuid in these paths ADDRESSES any partner on the platform. They are two collections for two audiences, and no query parameter widens one into the other — a single endpoint serving both is a shape this platform does not build.

The other SIDES-internal surfaces are not in this tag and that is deliberate: tier administration, category maintenance, app approval, review moderation and the support desk each live beside the partner-facing operations over the same rows, where a reader compares the two. What is here is what has no partner-facing twin.

engagement

44 operations

The community surfaces: the feature-request board partners propose on and vote on, and the comments under it.

The board is read across every partner, and that is its design rather than a leak. A board on which each company saw only its own wishes cannot be voted on, and voting is the feature. What makes it safe is the shape of the answer, not a filter: every entry names the proposing COMPANY and names the person only on that person's own opt-in, and no endpoint here says who voted — only how many did, and whether the caller is one of them.

Nothing on this board is public. Every path requires a token and there is no …/public/… route in this tag. A state called SHIPPED is SIDES answering a partner, not a publication state.

Two access classes: a partner proposes, votes and comments under engagement.feature-requests; SIDES changes a status, merges duplicates, pins, suspends and moderates under labs.admin.engagement. The second is inside the SIDES-internal tree, which is what makes "a partner cannot change a status" a fact about the authorization model rather than a check in a handler.

A proposal is not moderated on the way in and can be taken off the board afterwards. Its title and body reach every other partner the moment it is written, deliberately — and :suspend is the way back, the only operation here that removes a partner's words.

The full reference is in the partner portal. Every operation with its parameters, its responses and the scope it takes lives at the API reference, and so does the OpenAPI document itself. Registration opens both, and it is one step — it creates your business partner, your first administrator and your subscription together.

Authentication

Two planes, one kind of token

A machine client and a partner user authenticate differently and end up with the same kind of access token, checked by the same middleware. Which one you want depends on whether a person is present.

A server-to-server consumer — your CI pushing changelog entries, your backend syncing an app version. The credentials are a client id and a secret SIDES issues; there is no person and no refresh cookie.

POST /v1/oauth/token
grant_type=password
username=$CLIENT_ID
password=$CLIENT_SECRET
scope=publication.changelog:write

The scope you ask for is intersected with what the client is allowed. Asking for more is not an error and does not get you more.

A human signing in to the partner portal. The response carries a short-lived access token and sets a refresh cookie that is HttpOnly; Secure; SameSite=Strict — the only credential that survives a page reload, rotated every time it is used.

POST /v1/auth/sessions
{ "email": "[email protected]",
  "password": "…" }

200 { "access_token": "eyJhbGciOi...",
      "expires_in": 900 }
Set-Cookie: __Host-refresh=…; HttpOnly; Secure; SameSite=Strict

Second factor enabled? The first call answers MFA_REQUIRED and the code goes to /v1/auth/sessions:verify-mfa. It is a separate request on purpose: two calls, two budgets, two ledger rows.

Whichever plane issued it, the token goes in the Authorization header and nowhere else. Never in a query parameter — a URL reaches the access log, the browser history and the referrer header.

GET /v1/system/me
curl https://api.sideslabs.com/v1/system/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"

{ "principal_type": "PARTNER_USER",
  "scopes": ["partner:write", "marketplace.apps:write", …],
  "rate_limit": { "limit_per_second": 10, "remaining_day": 49873 } }

/v1/system/me is the endpoint to call first when something answers 403: it tells you which scopes the token actually carries, which is usually the whole answer.

Scopes

Four rules — the fourth one surprises people

Every endpoint has one
Default-deny. An endpoint without a scope exists only by an explicit, documented decision — health, the token endpoint, the public docs, and the deliberately public read surfaces. The reference says which for each one.
They nest by domain
marketplace → marketplace.apps → marketplace.apps:write. A parent grants its children, so a token carrying the parent reaches everything below it.
:write implies read
A scope without :write is read-only, and holding the write scope means you do not also have to ask for the read.
A read ancestor grants only read descendants
This is the one worth reading twice. marketplace grants marketplace.apps — and not marketplace.apps:write. A parent grants its children at its own access level, never above it.

Resources

Guides, collections and sample payloads

Rendered from GET /v1/publication/public/developer-resources. Everything here is public. There is more behind a partner sign-in — the resources whose audience is partner are not filtered out of this list, they were never in it.

Nothing published yet

No public guide or collection has been released so far. There may be partner-only material — that is behind a partner sign-in and is not counted here.

Questions

Before you write any code

Do I need to be a partner before I can look at anything?

No. The contract is the product: the reference, this page, the changelog and the blog need no token at all, and the OpenAPI document is served as YAML. You need a partnership to get credentials and to list software in the marketplace — not to evaluate whether the API does what you need.

What happens when the API changes?

Adding a field, an endpoint or an enum value is not a breaking change, and your client is expected to ignore what it does not recognise. Removing, renaming or re-typing one needs a new major version and at least six months of notice, with the sunset date in the changelog and in the X-SIDES-Deprecation header of every affected response.

How hard are the rate limits?

Two budgets, not one: a burst limit per second and a daily budget, both set by the partner tier. Every response carries X-RateLimit-*, and going over answers 429 RATE_LIMITED with a Retry-After you should honour rather than guess at. The tiers say what each level allows.

Is there a sandbox?

Not yet, and this page will not pretend otherwise. What exists today is a partner account on the free tier against the live API, with your own data — enough to build an integration, not enough to load-test one. Ask the support desk before you point a test suite at it.

Which language and which SDK?

Any, and none. The API is REST over HTTP with JSON bodies and OAuth 2.0 bearer tokens — there is no SDK you have to adopt and no client library that has to be kept in step. The Postman and Insomnia collections above are generated from the same contract the server enforces.

The first call takes about ten minutes

Registration creates your business partner, your first administrator and your subscription in one step. The free tier is enough to build against.