← API reference

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

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"]}

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.

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

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

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

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:

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,transactional

That 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.

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:cited

That keeps the prompts ChatGPT mentioned the brand for without citing it.

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.

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.

URL filter

Filter competitor and dashboard analytics by URL IDs:

?url_ids=42,43

Same validation rules as tag_ids.

Text search

Free-text search on supported endpoints:

?q=search+term

Searches 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>

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:

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")