Skip to main content

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

ParamTypeDefaultDescription
statusstringFilter 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 (optional, JSON)

FieldTypeDescription
namestringOptional label for the run (1-100 chars). Returned by this call and by both GET run endpoints; null when unset.
queryIdsstring[]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

StatusReason
400Invalid body or no valid queries to run
403Read-only key, or your plan's run limit for the period is reached
404Website not found or not in your organization
409A run is already in progress, or your plan is not fully configured
503Server 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.

ValueMeaning
CRONStarted by the daily scheduler
MANUALAnything that is neither the scheduler nor a current call to this endpoint
APIStarted 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

ParamTypeDefaultDescription
providerstringFilter by provider (e.g. gemini, openai)
queryIdstringFilter by specific 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": 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[]:

FieldTypeDescription
idstringResult ID.
queryIdstringID of the query this result answers.
providerstringLLM provider (gemini, openai, claude, perplexity).
modelstring | nullSpecific 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.
isMentionedbooleanWhether the brand was mentioned in the response.
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 not mentioned. Not a paragraph index, despite the name. (Results from before 2026-06-13 may carry a raw paragraph index instead.)
mentionContextstring | nullVerbatim excerpt of the sentence where the brand first appears.
sentimentstring | nullSentiment toward the brand: POSITIVE, NEUTRAL, or NEGATIVE. null when not mentioned.
extractedUrlsstring[]URLs found in the response text.
citationsobject | nullProvider-supplied citations (shape varies by provider); null if none.
followUpQuestionsstring[]Follow-up / related questions the response suggests.
brandInFollowUpbooleanWhether the brand appears in any follow-up question.
confidencenumber | nullConfidence in the brand-mention classification (0–1), when available.
groundingUrlsstring[]URLs the provider used to ground its answer (web-search results).
errorMessagestring | nullPopulated if the provider call failed for this result; null otherwise.
createdAtstringISO 8601 timestamp.
queryobjectThe query this result answers (fields below).

query object:

FieldTypeDescription
textstringThe query text sent to the provider.
queryTypestringOne of BRAND_REPUTATION, CATEGORY, COMPARISON, PROBLEM_SOLVING, LOCAL, BRAND_IDENTITY.
categoryTierstringIntent tier: SAFETY, STRONG_REACH, REACH, or FAR_REACH.
queryFocusstringPRODUCT 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