Developer reference

heyEO API docs

Use the private automation API to run the same evidence loop from CI or your own scripts. Each bearer token is bound to one project.

Private alpha

Preview host: https://heyeo-preview.mayurupadhyaya1.workers.dev. Confirm with the team before wiring production automation.
On this page

API overview

Bearer-token automation, signed-in management, and public read-only surfaces.

  • Automation API /api/v1/*, authenticated with a project-bound bearer token. Read scans, baselines, comparisons, management KPIs, recommendation actions and observations; create scans and drive recommendation work.
  • Token management /api/projects/:id/automation-tokens, using your signed-in session to list, create and revoke tokens.
  • Recommendation observations /api/projects/:id/recommendation-observations, using your signed-in session to review tracked recommendations that look actioned.
  • Site changes /api/projects/:id/site-changes, using your signed-in session for page-level corroboration and existing deep links.
  • Onboarding assist POST /api/onboarding/assist, using your signed-in session for the optional AI-assisted setup.
  • Public API /api/public/*, unauthenticated and read-only, exposes only heyEO's configured public evidence projections.
  • Private and session-authenticated responses use no-store. Errors share the stable { error: { code, message } } envelope.

Authentication

Automation calls use a bearer token; management calls use your signed-in session.

An automation token looks like heyeo_v1_<12-char selector>_<43-char secret> and is sent as a bearer token:

Authorization: Bearer heyeo_v1_XXXXXXXXXXXX_your-43-character-secret

Create one under Settings → Automation tokens → Create. The full value is shown once, so store it in your secret manager. heyEO retains only a hash; if you lose the token, revoke it and create another.

API quickstart

Create a token in the app, start a scan, then poll its recorded state.

1 · Environment
# Preview origin (private alpha). Confirm with the team before production wiring.
export HEYEO_API="https://heyeo-preview.mayurupadhyaya1.workers.dev"
export PROJECT_ID="123"                         # sample numeric project id — use yours
export HEYEO_TOKEN="heyeo_v1_XXXXXXXXXXXX_..."  # Settings → Automation tokens
2 · Create a scan (idempotent)
curl -X POST "$HEYEO_API/api/v1/projects/$PROJECT_ID/scans" \
  -H "Authorization: Bearer $HEYEO_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "measure": true,
    "expectedPromptCount": 4,
    "providerIds": ["openai", "gemini"]
  }'

The response includes a runId and credit price. Reusing the same Idempotency-Key with the same body returns the original run with replayed: true and no extra charge.

3 · Poll the run
curl "$HEYEO_API/api/v1/projects/$PROJECT_ID/scans/$RUN_ID" \
  -H "Authorization: Bearer $HEYEO_TOKEN"

Conventions

Behaviour shared across the automation API.

Errors

Every failure returns the same envelope with a stable code:

{
  "error": {
    "code": "insufficient_credits",
    "message": "You don't have enough credits to start this scan."
  }
}

Rate limiting

Automation tokens allow 60 requests per 60 seconds. Onboarding assist separately allows 5 requests per 60 seconds per signed-in user. A limited request returns 429 rate_limit_exceeded with a Retry-After header.

Idempotency

POST /scans requires an Idempotency-Key. The same key and body replay the original run; the same key with a different body returns 409 idempotency_conflict.

Pagination

List endpoints return an opaque nextCursor; pass it back as the cursor query parameter.

Project isolation

A token can see only its project. Unknown and out-of-scope resources return 404 not_found, so resources outside the project are not distinguishable.

Full status / code catalogue

StatusCodeWhen
400invalid_requestA field, query param, or the request body failed validation.
400invalid_cursorThe pagination cursor is invalid.
401invalid_tokenThe automation token is missing, malformed, unknown, expired or revoked.
401authentication_requiredA management endpoint was called without a signed-in session.
402insufficient_creditsNot enough credits to start the scan.
404not_foundThe resource does not exist, or belongs to another project (isolation).
404no_baselineNo baseline is selected for the project (comparisons).
409idempotency_conflictThe same Idempotency-Key was reused with a different payload.
409prompt_set_changedThe monitored prompt set changed between quote and reservation; re-quote.
409run_not_completeA run referenced by a comparison is not complete yet.
409token_limit_reachedThe project already has the maximum number of active tokens.
409assistance_exhaustedThe one funded AI-assisted setup has already been used for this request.
409assistance_in_progressAn AI suggestion is already being prepared (lease in progress).
405method_not_allowedHTTP method not allowed for this endpoint.
413request_too_largeThe request body exceeds 64 KiB.
415json_requiredA write request was not sent as application/json.
429rate_limit_exceededPer-token rate limit exceeded (Retry-After: 60).
502website_unreachableThe website could not be read (crawl failed).
502upstream_unavailableAI model or upstream service is temporarily unavailable (may return 503).
503service_unavailableWiring, rate limiter or auth backend is unavailable.
503queue_unavailableThe scan queue is unavailable (scan create).
503evidence_unavailableComparison evidence is unavailable for a corrupt run.
503unavailableService is temporarily unavailable (generic unavailability).
500internal_errorUnexpected server error, or a response failed its own schema.

Reference · Public API

Unauthenticated, read-only projections for heyEO's configured public project.

The auto-latest report returns the newest completed scan. The evidence endpoint returns only approved before-and-after cycles. Neither surface accepts a project, owner or run selector from the caller.

Cookies and Authorization headers do not change the representation or trigger a Set-Cookie response. Missing configuration or evidence fails closed with a 503.

Success responses use public, max-age=60, s-maxage=300; failures use no-store.

GET
/api/public/report

Get public report

Auto-latest public scan report for the configured heyEO project. Returns the newest completed scan with audit summary, inferred ICP, AEO visibility aggregates, and up to 25 recommendations. No per-scan approval—auto-published per ADR 0002. Fails closed (503) when HEYEO_PUBLIC_REPORT_PROJECT_ID is unbound.

Auth: None (public)

Response · 200

{ contractVersion: 1, identity: { projectName, domain }, generated: { completedAt, providerIds[], promptCount, measurementSuiteVersionId|null }, auditSummary: { total, pass, warn, fail, notChecked }, icp: null | { dimensions[{ key, values[], abstained }] }, visibility: { providers[{ provider, measuredCount, mentionCount, abstainedCount }] }, recommendations (max 25): [{ title, summary, source, effort, affectedUrls[] }] }. Providers: openai | gemini | perplexity. ICP keys: company_size | roles | industries | use_cases | pain_points | maturity | buying_triggers. Recommendation source: site_audit | ai_visibility | icp_drift. Effort: s | m | l.

{
  "contractVersion": 1,
  "identity": { "projectName": "ExampleCo", "domain": "example.com" },
  "generated": {
    "completedAt": "2026-09-10T18:30:00Z",
    "providerIds": ["openai", "gemini"],
    "promptCount": 4,
    "measurementSuiteVersionId": "v2.1"
  },
  "auditSummary": { "total": 42, "pass": 30, "warn": 7, "fail": 3, "notChecked": 2 },
  "icp": {
    "dimensions": [
      { "key": "company_size", "values": ["1-10", "11-50"], "abstained": false },
      { "key": "roles", "values": ["founder", "developer"], "abstained": false }
    ]
  },
  "visibility": {
    "providers": [
      { "provider": "openai", "measuredCount": 4, "mentionCount": 3, "abstainedCount": 0 },
      { "provider": "gemini", "measuredCount": 4, "mentionCount": 2, "abstainedCount": 1 }
    ]
  },
  "recommendations": [
    { "title": "Fix missing meta descriptions", "summary": "Add descriptions to 5 pages.", "source": "site_audit", "effort": "s", "affectedUrls": ["https://example.com/about"] }
  ]
}

Endpoint-specific errors

StatusCodeWhen
405method_not_allowedNon-GET request (POST, PUT, PATCH, DELETE). Response includes Allow: GET header.
503service_unavailablePublic report is temporarily unavailable (config unbound, no completed runs, or projection failure).
GET
/api/public/evidence

Get public evidence

Approval-gated public evidence cycles for the configured heyEO project. Returns 1–10 published before/after cycles, each with a recommendation, intervention, baseline/current run provenance, and a full comparison result. Fails closed (503) when HEYEO_PUBLIC_EVIDENCE_PROJECT_ID is unbound or no cycles are approved.

Auth: None (public)

Response · 200

{ contractVersion: 1, identity: { projectName, domain }, cycles (1–10): [{ slug, publishedAt, recommendation: { title, summary, source, effort, affectedUrls[] }, intervention: { changeUrl, implementedAt, eligibleForFollowUpAt }, baseline: { completedAt, providerIds[], promptCount, measurementSuiteVersionId|null }, current: { ... }, comparison: { status: comparable|partial|incomparable, ... } }] }. The comparison mirrors the full scan comparison structure with finding identifiers removed. Only approved cycles are included.

Endpoint-specific errors

StatusCodeWhen
405method_not_allowedNon-GET request. Response includes Allow: GET header.
503service_unavailablePublic evidence is temporarily unavailable (config unbound, no approved cycles, or projection failure).

Reference · Automation API

Project-bound bearer token required.

Common errors (all /api/v1 endpoints)

StatusCodeWhen
401invalid_tokenMissing/malformed Authorization, or unknown, expired or revoked token.
404not_foundThe token is bound to a different project than the one in the path (cross-project isolation).
400invalid_requestThe projectId in the path is not a positive integer.
429rate_limit_exceededMore than 60 requests in 60s for this token (Retry-After: 60).
503service_unavailableThe API is temporarily unavailable (transient backend failure).
500internal_errorUnexpected server error.
GET
/api/v1/projects/{projectId}

Get project

Fetch the project's metadata: name, domain, website, markets, topics, declared ICP, providers and scan frequency.

Auth: Bearer automation token

Response · 200

{ id, name, domain, websiteUrl, markets[], topics[], declaredIcp, createdAt, lastScanAt|null, scanFrequency (daily|weekly|monthly), providers[] }.

Endpoint-specific errors

StatusCodeWhen
404not_foundThe project was not found.
GET
/api/v1/projects/{projectId}/scans

List scans

List the project's scan runs, newest first, with cursor pagination.

Auth: Bearer automation token

Query parameters

FieldTypeNotes
limitinteger1–50, default 20.
stagestringall | complete | failed. Default all.
cursorstringOpaque cursor from a previous nextCursor.

Response · 200

{ items: [{ runId, displayName, mode, pageAllowance, stage, createdAt, completedAt|null, promptCount, providerIds[], failure|null }], nextCursor|null }.

Endpoint-specific errors

StatusCodeWhen
400invalid_cursorThe cursor is invalid.
POST
/api/v1/projects/{projectId}/scans

Create scan

Reserve credits and queue a new scan. Idempotent: reusing the same Idempotency-Key with the same body replays the original run (replayed: true) without charging again.

Auth: Bearer automation token

Headers

FieldTypeNotes
Content-Typerequiredapplication/json
Idempotency-KeyrequiredstringTrimmed, 1–120 chars. Use a unique key per logical request.

Request body

FieldTypeNotes
measurerequiredbooleanMeasure the monitored prompts against the providers.
expectedPromptCountrequiredinteger0–10. Must be > 0 when measure=true, exactly 0 when false (staleness guard).
providerIdsrequiredstring[]Non-empty subset of openai | gemini | perplexity.
limitsobjectOptional crawl limits. maxPages must be an exact crawl tier: 25, 50, 100, 150, 200, or 250 (default 25). Other values return invalid_request (Unsupported crawl tier). Crawl credits for those tiers are 1–6 on the crawl_audit line. Other optional positive ints: maxResponseBytes, maxSitemapBytes, maxSitemaps, maxSitemapUrls, maxExtractedTextChars, requestTimeoutMs, maxRedirects, maxDurationMs.

Response · 201

{ runId, reservationId, price: { billableCredits, pricingPolicyVersion, lineItems[] }, snapshot, replayed }.

{
  "runId": "8f3b...",
  "reservationId": "1a2c...",
  "price": {
    "billableCredits": 7,
    "pricingPolicyVersion": 1,
    "lineItems": [
      { "code": "crawl_audit", "label": "Crawl + audit", "billableCredits": 1, "quantity": 1, "pageAllowance": 25 },
      { "code": "icp_bundle", "label": "ICP analysis", "billableCredits": 2, "quantity": 1 },
      { "code": "prompt_measurements", "label": "Prompt measurements", "billableCredits": 4, "quantity": 4 }
    ]
  },
  "snapshot": { "runId": "8f3b...", "status": "running", "overallProgress": 0, "stages": [ /* ... */ ] },
  "replayed": false
}

Endpoint-specific errors

StatusCodeWhen
400invalid_requestMissing/invalid Idempotency-Key, body failed validation, or limits.maxPages is not a published crawl tier.
402insufficient_creditsNot enough credits.
409idempotency_conflictSame key, different payload.
409prompt_set_changedMonitored prompts changed since the quote; re-quote.
503queue_unavailableThe scan queue is unavailable.
GET
/api/v1/projects/{projectId}/scans/{runId}

Get scan

Poll a single scan run's live snapshot (stages, progress, status, result).

Auth: Bearer automation token

Response · 200

A ScanRunSnapshot: { runId, stages[{ id, label, section, state, detail? }], overallProgress (0–100), status, result?, failure? }.

Endpoint-specific errors

StatusCodeWhen
404not_foundrunId is not a UUID, or the run was not found.
GET
/api/v1/projects/{projectId}/baseline

Get baseline

The project's active baseline selection plus recent baseline history.

Auth: Bearer automation token

Query parameters

FieldTypeNotes
historyLimitinteger1–100, default 50.

Response · 200

{ active: { id, baselineRunId, selectionNumber, selectedAt }|null, history[], historyTruncated }.

POST
/api/v1/projects/{projectId}/baseline

Select baseline

Select a completed project scan as the active comparison baseline.

Auth: Bearer automation token

Request body

FieldTypeNotes
baselineRunIdrequireduuid
idempotencyKeyrequiredstring

Response · 200

The persisted baseline selection.

GET
/api/v1/projects/{projectId}/comparisons

Compare runs

Compare a run against the baseline (or an explicit baseline run). Site, AI and ICP sections compare independently; a prompt/provider mismatch now returns the matching sections as partial instead of hiding every difference.

Auth: Bearer automation token

Query parameters

FieldTypeNotes
currentRunIdrequireduuid
baselineRunIduuidOptional. Defaults to the project's active baseline.

Response · 200

{ baselineRunId, currentRunId, result: { status: comparable|partial|incomparable, ... } }.

Endpoint-specific errors

StatusCodeWhen
404no_baselineNo baseline is selected and none was supplied.
409run_not_completeEither run is not complete.
503evidence_unavailableA run's evidence is corrupt.
GET
/api/v1/projects/{projectId}/recommendation-actions

List recommendation actions

List recommendation actions (accepted/implemented fixes and their interventions), newest first.

Auth: Bearer automation token

Query parameters

FieldTypeNotes
limitinteger1–50, default 20.
statestringOptional: new | accepted | implementing | implemented | dismissed.
cursorstringOpaque pagination cursor.

Response · 200

{ items: [{ ...action, sourceRecommendation: { title, summary, source, effort, affectedUrls[] }, intervention|null }], nextCursor|null }.

Endpoint-specific errors

StatusCodeWhen
400invalid_cursorThe cursor is invalid.
GET
/api/v1/projects/{projectId}/recommendation-actions/{actionId}

Get recommendation action

Full detail for one recommendation action: the source recommendation, its state-change events, and the intervention.

Auth: Bearer automation token

Response · 200

{ action, sourceRecommendation (full), events[], eventsTruncated, intervention|null }.

Endpoint-specific errors

StatusCodeWhen
404not_foundactionId is not a UUID, or the action was not found.
POST
/api/v1/projects/{projectId}/recommendation-actions

Create recommendation action

Track one recommendation from a completed scan through implementation.

Auth: Bearer automation token

Request body

FieldTypeNotes
sourceRunIdrequireduuid
sourceArtifactIdrequiredinteger
sourceRecommendationIndexrequiredinteger
idempotencyKeyrequiredstring

Response · 200

The created recommendation action.

POST
/api/v1/projects/{projectId}/recommendation-actions/{actionId}/transitions

Transition recommendation action

Move a recommendation action through the same state machine as the UI.

Auth: Bearer automation token

Request body

FieldTypeNotes
toStaterequiredstring
notesstring
idempotencyKeyrequiredstring

Response · 200

The updated recommendation action.

POST
/api/v1/projects/{projectId}/recommendation-actions/{actionId}/intervention

Record implementation

Record what changed and when a follow-up becomes eligible.

Auth: Bearer automation token

Request body

FieldTypeNotes
changeUrlrequiredhttps URL
implementedAtrequiredISO timestamp
eligibleForFollowUpAtrequiredISO timestamp
idempotencyKeyrequiredstring

Response · 200

The recorded intervention.

POST
/api/v1/projects/{projectId}/recommendation-actions/{actionId}/follow-up

Attach follow-up scan

Attach an eligible completed scan as the action's follow-up evidence.

Auth: Bearer automation token

Request body

FieldTypeNotes
followUpRunIdrequireduuid
idempotencyKeyrequiredstring

Response · 200

The intervention with its attached follow-up scan.

GET
/api/v1/projects/{projectId}/management-report

Get management KPIs

Return the latest evidence-backed founder scorecard and movement. The comparison basis selects a completed run at least 7 days older, else the previous completed run, else the active baseline, else none. The comparisonBasis field reports which was used: weekly_anchor | previous_run | active_baseline | none. If the current evidence is bad, returns 503. If the baseline evidence is bad, degrades to current-only (comparisonBasis: none) instead of failing.

Auth: Bearer automation token

Response · 200

{ currentRunId, baselineRunId|null, comparisonBasis, measuredAt, hasMoved, site, aiVisibility, icp, comparisonStatus }.

Endpoint-specific errors

StatusCodeWhen
503evidence_unavailableCurrent run evidence is corrupt or unavailable.
GET
/api/v1/projects/{projectId}/recommendation-observations

List automatic detections

List automatically detected recommendation outcomes, with status and confirmation state filters.

Auth: Bearer automation token

Response · 200

{ items: [RecommendationObservation], nextCursor|null, unresolvedCount }.

GET
/api/v1/projects/{projectId}/recommendation-observations/{observationId}

Get automatic detection

Fetch one owner- and project-scoped automatic detection.

Auth: Bearer automation token

Response · 200

RecommendationObservation.

POST
/api/v1/projects/{projectId}/recommendation-observations/{observationId}/confirm

Confirm automatic detection

Confirm a candidate action, record its intervention and advance the linked recommendation through the same state machine as the product UI.

Auth: Bearer automation token

Request body

FieldTypeNotes
idempotencyKeyrequiredstring
expectedVersionrequiredinteger

Response · 200

The updated RecommendationObservation.

POST
/api/v1/projects/{projectId}/recommendation-observations/{observationId}/suppress

Suppress automatic detection

Mark a pending detection as not this change without dismissing its recommendation.

Auth: Bearer automation token

Request body

FieldTypeNotes
idempotencyKeyrequiredstring
expectedVersionrequiredinteger

Response · 200

The updated RecommendationObservation.

Reference · Token management

Signed-in session required. A missing session returns 401 authentication_required.

GET
/api/projects/{projectId}/automation-tokens

List automation tokens

List the project's automation tokens as safe metadata. The secret is never returned.

Auth: Signed-in session

Response · 200

{ items: [{ id, projectId, name, tokenPrefix, createdAt, expiresAt|null, lastUsedAt|null, revokedAt|null }] }.

POST
/api/projects/{projectId}/automation-tokens

Create automation token

Create a token. The full secret is returned exactly once in this response — store it now; only a hash is kept server-side.

Auth: Signed-in session

Headers

FieldTypeNotes
Content-Typerequiredapplication/json

Request body

FieldTypeNotes
namerequiredstringTrimmed, 1–80 chars.
expiresInDays30 | 90 | 365 | nullDefault 90. null = never expires.

Response · 201

{ token: { id, projectId, name, tokenPrefix, createdAt, expiresAt|null, ... }, secret: "heyeo_v1_..." }.

Endpoint-specific errors

StatusCodeWhen
409token_limit_reachedThe project already has the maximum active tokens.
POST
/api/projects/{projectId}/automation-tokens/{tokenId}/revoke

Revoke automation token

Revoke a token immediately. Idempotent — revoking an already-revoked token returns the same metadata.

Auth: Signed-in session

Response · 200

The revoked token summary (with revokedAt set).

Endpoint-specific errors

StatusCodeWhen
404not_foundtokenId is not a UUID, or the token was not found / not owned.

Reference · Recommendation observations

Signed-in review of tracked recommendations that look actioned after a later scan.

GET
/api/projects/{projectId}/recommendation-observations

List recommendation observations

List tracked-recommendation evidence observations for a project. Primary founder inbox for Gate 7: candidates that look actioned after a later comparable scan. Retitled tracked copies of the same site findings collapse into one candidate (fingerprint: familyKey + materialSignature + URL); siblings already confirmed for the project (any observing run) are omitted. Page-level site changes remain corroboration only and are not required for confirmation.

Auth: Signed-in session

Query parameters

FieldTypeNotes
statusstringOptional: unchanged | evidence_changed | candidate_actioned | coverage_limited | not_evaluable.
confirmationStatestringOptional: pending | confirmed | suppressed. Inbox defaults to pending.
limitinteger1–50, default 20.
cursorstringOpaque pagination cursor.

Response · 200

{ items: [{ id, projectId, recommendationActionId, observingRunId, sourceRunId, detectorVersion, status, summary, evidence{}, confirmationState, version, detectedAt, createdAt, updatedAt, actionTitle|null, actionState|null }], nextCursor|null, unresolvedCount }. unresolvedCount is unique pending fingerprints (plus items without a fingerprint), not raw pending row count.

Endpoint-specific errors

StatusCodeWhen
400invalid_requestQuery param validation failed.
400invalid_cursorThe pagination cursor is invalid.
GET
/api/projects/{projectId}/recommendation-observations/{observationId}

Get recommendation observation

Fetch one recommendation observation by id for the authenticated owner.

Auth: Signed-in session

Response · 200

RecommendationObservation (same shape as list items).

Endpoint-specific errors

StatusCodeWhen
400invalid_requestobservationId is not a valid UUID.
404not_foundThe observation was not found, or belongs to another project.
POST
/api/projects/{projectId}/recommendation-observations/{observationId}/confirm

Confirm recommendation observation

One-click confirm that a candidate_actioned observation was intentional. Atomically records an intervention (creating one when absent), advances the recommendation through accepted → implementing → implemented with append-only events, and marks the observation confirmed. Detection alone never transitions action state. Idempotent.

Auth: Signed-in session

Headers

FieldTypeNotes
Content-Typerequiredapplication/json

Request body

FieldTypeNotes
idempotencyKeyrequiredstringTrimmed, 1–120 chars. Use a unique key per logical request.
expectedVersionrequiredintegerOptimistic-lock version. 409 invalid_state if stale.
changeUrlstring (URL)Optional override; defaults from observation evidence / affected URLs.
implementedAtstring (ISO-8601)Optional override; defaults to observation detectedAt.

Response · 200

The updated RecommendationObservation.

Endpoint-specific errors

StatusCodeWhen
400invalid_requestBody validation failed, or observationId is not a UUID.
404not_foundThe observation was not found, or belongs to another project.
409idempotency_conflictSame idempotencyKey, different payload.
409invalid_stateexpectedVersion was stale, observation is not pending, or status is not candidate_actioned.
POST
/api/projects/{projectId}/recommendation-observations/{observationId}/suppress

Suppress recommendation observation

Mark a pending observation as Not this change. Suppresses only that observation; does not dismiss the recommendation or transition action state. Idempotent.

Auth: Signed-in session

Headers

FieldTypeNotes
Content-Typerequiredapplication/json

Request body

FieldTypeNotes
idempotencyKeyrequiredstringTrimmed, 1–120 chars. Use a unique key per logical request.
expectedVersionrequiredintegerOptimistic-lock version. 409 invalid_state if stale.

Response · 200

The updated RecommendationObservation.

Endpoint-specific errors

StatusCodeWhen
400invalid_requestBody validation failed, or observationId is not a UUID.
404not_foundThe observation was not found, or belongs to another project.
409idempotency_conflictSame idempotencyKey, different payload.
409invalid_stateexpectedVersion was stale, or observation is not pending.

Reference · Site changes

Signed-in page-level corroboration and existing change deep links.

GET
/api/projects/{projectId}/site-changes

List site changes

List material page changes detected between consecutive scans. Optionally filter by verification state or scope to a detecting scan via afterRunId. Returns change metadata, summary labels, unresolved count, and detecting-interval projection status when afterRunId is set.

Auth: Signed-in session

Query parameters

FieldTypeNotes
limitinteger1–50, default 20.
verificationStatestringOptional: unverified | intentional_expected | intentional_unexpected | not_mine | unsure.
cursorstringOpaque pagination cursor.
afterRunIdstring (UUID)Optional: scope to changes detected after this run. When set, response includes detectingInterval with projection status.

Response · 200

{ items: [{ id, projectId, beforeRunId, afterRunId, detectorVersion, canonicalPageUrl, changedSignals[], safeSummaries[], fingerprints{}, detectedAt, verificationState, verificationEventCount, verifiedAt|null, recommendationActionId|null, linkedAt|null, version, createdAt, updatedAt }], nextCursor|null, unresolvedCount, detectingInterval|null }. When afterRunId is supplied, detectingInterval: { afterRunId, projectionStatus (pending|completed|failed|skipped_no_baseline), unresolvedCount }; otherwise null. changedSignals: http_status | redirect_outcome | title | meta_description | canonical_url | language | robots_directives | heading_outline | structured_data | body_content.

Endpoint-specific errors

StatusCodeWhen
400invalid_requestQuery param validation failed (invalid limit, verificationState, or afterRunId UUID).
400invalid_cursorThe pagination cursor is invalid.
GET
/api/projects/{projectId}/site-changes/{changeId}

Get site change

Full detail for one detected site change: the change record, verification events, link events, suggested recommendation associations, and outcome history (later scans with comparisons).

Auth: Signed-in session

Response · 200

{ siteChange, verificationEvents: [{ id, eventNumber, fromState, verification, createdAt }], linkEvents: [{ id, eventNumber, eventKind (linked|unlinked), recommendationActionId, createdAt }], suggestedAssociations: [{ actionId, matchedUrl, matchSource (affected_url|evidence_source_url) }], outcomeHistory: { fixedBeforeRunId, entries: [{ kind (detecting_scan|later_scan), runId, completedAt|null, comparison|null, comparisonStatus }], confounders: [{ code, message, relatedSiteChangeIds[] }], observedAfterNote } }. Outcome entries max 6; comparison mirrors scan comparison structure.

Endpoint-specific errors

StatusCodeWhen
400invalid_requestchangeId is not a valid UUID.
404not_foundThe site change was not found, or belongs to another project.
POST
/api/projects/{projectId}/site-changes/{changeId}/verifications

Append site change verification

Record a founder verification (intentional_expected, intentional_unexpected, not_mine, unsure). Updates verificationState and appends a verification event. Idempotent.

Auth: Signed-in session

Headers

FieldTypeNotes
Content-Typerequiredapplication/json

Request body

FieldTypeNotes
verificationrequiredstringintentional_expected | intentional_unexpected | not_mine | unsure.
idempotencyKeyrequiredstringTrimmed, 1–120 chars. Use a unique key per logical request.
expectedVersionintegerOptional optimistic-lock version. When supplied, 409 invalid_state if stale.

Response · 200

The updated SiteChange record (same shape as list items).

Endpoint-specific errors

StatusCodeWhen
400invalid_requestBody validation failed, or changeId is not a UUID.
404not_foundThe site change was not found, or belongs to another project.
409idempotency_conflictSame idempotencyKey, different payload.
409invalid_stateexpectedVersion was stale (another update happened first).

Reference · Onboarding assist

Signed-in optional setup assistance with a separate per-user rate limit.

POST
/api/onboarding/assist

AI-assisted onboarding

One platform-funded website analysis per signed-in founder. Crawls up to 5 pages and drafts selected onboarding sections (declared ICP, competitors, markets/topics). Founder reviews and confirms suggestions before project creation.

Auth: Signed-in session

Headers

FieldTypeNotes
Content-Typerequiredapplication/json
AuthorizationrequiredBearer tokenSigned-in Supabase session token.
AcceptstringOptional: application/x-ndjson for streamed progress after lease acquisition.

Request body

FieldTypeNotes
websiteUrlrequiredstringHTTP(S) URL, trimmed, max 2048 chars.
productNamerequiredstringTrimmed, 1–120 chars.
sectionsrequiredarrayUnique non-empty array of: declared_icp | competitors | markets_topics (max 3).

Response · 200

{ assistanceId (uuid), replayed (boolean), sourceUrl, pages: { count (0–5), urls[] }, generatedAt (ISO datetime), provenance: { model, promptVersion, repaired, fallbackUsed }, sections: { declared_icp?, competitors?, markets_topics? each suggested or abstained per contracts } }. If Accept includes application/x-ndjson and lease is acquired, server may stream NDJSON progress (crawling/drafting/checking + heartbeat) then terminal result/error; otherwise plain JSON.

Endpoint-specific errors

StatusCodeWhen
401authentication_requiredNot signed in or session expired.
400invalid_requestBody validation failed or URL is invalid.
409assistance_exhaustedOne funded assist already used for this request shape.
409assistance_in_progressLease in progress for this request.
429rate_limit_exceededMore than 5 requests in 60s for this user (Retry-After header included).
502website_unreachableCrawl failed. Check the address or enter details manually.
502upstream_unavailableAI model or upstream unavailable (may also return 503).
503service_unavailableWiring or rate limiter missing.
503unavailableService temporarily unavailable.