← API reference

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:

On success, the response is 201 with the new prompt object. On error:

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

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:

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:

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:

Filters, all applied before the page is cut, so total counts the prompts under every filter:

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.

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:

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:

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:

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:

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:

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.