Filtering and sorting
Most list endpoints support a consistent set of filter and sort parameters. The available parameters for each specific endpoint are documented in the API reference.
Date range
Filter results to a date window using YYYY-MM-DD days:
?date_from=YYYY-MM-DD&date_to=YYYY-MM-DD- The window is exactly
date_fromthroughdate_to, inclusive, by UTC calendar day. On the analytics reads a check is dated by the UTC day its cycle started, so a check counts when its cycle started on one of those days and never on the day beforedate_from. A "last check" value describes the last check on or beforedate_to. - Both parameters are optional. When omitted, the window is the 30 days before today through today (UTC), the same check days as the app's 30-day view.
- A
date_tolater than today reads as today. Adate_fromafter today returns 422VALIDATION_ERROR. - Each date must be a
YYYY-MM-DDday. A datetime, or any other form, returns 422VALIDATION_ERROR. - Maximum window: 365 days. Requests with
date_to - date_from > 365 daysreturn 422DATE_RANGE_TOO_LARGE. Prompt detail,GET /prompts/{prompt_id}, allows up to 3660 days, so an earlydate_fromreturns a prompt's whole check history. - Inverted range (
date_from > date_to) returns 422VALIDATION_ERROR. - Every windowed endpoint honours both
date_fromanddate_to. That holds for thedashboard/*reads, thecompetitors/*analytics reads and their drill-downs,prompts/performance,prompts/summary,prompts/{prompt_id},urls/{url_id},gsc/*and the check reads. Trend axes (dashboard/llm-trendschecksand the overviewtrendsseries) list every day of the window,date_toincluded; a day with no answered check has anullrate. - The check reads (
checks,prompts/{prompt_id}/checks,checks/answersand thecheckson prompt detail) list a check when one of its engine answers was recorded on a day of the window, rather than by the day its cycle started. A check whose prompts ran across midnight UTC can therefore be in a check listing for a day and not in the analytics for it.
Several endpoints are not windowed at all and take neither date: the
prompts, tags and urls lists; GET /competitors, GET /brand-aliases
and the competitor tag reads, which return configuration rather than
measurements; and the topic reads and prompt-suggestions. A date sent to one
of them is dropped without being validated. competitors/{brand}/details and
dashboard/ugc-impact/{platform} also accept a deprecated trailing days,
which reads [today - days, today] and cannot be combined with either date.
What the response read: applied
Every windowed read, and the urls list, answers an applied object with the
window and the engines it actually read, after defaults and clamping:
"applied": {"date_from": "2026-03-22", "date_to": "2026-04-21", "engines": ["chatgpt", "google_aio", "perplexity"]}date_fromanddate_toare the days read. They arenullon theurlslist, which is not windowed, and onchecks/answersunderlatestorcheck_no.enginesare the public names of the engines the numbers cover: the requestedenginesthe project has enabled, or the project's enabled engines whenenginesis omitted. An engine dropped from your filter because the plan lacks it, or because a workspace Owner or Admin switched it off for the project, is absent here, so compare it with what you sent.enginesisnullon reads that are not split by AI engine:gsc/*,competitors/serp,competitors/bing-organic,competitors/trendswithtype=serportype=bing_organic, and the serp and bing-organic drill-downs.
Search Console
The gsc/* endpoints serve the Search Console data ziptie.ai has already
stored for the project, by UTC calendar day, under the same window rule and
default. When a property is connected ziptie.ai backfills at most the previous
30 days and then accumulates one day at a time, so history starts roughly 30
days before the property was connected. Requests for dates before that return
empty results rather than an error.
gsc/querieslists each day's top 1,000 queries by clicks, so a window can hold fewer rows than Google reports queries.gsc/trendsreturns Google's daily totals for the project's country, anonymized queries included, and those totals can be higher than the sum of the listed queries. A day whose total is not stored yet is summed from the stored queries instead.ctris a 0-1 fraction, andpositionis the impressions-weighted mean of Google's daily average position.clicks_deltaandimpressions_deltaon the query and page rows compare the window with the equal-length window just beforedate_from, and arenullwhen the row has no stored data there.gsc/trendsreturns one point per day that has stored data, oldest first; days without data are left out rather than zero-filled, and the last 2-3 days are usually missing because Google reports with a lag.- The data covers the country recorded on the project when the property was connected.
- A project with no connected property answers
gsc_connected: falsewith an emptydataarray. A connection whose Google token was revoked or expired answers 409GSC_REAUTH_REQUIRED: reconnect the property in the app.
Where engine and entity-ID filters apply
The engines, tag_ids, and url_ids filters are honoured only where the
data carries a per-engine, per-tag, or per-URL breakdown: the competitors/*
and dashboard/* analytics endpoints and the competitor drill-downs, the
prompt performance reads, the check reads and urls/{url_id}, plus engines
on the urls list and tag_ids on the prompts list. Coverage is not uniform
even within those groups - see the table below for the exact endpoint list per
filter. Note that competitors/* in that sentence means the analytics
endpoints under /competitors, not GET /competitors itself, which is
configuration and takes none of the three.
Every endpoint publishes, in the OpenAPI spec and in the MCP tool schemas,
only the filters it applies. A filter an endpoint does not apply is one of two
things. Where a caller sending it would reasonably expect narrower rows, it is
refused with 422 VALIDATION_ERROR, ahead of every other check. Anywhere
else it is not a parameter of the endpoint and is dropped when sent, like
any query parameter an endpoint does not declare, without being validated.
The prompts list refuses engines, because a prompt list has no per-engine
data; the per-engine scores live on prompts/performance. tags refuses
engines and tag_ids, and urls refuses tag_ids, because tags and URLs
are not filtered by tag IDs. GET /competitors, GET /brand-aliases,
GET /competitor-tags and GET /competitor-tags/{tag_id}/members refuse all
three, because neither the tracked-competitor roster, the brand-merge map nor
the competitor tags are scoped by engine, prompt tag or URL. GET /checks
refuses tag_ids and url_ids but honours engines: a check is recorded
against a prompt and is not scoped by tag or URL, while narrowing the engines
drops any check only the excluded engines produced, from data and total
alike. dashboard/ugc-impact/{platform}, competitors/{brand}/details,
prompts/performance and prompts/summary refuse url_ids on the same
footing - a cited source, a brand fact or a prompt's score is not scoped by a
URL you track. Use the free-text q filter on prompts, tags, urls,
competitors, brand-aliases, competitor-tags and
competitor-tags/{tag_id}/members instead.
The two organic-SERP surfaces, competitors/serp and competitors/bing-organic,
and their drill-downs drop engines and url_ids. Each reads results captured
alongside one engine's AI Overview checks - Google's and Bing's respectively -
so the rows have a single engine of origin by construction and there is nothing
for the filter to narrow. They are separate paths rather than one endpoint with
an engine parameter because they read different capture tables, and a consumer
comparing the two search engines wants both in one page load. gsc/*,
topic-clusters, topic-clusters/{cluster_id}/topics,
topics/{topic_id}/sources, prompt-suggestions and
prompts/{prompt_id}/ugc-evidence have no AI-engine axis at all and drop it
too.
The four AI drill-downs below are competitors/mentions/{brand}/prompts,
competitors/citations/{domain}/prompts, competitors/citations/{domain}/urls
and competitors/sources/prompts; the four organic ones are
competitors/serp/{domain}/prompts, competitors/serp/{domain}/urls,
competitors/bing-organic/{domain}/prompts and
competitors/bing-organic/{domain}/urls.
| Filter | Honoured by | Refused (422) by |
|---|---|---|
engines |
dashboard/*, checks, checks/answers, competitors/citations, competitors/mentions, competitors/sources, competitors/trends (except type=serp and type=bing_organic), competitors/{brand}/details, the four AI drill-downs, prompts/performance, prompts/summary, prompts/{id}/checks, urls, urls/{url_id} |
prompts, tags, competitors, brand-aliases, competitor-tags, competitor-tags/{tag_id}/members |
tag_ids |
prompts, prompts/performance, prompts/summary, checks/answers, competitors/citations, competitors/mentions, competitors/sources, competitors/serp, competitors/bing-organic, competitors/trends, competitors/{brand}/details, all eight drill-downs, dashboard/* |
tags, urls, competitors, brand-aliases, checks, competitor-tags, competitor-tags/{tag_id}/members |
url_ids |
competitors/citations, competitors/mentions, competitors/sources, the four AI drill-downs, dashboard/overview, dashboard/ugc-impact |
competitors, brand-aliases, checks, competitors/{brand}/details, dashboard/ugc-impact/{platform}, dashboard/ugc-attributes, dashboard/ugc-voices, prompts/performance, prompts/summary, competitor-tags, competitor-tags/{tag_id}/members |
q |
prompts, prompts/performance, tags, urls, competitors, brand-aliases, competitor-tags, competitor-tags/{tag_id}/members, dashboard/ugc-impact/{platform}, dashboard/ugc-attributes, dashboard/ugc-voices, gsc/queries, gsc/pages |
prompts/summary |
An endpoint listed in neither column drops the filter. The date window, sort
and paging follow the same rule:
| Parameter | Not a parameter of (dropped when sent) |
|---|---|
date_from, date_to |
prompts, tags, urls, competitors, brand-aliases, competitor-tags, competitor-tags/{tag_id}/members, topic-clusters, topic-clusters/{cluster_id}/topics, topics/{topic_id}/sources, prompt-suggestions, prompts/{id}/ugc-evidence |
sort |
dashboard/overview, dashboard/llm-trends, competitors/trends, competitors/{brand}/details, gsc/trends, prompts/summary, prompts/{id}, prompts/{id}/checks, urls/{url_id}, checks/answers |
limit, offset |
dashboard/overview, dashboard/ugc-impact, dashboard/llm-trends, competitors/trends, gsc/trends, prompts/summary, prompts/{id}, urls/{url_id} |
monitoring_status |
prompts/summary, which counts monitored and paused prompts alike |
dashboard/ugc-attributes and dashboard/ugc-voices order their groups by
how many sources carry the item, so they take no sort and refuse it with 422
VALIDATION_ERROR rather than drop it. prompts/{id}/ugc-evidence does the
same for its sources, which keep the order they were frozen in, and pages them
with limit and offset.
checks/answers takes limit and pages by cursor rather than offset, and
competitors/{brand}/details declares its own limit (default and maximum
100) and takes no sort or offset.
Engine filter
Filter analytics to specific AI engines using public engine names (see engine names):
?engines=chatgpt,google_aio,perplexity,google_ai_mode,copilot,bing_ai_overview,gemini- Omit to include all the engines the project has enabled: the workspace plan's engines minus any a workspace Owner or Admin switched off for the project.
- Comma-separated list with no spaces.
- An unrecognised engine name returns 400
INVALID_ENGINE_NAMEwith the invalid value echoed in the error message. - A filter naming only engines the workspace's subscription does not include
returns 400
ENGINE_NOT_ENABLED. A filter naming at least one included engine drops the others and covers the included ones;applied.engineslists the engines the response covers.
Example - competitor citations from ChatGPT and Google AI Overviews only:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/PROJECT_ID/competitors/citations?engines=chatgpt,google_aio"Tag filter
Filter prompts, prompt performance and the analytics by prompt tag IDs:
?tag_ids=123,456,789- Comma-separated integer IDs.
prompts,prompts/performanceandprompts/summarytake at most 100. - A row matches when its prompt carries any of the listed tags.
- IDs must belong to the current project. IDs from other projects are silently excluded on reads (prevents cross-project enumeration), so a list of only foreign ids matches nothing and answers an empty page.
- An empty value (
tag_ids=) returns 422VALIDATION_ERROR. Omit the parameter entirely to mean "no tag filter". - Non-integer values return 422
VALIDATION_ERROR.
Competitor tag filter
competitor_tag_ids names competitor tags; tag_ids names prompt tags, the
labels on prompts, and narrows analytics to the answers to those prompts. The
two never stand in for each other. GET /competitors takes
competitor_tag_ids and returns the tracked competitors carrying at least one
of the listed tags, while still refusing tag_ids. The five competitor
rankings (competitors/citations, competitors/mentions,
competitors/sources, competitors/serp and competitors/bing-organic) take
it too, and keep your own rows plus the rows carrying at least one of the
listed tags.
?competitor_tag_ids=42,43- Comma-separated integer IDs.
- An empty value (
competitor_tag_ids=) returns 422VALIDATION_ERROR. Omit the parameter entirely to mean "no competitor tag filter". - Non-integer values, and values outside 1 to 2147483647, return 422
VALIDATION_ERROR. - At most 100 ids.
Competitor marks: filter and aggregate
The competitor rankings apply the marks set on the app's Competitors page, so the rows match what the app shows for the same window and filters:
filter=all(the default) returns every row except the ones hidden in the app,filter=competitorskeeps the rows marked as competitors, andfilter=hiddenkeeps only the hidden rows. Your own domains and brands are kept in every mode and carryis_user: true.competitor_tag_idsis applied afterfilter.- Each row carries
is_competitor,is_hiddenand its competitortags.rankis the app's # column: the position over every ranked row, hidden ones included, so filtering never renumbers the others and a filtered page can skip numbers. filterandcompetitor_tag_idsapply togroup_by=domainoncompetitors/citationsand togroup_by=brandoncompetitors/mentions; with another grouping they return 422VALIDATION_ERROR.aggregate=average, oncompetitors/citations,competitors/mentionsandcompetitors/trends, applies togroup_by=tagonly. It divides each tag row's (or series') counts, shares, deltas and per-check values by itsmember_countand re-ranks the rows, as the app's Average toggle does, socount,last_check_countandshopping_countcan be fractions.aggregate=total(the default) keeps the tag totals.
When the marks cannot be read, a ranking answers 503 SERVICE_UNAVAILABLE
rather than show hidden rows.
Prompt filters
prompts/performance and prompts/summary take the filters the app's Prompts
page offers. Each is optional, and different filters are combined with AND, so
a prompt is kept only when it passes every filter sent. A filter that reads a
check reads the prompt's latest check in the window (on or before date_to).
| Parameter | Value | Keeps the prompts |
|---|---|---|
signal_filters |
Comma-separated rules [+|-]<engine>:<signal> |
that satisfy every rule; see Signal filters below |
score_min, score_max |
Integers 0-100 | whose AI Score (success_score) is at least score_min or at most score_max points |
score_change |
down:N or up:N (N 1-100), within:N (N 0-100) |
whose success_score_delta fell by at least N points, rose by at least N, or moved by at most N either way |
changes |
Comma-separated tokens <kind>:<engine> |
that match every token; see Changes below |
intents |
Comma-separated: informational, commercial, transactional, navigational |
classified with any of the intents |
funnel_stages |
Comma-separated: awareness, consideration, decision |
classified with any of the stages |
leader_engine |
A public engine name, or any |
whose favored_brand on that engine (with any, on any engine in applied.engines) is a tracked competitor, never one of your own brands |
leader_brands |
A JSON array of competitor names | whose leading competitor under leader_engine is one of these names; needs leader_engine |
organic |
google:top:N, google:between:A:B, google:unranked, or the same with bing |
by your organic search position: 1 to N, A to B, or not in the results |
?score_max=50&changes=lost_citation:any&intents=commercial,transactionalThat keeps the commercial and transactional prompts scoring at most 50 that lost a citation on any engine over the window.
Nulls never match. A prompt with a null success_score matches neither
score_min nor score_max, and one with a null success_score_delta (fewer
than two measured points in the window) never matches score_change.
success_score_delta is the last measured point of the prompt's score trend
minus the first in the window, in points.
Grammar and ranges.
score_minandscore_maxare whole numbers;score_minmust not be abovescore_max.- In
organic, N, A and B run from 1 to 100, with A at most B.googlereads Google's results and needsgoogle_aioenabled for the project;bingreads Bing's and needsbing_ai_overview. leader_brandstakes at most 20 names, each 1-200 characters and not blank, matched the way the app's Competitors page groups the spellings of a brand. URL-encode the JSON, for exampleleader_brands=%5B%22Acme%22%2C%22Globex%22%5Dfor["Acme","Globex"].- A parameter sent with an empty value, or a comma-separated list with an empty
item (such as a trailing comma), returns 422
VALIDATION_ERROR. Omit the parameter to apply no filter.
Signal filters
signal_filters is the app's advanced filter: comma-separated rules
[+|-]<engine>:<signal>, all of which a prompt must satisfy. <engine> is a
public engine name and <signal> one of cited, mentioned,
sentiment_positive, sentiment_mixed or sentiment_negative, read at the
prompt's latest check in the window. A leading - keeps the prompts the rule
does not match, and a leading + (or none) the prompts it matches.
?signal_filters=chatgpt:mentioned,-chatgpt:citedThat keeps the prompts ChatGPT mentioned the brand for without citing it.
- A rule on an engine whose check failed matches neither way, so the prompt is
left out under both
chatgpt:citedand-chatgpt:cited. - At most 35 rules and 1000 characters. An empty rule (such as a trailing
comma), an unknown signal or a malformed rule returns 422
VALIDATION_ERROR.
Changes
changes finds the prompts that lost or gained a mention or a citation over
the window. Each token is <kind>:<engine>, where <kind> is one of
lost_mention, lost_citation, gained_mention or gained_citation, and
<engine> is a public engine name or any for any engine in
applied.engines.
- A mention is lost when the engine's latest answered check in the window does not mention your brand and the answered check just before it did; it is gained the other way round. A citation follows the same rule on your domain being cited. A failed check is skipped.
- A prompt must match every token. At most 28 tokens; an unknown kind or a
token that is not
<kind>:<engine>returns 422VALIDATION_ERROR.
Engines in a filter
signal_filters, changes and leader_engine name engines with the same
public names as engines, and any in changes and leader_engine covers
the engines in applied.engines.
- With
enginesset, an engine a rule, a token orleader_enginenames must be one of those engines, or the request returns 422VALIDATION_ERROR: a filter on an engine the read does not cover could never match. - An engine that does not exist returns 400
INVALID_ENGINE_NAME. - An engine the project has not enabled (the plan lacks it, or it was switched
off for the project) returns 400
ENGINE_NOT_ENABLED, and so doesorganicongooglewithoutgoogle_aioenabled, or onbingwithoutbing_ai_overview. The message names the engine. Retrying will not help; drop the filter or the engine.
URL filter
Filter competitor and dashboard analytics by URL IDs:
?url_ids=42,43Same validation rules as tag_ids.
Text search
Free-text search on supported endpoints:
?q=search+termSearches prompt text, tag names, or URL values depending on the endpoint.
Case-insensitive partial match, with the term matched literally. An empty
value (q=) returns 422 VALIDATION_ERROR; omit the parameter entirely to
mean "no filter".
Sorting
Control sort order with the sort parameter:
?sort=<field>:<asc|desc>- One field at a time (multi-sort is not supported).
- The field must be in the endpoint's whitelist. Non-whitelisted fields
return 422
VALIDATION_ERRORwith the allowed list echoed. The one exception is the prompt check list, which always returns checks newest first and ignoressortentirely. - Both the field and the direction are required. The direction must be
ascordesc; omitting it (for examplesort=text) returns 422VALIDATION_ERROR.
Example - prompts sorted by text descending:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/PROJECT_ID/prompts?sort=text:desc"Common sortable fields by endpoint:
| Endpoint | Default sort | Available fields |
|---|---|---|
| Projects | name:asc |
name, created_at |
| Prompts | last_check_date:desc |
text, last_check_date, times_checked |
| Prompt performance | success_score:desc |
success_score, success_score_delta, last_check_date, text |
| Tags | name:asc |
name, prompt_count |
| URLs | citation_rate:desc |
url, prompt_count, citation_rate, last_cited |
| Competitor rankings | last_check_share:desc |
count, share, rank, total_rank, last_check_count, last_check_share, share_delta, last_check_share_delta, count_delta, rank_delta |
| Prompt checks | check_no:desc (fixed) |
not sortable; sort is not a parameter and is dropped when sent |
| GSC queries | clicks:desc |
clicks, impressions, ctr, position |
| GSC pages | clicks:desc |
clicks, impressions, ctr, position |
How nulls and ties sort:
- On the
prompts,tagsandurlslists, ties are broken byid, ascending, andtextandnamecompare case-insensitively. A nulllast_check_date(a prompt never checked),citation_rateorlast_citedsorts lowest: first ascending, last descending.times_checkedcounts a never-checked prompt as 0. The app's URLs table puts nulls last in both directions instead. - On
prompts/performance, anullsuccess_scoreorlast_check_datesorts last in both directions, anullsuccess_score_deltasorts as 0, the order the app shows, andtextcompares the text as stored, case-sensitively. - On the competitor rankings and drill-downs the sort is stable, so rows that tie keep the order the ranking produced them in.
Combining parameters
Filter and sort parameters that an endpoint accepts combine freely. Example - competitor citations tagged with tags 10 and 20, on the chatgpt engine, over a date window, sorted by count:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/PROJECT_ID/competitors/citations?tag_ids=10,20&engines=chatgpt&date_from=2026-06-10&date_to=2026-06-17&sort=count:desc&limit=50&offset=0"Composing filters
The filter parameters compose as logical AND: a request that sets
q=running&monitoring_status=true returns only the prompts that match both
criteria. Within one list-valued filter the ids are OR: tag_ids=10,20 keeps
the prompts carrying tag 10 or tag 20. There is no OR operator across filters;
if you need OR semantics, issue separate requests and merge client-side.
Remember that each endpoint only accepts the filters that apply to its data
(see the table above) - combining engines with a prompts request returns
422 rather than an empty intersection.
Example - prompts that contain "running" and are actively monitored, sorted by text:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/PROJECT_ID/prompts?q=running&monitoring_status=true&sort=text:asc"On the prompts, tags and urls lists, monitoring_status, q,
tag_ids, engines, the sort and the page are all applied in the database,
over the whole project, so total counts every row under the filters and every
row is reachable through offset.
Sort whitelist per endpoint
Each endpoint accepts a fixed set of sort keys. Send sort=field:asc or
sort=field:desc; both the field and the direction are required. Any
non-whitelisted field returns 422 VALIDATION_ERROR with the allowed keys
echoed in the response. The competitor ranking keys below are the ones listed
under "Competitor rankings" above.
| Endpoint | Default | Sort keys |
|---|---|---|
| GET /v1/projects | name:asc | name, created_at |
| GET /v1/projects/{id}/prompts | last_check_date:desc | text, last_check_date, times_checked |
| GET /v1/projects/{id}/prompts/performance | success_score:desc | success_score, success_score_delta, last_check_date, text |
| GET /v1/projects/{id}/checks | timestamp:desc | timestamp, prompt_id |
| GET /v1/projects/{id}/checks/answers | (prompt_id, check_no) (fixed) | not sortable |
| GET /v1/projects/{id}/prompts/{id}/checks | check_no:desc (fixed) | not sortable; sort is dropped when sent |
| GET /v1/projects/{id}/tags | name:asc | name, prompt_count |
| GET /v1/projects/{id}/urls | citation_rate:desc | url, prompt_count, citation_rate, last_cited |
| GET /v1/projects/{id}/brand-aliases | canonical_name:asc | canonical_name, created_at |
| GET /v1/projects/{id}/competitors | name:asc | name, type |
| GET /v1/projects/{id}/competitor-tags | name:asc | name, domain_count, brand_count |
| GET /v1/projects/{id}/competitor-tags/{id}/members | name:asc | name, type |
| GET /v1/projects/{id}/competitors/citations | last_check_share:desc | the ranking keys |
| GET /v1/projects/{id}/competitors/mentions | last_check_share:desc | the ranking keys |
| GET /v1/projects/{id}/competitors/sources | last_check_share:desc | the ranking keys |
| GET /v1/projects/{id}/competitors/serp | last_check_share:desc | the ranking keys, top3_share, top3_share_delta |
| GET /v1/projects/{id}/competitors/bing-organic | last_check_share:desc | the ranking keys, top3_share, top3_share_delta |
| GET /v1/projects/{id}/competitors/mentions/{brand}/prompts | mention_count:desc | mention_count, last_mentioned_at, text |
| GET /v1/projects/{id}/competitors/citations/{domain}/prompts | citation_count:desc | citation_count, last_cited_at, text |
| GET /v1/projects/{id}/competitors/citations/{domain}/urls | citation_count:desc | citation_count, last_cited_at, url |
| GET /v1/projects/{id}/competitors/sources/prompts | citation_count:desc | citation_count, last_cited_at, text |
| GET /v1/projects/{id}/competitors/serp/{domain}/prompts | rank_count:desc | rank_count, best_position, last_ranked_at, text |
| GET /v1/projects/{id}/competitors/serp/{domain}/urls | rank_count:desc | rank_count, avg_position, last_ranked_at, url |
| GET /v1/projects/{id}/competitors/bing-organic/{domain}/prompts | rank_count:desc | rank_count, best_position, last_ranked_at, text |
| GET /v1/projects/{id}/competitors/bing-organic/{domain}/urls | rank_count:desc | rank_count, avg_position, last_ranked_at, url |
| GET /v1/projects/{id}/dashboard/ugc-impact | citations:desc | citations, share, platform, trend_delta |
| GET /v1/projects/{id}/dashboard/ugc-impact/{platform} | source_url:asc | source_url, is_brand_mentioned, general_sentiment, publication_date |
| GET /v1/projects/{id}/gsc/queries | clicks:desc | clicks, impressions, ctr, position |
| GET /v1/projects/{id}/gsc/pages | clicks:desc | clicks, impressions, ctr, position |
| GET /v1/projects/{id}/topic-clusters | occurrence_count:desc | occurrence_count, size, last_seen_check_no |
| GET /v1/projects/{id}/topic-clusters/{id}/topics | occurrence_count:desc | occurrence_count |
| GET /v1/projects/{id}/topics/{id}/sources | created_at:desc | created_at, last_seen_at, use_count |
| GET /v1/projects/{id}/prompt-suggestions | created_at:desc | created_at |
The rankings default to the app's own order, last_check_share:desc. Send
sort=count:desc to page a ranking by volume, or sort=rank:asc for the
order of the app's # column.
Four rows in that table are gated. topic-clusters,
topic-clusters/{cluster_id}/topics, topics/{topic_id}/sources, and
prompt-suggestions belong to the UGC Impact module and are served only when
the workspace subscription has UGC Impact Analysis switched on. Without it they
return 404 NOT_FOUND, which is the same response as an unknown project,
cluster, or topic id, so an integration that works against one workspace can
404 against another with nothing in the response to tell the two cases apart.
Check the workspace's subscription before assuming the ids are wrong.
dashboard/ugc-impact and dashboard/ugc-impact/{platform} are not gated and
are available on every plan.
On dashboard/ugc-impact/{platform}, a row whose sorted field is null sorts
last in both directions rather than flipping to the front on desc, and
source_url ascending is always the secondary key, so equal-ranked rows keep a
stable order across requests. That matters when paging: general_sentiment and
publication_date are frequently null there, and a null block that moved
between pages would show you the same row twice.
Example - GSC queries sorted by CTR descending:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/PROJECT_ID/gsc/queries?sort=ctr:desc"Paging through a filtered list
Filters and limit/offset combine. The total in the response reflects the
count after filtering, so use it to determine how many pages exist. Walk
through all pages by incrementing offset by limit until offset >= total:
import requests
def fetch_all_filtered(project_id, token, filters):
"""
Fetch all prompts matching filters, paging through all results.
filters = {"q": "running", "monitoring_status": "true"}
"""
base_url = f"https://api.ziptie.ai/v1/projects/{project_id}/prompts"
headers = {"Authorization": f"Bearer {token}"}
limit = 100
offset = 0
while True:
params = {**filters, "limit": limit, "offset": offset}
resp = requests.get(base_url, headers=headers, params=params)
resp.raise_for_status()
page = resp.json()
yield from page["data"]
offset += limit
if offset >= page["total"]:
break
# Usage
all_prompts = list(fetch_all_filtered(
"abc123",
"zt_live_<token>",
{"q": "running", "monitoring_status": "true"}
))
print(f"Found {len(all_prompts)} prompts matching filters")