This article shows how to read a site's analytics with the Semantic Model API. A semantic model describes one analytics subject area (such as site traffic, revenue, etc.) and defines the measures, dimensions, and parameters you can query.
SCOPE.DC-ANALYTICS-AND-REPORTS.READ-SITE-ANALYTICS).Base path: https://www.wixapis.com/analytics/semantic-model/v3
| Step | Method | Endpoint |
|---|---|---|
| List models | GET | /semantic-models |
| Get model schema | GET | /semantic-models/{semanticModelId} |
| Query model data | POST | /semantic-models/query-data |
Always follow List → Get → Query. You cannot construct a valid query without first discovering the model's field names from Get Semantic Model.
measures, dimensions, and parameters to find the exact name values to query and their supported filters/sorting.interval, with optional filters, sorting, paging, and formatting.Run each step as its own separate API call, and stop to read the result before starting the next step. The inputs to each step do not exist until the previous step returns:
semanticModelId until List returns it. Never fabricate or guess a model ID — it must be a GUID copied verbatim from a List Semantic Models result. A plausible-looking GUID that List did not return will fail (SEMANTIC_MODEL_NOT_FOUND).fields, filters[].field, or sort.fieldName until you have read the schema from Get.Choosing the model and the field names is a reasoning step you perform by reading the returned JSON — not something to automate with string matching. Do not chain List, Get, and Query into a single script/execution.
❌ Don't do this — one execution that lists, gets, and queries in a single pass, picking fields by pattern-matching their names:
This guesses field names, ignores each field's dependencies (so those fields come back silently empty), and never lets you actually verify the model or schema.
✅ Do this — three separate calls, reading each result before composing the next:
id whose subject area matches the request.id. Read measures/dimensions/parameters; choose the exact names you need and note each field's dependencies.interval is required on every query. There is no way to query without a date range — omitting it fails.start/end are absolute UTC instants — not wall-clock in interval.timezone. The trailing Z is respected as a real point in time; interval.timezone does not reinterpret it, it aligns the range to local day boundaries (and materially changes the result — the same UTC window returned different totals under UTC vs Asia/Jerusalem vs America/Los_Angeles in testing). So to capture a local calendar range, send the UTC instants that equal local midnight in the site's time zone — not ...T00:00:00.000Z. Example — all of January 2026 for a America/New_York site (EST, UTC−5): start: "2026-01-01T05:00:00.000Z", end: "2026-02-01T05:00:00.000Z". Using ...T00:00:00.000Z would actually start the range at 7 pm on Dec 31 local and misalign every day boundary.interval is start-inclusive, end-exclusive ([start, end)). start is included, end is not — set end to the local-midnight instant of the day after the last day you want (e.g. for all of January, end is Feb 1 local midnight, in UTC).America/New_York is UTC−4 in summer (June local midnight = 04:00:00.000Z) but UTC−5 in winter (January local midnight = 05:00:00.000Z), so a range spanning a DST switch has different offsets at its two ends. Don't hardcode one offset across a range.interval.timezone to the site's time zone to match the Wix dashboard. Analytics in the Wix business manager are bucketed by the site's time zone. If interval.timezone is omitted it defaults to UTC, so day boundaries shift and your numbers won't match what the owner sees in the dashboard (and the same goes for using a different time zone). Get the site's IANA time zone from properties.timeZone via Get Site Properties (GET https://www.wixapis.com/site-properties/v4/properties) and pass it through.Get Semantic Model. The fields, filters[].field, and sort.fieldName values must exactly match a name returned by the model schema (e.g. traffic.sessions_count). Do not guess field names.<slug>.<field>. A model's fields often use a different prefix — e.g. the model with slug crm-people-subscribers exposes its measure as people.contacts_count, not people_subscribers.contacts_count. Copy the exact name strings from Get Semantic Model; inferring the prefix from the slug produces a field the model doesn't have.measures and dimensions lists; do not cap them with .slice(0, N) or otherwise return only the first few. Lists are often long and alphabetical, so a cutoff silently hides exactly the field you need (e.g. on traffic, a .slice(0, 25) drops traffic.referrer_category_name, traffic.referrer_source_name, and traffic.visitor_type). Choosing from a partial list re-introduces guessing — the agent assumes a missing field doesn't exist or fabricates a name. The same applies to a field's full dependencies array and to the model list from List Semantic Models.fields/filters[].field/sort.fieldName value that isn't a valid model field either (a) rejects the query with a 4XX error and a self-explanatory string code (e.g. fieldIsInvalid), sometimes listing the model's available field names, or (b) is silently dropped — the query returns 200 and that field just doesn't appear in results[].fields (same as a missing dependency). In testing, an unknown field in fields was silently omitted, not errored. Never rely on an error to catch a bad field. (See Handling wrong fields below.)dependencies array is also included in the same query; otherwise it's silently omitted from results (no error). For example, a measure may only return data when a specific dimension is also requested.sort.nullsLast: true. nullsLast defaults to false and only affects descending (DESC) order. If a measure can return null and you sort it DESC, the null rows sort first — so a "top N" query surfaces nulls before your real values. Set nullsLast: true to push nulls to the end.results is capped at 1,000 rows — paginate with paging.offset for larger datasets.formattingEnabled: true to also receive a human-readable formattedValue per cell (e.g. 1500 → "$1,500.00" or 1.5K). Raw typed values are always returned.totalsIncluded: true to get a totals row summing numeric fields across the full (unpaginated) result set.unique measures (e.g. unique visitors), query the exact time range you want in a single request — never sum values from separate date-range queries. Uniques are deduplicated within each queried range, so adding per-range results double-counts anyone who appears in more than one range and overstates the true total.Response:
Pick the model whose subject area matches the request, and keep its id.
Response shape:
Each Field (in measures, dimensions, and parameters):
| Property | Meaning |
|---|---|
name | The exact value to use in fields, filters[].field, and sort.fieldName. |
type | STRING, NUMBER, BOOLEAN, DATE, DATE_TIME, OBJECT, or ARRAY. |
filters | Supported filter prefixes (IS/NOT) and conditions (e.g. EQUAL, RANGE_II, CONTAINS_ANY). |
sortable | Whether the field can be used in sort. |
enumerations | Allowed values, for enumerated fields. |
dependencies | Other field names this field needs present in the query to return data (see sharp edges). |
groupSlug | Fields sharing a groupSlug are logically related. |
description | Human-readable description of the field. |
POST /semantic-models/query-data (body requires Content-Type: application/json).
Required body fields: semanticModelId, interval, fields.
The interval above uses 04:00:00.000Z, not 00:00:00.000Z: June is EDT (UTC−4) in America/New_York, so 04:00Z is local midnight. start/end are absolute UTC instants — set them to local-midnight-in-UTC for the site's time zone, and recompute the offset per date (DST). See the interval sharp edges above.
| Field | Required | Notes |
|---|---|---|
semanticModelId | Yes | GUID from List/Get. |
interval | Yes | { start, end } absolute UTC ISO instants (trailing Z), plus timezone. start/end are real points in time, not wall-clock in timezone — to capture a local calendar range set them to local-midnight-in-UTC for the site's time zone (offset varies with DST). Range is start-inclusive, end-exclusive ([start, end)) — set end to the local midnight after the last day you want. Set timezone to the site's IANA time zone (see Time zone section) so results align to the Wix dashboard; defaults to UTC when omitted. |
fields | Yes | Up to 60 field names from the model schema. |
filters | No | Array of { field, values[], condition, prefix }. condition defaults to EQUAL, prefix defaults to IS. For RANGE_* conditions provide exactly 2 values. |
sort | No | { fieldName, order, nullsLast }. order defaults to ASC; field must be sortable. nullsLast (default false) applies only to DESC order — set it to true when the sorted measure can contain nulls, otherwise nulls sort before real values. |
paging | No | { limit, offset }. Defaults: limit 50, offset 0. |
formattingEnabled | No | Default false. Adds formattedValue per cell. |
totalsIncluded | No | Default false. Adds a totals row. |
Each cell in results[].fields is a typed value — one of numericValue, stringValue, booleanValue, timestampValue, arrayValue, or objectValue — plus formattedValue when formattingEnabled is true. totals is present only when totalsIncluded is true.
Analytics shown in the Wix business manager are aggregated by the site's time zone. To return numbers that match what the site owner sees, pass that time zone in interval.timezone on every query. When timezone is omitted, the API defaults to UTC — which shifts day boundaries and produces totals that don't line up with the dashboard (the same applies to any non-site time zone).
Two independent things both matter, and both use the time zone:
interval.timezone aligns aggregation to the site's local day boundaries. It is not cosmetic — the same start/end return different totals under different time zones.start/end themselves are absolute UTC instants. To line them up with local calendar days you must send local-midnight-in-UTC — i.e. add the site's UTC offset for that date (DST-aware). For America/New_York: local midnight is 05:00:00.000Z in winter (UTC−5) and 04:00:00.000Z in summer (UTC−4). Sending ...T00:00:00.000Z with a non-UTC timezone starts the range partway through the previous local day and skews the result.Get the site's time zone from Get Site Properties:
The IANA time zone string is returned in properties.timeZone:
Use that value as interval.timezone in Query Semantic Model Data. The Site Properties timeZone reflects the site's primary business address and requires the SITE_SETTINGS.VIEW permission.
To retrieve more than 1,000 rows, page through with offset:
paging: { limit: 1000, offset: 0 }.offset by the page size until pagingMetadata.count is less than the requested limit.A field can be "wrong" in three ways — guard against each:
Rejected (4XX). The query fails with a self-explanatory string code (e.g. fieldIsInvalid) and, when provided, the model's valid field names. Recovery: read them, pick the correct name, and re-query — do not retry the rejected name or invent a new one. If no field list is returned, fall back to Get Semantic Model (Step 2) and re-read the schema.
Silently omitted (200). The query succeeds but a requested field is absent from every results[].fields entry (and from totals). This happens for unknown field names and for fields whose dependencies weren't included — no error is raised. So after every query, verify that each name in fields actually appears in the results. If one is missing: re-check its exact name via Get Semantic Model, add any required dependencies, and re-query. Treat a silently-missing field as a failure, not as empty data.
Valid but semantically wrong. The name exists, so you get 200 and real-looking numbers — but it answers the wrong question (e.g. using traffic.page_url instead of traffic.page_url_from for per-page traffic, or a field whose description says "Do not use it ever"). This is the most dangerous case because nothing looks broken. Prevent it in Step 2: choose fields by reading each field's description, honor "do not use" notes, and never pick by name pattern alone. (This is why List → Get → Query are separate steps — see Decision flow.)
Cases 1 and 2 are caught after the query by validating the response; case 3 is prevented before the query by reading field descriptions when you choose fields.
Analytics questions are answered from the site's semantic models, discovered via List Semantic Models. This includes data that originates from a third party — e.g. Google Search / Search Console metrics (impressions, clicks, CTR, average position, top queries and pages) — which Wix ingests and often enriches with its own site data. So: do not ask the user whether a source is connected, and do not call the external provider (e.g. Google) directly — find the matching model and query it. If a query returns no data for the requested period, report that there's no data (the source may not be connected) rather than asking the user to connect first.
| HTTP | Code | Meaning |
|---|---|---|
| 4XX | fieldIsInvalid | A fields/filters[].field/sort.fieldName value doesn't exist in the model, and this query rejected it (some invalid fields are instead silently dropped from a 200 — see Handling wrong fields). When present, the error lists the model's available field names — pick the correct one and re-query. |
| 401 | NO_ACCOUNT_IDENTITY / UNAUTHENTICATED | Caller isn't authenticated; provide valid credentials. |
| 404 | SEMANTIC_MODEL_NOT_FOUND | The semanticModelId doesn't exist for this site. |
Error codes are self-explanatory strings (e.g. fieldIsInvalid) — read the code and the fields it returns rather than parsing a fixed body shape. Note: not every invalid field errors; some are silently omitted from a 200 response, so also validate the response (see Handling wrong fields).
Silent gap (no error): a requested field returns no data because none of its dependencies were included in the query — add a dependency field and re-query.
Get Semantic Model.properties.timeZone from Get Site Properties) in interval.timezone and set start/end to local-midnight-in-UTC for that zone (DST-aware) so results match the Wix dashboard. start/end are absolute instants, not wall-clock.dependencies in the query, or expect that field to be silently dropped.results[].fields. A wrong field either errors (4XX, e.g. fieldIsInvalid) or is silently dropped from a 200; a missing field means an unknown name or a missing dependency. Fix and re-query rather than trusting the partial result. Choose fields by reading their description (honoring "do not use" notes), never by name pattern.formattingEnabled: true for anything shown directly to a user; keep raw values for calculations.totalsIncluded: true to get period totals alongside a paged breakdown in a single call.fields minimal (projection) and paginate large result sets with offset.timeZone)Last updated: 30 July 2026