Working with prompts
Prompts are the queries your project monitors against AI engines. This guide walks through the full lifecycle: creating, reading, updating, inspecting checks, and deleting. Every request requires an Authorization: Bearer zt_live_... header. Listings and check reads need read or higher, creates and updates need write or higher, and deletes need admin.
Creating a prompt
Use POST /v1/projects/{project_id}/prompts to create a new prompt:
curl -X POST https://api.ziptie.ai/v1/projects/abc123/prompts \
-H "Authorization: Bearer zt_live_<token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"text": "best running shoes for flat feet",
"monitoring_status": true,
"tag_ids": [8, 12],
"url_ids": [42]
}'The request body accepts:
text(string, required) - the prompt to monitor. 1 to 500 characters; anything outside that returns 422VALIDATION_ERROR.monitoring_status(boolean, optional, defaulttrue) - whether to actively monitor this prompt.tag_ids(array of integers, optional) - tag IDs to associate with the prompt (must belong to this project).url_ids(array of integers, optional) - URL IDs for citation tracking (must belong to this project).
On success, the response is 201 with the new prompt object. On error:
- 409
PROMPT_ALREADY_EXISTS- the project already holds this prompt, and nothing was created or billed. The comparison is not on the exact text: leading and trailing whitespace is removed, every run of internal whitespace is collapsed to one space, and the result is lower-cased, soAcme CRMandacme crmare one prompt. It is applied within the project only, so the same text in another project is a separate prompt. The response includes anexisting_idfield so you can reuse or update the prompt it matched. Naming it takes a second read over the whole project, which is not retried, so the field isnullwhen that read fails; the 409 still means the text matched. The matched prompt keeps the text it was created with, so posting a different capitalisation does not rename it. - 402
PROMPT_LIMIT_REACHED- the workspace has hit its prompt cap for the cadence this project runs on. The cap is not one daily number: the plan buys a separate allowance per cadence, and the bucket a create spends from is chosen by the project's owncheck_frequency_days(read it fromGET /v1/projects/{project_id}), so free daily slots do nothing for a weekly project. Within a cadence the allowance is counted across every project in the workspace on it, so prompts in your other projects consume it too. Only monitored prompts hold a slot: creating withmonitoring_status: falsecosts nothing, is never measured against the cap, and so never returns this code even in a workspace with no slots left, and a workspace may hold as many paused prompts as it likes. Pausing a monitored prompt returns its slot, and so does deleting one. See "Updating a prompt" below. - 422
VALIDATION_ERROR- a required field is missing or a tag/URL ID doesn't belong to this project.
Send an Idempotency-Key header to make the request safe to retry. See idempotency for details.
Creating prompts in bulk
Use POST /v1/projects/{project_id}/prompts/bulk to load many prompts at once.
One request takes 1 to 500 texts, counts as one request in each rate-limit
bucket and queues the new prompts' first checks together, so a 10,000-prompt load
is 20 requests rather than 10,000.
curl -X POST https://api.ziptie.ai/v1/projects/abc123/prompts/bulk \
-H "Authorization: Bearer zt_live_<token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001" \
-d '{
"prompts": ["best running shoes for flat feet", "which running shoes last longest"],
"monitoring_status": true,
"tag_ids": [8],
"url_ids": [42]
}'prompts(array of strings, required) - 1 to 500 texts, each 1 to 500 characters and not blank.monitoring_status,tag_idsandurl_ids- as on the single create, and applied to every prompt in the request. To give prompts different tags, URLs or monitoring states, send one request per group.
The reply is {"created": [...], "skipped": [...]}. created holds the prompts in
the order you sent them, shaped as the single create returns one. skipped lists
each text that created nothing: the project already holds the prompt (compared
with case and spacing ignored, as on the single create), the text repeats an
earlier one in the request, or nothing is left of it once search operators are
removed. Each entry has the zero-based index of the text in your prompts list,
the submitted text, and existing_id, the identifier of the prompt the project
already tracks under that text (null for a repeat within the request and for a
text with nothing left). Skipped texts cost nothing. A request where every text is
skipped still answers 201 with an empty created, so branch on the two lists
rather than on the status code.
A text whose search operators are removed (best shoes site:nike.com) is created
under the spelling that is stored (best shoes nike.com) and appears once, in
created; it is never also listed in skipped.
If a request fails with a 5xx or times out, the prompts may still have been
created, and nothing is stored under the Idempotency-Key. Send the same texts
again: each one already created is skipped with its existing_id, so the retry
returns the identifiers.
An invalid request is refused whole with 422 VALIDATION_ERROR and creates
nothing: an empty list, more than 500 texts, a blank text, a text over 500
characters, an unknown field, or a tag or URL ID from another project. A refusal
for the plan allowance (402) or the subscription (403) is whole too.
The monitored-prompt allowance is measured for every distinct text you send,
before the project's existing prompts are compared, so a monitored request can
return 402 even where some texts are already tracked. Send fewer texts, or create
them paused with monitoring_status: false, which is never measured. The bulk
create accepts Idempotency-Key on the same terms as the single create.
Listing prompts
Use GET /v1/projects/{project_id}/prompts to list all prompts in a project:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/abc123/prompts?limit=50&offset=0&monitoring_status=true&q=running&sort=text:asc"Supported query parameters:
limit,offset- pagination (see pagination).monitoring_status- filter to active (true) or paused (false) prompts.q- free-text search on prompt text: a case-insensitive substring, matched literally.tag_ids- keep the prompts carrying any of the listed tags (at most 100 ids). An id the project does not hold matches nothing, so it answers an empty page rather than an error.sort- sort field and direction infield:asc/field:descform; one oftext,last_check_date,times_checked(see filtering-and-sorting). Ties are broken byid, ascending.
Every filter, the sort and the page are applied over the whole project, so
every prompt is reachable through offset and total counts the prompts under
every filter.
The prompts endpoint does not accept engines - passing it returns 422
VALIDATION_ERROR, because the prompt list carries no per-engine data. The
per-engine scores live on GET .../prompts/performance, described below, and
engines is also honoured on the prompt check endpoints and on the analytics
endpoints that carry per-engine data; see engine names
for the exact scope.
The response is a paginated list where each prompt includes id, text, monitoring_status, monitoring_frequency, times_checked, last_check_date, tags, intent, and funnel_stage.
Reading a single prompt
Use GET /v1/projects/{project_id}/prompts/{prompt_id} for one prompt with the
associations and the recent score series the list does not carry:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/abc123/prompts/12345?date_from=2026-03-20&date_to=2026-04-19"date_from and date_to pick the checks the response lists. They default to
30 days before today through today (UTC) and may span up to 3660 days, so an
early date_from returns the prompt's whole check history, as the app's prompt
detail shows it; a longer span returns 422 DATE_RANGE_TOO_LARGE. A check is
in the window when one of its engine answers was recorded on a day of it, as on
the check listings below. applied echoes the window read and the engines the
scores cover.
On top of id, text, monitoring_status, monitoring_frequency,
last_check_date, intent, and funnel_stage, the response adds:
tags- objects withid_tag,name, andcolor, rather than the plain tag names the list returns. This is where you get the IDs to send back in aPATCH.urls- objects withid_lite_urlandurl.checks- one{check_no, timestamp, success_score, answered_checks, failed_checks}entry per check in the window, oldest first. Note the opposite ordering to the check list endpoint below, which is newest first. The score and the two counts follow the rules in "Inspecting check history" below.
times_checked is not on this response; read it from the list endpoint.
monitoring_frequency is the cadence in days the prompt is checked on, and it
belongs to the project rather than to the prompt: a prompt created over this
API is stamped with the project's check_frequency_days (GET /v1/projects/{project_id}) when monitoring_status is true, and left
null when it is false. The field is not settable from the API; change the
cadence by changing the project. Where a prompt's stamp and its project's
check_frequency_days ever disagree, the project's value is the one the
checks run on, so read cadence from the project when it matters.
A prompt_id that belongs to a different project returns 404 NOT_FOUND
rather than 403, so prompt IDs cannot be probed across projects.
Prompt performance and the summary card
GET /v1/projects/{project_id}/prompts/performance returns the prompts as the
app's Prompts table shows them, and GET /v1/projects/{project_id}/prompts/summary
returns the summary card above that table. Both take the same window,
engines, tag_ids and Prompts filters. The table also takes
monitoring_status and q, which the card does not (see "The summary card"
below), so with either of those set the card covers more prompts than the
table lists. Use them to rank prompts by how well the brand does in AI answers,
to find the prompts that declined, lost a citation or that no engine cites, or
to put the Prompts table into your own reporting. The MCP tools are
list_prompt_performance and get_prompts_summary.
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/abc123/prompts/performance?date_from=2026-03-22&date_to=2026-04-21&changes=lost_citation:any&score_max=50&sort=success_score:asc"The window. Exactly date_from through date_to, inclusive, defaulting to
the 30 days before today (UTC) through today, the app's 30-day view. A check is
dated by the UTC day its cycle started. The rows are the monitored prompts with
a check in the window plus every paused prompt. With a date_to before today,
each row reads the prompt's latest check on or before date_to, as the app
does for a past range. applied echoes the window and the engines the scores
cover.
Scores are percentages on a 0-100 scale, at one decimal, unlike the 0-1 per-check score on the check endpoints below. On each row:
success_score- the prompt's AI Score at its latest check in the window: the mean over the engines inapplied.enginesthat answered that check. A failed engine is left out rather than counted as 0, and the score isnullwhen no engine answered.success_score_delta- the last measured point of the trend minus the first in the window, in points;nullwith fewer than two measured points.success_score_trend- one value per entry of the response'scheck_dates,nullon a day the prompt has no check or no engine scored.engines- one entry per engine inapplied.engines, keyed by public name:cited,mentioned,sentiment,favored_brand,failedandresponse_statusat that check.failedistruewhen the engine's run failed (its answer fields are thennull), and the entry is allnullwhen the engine has no row at that check.
Filters, all applied before the page is cut, so total counts the prompts
under every filter:
enginesnarrows the scores, theenginesblock, and the enginesanycovers inchangesandleader_engine.tag_idskeeps the prompts carrying any of the tags. Each row'stagsis the prompt's full tag list either way.monitoring_statuskeeps monitored (true) or paused (false) prompts.qkeeps the prompts whose text contains it, case-insensitively.- The Prompts filters, combined with AND:
signal_filters(cited, mentioned or sentiment per engine),score_minandscore_max(the AI Score range),score_change(down:N,up:Norwithin:Nonsuccess_score_delta),changes(a mention or citation lost or gained over the window),intents,funnel_stages,leader_enginewithleader_brands(a tracked competitor is the brand an engine favoured) andorganic(your organic search position). Their grammars, ranges and errors are in filtering and sorting.
sort takes success_score (the default, success_score:desc),
success_score_delta, last_check_date or text. A null success_score
or last_check_date sorts last in both directions. A null
success_score_delta sorts as 0, the order the app shows. text compares the
text as stored, case-sensitively.
The summary card takes the same date_from, date_to, engines,
tag_ids and Prompts filters as the table. It aggregates every prompt those
filters keep, so it takes no sort or paging (both are ignored), and q is
refused with 422 VALIDATION_ERROR. It takes no monitoring_status either:
the card counts monitored and paused prompts alike, as the app's card does,
and a monitoring_status sent to it is dropped without being validated. To
compare the card with a table read, read the table without q and without
monitoring_status.
total_count,active_countandpaused_countcount the prompts. Paused prompts are counted but do not enter the average.average_success_score_trendis the mean AI Score of the active prompts measured each day ofcheck_dates,nullon a day none was.average_success_scoreis its latest measured point, andaverage_success_score_deltathe last measured point minus the first in the window, in points.failed_checkscounts the failed engine checks of the active prompts on the dayaverage_success_scoreshows.newer_failed_checksandnewer_failed_atreport failures on later days on which none of those prompts scored, so a score that looks current can say that it is not.best_promptis the active prompt with the highest latest score.worst_promptis the one with the lowest, and is set only when that score is below 50 and the prompt is notbest_prompt, as in the app.
Neither endpoint takes url_ids; sending it returns 422 VALIDATION_ERROR,
because a prompt's score is not scoped by URL.
Intent and funnel stage
Every prompt on the list, detail, and update responses carries two classification fields:
| Field | Values |
|---|---|
intent |
informational, commercial, transactional, navigational |
funnel_stage |
awareness, consideration, decision |
Both are assigned automatically shortly after a prompt is added, and a member
with edit access can change them in the app. Either is null while the prompt
is unclassified, so a prompt you created a moment ago reads null at first:
read it again later rather than treating null as a category of its own. The
two are set independently, so one can be null while the other has a value.
Neither field can be set through the API, and the prompt list does not filter
on them. prompts/performance and prompts/summary do: intents and
funnel_stages keep the prompts classified with any of the listed values, and
an unclassified prompt matches neither. On the prompt list, fetch the prompts
and filter them in your own code.
Updating a prompt
Use PATCH /v1/projects/{project_id}/prompts/{prompt_id} to change a prompt in
place. It needs write scope.
curl -X PATCH https://api.ziptie.ai/v1/projects/abc123/prompts/12345 \
-H "Authorization: Bearer zt_live_<token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-d '{"monitoring_status": false, "tag_ids": [8]}'The body accepts exactly three fields, and at least one of them must be present
or the request returns 422 VALIDATION_ERROR:
monitoring_status(boolean) - pause (false) or resume (true) monitoring.tag_ids(array of integers) - replaces the prompt's whole tag set. Send[]to clear every tag; omit the field to leave the associations alone.url_ids(array of integers) - replaces the prompt's whole URL set, on the same terms.
Any other key in the body is rejected with 422 VALIDATION_ERROR. text is
not updatable, so rewording a prompt means creating the new one and deleting
the old one, and the new prompt starts with an empty check history. Tag and URL
IDs must belong to this project; if one of them does not, the whole update is
rejected and the prompt is left untouched.
Pausing frees capacity. A prompt holds one slot of the workspace allowance for
its project's cadence only while monitoring_status is true, so PATCH with
monitoring_status: false both stops the checks and returns the slot. If the
goal is room for new prompts, pausing is enough and the check history is kept;
DELETE returns the same one slot and discards the history with it. Resuming
with monitoring_status: true claims a slot again and returns 402
INSUFFICIENT_CREDITS when none is free.
A prompt created with monitoring_status: false never took a slot, so pausing
it is a no-op and deleting it returns nothing. There is no limit on how many
paused prompts a workspace may hold, which makes them a safe place to keep
prompts you are not monitoring yet.
PATCH accepts Idempotency-Key on the same terms as POST. See
idempotency.
Inspecting check history
Use GET /v1/projects/{project_id}/prompts/{prompt_id}/checks to retrieve the full check history for a prompt:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks?date_from=2026-04-10&date_to=2026-04-19&limit=100"Query parameters:
date_from,date_to-YYYY-MM-DDUTC days, inclusive (default: 30 days before today through today). A check is listed when one of its engine answers was recorded on a day of the window, andappliedechoes the window and the engines read.limit,offset- pagination (max 100 per page).engines- comma-separated public engine names. A check that none of the listed engines produced is dropped from both the page andtotal.
Checks are always returned newest first, highest check_no first. A sort
parameter is accepted here but has no effect on the ordering.
Each check in the response includes:
check_no- the check number. Since June 2026 a check number is one project check cycle, shared by every prompt answered in that check, so a prompt's numbers start at the first cycle it joined and can have gaps. Before June 2026 each prompt numbered its own checks 1, 2, 3.timestamp- ISO 8601 UTC timestamp of when the check was run.success_score- the cross-engine score for the check,0to1, ornullwhen no engine answered it.answered_checksandfailed_checks- how many of the engines thatsuccess_scoreaverages answered this check, and how many failed it.engines- object keyed by public engine name, each{cited, mentioned, failed}.
An engine appears in engines only for checks where it actually ran, so the
key set can differ between checks on the same prompt.
success_score averages every engine that answered the check: ChatGPT,
Perplexity, Google AI Mode, Microsoft Copilot, Google AI Overviews, Bing AI
Overview and Google Gemini. A Google AI Overviews or Bing AI Overview run
where the search showed no overview did not answer, so it is left out of the
average and of both answered_checks and failed_checks.
Failed checks. An engine whose run of the check failed returned no
answer. It appears in engines with cited and mentioned both null, and
failed is true. It is left out of success_score rather than scored as
a miss. When no engine answered the check, because every run failed or
showed no overview, success_score is null and answered_checks is 0:
there is nothing to measure, so skip the check when you plot or average
scores. The same
rule applies to the checks on prompt detail, to the project-wide
GET /v1/projects/{project_id}/checks and to
GET /v1/projects/{project_id}/checks/answers.
Getting the latest check
Use GET /v1/projects/{project_id}/prompts/{prompt_id}/checks/latest to fetch only the most recent check without pagination:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks/latest"This returns a single check object (not paginated). Use this endpoint when you want the current status of a prompt without the overhead of listing history.
A prompt that no check has answered yet, such as one created moments ago,
answers 404 NOT_FOUND with the message "No checks yet for this prompt".
Treat that 404 as "not yet" when you poll for a first result.
The response has the same shape as a single check, described in "Getting one
check in full" below: check_no, timestamp, user_brand,
competitor_brands, and per engine the answer text, the brand and citation
flags, the sentiment, the named brands and the cited sources, and failed.
One difference: an engine with no answer for this check is omitted rather than
null, so every engine present carries a result, a failed one included.
The response also carries four deprecated fields, kept so existing clients do
not lose a key: success_score and per-engine cited / mentioned, which
are always null here, and a top-level bing_ai_overview object that
duplicates engines.bing_ai_overview. Do not build on them. Read
success_score, cited and mentioned from the check list, whose first row
is the latest check (per engine, citations and brand_mention in this
response carry the same answers as cited and mentioned), and read the Bing
AI Overview answer from engines.bing_ai_overview.
Getting one check in full
Use GET /v1/projects/{project_id}/prompts/{prompt_id}/checks/{check_no} to
open a single check by its check number. It returns what the engines actually
said for one prompt, so it is where a per-prompt drill-down gets its detail. To
read the answers of many prompts at once, see "Reading answers in bulk" below:
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks/7"Alongside check_no and timestamp, the response carries user_brand,
competitor_brands, and an engines object keyed by public engine name. Each
engine that ran returns:
response_text- the answer the engine produced.brand_mention,citations,sentiment,favored_brand, andresponse_length.sources- the citations that engine used.brands- one entry per brand detected in the answer, each withbrand,sentiment,order_of_mention,prominence, anddetails, a list of{detail, sentiment}facts extracted about that brand. Only this endpoint,checks/latestandchecks/answersexpose these per-brand facts.failed-truewhen the engine's run of this check failed. Such an engine returned no answer:brand_mention,citationsandsentimentarenull, anderror_messagesays why it failed.failedisfalsefor every other engine.
An engine with no answer for this check is null rather than absent, so you
can tell "not checked" from "checked and found nothing". There is no
success_score on this response; take it from the check list or from prompt
detail.
Reading answers in bulk
Use GET /v1/projects/{project_id}/checks/answers to read the same evidence for
many prompts at once: one item per (prompt, check) pair, each carrying
prompt_id, prompt_text, the prompt's tags, check_no, timestamp,
success_score, and an engines object with every engine's full answer. Pick
exactly one selection per request:
latest=true- each prompt's own latest check.check_no=N- one check: every prompt's answers numberedN.- a
date_from/date_towindow - every check with an answer in the window, at most 365 days. This is the selection when neither of the other two is given, and it defaults to 30 days before today through today (UTC).
applied echoes the window read (its dates are null under latest or
check_no) and the engines the answers cover.
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/abc123/checks/answers?latest=true&limit=25"engines, tag_ids and prompt_id narrow any of the three. The endpoint pages
by cursor rather than by offset: follow next_cursor until it is null, and
in window mode pass the same explicit date_from and date_to on every page.
Pagination shows the walk. Each engine carries the fields
listed above except error_message, which this endpoint does not publish, plus
subtopics, followup_queries and response_status. An engine whose run
failed is present: failed is true, response_text is empty,
response_length is 0 and the flags are null. It is left out of the
item's success_score, which carries answered_checks and failed_checks
as on the check list.
Each item also carries google_organic_position and bing_organic_position:
the best organic rank (1 is the top result) a URL from the project's domain
held at that check. Google organic results are captured by google_aio checks
and Bing organic results by bing_ai_overview checks, so each rank is null
unless its engine is requested; engines=chatgpt returns both as null.
Either is also null when the domain did not rank or the check has no organic
data. With check_no=N the endpoint lists every prompt's ranks at that one
check, which the prompt list, reading each prompt's latest check, cannot.
Deleting a prompt
Use DELETE /v1/projects/{project_id}/prompts/{prompt_id} to delete a prompt:
curl -X DELETE https://api.ziptie.ai/v1/projects/abc123/prompts/12345 \
-H "Authorization: Bearer zt_live_<token>"The response is 204 on success. Deletion is idempotent - a second delete of an already-deleted prompt also returns 204.
End-to-end example
Create a prompt, poll until a check arrives, and print per-engine results:
import time
import uuid
import requests
def monitor_prompt(project_id, token):
# Step 1: Create the prompt
create_url = f"https://api.ziptie.ai/v1/projects/{project_id}/prompts"
create_body = {
"text": "how do I improve my credit score",
"monitoring_status": True,
"tag_ids": [],
}
create_headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
}
resp = requests.post(create_url, json=create_body, headers=create_headers)
resp.raise_for_status()
prompt_id = resp.json()["id"]
print(f"Created prompt {prompt_id}")
# Step 2: Poll for the latest check
checks_url = f"https://api.ziptie.ai/v1/projects/{project_id}/prompts/{prompt_id}/checks/latest"
check_headers = {"Authorization": f"Bearer {token}"}
# A brand new prompt is checked soon after it is created, on every engine
# the workspace has enabled, and its first check usually lands within a
# few hours. Poll on a slow cadence from a background job rather than inline.
for i in range(60): # Poll once a minute for up to an hour
time.sleep(60)
resp = requests.get(checks_url, headers=check_headers)
if resp.status_code == 200:
check = resp.json()
print(f"Check arrived at {check['timestamp']}")
break
elif resp.status_code == 404:
print(f"Still waiting... (attempt {i + 1}/60)")
else:
print("No check yet. The project has not run since the prompt was added.")
return
# Step 3: Display per-engine results
engines = check.get("engines", {})
for engine_name, engine_data in engines.items():
cited = engine_data.get("citations")
mentioned = engine_data.get("brand_mention")
print(f" {engine_name}: cited={cited}, mentioned={mentioned}")
# Usage
monitor_prompt("abc123", "zt_live_<your_token>")This script creates a prompt, then waits for its first check before printing the per-engine citation status. A new prompt is checked straight away on every enabled engine rather than at the project's next scheduled run, so the first check usually appears within a few hours, beyond the hour this loop waits. Later checks follow the project's check frequency, and there is no way to trigger one on demand.