Fond Docs

REST API Reference

Reference for the Fond REST API: authenticate with an API key, retrieve brand-visibility analytics, and trigger query runs under /api/v1/.

The Fond REST API lets you get analytics data and trigger runs from your own code. All endpoints are under /api/v1/.

Authentication

Each request must include an API key. An admin or owner can create keys in the dashboard at Settings > API Keys.

Send the key in one of these headers:

Authorization: Bearer sk-your-key-here
X-API-Key: sk-your-key-here

Fond shows a key only once, when you create it, so store it securely.

Error responses

A key that is invalid, revoked or expired returns 401:

{ "error": "Invalid API key" }

The three cases look the same on purpose, so a revoked key does not show that it was ever real.

A missing or malformed Authorization header returns 401 with a different body, { "error": "Missing or invalid API key" }.

A read-only key used with a method that changes state returns 403:

{ "error": "This API key is read-only" }

Read-only keys can use only GET and HEAD, so they cannot call POST /websites/{websiteId}/runs. That call uses one of the runs that your plan allows. It does not use up your question limit, which is a standing limit.

Give a read-only key to an integration that must not spend, such as a BI dashboard or a reporting job.

A 401 means that the credential is not usable, and a new credential can help. A 403 means that Fond refused what a valid key tried to do. A retry of a read-only write with the same key always fails.

The plan run limit on POST /runs is the only 403 that you can retry, and only on a recurring plan. A recurring plan counts runs over a rolling window, so the limit clears as older runs leave the window.

On a one-time plan (audit tier), the same limit is a lifetime cap with no window. Its 403 never becomes retryable, so a client that retries it on a schedule loops without end.

Only a plan change, or one replacement for a stranded run, clears the cap. A stranded run is a terminal run with a failure that Fond cannot dispatch again. This occurs when Fond no longer collects from the provider the way it measured that failure.

A run whose failures Fond can all run again is not stranded, and it still consumes the cap. Fond excuses exactly one stranded run per site.


Endpoints

List websites

GET /api/v1/websites

Returns all websites in the organization whose setup status is COMPLETE.

Response

{
  "websites": [
    {
      "id": "clx...",
      "url": "https://example.com",
      "name": "Example Store",
      "createdAt": "2026-01-15T10:30:00.000Z"
    }
  ]
}

List runs

GET /api/v1/websites/:websiteId/runs

Query parameters

ParamTypeDefaultDescription
statusstring—Filters by status: PENDING, RUNNING, PROCESSING, ANALYZING, COMPLETED, COMPLETED_WITH_ERRORS, FAILED
limitinteger20Results per page (1-100)
offsetinteger0Number of results to skip

Response

{
  "runs": [
    {
      "id": "clx...",
      "runDate": "2026-03-06T00:00:00.000Z",
      "status": "COMPLETED",
      "triggeredBy": "MANUAL",
      "name": "Weekly check",
      "totalQueries": 24,
      "totalResults": 72,
      "failedResults": 0,
      "locale": "en-US",
      "startedAt": "2026-03-06T12:00:00.000Z",
      "completedAt": "2026-03-06T12:05:30.000Z",
      "createdAt": "2026-03-06T12:00:00.000Z"
    }
  ],
  "total": 42,
  "limit": 20,
  "offset": 0
}

Trigger a run

POST /api/v1/websites/:websiteId/runs

Starts a new query run for the website. Returns 409 if a run is already in progress.

Request body

The body is optional. If you send one, it is JSON.

FieldTypeDescription
namestringOptional label for the run (1-100 characters), returned by this call and both GET run endpoints. null when not set.
queryIdsstring[]Runs only these queries, at most 500 for each request. Omit it to run all queries. Fond drops an id that is not an active query on this website, or is beyond your plan's question limit, and runs the rest. Only an empty remainder returns 400, so a partly wrong list still returns 201. Compare totalQueries in the response with the number of ids you sent.

Example

curl -X POST https://your-domain.com/api/v1/websites/clx.../runs \
  -H "Authorization: Bearer sk-your-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "Weekly check"}'

Response

The status is 201 Created.

{
  "id": "clx...",
  "status": "RUNNING",
  "triggeredBy": "API",
  "name": "Weekly check",
  "totalQueries": 24,
  "totalResults": 0,
  "failedResults": 0,
  "startedAt": "2026-03-06T14:00:00.000Z",
  "completedAt": null
}

Error responses

StatusReason
400Invalid body, no valid queries to run, or no provider enabled for the website's plan
403Read-only key, or your plan has reached its run limit for the period
404Website not found or not in your organization
409A run is already in progress, or your plan is not fully configured
503The server has no active prompt version. Contact support.

triggeredBy values

Each run records what started it. A run that this endpoint creates is API.

ValueMeaning
CRONThe daily scheduler started it
MANUALAnything that is neither the scheduler nor a current call to this endpoint
APIA call to this endpoint started it

MANUAL covers Run Now in the dashboard, the onboarding auto-run, demo provisioning, and runs that this endpoint created before API existed. You cannot tell these apart, and MANUAL does not mean that a person started the run.


Get run details

GET /api/v1/websites/:websiteId/runs/:runId

Returns a single run with summary statistics.

Response

{
  "id": "clx...",
  "runDate": "2026-03-06T00:00:00.000Z",
  "status": "COMPLETED",
  "triggeredBy": "MANUAL",
  "name": "Weekly check",
  "totalQueries": 24,
  "totalResults": 72,
  "failedResults": 0,
  "locale": "en-US",
  "modelVersions": { "gemini": "gemini-2.0-flash", "openai": "gpt-5-mini" },
  "repetitions": 3,
  "startedAt": "2026-03-06T12:00:00.000Z",
  "completedAt": "2026-03-06T12:05:30.000Z",
  "errorMessage": null,
  "createdAt": "2026-03-06T12:00:00.000Z",
  "summary": {
    "totalResults": 72,
    "mentionCount": 45,
    "citationCount": 12
  }
}

List results

GET /api/v1/websites/:websiteId/runs/:runId/results

Returns paginated query results for a run.

Query parameters

ParamTypeDefaultDescription
providerstring—Filters by provider (for example, gemini or openai)
queryIdstring—Filters by one query
limitinteger50Results per page (1-100)
offsetinteger0Number of results to skip

Response

{
  "results": [
    {
      "id": "clx...",
      "queryId": "clx...",
      "provider": "gemini",
      "model": "gemini-2.0-flash",
      "isMentioned": true,
      "isCited": false,
      "isLinked": false,
      "mentionPosition": 2,
      "mentionContext": "...recommended by Example Store...",
      "sentiment": "POSITIVE",
      "extractedUrls": [],
      "citations": {},
      "followUpQuestions": ["What products does Example Store offer?"],
      "brandInFollowUp": true,
      "confidence": null,
      "groundingUrls": ["https://example.com/products"],
      "errorMessage": null,
      "createdAt": "2026-03-06T12:01:00.000Z",
      "query": {
        "text": "best online stores for electronics",
        "queryType": "CATEGORY",
        "categoryTier": "SAFETY",
        "queryFocus": "PRODUCT"
      }
    }
  ],
  "total": 72,
  "limit": 50,
  "offset": 0
}

Response fields

Each object in results[] has these fields:

FieldTypeDescription
idstringResult ID.
queryIdstringID of the query this result answers.
providerstringLLM provider (claude, gemini, openai, perplexity, google_ai_mode). These are the values that can appear. A run uses only the providers that the plan and the site configuration allow.
modelstring | nullThe model that gave the answer (for example, gemini-2.0-flash). Usually the model that Fond chose for the run. For an assistant that does not let Fond choose, the model that it reported, or null (unknown, not an error).
isMentionedbooleanWhether the response mentions the brand.
isCitedbooleanWhether the response cited the brand as a source.
isLinkedbooleanWhether the response linked to the brand.
mentionPositioninteger | nullWhich third of the response the brand first appears in: 0 = top third, 1 = middle, 2 = lower. null when the response does not mention the brand. Not a paragraph index, despite the name. (Results from before 2026-06-13 can carry a raw paragraph index.)
mentionContextstring | nullExcerpt of the sentence where the brand first appears. Fond removes bold, italic and link syntax. Other Markdown stays, so escapes such as \[1\], code spans and strikethrough can appear. (Results from before 2026-09-22 also keep emphasis and link source, for example **Brand**.)
sentimentstring | nullSentiment toward the brand: POSITIVE, NEUTRAL, or NEGATIVE. null in three cases that it cannot tell apart. These are no mention, a mention that the run has not classified yet, or a literal parser match with no brand entry. Wait for a terminal run status and read isMentioned before you take null as no sentiment.
extractedUrlsstring[]URLs found in the response text.
citationsobject | nullCitations that the provider supplied, in a shape that changes with the provider. {} when it supplied none. null only on a failed row, which has errorMessage.
followUpQuestionsstring[]Follow-up or related questions that the response suggests.
brandInFollowUpbooleanWhether the brand appears in any follow-up question.
confidencenumber | nullConfidence in the brand-mention classification (0–1). Nothing fills it today. Expect null, and do not depend on it.
groundingUrlsstring[]URLs that the provider used to ground its answer (web-search results).
errorMessagestring | nullSet if the provider call failed for this result. Otherwise null.
createdAtstringISO 8601 timestamp.
queryobjectThe query this result answers (fields below).

In the query object, queryType, categoryTier and queryFocus are the values that applied when the run executed. If you retag a query, its earlier runs stay in their old buckets. Rows from before Fond versioned each result report today's values. text cannot change, because editing a query's wording creates a new query.

FieldTypeDescription
textstringThe query text sent to the provider.
queryTypestringOne of BRAND_REPUTATION, CATEGORY, COMPARISON, PROBLEM_SOLVING, LOCAL, BRAND_IDENTITY.
categoryTierstringHow competitive the query is: SAFETY (closest to the brand, least competition), then STRONG_REACH, REACH, and FAR_REACH (category-level). Not an intent tier. Intent is queryType.
queryFocusstringPRODUCT or BRAND.

At the top level, total is the number of results that match the filter, for pagination. limit and offset repeat the values from the request.


Pagination

The runs and results endpoints accept limit and offset and return a total count. GET /websites is not paginated. It returns every completed website under websites, with no total, limit or offset.

Both paginated endpoints sort by createdAt and then by id. Runs are newest first, and results are oldest first. The id tie-break keeps rows that share a timestamp in a fixed order across pages.

The results of a finished run do not change, so their pages are stable. In the runs list, a run that arrives during your walk moves each later page by one. You then see one run twice and miss a different run.

When you page the runs list, skip ids that you already saw. If total increased between your first and last page, a run arrived during your walk. Only a new walk tells you which run it was.

To page through the runs or results endpoint:

# Page 1
GET /api/v1/websites/:id/runs?limit=20&offset=0

# Page 2
GET /api/v1/websites/:id/runs?limit=20&offset=20

Rate limits

The API enforces no rate limits today. Do not poll run status in a tight loop.


Common workflows

Poll for run completion

set -euo pipefail   # a failed request must stop the poll, not be polled around

# 1. Trigger a run
RUN_ID=$(curl -fsS -X POST .../runs -H "Authorization: Bearer $KEY" | jq -r '.id // empty')
[ -n "$RUN_ID" ] || { echo "no run id in the response" >&2; exit 1; }

# 2. Poll status every 30 seconds
while true; do
  STATUS=$(curl -fsS .../runs/$RUN_ID -H "Authorization: Bearer $KEY" | jq -r '.status // empty')
  [ -n "$STATUS" ] || { echo "no status in the response" >&2; exit 1; }
  echo "Status: $STATUS"
  case "$STATUS" in COMPLETED|COMPLETED_WITH_ERRORS|FAILED) break;; esac
  sleep 30
done

# 3. Fetch results
curl -fsS .../runs/$RUN_ID/results -H "Authorization: Bearer $KEY" | jq .

Export every result from one run

set -euo pipefail   # a failed page, a bad body or a failed write all stop here

# Only a finished run can be exported whole. `completedAt` is written with the
# run's terminal status and cleared again by a retry, so it is the one field
# that answers whether this run is still moving.
RUN=$(curl -fsS ".../runs/$RUN_ID" -H "Authorization: Bearer $KEY") \
  || { echo "could not read run $RUN_ID" >&2; exit 1; }
echo "$RUN" | jq -e '.completedAt != null' > /dev/null \
  || { echo "run $RUN_ID has not finished; poll it first" >&2; exit 1; }

OFFSET=0
: > results.jsonl   # start empty, so a re-run replaces rather than appends
while true; do
  RESPONSE=$(curl -fsS ".../runs/$RUN_ID/results?limit=100&offset=$OFFSET" \
    -H "Authorization: Bearer $KEY") \
    || { echo "request failed at offset $OFFSET" >&2; exit 1; }
  echo "$RESPONSE" | jq -c '.results[]' >> results.jsonl
  TOTAL=$(echo "$RESPONSE" \
    | jq -r '.total | numbers | select(. >= 0 and floor == .)')
  [ -n "$TOTAL" ] || { echo "no usable total in the response" >&2; exit 1; }
  OFFSET=$((OFFSET + 100))
  if [ "$OFFSET" -ge "$TOTAL" ]; then break; fi
done

Export only a finished run. The results endpoint serves the rows that exist when you ask, with no run-status filter. A running run continues to add rows and to fill sentiment, so its export is a snapshot that looks complete.

The script empties results.jsonl first, so you can safely run it twice. Without that, a second run appends a second export, and nothing in the file shows it.

The script exports the one run that $RUN_ID names. To export a website's whole history, loop over the paginated GET /websites/:websiteId/runs and run the inner loop for each run id. Truncate the file once, before that outer loop.

The script writes one JSON object per line. jq -c '.results[]' unwraps each response, because a file of whole responses has several top-level objects, and a JSON parser rejects it.

On this page