API keys and merchant access (ADSR)
ADSR apps do not run the OAuth flow. A mall reporting feed is a nightly batch job pulling a hundred tenants, often from a system that has no browser anywhere near it, and the thing the tenant actually cares about is which stores you can see. So ADSR uses a static credential plus explicit, store-level merchant approval:
- An API key identifies your app. You create it in the console; it never expires.
- An access request asks a specific merchant for a specific set of stores. Their account owner approves it in their back office.
Neither half works alone. A key with no approved request reads nothing; an approved request with no key has nothing to authenticate.
API keys
Section titled “API keys”Create keys under Apps → your app → API keys. Give the key a name you will recognise in
six months — mall-nightly-prod, not key 2.
name mall-nightly-prodenv PRODUCTIONkey lp_ak_pk_7f3c91b2e84a05d6c7b1a394f8e02d5c6a7b9013| Property | Detail |
|---|---|
| Format | lp_ak_pk_… for production, lp_ak_sb_… for sandbox |
| Visibility | Displayed once at creation. Only a hash is stored — there is no endpoint that reads it back |
| Lifetime | No expiry. It works until you revoke it |
| Limit | Five active keys per app, per environment |
| Production | Only issued once the app is APPROVED. Sandbox keys are available immediately |
Rotate by creating the replacement key first, deploying it, and revoking the old one once nothing is using it — two keys can be active at the same time, so rotation needs no downtime. Revocation is immediate at the source and takes effect on live traffic within five minutes, as verification caches expire.
Access requests
Section titled “Access requests”A request names the merchant and the stores you need.
-
Ask the tenant for their company id
Section titled “Ask the tenant for their company id”Every LithosPOS merchant has a numeric company id. The tenant gives it to you — usually alongside the store list the landlord agreed to.
-
File the request
Section titled “File the request”From Apps → your app → Access:
Field Required Notes companyIdyes The tenant’s LithosPOS company id storeIdsno The stores you need. Omit to request every store in the company noteno One line the merchant will read — say which mall and which lease this is for Access requests are a production mechanism: the company must exist, be active, and be a live merchant. Sandbox tenants need no request — see Sandbox below.
-
The merchant decides
Section titled “The merchant decides”The company’s account owner sees the request in Settings → Connected apps in their back office, with your app name, your organization, the stores you asked for and your note. They can approve it as filed, approve it with fewer stores than you asked for, or deny it. LithosPOS emails them when the request arrives.
You cannot widen what they granted. There is one live connection per merchant: filing a second request while one is pending, or while a grant is active, is refused with
developer.access_request_exists. Changing the store list — a tenant opening a second unit in the mall — means the merchant removing the connection and approving a fresh request. -
Pull the feed
Section titled “Pull the feed”The grant exists the moment they approve, and live traffic picks it up within five minutes as verification caches expire.
A request that is neither approved nor denied expires after 30 days. Cancel a pending request yourself from the same screen if the deal changes — a stale request that a merchant approves six months later is worse than no request at all.
Making a call
Section titled “Making a call”Send the key in X-API-Key and name the merchant with a companyId query parameter.
curl -s "https://api.lithospos.com/v1/reports/daily-sales?companyId=1033&storeId=2&from=2026-07-01&to=2026-07-31" \ -H 'X-API-Key: lp_ak_pk_7f3c91b2e84a05d6c7b1a394f8e02d5c6a7b9013'const url = new URL('https://api.lithospos.com/v1/reports/daily-sales');url.searchParams.set('companyId', String(tenant.companyId));url.searchParams.set('from', '2026-07-01');url.searchParams.set('to', '2026-07-31');
const response = await fetch(url, { headers: { 'X-API-Key': process.env.LITHOSPOS_API_KEY },});
const { data } = await response.json();{ "data": [ { "date": "2026-07-01", "grossSales": 12345.67, "taxAmount": 617.28, "discountAmount": 120.00, "refundAmount": 89.50, "netSales": 11518.89, "transactionCount": 214 } ]}Authorization: Bearer lp_ak_pk_… is accepted as an alternative to the header if your HTTP
client makes bearer auth easier to configure. Everything else — the envelope, the
error codes, the rate limit, pagination — is exactly
as documented for token-authenticated calls.
What a key can read
Section titled “What a key can read”The key inherits your app’s ADSR scope bundle — adsr.read, reports.read, stores.read —
which resolves to six routes:
| Route | Purpose |
|---|---|
GET /v1/reports/daily-sales | The canonical daily row |
GET /v1/stores | The tenant’s stores, so you can map ids to names |
GET /v1/reports/business-summary | Totals for reconciliation |
GET /v1/reports/sales | The sales list for the window |
GET /v1/reports/items | Item-level sales |
GET /v1/reports/pay-types | Split by payment type |
Anything outside the bundle returns 403 partner.scope_denied. That narrowness is the point:
a tenant approving a landlord’s reporting provider can see exactly what it will read.
Store scoping
Section titled “Store scoping”If the merchant granted specific stores, that restriction applies whether or not you send
storeId:
- No
storeId— every store in the grant, and nothing else. storeIdinside the grant — that store.storeIdoutside the grant — no rows, not an error. Reconcile againstGET /v1/storesrather than guessing why a store is empty.
Sandbox
Section titled “Sandbox”Sandbox keys need no access request. Your organization’s own sandbox merchants
are granted to every app in the organization automatically, so a lp_ak_sb_… key works the
moment you create it.
curl -s "https://api.lithospos.com/v1/reports/daily-sales?companyId=104271&from=2026-07-01&to=2026-07-07" \ -H 'X-API-Key: lp_ak_sb_2b8d40f19c7e35a6d0f8b241e93c5a70d6491fb2'companyId is still required — the parameter is a property of key authentication, not of the
environment. A sandbox key against a live company, or a production key against a demo one,
fails the environment check the same way credentials always have.
Errors
Section titled “Errors”Failures on the call itself use the partner API envelope,
with a stable code:
| Code | Status | Meaning | Fix |
|---|---|---|---|
partner.api_key_invalid | 401 | Unknown, revoked or malformed key | Check the secret you deployed; create a new key if it was revoked |
partner.company_param_required | 400 | companyId missing from a key-authenticated request | Add it |
partner.company_not_granted | 403 | That merchant has not approved your app, or the approval was withdrawn | File an access request, or stop scheduling the tenant |
partner.api_key_product | 403 | The key belongs to an app whose product is not ADSR | Keys are an ADSR mechanism; other products use OAuth |
partner.api_key_unavailable | 503 | Key verification is temporarily unavailable | Retry with backoff. Requests fail closed rather than being let through unverified |
gateway.company_required | 400 | companyId missing, rejected at the edge | Add it |
gateway.company_unknown | 404 | No merchant with that id | Confirm the id with the tenant |
Console-side failures while managing keys and requests use the developer API envelope:
| Code | Status | Meaning |
|---|---|---|
developer.api_keys_adsr_only | 400 | Only ADSR apps can hold API keys |
developer.api_key_limit | 400 | Five active keys already exist for that environment |
developer.app_not_approved | 409 | Production key or production access request before review approval |
developer.access_requests_adsr_only | 400 | Only ADSR apps file access requests |
developer.access_request_exists | 409 | A pending request, or a live grant, already covers that merchant |
developer.merchant_not_found | 404 | No company with that id |
developer.merchant_inactive | 403 | The company exists but is not active |
developer.env_mismatch | 403 | An access request naming a demo company — sandbox tenants are granted automatically |
Running it in production
Section titled “Running it in production”The operational side of a mall feed — scheduling, trading days, backfill, reconciliation and what to do when a tenant disconnects — is covered in mall integration. Two things change when you authenticate with a key rather than a token:
- There is no token to cache. Send the key on every request. The 300-requests-per-minute app budget still applies, so keep staggering tenants.
- A withdrawn approval surfaces as
partner.company_not_grantedrather than a mint failure. Treat it as “stop, permanently” for that tenant and alert your operations team.