Skip to content

Authentication

Your app authenticates as itself, never as a member of the merchant’s staff. How it gets that credential depends on the product it was created with and the environment it is running in.

ProductProductionSandbox
Full APIOAuth 2.0 authorization code — the merchant approves your app on a LithosPOS consent screenThe same OAuth flow against your own sandbox merchants, or the client-credentials quickstart
Online Order APIOAuth 2.0 authorization codeSame as Full API
ADSR APIAPI key plus an access request the merchant approvesAPI key, granted automatically against your own sandbox merchants

Two of those paths end with a 900-second bearer token; the ADSR key is a long-lived credential sent on each request. Everything you call as a partner — token minting included — lives on api.lithospos.com. The one exception is the merchant-facing consent screen, which lives on my.lithospos.com because it is part of the merchant’s own back office.

OAuth 2.0 — Full API and Online Order API

Section titled “OAuth 2.0 — Full API and Online Order API”

The merchant clicks Connect in your product, approves your app on a LithosPOS consent screen, and lands back on your redirect URI with an authorization code. Your backend exchanges that code at POST https://api.lithospos.com/v1/oauth/token for a 900-second access token and a refresh token, and refreshes from then on. The refresh token rotates on every use, so the newest one must be persisted before the access token is used.

The flow works against sandbox tenants too, so you can build and test the whole connection experience before app review.

→ Connect merchants with OAuth — authorize parameters, the callback, token exchange, PKCE, refresh rotation, revocation and the full error table.

An ADSR app creates a named API key in the console and files an access request naming the merchant and the stores it needs. The merchant’s account owner approves it — optionally with fewer stores than you asked for — and your key can read that tenant from then on. Requests expire after 30 days if nobody decides.

Terminal window
curl -s "https://api.lithospos.com/v1/reports/daily-sales?companyId=1033&from=2026-07-01&to=2026-07-31" \
-H 'X-API-Key: lp_ak_pk_7f3c91b2e84a05d6c7b1a394f8e02d5c6a7b9013'

companyId is required on every key-authenticated request — a key, unlike a token, does not carry a merchant inside it.

→ API keys and merchant access — key lifecycle, access requests, store scoping and the error table.

POST https://api.lithospos.com/v1/token

The original client-credentials endpoint still exists and is unchanged. It is the shortest path from a new app to a first API call: two fields, one request, no browser. It only mints tokens for sandbox credentials against your own organization’s sandbox merchants.

This endpoint accepts JSON or application/x-www-form-urlencoded. The form encoding exists for OAuth2 tooling that insists on grant_type; the value is ignored beyond validation.

Terminal window
curl -s https://api.lithospos.com/v1/token \
-H 'Content-Type: application/json' \
-d '{
"client_id": "lp_app_sbK2m9Qx7Rt4",
"client_secret": "lp_sk_sb_5ZkQ5vJ2rN8pXw3TfM6bD1yH4sC7aE0g",
"merchant_id": 104271
}'
FieldTypeRequiredNotes
client_idstringyeslp_app_sb…. A production lp_app_pk… credential is refused here
client_secretstringyeslp_sk_sb_…. Shown once at creation or rotation
merchant_idnumberconditionalThe sandbox merchant’s company id. Optional only when the organization has exactly one active sandbox merchant
grant_typestringnoAccepted as client_credentials for OAuth2 clients; other values are rejected
200 OK
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjYtMDQifQ…",
"token_type": "Bearer",
"expires_in": 900,
"scope": "stores.read items.read items.write categories.read categories.write …",
"api_base_url": "https://api.lithospos.com/v1/"
}
FieldMeaning
access_tokenRS256 JWT. Send as Authorization: Bearer <token>
token_typeAlways Bearer
expires_inLifetime in seconds. Always 900
scopeSpace-delimited resolved scopes for this app’s product
api_base_urlBase URL for every partner request, with a trailing slash. Currently https://api.lithospos.com/v1/

There is no refresh token on this grant — mint again when the token expires. The OAuth flow returns the same access token shape plus refresh_token, refresh_token_expires_in and merchant_id.

LithosPOS runs separate clusters for the UK, US, India and UAE, and a merchant’s data lives in exactly one of them. You do not need to know which. Every partner request goes to the same host:

https://api.lithospos.com/v1/…

The gateway reads the region claim from your token, routes the request to that merchant’s origin, and returns the response. Routing happens at the edge, so there is no redirect to follow and no extra round trip. Key-authenticated ADSR calls are routed the same way, on the companyId they carry.

const endpoint = new URL('items', api_base_url);
// → https://api.lithospos.com/v1/items

The access token is a signed JWT, identical whether it came from the OAuth exchange, a refresh or the sandbox quickstart. Treat it as opaque for authorisation decisions — the partner API verifies it against the LithosPOS JWKS on every request — but you may read exp for cache scheduling.

ClaimExampleMeaning
audlithospos-appAudience. App tokens are rejected on merchant-user endpoints and vice versa
sub"412"Your app id
orgId88Owning organization
companyId104271The merchant this token may touch — one token, one merchant
regioninCluster holding this merchant’s data. The gateway routes on it; you never send it yourself
productFULLFULL, ONLINE_ORDER or ADSR
scopes["items.read", …]Resolved scope bundle for the product
envSANDBOXSANDBOX or PRODUCTION
allowedIps["203.0.113.7"]Present only when the credential has an allowlist
jti / iat / exp—Token id and 900-second window

Hold one access token per merchant and reuse it for its full 900 seconds. A fresh mint on every request will hit the token endpoint throttle and adds latency for no benefit. This is the same for both mint paths — an OAuth access token is not longer-lived than a quickstart one, it is just renewable without the merchant.

const cache = new Map(); // merchantId → { token, apiBaseUrl, expiresAt }
async function getToken(merchantId) {
const hit = cache.get(merchantId);
// Renew 60s early so an in-flight request never expires mid-call.
if (hit && hit.expiresAt - 60_000 > Date.now()) return hit;
const minted = await mintToken(merchantId); // or refresh(), on the OAuth path
const entry = {
token: minted.access_token,
apiBaseUrl: minted.api_base_url,
expiresAt: Date.now() + minted.expires_in * 1000,
};
cache.set(merchantId, entry);
return entry;
}

Guard the renewal with a single-flight lock per merchant so a burst of concurrent requests produces one mint, not fifty. Minting a new access token does not invalidate the previous one — it stays valid until it expires. Refresh tokens behave differently: they rotate on every use, and only the newest one is live. See refresh rotation.

Every credential belongs to one environment and the environment is enforced on both ends.

SandboxProduction
Client id markerlp_app_sb…lp_app_pk…
Secret prefixlp_sk_sb_…lp_sk_pk_…
API key prefixlp_ak_sb_…lp_ak_pk_…
IssuedWith the app, immediatelyBy LithosPOS when app review is approved
Merchants reachableYour organization’s sandbox merchants, granted automaticallyMerchants that connected you through OAuth, or approved your ADSR access request
Merchant flagDemo companies onlyLive companies only
client_credentialsSupportedRefused — use OAuth or an API key

A sandbox credential presented for a live company, or a production credential for a demo company, fails with developer.env_mismatch at mint time and partner.env_mismatch if it somehow reaches the partner API. Production additionally requires the app to be APPROVED (developer.app_not_approved) before a merchant can authorize it at all.

Rotate from Apps → Credentials → Rotate. The new secret is displayed once and works immediately. The previous secret keeps working for 24 hours so you can deploy without a window of downtime, then it is revoked automatically.

Rotating the client secret does not disturb existing merchant connections: refresh tokens belong to the consent, not to the secret. The next refresh simply has to present the new secret.

A credential can carry an allowlist of source addresses. When set, the addresses are copied into the token as allowedIps and enforced on every request made with that token; calls from anywhere else return 403 partner.ip_denied.

Use it when your integration runs from fixed egress IPs. Leave it empty when it does not — an allowlist that drifts out of date is an outage, not a control.

The two token endpoints report failures differently, because one of them has to be interoperable with off-the-shelf OAuth clients.

  • POST /v1/oauth/token and POST /v1/oauth/revoke return RFC 6749 bodies — { "error", "error_description", "code" }. The table is in the OAuth guide.
  • POST /v1/token returns the developer API envelope — { "error": { "code", "message" } }.
CodeStatusCause
developer.invalid_client401Unknown client id, wrong secret, or a revoked credential. Deliberately indistinguishable
developer.merchant_required400Sandbox only: merchant_id omitted and the organization has more than one sandbox merchant. A production credential is never asked for merchant_id here — it gets developer.use_oauth or developer.use_api_key
developer.app_archived403The app has been archived in the console
developer.app_not_approved403Production credential used before app review approval
developer.grant_missing403No active grant links this app to that merchant
developer.env_mismatch403Sandbox credential against a live merchant, or the reverse

A production credential on either endpoint is refused with developer.use_oauth (Full and Online Order apps) or developer.use_api_key (ADSR apps). Both mean the same thing: the merchant has to authorize you, and the way they do that depends on your product.

See Errors for the complete code table and the partner API envelope.