API changelog
Every change to the public surface of the actions, with its date, version and
kind (doc 17 §6). The version is the one in
backend/tests/KStra.Tests/Contracts/v1/_contract.json: the contract test
does not accept a new snapshot without its section here.
- Additive: a caller written for the previous version keeps working (new optional input field, new output field, extra enum value, new action). Bumps the minor version.
- Deprecating: something will be removed in a major version; it stays
served, with the
Sunsetheader, for at least twelve months. - Changes to descriptions alone do not bump the version.
1.42 — 16/10/2026 — additive
People & Economy: ten topics instead of fifteen, and housing. Several topics added one or two columns or repeated figures of another, so they are merged; each topic still costs 2 credits and returns more tables than before.
| Topic | Tables | Replaces |
|---|---|---|
residents |
sociodemo (population, sex, families and residents born overseas by band), by_locality, foreigners_by_postcode |
main, families, foreigners |
postcodes |
postcodes |
postcodes |
age_classes |
age_classes, age_indicators, age_classes_5y, age_groups |
age_classes |
education_marital |
education, marital_status |
education, marital_status |
trend |
trend |
trend |
employment |
occupation, employment_rates |
occupation, employment_rates |
income |
wealth_indices, income_by_band |
wealth_indices, income_by_band |
spending |
consumption_categories, potential_market_by_band |
consumption_categories, potential_market_by_band |
housing |
housing_tenure, dwelling_type, cars_per_dwelling (new) |
— |
territorial_comparison |
territorial_comparison |
territorial_comparison |
- The earlier codes are still accepted and count as the topic that now contains them:
occupationreturns both employment tables, and["main", "families"]is one topic, 2 credits instead of 4. A request with the earlier codes never gets fewer tables than before, only the other tables of the same topic; insociodemothe family and born-overseas columns now come with the population ones.income_consumption(the single topic before 4/9/2026) counts asincomeandspending. - Housing (new): counts of occupied private dwellings from the 2021 Australian Census.
housing_tenure: owned outright, owned with a mortgage, rented (with the rowof which social housing), other tenure, not stated.dwelling_type: separate house; semi-detached, terrace or townhouse; flat or apartment (with the rowof which in buildings of 4+ storeys); other dwelling; not stated.cars_per_dwelling: no car, 1, 2, 3 or more, not stated. Each table has the count by band, area or polygon and the total, then the comparison columns (compareFrom,compareLevels) with the shares of occupied dwellings for the catchment, suburb, LGA, state and Australia; the totals row isOccupied dwellings. Anof whichrow is already part of the row above it: do not add it to the others. Shares can stay a little under 100 where the census declared no category for a few dwellings. Outside Australia the housing tables are not returned. - Prices: all topics together cost 20 credits (were 30).
enrich.pointandenrich.pointscharge the merged topics: thepeoplesection is 3 topics (6 credits, were 12) and theeconomysection 3 topics (6 credits, were 12), with the same tables. - The MCP tools of
analysis.people_economyaccept the same codes.
16/10/2026 — MCP tools return the real validation message, no version change
- When an action refused its input (for instance
mixNeighbourswith two sectors), an MCP tool answered only "An error occurred invoking …". It now returns, like every other MCP error, a result withisError: trueand the same problem the REST API sends:error: "validation_error"(status 400) with thedetailthat says what to change;not_found(404) andengine_unavailable(503) likewise.
1.41 — 16/10/2026 — additive
The category mix opens to the API and MCP.
- New public action
analysis.category_mix: how many stores of each chosen category stand inside the territory, by band, area or polygon, how many for every 10,000 residents next to the local government area, the state and Australia, and the list of the stores. Categories:sectors(codes frommarkets.sectors, 1 to 20, 1 credit each) orneighboursOf, a sector whose neighbouring categories (the businesses that share its customers or its footfall, up to 12) are counted together for 2 credits. The preview returns the quote and the categories it would count. analysis.markets_sectoracceptsmixNeighbours(also count the neighbouring categories of the one sector, 2 credits more) ormixSectors(also count these categories, 1 credit more each) and returnsmixwith the categories counted and their stores, in one quote and one charge. The mix tables,category_mixandcategory_mix_stores, come after the sector tables intables.mixis absent from the response when it was not asked for.- Both are also MCP tools. Nothing changes for a caller that does not use them.
1.40 — 16/10/2026 — additive (internal)
analysis.markets_sector can count the category mix in the same run, for the web and the assistant only.
- New internal inputs
mixNeighbours(also count the neighbouring categories of the one sector, 2 credits more for the set) andmixSectors(also count these categories, 1 credit more each), and the internal output fieldmix. They are the rules and tariffs of the internalanalysis.category_mix, in one quote and one charge. - Nothing changes on
/api/v1and MCP: these fields are not in the OpenAPI specification nor in the tools, and a request that sends them gets400 invalid_input(unknown field).mixnever appears in their responses.
15/10/2026 — clearer error for analysis.people_economy without topics, no version change
- Without
topics, or with an empty list, the 400 response said "Invalid indicators". It now says "Choose at least one topic in topics:" followed by the topic codes; an unknown code gives "Unknown topic: <code>" with the same list. Same status and error code, only the message changes.
15/10/2026 — sales.territories accepts SA1 blocks, no version change
sales.territories (internal) divides into SA1 blocks.
unitLevelaccepts 9: SA1 blocks, about 400 residents each, for territories on foot. With SA1 units the area is made of the SA2 areas where the points are, not of the local government areas, which would hold thousands of blocks; the same joining of separate parts applies, with SA2 areas.bridgeLgaslists SA2 areas when the units are SA1 (the name stays, the field is the same).
1.39 — 15/10/2026 — additive (internal)
sales.territories (internal) joins separate parts of the area.
- The area used to be only the local government areas where the points are. When those formed separate parts (Wollongong and Sydney with no point in Sutherland Shire, between them), a part stayed an island and its seller could not grow. Now the local government areas without points that join separate parts are added too: the shortest path between two parts, at most 3 areas long, at most 10 areas in all, and none if the area would then exceed 2,000 units. A real island stays separate.
- New output field
bridgeLgas: the names of the areas added this way. - Behaviour: when moving a unit would cut off a part of the territory giving it, that part now moves together with the unit, if the whole move improves the balance and the part holds no seller location and no unit fixed by hand. Before, such a unit never moved, and a territory joined to the rest by a single unit could not give anything.
15/10/2026 — sales.territories crosses areas without measure, no version change
- Behaviour of the internal
sales.territories, same inputs and outputs. A unit whose measure is zero (a park, a reserve, an industrial area; with clients, any area without clients) used to never move, and could wall a territory in. When no move improves the balance any more, such a unit now passes to the lighter neighbouring territory, but only if a unit behind it is worth moving there; each unit does so at most once.
1.38 — 15/10/2026 — additive (internal)
sales.territories (internal) accepts units fixed by hand.
- New input field
pins: a list of{ unitId, sellerCode }, at most 2,000. A fixed unit stays with that seller while the rest is balanced, even if another seller is nearer or beyond the drive time limit. A seller with fixed units is never left out by the minimum per territory. With proposed sites, the fixed units go to the site matched to their seller. An unknown seller code is rejected; a unit outside the area, or fixed to a seller left without a site, is ignored. - New output fields:
pinnedUnitsandignoredPinson the result,pinnedon every unit.anchoris now true also for the units fixed by hand.
15/10/2026 — descriptions of the territory inputs, no version change
- Descriptions only. The territory inputs (
origins,bandSeconds,bandMetres,areas,polygons,mode,traffic) ofanalysis.people_economy,analysis.markets_brands,analysis.markets_sector,analysis.brand_coverage,analysis.realestateand of the internalanalysis.category_mixandrealestate.precountnow share one wording, with the limits that are actually checked: 1 to 10 origins, 1 to 6 bands, 1 to 12 areas at levels 1-9, 1 to 10 polygons. polygonsofanalysis.people_economysaid "Polygon geometries": MultiPolygon geometries were already accepted, and the description now says so.modeandtrafficgain a description, andanalysis.realestategains one on every territory input, where it had none.- Nothing changes in the inputs, outputs, prices or flow.
1.37 — 15/10/2026 — additive (internal)
sales.territories (internal) can balance on the sum of a column.
- New input field
sumColumn: a numeric column of the clients dataset (measureclients) or of the prospects dataset (measureprospects). The measure becomes the sum of that column over the rows, for example the fleet size of each client, instead of the number of rows. Empty and negative values count as zero. Any other measure, or a column that is not numeric, is rejected. - New output field
sumColumn: the column summed, or null when the rows are counted.
1.36 — 14/10/2026 — additive (internal)
sales.territories (internal) gains the minimum per territory and the
proposed sites.
- New input fields:
minPerTerritory: smallest measure a territory may hold. With today's seller locations, a seller whose territory stays below it is left out, the lightest first, and the others share the area. With proposed sites, fewer sites are proposed until every territory reaches it;proposeSites: true draws the territories around new sites placed where the measure is (as many as the sellers, fewer with the minimum), each matched to the nearest seller.
- New output fields:
- on the result:
proposedSites,minPerTerritory,dropped(seller codes left without a territory); - on every territory:
siteName,siteLat,siteLngandsiteKmFromSeller(null with today's locations).
- on the result:
1.35 — 14/10/2026 — additive (internal)
New internal action sales.territories (not exposed in /api/v1 nor in
MCP): the Balanced territories view of Sales. It divides the area of a Sales
run among its sellers, so that every territory holds a similar share of the
chosen measure.
- Input:
- the sellers dataset and seller code column of the Sales run, with its filters, and optionally the clients and prospects datasets;
unitLevel: 4 local government areas, 5 postcodes (default), 6 SA3, 7 suburbs, 8 SA2;measure:clients,prospects,populationorfamilies;maxMinutes: optional drive time limit, 5-120;tolerancePct: accepted gap from the average, 2-50, default 10;modeandtraffic.
- The area is every unit of the chosen level in the local government areas where the points are.
- How units are divided:
- each seller keeps the unit where they are;
- the other units go to the nearest seller by drive time;
- units then move across the borders until every territory is within the tolerance, each in one piece;
- units beyond the time limit stay uncovered.
- Output:
territories: one row per seller, with the measure, the gap from the average, residents, families, average and longest drive time, the measure with the nearest seller only and the measure today (the units where the seller has most clients);units: one row per unit, with the seller it goes to, the nearest seller and the seller today;- the largest gap after balancing, with the nearest seller only, and today.
- Free: the Sales run has already been paid for. At most 100 sellers and 2,000 units.
- No change for users of
/api/v1or MCP.
14/10/2026 — search.addresses reads the locality, no version change
search.addresses now uses the locality written after the street, and the
results rank by it:
- Where it can be written: after a comma (
151 George Street, Sydney NSW 2000) or straight after the street type (12 Station Street Box Hill). Before, a locality without a comma was read as part of the street name, and the results came from other states. - How it ranks the results, in order: suburb, then postcode or local
government area, then state (full name or code:
Armstrong Creek VICfinds the suburb in Victoria, not the one in Queensland). Before, only the local government area and the state counted. - What is searched: the addresses of the street inside the written locality are always among the candidates, with the exact house number first and then the nearest one.
- The locality still ranks and does not filter.
- When the house number asked for is in the pole's list,
numberstarts with it (120, 116A, 102B, …): the first number is the one to write. Before, the list came in the data's order, and120 Moorabool Streetwas written as116A Moorabool Street.
A street whose name exists is no longer searched by similarity when the house
number is rare: 120 Moorabool Street, Geelong answers in about 0.3 s instead
of 9. Similarity is kept for misspelt streets (Powel Street). The schema
changes only in the description of query.
14/10/2026 — People & Economy on any administrative area, no version change
analysis.people_economy on administrative areas no longer has a size limit:
a state or the whole country can be analysed, and answers in about a second.
The figures of an area come from the area's own statistics, which add up to the
same residents as before. For states and the country, the breakdowns by
municipality and postcode list the municipalities and postcodes that belong to
the area, each counted whole; below the state, a postcode or municipality that
crosses the border still counts only for the part inside. Isochrones, drawn
polygons and the other actions keep the 1,000,000-pole limit. Schemas do not
change.
12/10/2026 — plainer source notes, no version change
Source notes (sourceNote of the tables) say what a figure is and where it
comes from in plain words, without formulas or model names: they end up under
the tables of the reports people hand to their clients. Table keys, titles,
column names and values do not change. The method behind each figure is the
same as before.
analysis.people_economyand the People & Economy tables ofenrich.point:- population, ages, education and employment:
… the other figures Census 2021, shared out over the addresses so that each LGA keeps its official total(was… the other variables Census 2021. Spread onto addresses with the 2021 SA1 mesh, keeping each municipality's total); - income:
income index from ABS Personal Income 2023 (tax data) where measured, a K-Stra estimate elsewhere; Australia = 100(was… (ATO/LEED) 2023 …; 100 = Australia); - consumption:
ABS national accounts 2025: the average spending per resident on each category, raised or lowered by the area's consumption index(wasspending per head = the area's consumption index × ABS national accounts coefficients 2025, on the population above); territorial_comparisonandage_indicatorsdefine area, population change, old-age index and average age in words (Old-age index: people aged 65 and over for every 100 aged 0-14), no longer as formulas.
- population, ages, education and employment:
analysis.site_comparison: the notes ofsite_comparisonandsite_estimateno longer name the Huff model, the sector code, the distance decay or the percentiles (Shares from the K-Stra gravity model of the sector; low, central and high are a new store as attractive as the weakest quarter, a typical one and the strongest quarter of the sector's stores).
11/10/2026 — plainer table titles, no version change
Table titles say the same thing in plain words; table keys and column names do not change.
analysis.people_economy:by_localityis titledPopulation by LGA(wasPopulation by locality): the rows are local government areas.analysis.markets_brands:brand_segmentationis titledStores per brand by band(or by area), wasPlayer segmentation by …;brand_playersis titledStores by distance/Stores by …, wasPlayers by ….analysis.brand_coverage:Cannibalization · shared population between stores(was… between players).
11/10/2026 — money and counts in text, no version change
site.rank: money inreasonsis written with the currency symbol in front,A$2.1MandA$1,240(before:AUD 2.1m,AUD 1,240).- Counts in table titles and source notes have the thousands separator
(
first 500 of 1,017), inanalysis.markets_sector,analysis.markets_brands,analysis.category_mixandanalysis.people_economy. - Column names do not change: they still carry the currency code
(
Value (AUD m),Total (M AUD/year)) and units (Distance (m)), so code that reads tables by column name keeps working. The web app shows them asValue (A$ M).
1.34 — 11/10/2026 — additive
Addresses are written the way the country of the address writes them. In
Australia: house number first, then the street in normal case, then suburb,
state code and postcode — 64 Shirley Road, Crows Nest NSW 2065. One house
number: an address pole groups several entrances, and its first number is the
one written.
search.addresses,search.resolve_address,search.reverse_address: new output fieldcountry, the country of the address (ISO 3166 alpha-2,AU).localityis now the suburb of the address (Bondi Junction), as an Australian address writes it; the local government area only where the suburb is not known. Before, it was always the local government area (Waverley).streetandnumberstay as they are in the data (the street in capitals, the number as the list of the pole's entrances).analysis.markets_sector: the addresses of the best locations (topLocations[].address, thesector_top_*tables and the "Near" column ofsector_listings_*) follow the rule above. Before:Shirley Road 64, North Sydney— street first, then the local government area.analysis.brand_coverage: the address given to a store placed on the map without one now carries state code and postcode too (25 Katandra Road, Ormond VIC 3204).
11/10/2026 — the listings table is always there, no version change
analysis.markets_sector: the tablesector_listings_<sector>is now returned wheneverlistingsis a list, also when it is empty: zero rows, and a source note saying no commercial property was seen recently near the best locations (which does not mean there is none). Before, the table was left out when there were no listings.
1.33 — 10/10/2026 — additive
analysis.markets_sector: commercial properties on the market near the best
locations, included in the price.
- Output: new optional fields
listingsandlistingsWindowDays. For every best location by opportunity, up to 3 commercial listings within 500 m, of the property types that suit the sector (shop,office,industrial,warehouse,building,business— a going business for sale), for lease or for sale. Each listing has the rank of the location it is near, address, type, contract (lease,sale,auction), price (nullwhen on request), floor area, distance in metres, agency, link andlastSeen, the day the listing was last seen on the portal. - Only listings seen in the last
listingsWindowDaysdays (60) are returned: an old commercial listing may already be let.listingsis an empty list when none was found, andnullfor sectors without an opportunity ranking (shopping centres, outlet villages, food halls). - New table
sector_listings_<sector>with the same listings, when there is at least one.
10/10/2026 — the local government area is always "… LGA", no version change
- Wherever a table names the local government area on its own, as one of the
areas it compares with, the name now always ends in
LGA:Sydney LGA,Waverley LGA. A suburb can share its name with its municipality (Sydney, Melbourne), and the suffix used to appear only when both stood in the same table, soSydneycould mean the suburb in one table and the municipality in the next. NowSydneyalone is always the suburb. - Affected: the comparison columns (
compareLevels=lga) of theanalysis.people_economyandenrich.pointtables and of the internalanalysis.category_mix, the territorial comparison, and the area rows of thesector_density_*tables ofanalysis.markets_sectorandenrich.point. Addresses, states, suburbs, table keys and field names do not change. Read the level fromcompareLevels, not from the column name. - Versions saved earlier keep their old labels until they are updated.
1.32 — 9/10/2026 — additive (internal)
New internal action analysis.category_mix (not exposed in /api/v1
nor in MCP): how many stores of several categories stand inside the
territory, by band, area or polygon, and how many for every 10,000 residents
next to the local government area, the state and Australia.
- Input: the usual territory (isochrone, areas or polygons) and either
sectors(1 to 20 codes frommarkets.sectors, 1 credit each) orneighboursOf(a sector code: the categories that share its customers or its footfall, a fixed set of up to 12, 2 credits for the set). The preview returns the quote and, insectors, the categories that would be counted. - Output:
category_mix(one row per category, the counts per band when there is more than one,Stores,Per 10,000 residentsand the comparison ladder fromcompareFrom),category_mix_stores(the stores, nearest first with an isochrone),playersfor the map (at most 4,000),sectorsandcategoryCodes(the category code of each sector, thesectorCodeof the players). - No change for users of
/api/v1or MCP.
9/10/2026 — sector stores with minutes, and stores per 10,000 residents, no version change
On analysis.markets_sector, with no new fields:
sector_players_<sector>has a new columnTime (min), afterAddress, when the territory is an isochrone by time: the minutes from the starting point (the nearest one, with several) to the store along the road network. The rows keep their order (the model's weight, then reviews); sort by the column for the nearest stores.sector_density_<sector>, new table: how many stores of the sector there are for every 10,000 residents in the territory, next to the local government area and the state where most of its residents live, and Australia. ColumnsArea,Stores,Residents,Per 10,000 residents. Stores are counted where they stand, like the list.
9/10/2026 — site comparison wider than the sector's catchment, no version change
analysis.site_comparisonwithoutopenTogether(and sosite.rank): whenminutesis more than the sector's own catchment (by car), the sector's competitors now enter the estimate one by one with the same minutes, as they already did withopenTogether: true. The sector model's competition is computed on the sector's own catchment, so where no existing store reaches within it the competition was zero and a candidate with a wider catchment took all of that spend: the estimate came out too high in towns with sparse stores (a supermarket in a regional city with 20 minutes on a 10-minute sector: 13.4M captured instead of 5.1M) andsite_originput first a far-away suburb with no store nearby. Within the sector's catchment the figures are unchanged. Inputs and outputs are unchanged.
9/10/2026 — site comparison figures in millions, no version change
analysis.site_comparisonandsite.rank: the tables that give money in millions (site_comparison,site_ranking,site_market_split,site_estimate,site_origin) now keep at least three significant digits (two decimals at least):0.00124instead of0for a small catchment, where two fixed decimals rounded the spend captured to zero. The typed fields in currency (capturedSpend,netNewSpend, ...) are unchanged.
1.31 — 9/10/2026 — additive
The site estimate as a range, and where the spend comes from. In
analysis.site_comparison (and so in site.rank, which runs it) the spend a
candidate would capture is also given as a range.
- Each entry of
candidates[]has four new fields, in the currency of the result:capturedLowandcapturedHigh, the spend captured by a new store as attractive as the weakest quarter and the strongest quarter of the sector's stores (25th and 75th percentile of the pull the sector model gives them;capturedSpendstays the typical store, the median);netNewLowandnetNewHigh, the net new spend in the same two cases. In sectors where every store has the same pull in the model there is no range: the four fields arenull. WithopenTogether: truethe ends are computed with all the candidates weak, or all strong. - New table
site_estimate: for each candidate, the spend captured and its share of the residents' spend within reach, plus what is taken from the own stores and the net new spend when own stores are given. ColumnsSite,Measure,Low,Central,High; a sector without a range hasSite,Measure,Estimate. The source note states the assumptions: residents' spending captured, not turnover, a minimum in city centres and tourist areas. - New table
site_origin: the spend each candidate would capture (central estimate) by the suburb where the residents live, the ten largest suburbs and the others together, with aTotalrow; with one candidate, also the share of the capture. - New table
site_ranking(with two or more candidates), "Why this order": rank, site, net new spend and the reasons ofsite.rank(what stands out against the other candidates, and a caution when there is one). The relative score ofsite.rankis not in it: the order and the value in currency are. - Table order:
site_comparison,site_ranking,site_market_split(withopenTogether),site_estimate,site_origin,site_overlap. Read tables bykey, not by position. - Fix, with
openTogether: true:takenFromOwnStores(and the "From your stores" column) also counted the spend the candidates take from the competitors of the sector, which since 26/9/2026 enter the simulation one by one; without own stores it was not zero. It now counts the own stores only, sonetNewSpend, and the ranking that uses it, can change. What the competitors lose is still insite_market_split.
9/10/2026 — "Born overseas" instead of "Foreigners", no version change
- In Australia the
foreignerstopic and thestrangersindicator count residents born overseas (ABS Census 2021, table G01,Birthplace_Elsewhere_P: country of birth), not foreign citizens: many of them are Australian citizens. The labels now say so. analysis.people_economyandenrich.point, column and table titles only:Foreigners (n)becomesBorn overseas (n)in the population table;foreigners_by_postcodekeeps its key and is titled "Born overseas by postcode", with the columnsBorn overseas (n),Born overseas (% of residents)andShare of all born overseas (%); the territorial comparison rowForeigners (%)becomesBorn overseas (%). Topic codes, table keys and field names do not change. Versions saved earlier keep their old titles.- Descriptions of the
topicsinput, ofenrich.pointand of theforeignersBySubAreaoutput ofareas.dashboardupdated to match.
1.30 — 9/10/2026 — additive
Comparison columns. The socio-demographic tables of People & Economy now compare the catchment with the areas around it: catchment, suburb, local government area (LGA), state and Australia, for the same indicators, as shares or indices (never totals).
- Tables (
PeTable, in every action that returns tables): two new optional fields.compareFromis the index of the first comparison column, ornull;compareLevelssays what each comparison column is, in order:catchment, thensuburb,lga,state,countrywhen present. Only People & Economy fills them (andenrich.point, which carries its tables); the other tables havenull. analysis.people_economyandenrich.point, the comparison columns are added at the END of the tables, after the existing ones:age_classesandage_classes_5y: the share of each age class in the residents,Catchment (%)then one column per area (Geelong (%),Greater Geelong (%), …); the totals row says 100.education: shares of the residents counted in the four levels.marital_status: shares of residents aged 15 and over; totals row 100.age_groups:Share of residents (%)is the catchment, then one column per area.age_indicatorsandemployment_rates:Totalis the catchment, then one column per area.
territorial_comparison: the table IS the comparison (compareFrom: 1). It gains the suburb column, inserted betweenCatchmentand the LGA: read the columns bycompareLevels, not by position. The rows do not change.- Which areas: with one origin, the suburb, LGA and state where the location is (before, the LGA was the one holding most of the residents; for a site inside its catchment it is almost always the same LGA). With several origins, polygons or administrative areas there is no location: the LGA holding most of the residents and its state, and no suburb (except for areas smaller than a suburb, levels 8 and 9). With administrative areas, levels not larger than the areas themselves are left out: an LGA is compared with its state and Australia only.
- Column headers are area names, as in
territorial_comparisonbefore. A suburb named like its LGA (Sydney, Melbourne) makes the LGA columnSydney LGA. The source note of each table says what it is compared with.
7 Oct 2026 — descriptions of the charged actions, no version change
- Description only. The charged actions (
analysis.people_economy,analysis.markets_brands,analysis.markets_sector,analysis.brand_coverage,analysis.site_comparison,analysis.realestate, and the internalanalysis.loyaltyandanalysis.sales) no longer end with "call it with preview=false: the platform does NOT execute, it shows the confirmation card". That sentence was meant for the in-app assistant and was wrong for API and MCP callers:/executeandrun_*do run the analysis and charge it. enrich.point,site.rankandnetwork.coveragenow say "Quote first, then execute with the quoted credits" without(preview=true): there is nopreviewfield in the public input, the quote has its own route (/quote) and its own MCP tool (quote_*).- Nothing changes in the inputs, outputs, prices or flow.
7 Oct 2026 — analysis.isochrone pricing, no version change
analysis.isochroneis now a charged action over the API and MCP: 3 credits per call, whatever the number of origins and bands. Drawing an area in the web app stays free, and the catchments computed inside other charged actions (analysis.people_economy,analysis.markets_sector,enrich.point, …) remain included in their price.- The input and output schemas do not change, but the call now follows the
charged flow:
POST /api/v1/actions/analysis.isochrone/quotereturnsquotedCredits: 3without computing anything, and/executeneedsquotedCredits(400 quote_requiredwithout it) and a key with therunscope (403 scope_requiredwith a read-only key). A run that fails or is throttled (429) is not charged; the sameIdempotency-Keyis not charged twice. - MCP: the tool
analysis_isochroneis replaced by the pairquote_analysis_isochroneandrun_analysis_isochrone. - The tariff code is
analysis.isochrone.api(x-kstra-tariffin the specification).
7/10/2026 — analysis.loyalty price in the description, no version change
- Description only, on an internal action (not served under
/api/v1). The description ofanalysis.loyaltygave two prices: 20 credits per store, and the opening simulation "included in the 10 credits", left over from before 2/10/2026. The price is 20 credits for each store analysed, every store of a network analysis included (three stores cost 60 credits); the opening simulation is included at no extra cost. The tariff and the charge did not change: only the text was wrong.
7/10/2026 — descriptions in English, no version change
- Descriptions only. The descriptions in the specification that were still in
Italian are now in English, without changing meaning, numbers, field names or
enum values: the actions
search.addresses,search.resolve_address,search.reverse_address,search.brands,analysis.isochrone,areas.list,analysis.people_economy,analysis.markets_brands,markets.sectors,analysis.markets_sector,analysis.brand_coverage,analysis.realestate, and the fields of their inputs (bands, areas, polygons, Coverage points) andcaptureNotein the output ofanalysis.brand_coverage. - The descriptions no longer refer to the web interface or to actions outside
the public API.
search.resolve_address:l4Idis the id of a level-4 area fromareas.list; when the house number is not found, a number that starts the same way is returned, otherwise the first number of the street.search.addressesandsearch.reverse_addressno longer mention the web search step;analysis.brand_coveragesays "map coordinates" instead of "a click on the map". - This changelog is now in English.
1.29 — 6/10/2026 — additive
Shopping centres, outlets and food halls.
markets.sectors: new fieldspendingModel,falsefor sectors that contain other businesses (shopping_centers,outlet_villages,food_halls).shopping_centers,outlet_villagesandfood_hallsnow haveavailable: true. They are containers of other businesses, with no spending model:analysis.markets_sectoranalyses them for supply, competition and stores, and in the summaryOpportunityandDemandarenull(not zero), with a note explaining why. No opportunity rankings or market gap for them; the cell map carries supply (supply), withopportunityanddemandset to zero.markets.sectorsno longer lists sectors with no locations at all (retail_parksandmixed_usetoday):analysis.markets_sectorrejects them as before with400 validation_error.
5/10/2026 — families depend on the country, no version change
- Descriptions only.
familiesare families as counted by the country's statistics office: the census definition of families (couples and one-parent families, excluding people living alone) is the Australian one from the ABS. In another country the same column may mean something else (for ISTAT "famiglie" are the people living in the same dwelling); the descriptions ofmeasure,planMeasure,familiesand of the Loyalty fields now say so. areas.dashboard: the Australian labels (families, foreigners, educational qualifications) apply only to Australian areas; elsewhere the label from the continent's metadata applies.
1.28 — 4/10/2026 — additive
Families are called by their name. The figure is census families: couples, with or without children, and one-parent families. People living alone and group households are not included, so families are fewer than "households" (in Sydney 46,705 against 97,743 occupied dwellings) and must not be read as such.
network.coverage(measure) andanalysis.brand_coverage(planMeasure): new valuefamilies.householdsis still accepted and counts the same thing: it is the previous name, deprecated, and will be removed only in a major version. In the plans the measure is called "families".analysis.site_comparison(candidates[]) andsite.rank(ranking[]): new fieldfamilies.householdsremains with the same value, deprecated.analysis.site_comparison: in thesite_comparisontable the column "Households" becomes "Families".- Descriptions only: in
analysis.loyaltythe fieldshouseholdsandcardsPerHhPctare described as census families; the names do not change.
3/10/2026 — Oceania only, no version change
- The European test database has been removed: every action with
continent: "europa"responds400 validation_error("Unknown continent") before any charge. The value stays in the schema enums, which do not change: it will be served again when the data is available.
2/10/2026 — network.coverage pricing, no version change
network.coveragecosts 5 credits per store and per candidate (previously 2), like a point ofanalysis.brand_coverage. Only the action's description changes; the schema does not. Callers that execute withquotedCreditsfrom the quote need to do nothing.- No tiered discounts in the pricing: volume is rewarded in the price of the credit (monthly plans, enterprise). The Credits page has the Volume section.
2/10/2026 — dataset storage fee, no version change
- The organization's datasets have a monthly fee,
dataset.storage: the first 50 MB free, then 2 credits a month per 100 MB, on the month's average, charged on the first of the month to the same balance as the keys. - If the balance is insufficient the datasets are suspended: the actions that read one
(
analysis.brand_coverageandanalysis.site_comparisonwith pointssource: "dataset",site.rank,analysis.loyalty,loyalty.cards) respond400 validation_errorbefore any charge. After 90 days of suspension the datasets are deleted. The schema does not change.
2/10/2026 — Real estate pricing, no version change
analysis.realestatecosts 20 credits per analysis, at a flat price (previously 1 credit per 1,000 listings): territory, number of listings, period and indicators do not affect the price. The quote line now has quantity 1 and unitanalyses(previously quantity = listings, unitlistings); the number of listings remains inlistingsofrealestate.precount. This also applies to therealestatesection ofenrich.point/enrich.points: 20 credits per point.- An isochrone territory now has the same cap as areas and polygons: above one
million address points the request is rejected with
400, before any charge. - The schema does not change. Callers that execute with
quotedCreditsfrom the quote need to do nothing.
2/10/2026 — Markets pricing, no version change
You pay for what you ask, not for how large the territory is:
analysis.markets_sector: 40 credits per sector, on any territory (catchment, areas, polygons). The quote now has a single line,analysis.markets.sector(unitsectors):analysis.markets.sector.poles(1 credit per 2,000 address poles) andanalysis.markets.sector.areas(5 per area) are gone. The cap on territory size remains. This also applies to thesectorssection ofenrich.point/enrich.points: with"auto"there are three sectors, 120 credits per point.analysis.markets_brands: 5 credits per brand (previously 3).analysis.brand_coverage: 5 credits per point, with its own line,analysis.brand_coverage(unitpoints); previously it used the brands' line and price (analysis.markets.brand, 3).analysis.site_comparisonandsite.rank: 20 credits per candidate (previously 5).- The schema does not change. The codes in the quote lines change:
callers that read them, or send them to
credits.quote, must use the new ones (analysis.markets.sector,analysis.brand_coverage). Callers that execute withquotedCreditsfrom the quote need to do nothing.
2/10/2026 — pricing, no version change
analysis.people_economycosts 2 credits per topic (previously 0.3, with the total rounded to the integer): one topic 2 credits, all 15 topics 30. The territory still costs nothing. The People section ofenrich.pointand of every point ofenrich.pointschanges with it, being charged per topic in the same way. The quote line remainsanalysis.people.topic.- The schema does not change: only the number in the description of
topicschanges. Callers that execute withquotedCreditsfrom the quote need to do nothing.
1.27 — 2/10/2026 — additive
analysis.brand_coverage (Coverage): closure and opening plans for your own
points, the same calculation as network.coverage.
- With two or more existing own points (
role: "mine") thebrand_closure_plantable is returned: the order in which to close them leaving the fewest people without any own point within the analysis minutes, recalculated at each step (at most 20 steps). Competitors do not count: those left uncovered are those you risk losing to them. - With one or more planned own points (
planned: true) you getbrand_opening_plan: the order in which to open them by new people reached. - Input:
planMeasure(populationby default,households,under_5,aged_65_plus,aged_70_plus). The plans do not change the price.
1.26 — 2/10/2026 — additive
network.coverage: closure and opening plans (fix 11 of the
case evaluation) and larger networks.
- Input:
closeCount(1-50),candidates(up to 100, likestores),openCount(1-20),measure(population,households,under_5,aged_65_plus,aged_70_plus).storesup to 500 (was 100),minutesup to 60 (was 30). Candidates are charged like stores. - Output:
measure,closurePlan(step, store, people lost, coverage remaining) andopeningPlan(step, candidate, new people, coverage reached). Without the new fields the response is the same as before, with two empty lists. - A network whose catchments add up to more than 50 million address poles is rejected with a 400 before charging (previously there was no cap).
analysis.brand_coverage(Coverage): up to 500 points (was 100); the load is always stopped by the cap on work.analysis.loyalty(internal): network analysis up to 60 stores (was 20, and with more than ten catchment bands the run failed).
Areas, ages, reasons and search — 2/10/2026 — behaviour, no schema version change
Second round of fixes from the case evaluation (P5-P8). The schemas do not
change: the new tables are extra elements of the tables array.
analysis.people_economy(and therefore thepeoplesection ofenrich.point/enrich.points): the Area (km²) column and the density of the territorial comparison give the area of the territory as it is seen - the drawn isochrone band, the polygon, the official area of the administrative area - and no longer the sum of the address cells, which left out cells without addresses (up to 14% less). The same applies to the "Per km²" ofanalysis.markets_brands.- With the
age_classestopic two more tables are returned:age_classes_5y(the fifteen five-year classes, 0-4 ... 70+) andage_groups(0-4, 0-14, 15-64, 65+, 70+ with the share of residents).age_classesstays as it was. site.rank: a theme (spending, competitors, income, residents) is either a reason in favour or a caution, never both; income is a strength only above the national average (index 100).network.coverage: a store withoutnameis called by itsid; "Store N" only if that is missing too.search.locations(internal): the typed street number comes before nearby numbers; a phrase starting with the name of an area suggests the area ("Armstrong Creek Town Centre" -> Armstrong Creek, Vic.); on a tie the suburb written after the comma wins, and the locality after the comma must match in full ("Mount Duneed" does not let Mount Low in Queensland through).- Another 53
detailmessages from Italian to English ("Unknown traffic", "Unknown continent", "No execution context.", ...).
Messages and positions — 1/10/2026 — behaviour, no schema version change
Fixes arising from the second evaluation of the geomarketing cases
(docs/VALUTAZIONE_KSTRA_CASI_GEOMARKETING_2026-10.md, P1-P4). The schemas do not
change.
detailalways in English. 140 validation messages were in Italian even on public actions (e.g. "Chiave di idempotenza mancante o troppo lunga" onenrich.point, now "The idempotency key is missing or longer than 80 characters"). Theerrorcodes andtypevalues do not change; a test in the suite prevents an Italian message from coming back, or one that mentions address poles or nodes.- Origins out of dead ends. A driving start point that falls on a linkable street with no way out (a one-way street that does not lead out) is moved to the nearby address pole from which the network can be reached: this applies to isochrones, catchments and all actions that start from a point. Previously it produced a catchment of a few metres and one resident.
- Geocoding within the postcode (My Data, not a registry action): with
a postcode in the row, the point must fall there; otherwise the row stays at the
centre of its postcode as
approximate, never in another state. analysis.loyalty(internal): with zones by postcode, cards with only a postcode count; a run that counts no cards in its zones stops with a 400 and does not charge.
1.25 — 1/10/2026 — additive (internal)
analysis.loyalty (internal action, not exposed in /api/v1): network
analysis.
- Input:
networkStores, the other stores of the file read together withstoreCodeas a single system (at most 19, i.e. 20 in total). Cards, turnover, zones, share of wallet, trend and spending classes cover all stores; catchment bands are measured from the nearest store of the network; the card postcodes are those of the whole network; "where else they shop" are the stores outside the network. The run costs the price once per store. - In the opening simulation all stores of the network are "of this
analysis" (
isAnalysedStore) and always enter the comparison. - Output:
network, the stores read with the main one first;nullfor a single store.
1.24 — 1/10/2026 — additive (internal)
analysis.loyalty (internal action, not exposed in /api/v1): the opening
simulation becomes a network scenario.
- Input:
openingStores, the codes of the stores of the file to compare with the candidate (at most 20, the analysis store is always included; without the field, the 10 nearest as before), andclosedStores, those that close in the scenario. The card spending of a closing store is split among the stores still open and the candidate in proportion to attraction (the same Huff model); spending that no store reaches leaves the network. Both only together withcandidateLat/candidateLng. - Output:
opening.stores[].closed,.gain(how much it takes from the cards of closing stores) and.net(gain minus loss);opening.storesChosen,opening.closedStoresandopening.lostToNetwork. With no choices and no closures the numbers are identical to before. - Descriptions: six monetary fields literally said
''' + CUR + ''', a leftover of phase 0; they now say "in the currency of the uploaded file".
1.23 — 1/10/2026 — internal and behaviour
analysis.brand_coverage: includeSectorStores now defaults to
false (it was true since 1.21). Without the field the run covers the listed
points, i.e. the stores of the chosen brands and the user's points; with
true it adds, as before, the other stores of the sector within the area,
not charged.
search.brands with sector: the main brands, not all of them.
At most 50 brands per sector (was 100).
A brand is recognised by its website: the stores that link to the same domain, with the name that most of them have in common. "Coles Epping" and "Coles Burnside" are Coles, "Supa IGA Toowoomba" and "IGA Mount Barker" are IGA. Previously grouping was by exact name, and the large chains came out as single stores.
A website without a common name is not a brand, and neither are social networks, booking portals (booking.com, Airbnb...) and public websites (
.gov.au). A name remains a brand only if at least half of the sector's stores that carry it are on its websites: "Hair", "Medical", "Cafe" as a common phrase of a portal are not.Different websites of the same brand are merged (foodworks.com.au and myfoodworks.com.au are FoodWorks), as are names that start with the name of a larger brand ("IGA Express" within IGA).
locationsis the sum of their stores in the sector.New internal action
places.count(free, not on the public surface nor among the assistant's tools): how many active businesses the places catalogue has in Australia. It is used by the brand search box ("Search 1,377,129 businesses in Australia"). No change for users of/api/v1or MCP.
1.22 — 1/10/2026 — additive and behaviour
search.brands also searches by sector.
- New input field
sector(optional): a code frommarkets.sectors, for examplefootwear. Returns the brands of that sector with at least two stores, best known first (the same popularity as the empty-field list: locations and reviews together), up to 100. The sector is the model's, the same as in Whole sector mode. Withqueryas well (at least two characters) it filters them by name. An unknown code responds 400. - With an empty field and no sector the best-known brands are now 20, no longer 8. Search by name stays at 8 results.
1.21 — 28/9/2026 — additive and behaviour
analysis.brand_coverage reads the territory with all its players.
- New input field
includeSectorStores(boolean, defaulttrue): the other stores of the sector within the area enter the run as competitors, each with its own catchment ofminutesminutes, the heaviest in the model first and up to 60. They are not charged: the cost stays that of the chosen points. Withfalsethe run is as before, and the rest of the sector stays in the model's supply. - In the output,
points[].sectorStore(boolean) marks the points added this way; their source iscatalogand their rolecompetitor. The same field appears in the points ofanalysis.site_comparisonandsite.rank, which use the same object, and is alwaysfalse. - In the tables they appear after the chosen points, role
Sector store: the 10 that matter most (by population reached, spending captured or spending lost, depending on the table) have their own row, the others are in a row "N other stores of the sector" withoutId. They count in the contested territory (brand_contested, columnCompetitors) and in the captured share; the cannibalization remains among the chosen points. - Among the
catchments, the 12 most populous sector catchments haverole: "competitor". - If the catchments together exceed the run's work cap, the lightest sector stores give way first; the table note says so.
1.20 — 27/9/2026 — behaviour
No field changes shape; the tables of analysis.brand_coverage change.
- The tables
brand_coverage,brand_cannibalization,brand_contested,brand_captureandbrand_impacthave anIdcolumn at the end: the id of the point, the same aspoints[].id. It is not a figure to read but the handle with which a client finds the point on the map, as in the tables of the sector's stores. - When a brand is repeated among the points, the name carries the locality
("7-Eleven · Sandringham"); two in the same suburb get a number.
This applies to the tables, to
pointsand tocatchments. - Points placed on the map (
source: manual) now have an address and a locality, taken from the nearest linkable address pole that has a street, without geocoding, written like those of the catalogue ("25 Katandra Road, Ormond"), with the suburb as locality: "near …" when the point is more than 25 m from that street number. The same applies to the own stores ofanalysis.site_comparison.
1.19 — 26/9/2026 — additive
analysis.site_comparisonwithopenTogether: true(even with a single candidate): the sector competitors that can reach the area from twice the catchment enter the simulation one by one, each with its own catchment, instead of only as the model's aggregate pressure. Up to 60, the heaviest first; the others stay in the "rest of the market".- New table
site_market_split: who takes the residents' spending in the area, today and with the new sites — the new sites, the own stores, the ten competitors that take the most, the other nearby competitors summed, the rest of the market and the residents that no store reaches today within the catchment — with the change. The total is the area's resident spending, the same before and after: the new sites take it from the others or from those not served today. catchments[].role(optional):candidatefor the catchments of the new sites,competitorfor those of competitors. The catchments of the twelve competitors that take the most in the area are returned.
1.18 — 26/9/2026 — additive
analysis.site_comparisonaccepts from 1 to 5 candidates (previously from 2): with a single candidate it is the simulation of an opening against today's competitors.- New field
openTogether(optional). Withtruethe opening of ALL candidates together is simulated: each competes with the players already present, with the own stores and with the other candidates, and what the own stores lose is split address pole by address pole among the candidates that take it (the sum equals the total loss). Omitted orfalse: each candidate is weighed on its own against today's market, as before — it is the comparison of alternatives, the one used bysite.rank. The web panel usestrue. - In the same version, with no new fields, for all actions that start from
an origin (isochrones, sector, people and economy, site comparison,
coverage, enrichment): the origin now snaps to the nearest linkable address pole
(streets
PlayerLinkable = 1), the same rule as the imports. The engine snapped to the nearest node traversable in the mode, and on foot a point could end up on a small island ofunknownstreets detached from the rest: a catchment of one resident.snapDistMadds up the displacement. The radius (radius) does not change. search.reverse_addressreturns only linkable address poles (still the nearest one with a street, but among those on streetsPlayerLinkable = 1): the point returned is the one the analysis will start from.
1.17 — 26/9/2026 — additive
- Dataset selection in Sales.
sales.precountandanalysis.salesacceptsellersFilter,clientsFilterandprospectsFilter;sales.postcode_recordsandsales.find_recordsacceptclientsFilterandprospectsFilter;sales.catchmentsacceptssellersFilter. All optional: without a filter all rows of the dataset are included, as before. - The filter (
DatasetFilter) is the same as in the My Data table:areas(administrative areas containing the row's address pole, levels 1-9, any one is enough),conditions(column, operator amongin,not_in,contains,gte,lte,empty,not_empty, and values; all must hold) andsearch(text in any text column). Only the filtered rows are counted, charged and analysed: it is the way to stay under 100 sellers without cutting the file. - Within the same run the filter must be passed identically to all actions that read those datasets (precount, run, postcode_records, find_records, catchments): otherwise a reassignment by postcode also picks up rows outside the selection.
In the same version, with no new fields, on analysis.markets_sector:
- Removed the
sector_competitors_<settore>table introduced in 1.16: its five rows were the first five ofsector_players_<settore>, which remains the list of stores in the territory. sector_players_<settore>(andplayers) on equal weight is now sorted by reviews, then by id. In the 36 sectors with "fixed" weight the model gives 1.000 to all, and the order — including which stores stay under the cap of 4,000 — was the order in which the database returned the rows.
1.16 — 25/9/2026 — behaviour
No field changes shape: WHAT is inside topLocations changes, and which
tables come out of analysis.markets_sector. A caller that reads the fields
keeps working; one that expects five addresses for demand or
for competition will no longer find them.
topLocationsonly foropportunityandgap. Demand is a value per AREA — on an area of 246 address poles a single distinct demand value and one of opportunity, against 236 of competition — so five addresses for demand were five address poles drawn at random within the best areas, with the same number repeated. And the competition ranking listed the MOST contested places, i.e. the worst ones to open in.sector_competitors_<settore>, new table: the five most reviewed stores in the territory (name, address, rating, reviews). It is the answer to "who am I up against". It is sorted by reviews and not by the model's weight: in the 30 sectors with "reviews" weight it is the same thing, but in the 36 with "fixed" weight the model gives 1.000 to all and the order would be random.sector_demand_bands_<settore>, new table: the ten bands of the demand map with, for each, how many cells, how many residents and how much they spend in the sector. Population and resident spending are the model's only summable quantities; the catchment does not appear because summing it would count the same people once per point.- On equal value the ranking is now decided by the market gap, then by the pid. Within an area the opportunity is identical on all address poles: so the areas are ordered by opportunity and the point within the area is chosen by the gap, i.e. the least contested.
- The tables
sector_top_<settore>_demandandsector_top_<settore>_supplyare removed.
1.15 — 25/9/2026 — additive
analysis.markets_sector: every "best location" also says at what scale the zone is —areaBuiltPct(how much of the 150 m circle is built up),freeLandM2(land not covered by buildings in the same circle: it is a measurement, not a buildable lot — it includes roads and gardens),largestNearbyM2(the largest building within 150 m),sectorTypicalM2(the largest structure that usually stands next to a store of that trade) andlargeStructureNearby(the address is already at that scale). It serves large formats: a hypermarket does not open where there are only houses around.- The format measure is derived from the stores already catalogued: the
largest structure within 150 m of a store of that trade, as a
median over two hundred samples. Hypermarkets 7,205 m2, furniture stores 3,708,
supermarkets 2,114, hairdressers 1,464. It is NOT the floor area of the store:
buildings.area_mqis the building footprint, and a pharmacy inside a shopping centre "occupies" the shopping centre's polygon. - A tight address does not disappear: it drops to the bottom of the ranking and the
Sitecolumn says so ("tight for this format"). - The rankings no longer suggest buildings that are not a commercial site: schools, hospitals, barracks, airports, car parks, stations, churches, stadiums and zoos, greenhouses and silos. It is not a matter of size — for a hypermarket a large shed or arcade is the right answer — but of trade.
1.14 — 25/9/2026 — additive
analysis.markets_sector: every "best location" now says what is at the address —hasBuilding(false = vacant lot),buildingClass(retail, residential, industrial...) andbuildingAreaM2(the footprint in square metres). The value says what the point is worth, not whether you can open there: a third of Australian addresses are not linked to any building. The fields also appear insideenrich.point, which embeds the same locations. Versions saved before this date do not have them.- In the same action, the rankings no longer suggest places where a store cannot be (parks, protected land, cropland, cemeteries, golf courses, landfills, water): the address is discarded and replaced by the next best. Vacant lots remain, flagged.
- The
sector_top_*tables have one more column, Site, between the address and the value.
1.13 — 24/9/2026 — additive
- New action
network.coverage(credit-based): from 2 to 100 stores, one catchment each, residents counted once — covered by one store, by two or more (cannibalization and overlapping pairs), not reached in the administrative areas the network touches (gaps, by level 4-9). Per store: reached, exclusive, shared and with whom. 2 credits per store (pricenetwork.coverage, editable from the console).
1.12 — 24/9/2026 — additive
- New action
site.rank(credit-based, composite): from 2 to 20 candidate sites for a sector, evaluated in groups of five withanalysis.site_comparisonand ranked by net new spending, with a score (the best = 100) and the reasons in words for each site (two strengths and a warning, comparing each site with the median of the set). 5 credits per candidate, a single line in the quote.
1.11 — 24/9/2026 — additive
- New action
enrich.points(credit-based, composite, as a job): up to 500 points with the sections ofenrich.point. The quote is an estimate on three sample points multiplied by the number of points; the run responds immediately withjobIdand the job charges point by point without exceedingmaxCredits(default: the estimate): points beyond the cap remainskippedwithbatch_cap. Outcome via thejob.completedwebhook or fromGET /api/v1/jobs/{id}(summary inresult); results fromGET /api/v1/jobs/{id}/results(paged) and.../results/{index}(in full), kept for 30 days. MCP toolsget_jobandget_job_results.
1.10 — 24/9/2026 — additive
- New action
enrich.point(credit-based, composite): enriches a point with everything the engine knows about its catchment in one call — People & Economy, Real estate, brands, sectors (also chosen from the catchment withsectors: ["auto"]). Sections are chosen withinclude, each costs the price of the analysis that produces it and the quote lists them line by line; a section with no input or no data in the catchment is skipped and reported inskipped.detail: "summary"(default) omits the lists (listings, stores, geometry),"full"includes them. If credits run out halfway, the sections already calculated remain charged and delivered andblockedBysays where it stopped. - Descriptions only: the fields of Real estate (indicators, postcodes, series, listings), of brands and of sectors (stores, best locations) now have their description in the output schema.
Public perimeter — 24/9/2026 — no version change
- The public surface (REST
/api/v1, OpenAPI, MCP) exposes ONLY the actions markedPublicApi: the geomarketing analyses (analysis.isochrone,analysis.people_economy,analysis.markets_sector,analysis.markets_brands,analysis.brand_coverage,analysis.site_comparison,analysis.realestate) and the lists needed to build their input (search.addresses,search.resolve_address,search.reverse_address,search.brands,areas.list,markets.sectors). The other 16 actions of the registry remain in the internal contract with the SPA: for v1 they "do not exist" (404unknown_action). The action schemas do not change; the contract snapshots remain for all actions.
Webhook — 23/9/2026 — no version change
- New routes
GET/POST /api/v1/webhooks,DELETE /api/v1/webhooks/{id},POST .../enable,GET .../deliveries,POST .../ping(scopemanage). Eventsjob.completed,job.failed,analysis.version.created,ping; signed POSTX-KStra-Signature: t=…,v1=HMAC-SHA256(secret, "t.body"); four attempts with increasing backoff; endpoint disabled after 20 consecutive failures. The payloads are the same objects as the REST API (jobas inGET /api/v1/jobs/{id},versionas in executions). Page/developers/webhooks. The action schemas do not change.
MCP surface — 23/9/2026 — no version change
- MCP server at
/mcp(stateless streamable HTTP transport, same Bearer key). Tools generated from the registry: one per free action (search_addresses), two per credit-based action (quote_…andrun_…, the second with mandatoryquotedCreditsand aquote_changedrejection without charge), pluslist_analyses,get_analysis,get_credits,get_data_versions. Resourceskstra://analyses/<code>. Tool errors carry the same JSON as Problem Details. Page/developers/mcp. The action schemas do not change: the version stays 1.9.
Behaviour — 24/9/2026 — no version change
search.brandsandanalysis.markets_brands: a store that joins a brand by name and domain must also carry out the chain's prevailing trade (K-Stra sector ofanalysis.player): "Woolworths Petrol" is no longer a Woolworths supermarket. Callers that pick exactly that brand still find it, because the exact name is always included.analysis.people_economy, tableage_classes:totalsis now present (row "Total", sum of the classes); previously it was always null. Versions saved earlier remain without it.
Behaviour — 23/9/2026 — no version change
/quoteand/executecheck the schema's required fields before executing: if one is missing they respond 400validation_errorwith the field inmissingand indetail(previously the error referred to an internal parameter).- Errors that come from an exception under
/api/v1have the full form of Problem Details (type,title,error,instance); new codes in the catalogue: 503engine_unavailable, 500internal_error.
Behaviour — 23/9/2026 — no version change (brands)
search.brandsandanalysis.markets_brands: a brand also includes the stores that carry its name as a word and are on its domain ("Coles Lara" on coles.com.au is Coles), not only those named exactly like it. Schemas unchanged; the numbers change: "Coles" goes from 9 to over 900 stores. Inmarkets_brandsthebrandof each point is the requested brand (for the exact name the catalogue spelling remains).
1.9 — 23/9/2026 — additive
- New action
sales.find_records(free, read-only): searches clients and prospects by text (code, name or any text column) among all the records of the datasets, not only those with a marker on the map, with current seller, postcode, coordinates and the clients' numeric values. It serves to reassign a single record from the data.
1.8 — 23/9/2026 — additive
- New action
sales.postcode_records(free, read-only): all the clients and prospects of the datasets located in a postcode, with the current seller code and, for clients, the values of the numeric columns. It serves to reassign an entire postcode from the data and not from the map markers, which stop at 5000 per dataset.
1.7 — 23/9/2026 — additive
analysis.sales, output:clientsBySeller[].counts, per numeric column how many of the seller's clients have a value (the basis of the average: blanks do not count). It serves to recompute the average exactly when the scenario reassigns clients. Earlier responses do not have it.
1.6 — 23/9/2026 — additive
analysis.loyalty, input:byPostcode(optional), the geography that comes from the data: the zones are the postcodes where the store's cards are in the period, the 20 with the most cards (a card is in the postcode of its address pole if it is geolocated, otherwise in that of its address). Alternative tobandMinutes,areasandpolygons. The result declares the geographyareas, with level 5 zones. The web panel no longer sendsareasandpolygons(the geography does not come from the Where step), which are still accepted.
No version change — 23/9/2026 — Sales behaviour and pricing
sales.precountandanalysis.salesno longer read the territory: they include all the sellers, clients and prospects of the datasets linked to the network, and the territory of the comparisons is the postcodes where they are.origins,bandSeconds,bandMetres,areasandpolygonsare still accepted but are ignored (their descriptions say so).polesInvolvedare the address poles of those postcodes.- Pricing:
analysis.sales1 credit per seller, plus the new itemanalysis.sales.records, 1 credit per 1000 clients and prospects (previously: 1 credit per 10 items). The quote carries the two lines. - The schema does not change.
1.5 — 24/9/2026 — additive
analysis.loyalty, input: the opening simulation (candidateLat,candidateLng) works with any territory, no longer only with catchment bands. Without bands (administrative areas or polygons) the reachability window is given by the new optional fieldcandidateMinutes(5-60, default 20); with bands it remains the last band andcandidateMinutesis ignored. A previous call behaves as before: without bands it received an error and now gets the simulation.
1.4 — 24/9/2026 — additive
areas.dashboard, output:foreignersBySubArea, the sub-areas of the level below ordered by foreigners (the top 20, with the share of residents and of the area total). From level 3 down each sub-area counts only its part within the area, summing the address poles: the levels do not nest. Among thedemographicsindicators there are alsomedian_age_males,median_age_females,old_age_index,old_age_index_males,old_age_index_females(derived from the age classes by sex) andpop_change_avg_yearly(average annual % change of the population over the last eight years: the local government area series, summed for states and country, weighted by residents for the other levels). Earlier responses do not have them.analysis.markets_brands, output:points[].state(the state of the address the point is snapped to) andpoints[].status(the status of the business when it is not open). Thebrand_playerstable carries the columns State, Rating, Reviews and Note.analysis.people_economy: new tablesage_indicators(average age and old-age index for males, females and total, with theage_classestopic) andforeigners_by_postcode(with theforeignerstopic); inby_localitythe columnAvg distance (km)when the territory is an isochrone; interritorial_comparisonthe rows Area (km²), Population density, Population change avg yearly (%) and Old-age index. The tables arePeTable: the schema does not change, the data does.
1.3 — 24/9/2026 — additive
analysis.brand_coverage, output:points[].outside(boolean). A dataset now also carries its points just outside the area, those whose catchment can enter it: in the quote they are candidates, in the result only those whose catchment actually enters the area remain, and the others are removed and not charged (creditsChargedmay therefore be lower thanquote.totalCredits).analysis.brand_coverage: the cap on points goes from 20 to 100 (the same applies toownStoresofanalysis.site_comparison). The real cap is the sum of the catchments' address poles: above 50 million the run is rejected with a validation error, above 12 millioncatchmentscomes back empty andcaptureNotesays so. Descriptions only, no field changed.
1.2 — 23/9/2026 — additive
- New action
loyalty.cards: the card search of a loyalty dataset (doc 07, operational views). One row per card — code, address, suburb, postcode, position quality, stores and months of activity, transactions, turnover and average receipt in the period — with filters by store, text, thresholds and map rectangle, sorting and pages up to 20,000 rows, and the totals of all cards that pass the filters. Free. It is not among the assistant's tools: it returns personal data of the user's customers.
1.1 — 23/9/2026 — additive
- New action
analysis.site_comparison: 2-5 candidate sites compared with a sector's spending model — population and families in the catchment, income and consumption indices, resident spending, competitors, spending captured (Huff share), how much it takes from own stores and net new spending, with the ranking; and the population the candidates share. Priceanalysis.site_comparison, 5 credits per candidate. analysis.sales, output: new fields of the clients and prospects list —clientPoints[].accuracyand.values, the same forprospectPoints, andclientListColumns/prospectListColumns(the columns thatvaluesrefers to, in order). Missing in versions saved before today.analysis.sales, output:coverage[].popis now the population of the postcode's addresses, the same unit ascoveredPopandoverlapPop; previously it was the postcode's census population, and a postcode reached in full exceeded 100%. The type does not change, the meaning does: it may differ by a few points frompostcodes[].pop, which remains that of the postcode. Versions saved earlier keep the old value.analysis.brand_coverage, output:points[].sourceandpoints[].roledeclare their allowed values.- Descriptions added to records shared by many actions (
QuoteResult,QuoteLine,PeTable,CoverageShape,CoveragePointOut): output fields without a description drop from 571 to 461.
1.0 — 23/9/2026 — initial
First snapshot of the contracts: the 25 actions of the registry, with the input schema and, for the first time, the output schema.
- The result type is derived from
IRequest<T>and its schema is generated with the same generator as the input. Output fields that are not guaranteed are[tipo, "null"]: they always arrive, sometimes null.search.resolve_address,search.reverse_addressandrealestate.listingmay return null (ResultMayBeNull). - Fixed the generator: a type used twice in the same schema (two
lists of tables, two quote lines) became an object without
properties from the second time onwards. Dictionaries are objects with free
keys (
additionalProperties), not lists of pairs; dates aredate-timestrings; anobjectfield is the empty schema, "any value". - Known limitation: the GeoJSON polygons of the inputs (
polygons) are declaredobjectand are an array. It stays this way because the same schema is the one read by the assistant, which wants a type: the field description says so.
Public surface /api/v1 — 23/9/2026 — no schema version change
Phase 1 of doc 17. The action schemas do not change; the public door is born:
- User API keys (
Authorization: Bearer ks_live_…), created from Account → API with scoperead/run/manageand an optional monthly cap; the ledger and the activity log carryapi_key_id, the channel isapi. GET /api/v1/me,/actions(withinputSchemaandoutputSchema),/credits,/data,/jobs/{id}; analyses for reading (/analyses,/{code},/versions,/versions/{no}) and for writing withmanage.POST /api/v1/actions/{id}/quote(does not charge) andPOST /api/v1/actions/{id}/execute: credit-based actions requirequotedCreditsequal to the quote (400quote_required, 409quote_changed), a block for insufficient balance is a 402 withblockedBy.- RFC 9457 errors (
application/problem+json, stabletype), headerIdempotency-Key, quotas per key (600/min, 60 credit-based executions/min). GET /api/v1/openapi.json: OpenAPI 3.1 generated from the registry, one operation per action with examples; site/developers(Quickstart, Credits, Analyses, Errors, Changelog, interactive reference).