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.
Scheduling
Section titled “Scheduling”-
Run after the trading day closes
Section titled “Run after the trading day closes”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.
-
Pull a window, not a day
Section titled “Pull a window, not a day”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.
-
Stagger tenants
Section titled “Stagger tenants”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.
-
Store what you sent
Section titled “Store what you sent”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.
Trading days and time zones
Section titled “Trading days and time zones”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.
Late corrections
Section titled “Late corrections”| Event | Effect on the feed |
|---|---|
| Refund raised the next day | Reduces the refund day, not the original sale day |
| Bill voided by a manager | Disappears from every figure for its day |
| Bill closed after the shift | Appears 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.
Backfill
Section titled “Backfill”To load history when a tenant first connects, walk backwards in 31-day windows:
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.
Reconciliation
Section titled “Reconciliation”Two checks catch almost every dispute before the landlord sees it:
- Internal consistency. For each row,
netSalesshould equalgrossSales − discountAmount − refundAmountallowing for rounding. A large unexplained gap is worth investigating before you report it. - 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.
When a tenant disconnects
Section titled “When a tenant disconnects”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.
Monitoring
Section titled “Monitoring”Alert on these, per tenant:
| Signal | Why it matters |
|---|---|
| No successful pull in 48 hours | Silent failure — credentials, grant or scheduling |
| A day that stays at zero for a trading store | The store may not be closing its day, or trading stopped |
A sudden step change in transactionCount | A store opened, closed or changed how it rings up sales |
Repeated 429 | Your schedule is too bunched; spread the run |