Skip to content

Mall integration

A mall feed is a nightly job that runs for years without anyone watching it. This page is about the operational details that decide whether it stays quiet.

  1. Stores keep trading past midnight. Pull yesterday’s row at 04:00–06:00 store-local, not at 00:05. Pulling too early gives you a day that is still being written.

  2. Ask for the last seven days on every run, not just yesterday. Late corrections — a refund posted the next morning, a bill closed after the shift — land inside that window and your feed self-heals without a special backfill path.

  3. A mall with 120 tenants is 120 tokens and 120 requests. Spread them over the window rather than firing at the same second: your app’s 300-requests-per-minute budget is shared across every tenant.

  4. Keep the raw row you received alongside whatever you reported to the landlord. When a tenant disputes a figure six months later, the answer is in that record.

// One nightly pass over every tenant.
async function nightlyRun(tenants) {
const to = isoDate(new Date());
const from = isoDate(daysAgo(7));
for (const tenant of tenants) {
try {
const { token, apiBaseUrl } = await getToken(tenant.companyId);
const url = new URL('reports/daily-sales', apiBaseUrl);
url.searchParams.set('from', from);
url.searchParams.set('to', to);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
await recordFailure(tenant, await response.json());
continue;
}
const { data } = await response.json();
await upsertDailyRows(tenant, data); // keyed on (tenant, store, date)
} catch (error) {
await recordFailure(tenant, error);
}
await sleep(500); // stay well inside the rate limit
}
}

Upsert on (tenant, store, date). Re-pulling a day must replace the stored row, never append.

date is the store’s own trading date. It is not a UTC date, and it is not derived from your server’s clock. A sale rung up at 01:20 during a late shift belongs to the trading day the store was in.

Never convert date between time zones. Store it as the plain YYYY-MM-DD string you received and report it unchanged.

EventEffect on the feed
Refund raised the next dayReduces the refund day, not the original sale day
Bill voided by a managerDisappears from every figure for its day
Bill closed after the shiftAppears on the trading day it belongs to

This is why the rolling seven-day window matters: a feed that pulls exactly one day and never looks back will drift away from the merchant’s own reports within a month.

To load history when a tenant first connects, walk backwards in 31-day windows:

Terminal window
curl -s "$API_BASE_URL/reports/daily-sales?from=2026-06-01&to=2026-06-30" \
-H "Authorization: Bearer $ACCESS_TOKEN"
curl -s "$API_BASE_URL/reports/daily-sales?from=2026-05-01&to=2026-05-31" \
-H "Authorization: Bearer $ACCESS_TOKEN"

Data exists from the point the merchant started trading on LithosPOS. Windows that predate that return zero rows, not an error — treat the first all-zero month as the boundary and stop.

Two checks catch almost every dispute before the landlord sees it:

  1. Internal consistency. For each row, netSales should equal grossSales − discountAmount − refundAmount allowing for rounding. A large unexplained gap is worth investigating before you report it.
  2. Against the merchant’s own report. The tenant can open their business summary in the LithosPOS back office for the same window. Same source, same arithmetic — if your number differs, the difference is in your processing, not in the feed.

Grants are revoked when a merchant leaves the mall or ends the arrangement. From that moment:

  • New token mints fail with developer.grant_missing.
  • Tokens already minted keep working until they expire — up to 900 seconds.
  • Requests then fail with 403 partner.grant_missing.

Treat that code as “stop, permanently” rather than an error to retry. Alert your operations team, mark the tenant inactive and stop scheduling them. A feed that keeps retrying a revoked tenant burns rate limit that live tenants need.

Alert on these, per tenant:

SignalWhy it matters
No successful pull in 48 hoursSilent failure — credentials, grant or scheduling
A day that stays at zero for a trading storeThe store may not be closing its day, or trading stopped
A sudden step change in transactionCountA store opened, closed or changed how it rings up sales
Repeated 429Your schedule is too bunched; spread the run