REST API Reference
The Fond REST API lets you retrieve analytics data and trigger runs programmatically. All endpoints live under /api/v1/.
Authentication
Every request must include an API key. You can create keys from the dashboard at Settings > API Keys (admin/owner role required).
Pass the key in either header:
Authorization: Bearer sk-your-key-here
X-API-Key: sk-your-key-here
Keys are shown once at creation. Store them securely — they cannot be retrieved later.
Error responses
A key that is invalid, revoked, or expired returns 401:
{ "error": "Invalid API key" }
Those three are deliberately indistinguishable: the response does not tell a caller which of them went wrong, so a revoked key reveals nothing about whether it was ever real.
A missing or malformed Authorization header returns 401 with a different
body, { "error": "Missing or invalid API key" }, because sending no
credential is a caller mistake rather than a fact about a key.
A read-only key used with a method that changes state returns 403:
{ "error": "This API key is read-only" }
Read-only keys may use GET and HEAD. Any other method is refused, including
POST /websites/{websiteId}/runs, which starts a run and consumes your plan's
query budget. Issue a read-only key when an integration only needs to read - a
BI dashboard, a reporting job, anything you paste a credential into that should
not be able to spend.
Handle 403 separately from 401. A 401 means the credential is not usable
and refreshing or reissuing it may help; a 403 means the key is valid and was
refused for what it tried to do. For a read-only key that tried to write, retrying
with the same key will always fail. The one retryable 403 is the plan run limit
on POST /runs, and only on a recurring plan, where it clears once the period
rolls over. On a one-time plan (audit tier) the same limit is a lifetime cap:
checkPlanLimit("runs") counts every non-failed run ever and applies no window, so
there is no rollover to wait for and the 403 never becomes retryable. That one
clears only on a plan change — a client retrying it on a schedule will loop
forever.
Endpoints
List websites
GET /api/v1/websites
Returns all websites with setup status COMPLETE in the organization.
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
| Param | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by status: PENDING, RUNNING, PROCESSING, ANALYZING, COMPLETED, COMPLETED_WITH_ERRORS, FAILED |
limit | integer | 20 | Results per page (1-100) |
offset | integer | 0 | Number 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 (optional, JSON)
| Field | Type | Description |
|---|---|---|
name | string | Optional label for the run (1-100 chars). Returned by this call and by both GET run endpoints; null when unset. |
queryIds | string[] | Run only specific queries. Omit to run all. |
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 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
| Status | Reason |
|---|---|
400 | Invalid body or no valid queries to run |
403 | Read-only key, or your plan's run limit for the period is reached |
404 | Website not found or not in your organization |
409 | A run is already in progress, or your plan is not fully configured |
503 | Server is missing an active prompt version — contact support |
triggeredBy values
Every run records what started it. A run created through this endpoint today is API.
| Value | Meaning |
|---|---|
CRON | Started by the daily scheduler |
MANUAL | Anything that is neither the scheduler nor a current call to this endpoint |
API | Started through this endpoint |
API was added after this endpoint shipped, so MANUAL is a residue rather than a
claim about a person: a dashboard Run Now, the onboarding auto-run, demo provisioning,
and every run created through this endpoint before API existed, none of which can be
told apart. Read MANUAL as "not the scheduler" rather than as "a person clicked it".
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
| Param | Type | Default | Description |
|---|---|---|---|
provider | string | — | Filter by provider (e.g. gemini, openai) |
queryId | string | — | Filter by specific query |
limit | integer | 50 | Results per page (1-100) |
offset | integer | 0 | Number 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": null,
"followUpQuestions": ["What products does Example Store offer?"],
"brandInFollowUp": true,
"confidence": 0.85,
"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[]:
| Field | Type | Description |
|---|---|---|
id | string | Result ID. |
queryId | string | ID of the query this result answers. |
provider | string | LLM provider (gemini, openai, claude, perplexity). |
model | string | null | Specific model used (e.g. gemini-2.0-flash). null when the answer came from a scraped consumer surface that did not report one, which we observe rather than choose. Treat it as unknown, not as an error. |
isMentioned | boolean | Whether the brand was mentioned in the response. |
isCited | boolean | Whether the response cited the brand as a source. |
isLinked | boolean | Whether the response linked to the brand. |
mentionPosition | integer | null | Which third of the response the brand first appears in: 0 = top third, 1 = middle, 2 = lower. null when not mentioned. Not a paragraph index, despite the name. (Results from before 2026-06-13 may carry a raw paragraph index instead.) |
mentionContext | string | null | Verbatim excerpt of the sentence where the brand first appears. |
sentiment | string | null | Sentiment toward the brand: POSITIVE, NEUTRAL, or NEGATIVE. null when not mentioned. |
extractedUrls | string[] | URLs found in the response text. |
citations | object | null | Provider-supplied citations (shape varies by provider); null if none. |
followUpQuestions | string[] | Follow-up / related questions the response suggests. |
brandInFollowUp | boolean | Whether the brand appears in any follow-up question. |
confidence | number | null | Confidence in the brand-mention classification (0–1), when available. |
groundingUrls | string[] | URLs the provider used to ground its answer (web-search results). |
errorMessage | string | null | Populated if the provider call failed for this result; null otherwise. |
createdAt | string | ISO 8601 timestamp. |
query | object | The query this result answers (fields below). |
query object:
| Field | Type | Description |
|---|---|---|
text | string | The query text sent to the provider. |
queryType | string | One of BRAND_REPUTATION, CATEGORY, COMPARISON, PROBLEM_SOLVING, LOCAL, BRAND_IDENTITY. |
categoryTier | string | Intent tier: SAFETY, STRONG_REACH, REACH, or FAR_REACH. |
queryFocus | string | PRODUCT or BRAND. |
Top-level, total is the count of results matching the filter (for pagination); limit and offset echo the request.
Pagination
All list endpoints support limit and offset parameters and return a total count. To iterate through all results:
# 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
There are currently no enforced rate limits on the API. Be considerate with request frequency — avoid tight polling loops when checking run status.
Common workflows
Poll for run completion
# 1. Trigger a run
RUN_ID=$(curl -s -X POST .../runs -H "Authorization: Bearer $KEY" | jq -r .id)
# 2. Poll status every 30 seconds
while true; do
STATUS=$(curl -s .../runs/$RUN_ID -H "Authorization: Bearer $KEY" | jq -r .status)
echo "Status: $STATUS"
case "$STATUS" in COMPLETED|COMPLETED_WITH_ERRORS|FAILED) break;; esac
sleep 30
done
# 3. Fetch results
curl -s .../runs/$RUN_ID/results -H "Authorization: Bearer $KEY" | jq .
Export all results for a website
OFFSET=0
while true; do
RESPONSE=$(curl -s ".../runs/$RUN_ID/results?limit=100&offset=$OFFSET" \
-H "Authorization: Bearer $KEY")
echo "$RESPONSE" >> results.json
TOTAL=$(echo "$RESPONSE" | jq .total)
OFFSET=$((OFFSET + 100))
[ $OFFSET -ge $TOTAL ] && break
done