Hoverboard API (v1)

The Hoverboard API lets Enterprise customers pull reporting data into their own dashboards, BI tools, and client reports. It is read-only and every request is scoped to the organization that owns the API key.


Authentication

Send your key as a bearer token on every request:

Authorization: Bearer hb_live_xxxxxxxxxxxxxxxxxxxxxxxx

Keys are created in the dashboard under Settings → API Keys (visible to org admins on Enterprise plans). The full key is displayed only once at creation — store it in your secret manager. Keys are stored hashed; they can be revoked at any time, which takes effect immediately.

Requirements:

Never expose an API key in browser or mobile client code. Call the API from your server.


Rate limits

LimitValue
Requests per organization per day (UTC)10,000
Max limit per page1,000
Default limit per page100

Exceeding the daily limit returns 429 Too Many Requests. The counter resets at 00:00 UTC.

Every request is logged (method, path, query params, status code, response time) for usage auditing.


Common query parameters

Pagination (list endpoints):

ParameterTypeDefaultNotes
limitinteger100Clamped to 1–1000
offsetinteger0Items to skip

Date range (results, sources, urls, stats, visibility, competitors, prompt stats, sentiment):

ParameterTypeDefaultNotes
start_dateISO date/datetimeend_date − 30 daysInclusive
end_dateISO date/datetimenowInclusive

Dates accept YYYY-MM-DD or full ISO-8601 timestamps. Responses echo the resolved range in date_range. Bare dates resolve to UTC: start_date=2026-08-12 begins at 00:00:00.000Z; end_date=2026-08-12 includes that day through 23:59:59.999Z. Explicit timestamps retain their supplied instant on timestamp-based endpoints; daily rollup endpoints (sources, urls, visibility, and daily brand trends) use the resolved UTC calendar dates.


Canonical values

AI sources. Every source value returned by the API — enabled_sources, response_metadata.source, sources[].source_type, and the source field on aggregated rows — uses these canonical values:

ValueAI surface
chatgptChatGPT (OpenAI)
perplexityPerplexity
geminiGoogle Gemini
google_ai_overviewGoogle AI Overviews
google_ai_modeGoogle AI Mode
claudeClaude

Internal provider labels (for example openai or google_ai_overviews) are never returned. Where a request accepts a source value, both the canonical value and the legacy internal label are accepted.

Locations. location_name is normalized to a de-duplicated City, State, Country string (e.g. El Paso, Texas, United States). Repeated geography segments and redundant state abbreviations are removed.


Endpoints

1. GET /organizations

Returns the organization tied to the API key plus any child organizations.

{
  "organizations": [
    {
      "id": "145a5bee-...",
      "name": "Example Organization",
      "description": null,
      "website": "https://example.com",
      "owner_id": "9f0c...",
      "parent_organization_id": null,
      "created_at": "2026-01-14T18:02:11.000Z"
    }
  ]
}

2. GET /projects

Lists projects in the organization, newest first. Supports pagination.

{
  "data": [
    {
      "id": "0d5f...",
      "name": "Example Client",
      "business_name": "Example Client",
      "alternative_names": ["Example Client Legal Group"],
      "website_url": "https://example-client.com",
      "schedule": "daily",
      "custom_interval": null,
      "enabled_sources": ["chatgpt", "perplexity", "gemini", "google_ai_overview"],
      "location_code": 2840,
      "location_name": "United States",
      "created_at": "2026-02-01T12:00:00.000Z",
      "updated_at": "2026-08-01T09:31:22.000Z",
      "organization_id": "145a5bee-..."
    }
  ],
  "pagination": { "limit": 100, "offset": 0, "total": 12 }
}

3. GET /projects/:id

Single project lookup. Returns 404 if the project is not in your organization.

{
  "project": {
    "id": "0d5f...",
    "business_name": "Example Client",
    "alternative_names": ["..."],
    "organization_id": "145a5bee-..."
  }
}

4. GET /projects/:id/prompts

Active (non-archived) prompts for a project. Supports pagination. intent_type is the classified search intent (null until classified); branded is true when the prompt names your business or an alternative name.

intent_type is one of: best_in_category, comparison, buying_criteria, use_case, problem_aware, informational. intent_confidence is the classifier's confidence (0–1). A prompt is branded when it contains your business name or one of its alternative names as whole words — this is computed from your project settings, not by the classifier.

{
  "data": [
    {
      "id": "b91e...",
      "project_id": "0d5f...",
      "text": "best example service in example city",
      "archived": false,
      "exclude_from_visibility": false,
      "tags": ["core", "local"],
      "intent_type": "best_in_category",
      "intent_confidence": 0.94,
      "branded": false,
      "created_at": "2026-02-01T12:04:00.000Z",
      "updated_at": "2026-02-01T12:04:00.000Z"
    }
  ],
  "pagination": { "limit": 100, "offset": 0, "total": 25 }
}

5. GET /projects/:id/results

Individual AI answer checks (one row per prompt × model × run). Supports pagination and date range.

{
  "project": { "id": "0d5f...", "business_name": "Example Client" },
  "date_range": { "start": "2026-07-13T00:00:00.000Z", "end": "2026-08-12T00:00:00.000Z" },
  "data": [
    {
      "id": "77aa...",
      "check_id": "3c9d...",
      "project_id": "0d5f...",
      "prompt_id": "b91e...",
      "prompt_text_snapshot": "best example service in example city",
      "response_text": "Here are several highly rated...",
      "timestamp": "2026-08-11T14:22:03.000Z",
      "brand_mentioned": true,
      "brand_position": 2,
      "other_brands": ["Competitor A", "Competitor B"],
      "recommended": true,
      "recommended_probability": 0.91,
      "citations": [
        { "url": "https://example-directory.com/...", "title": "Top Example Services" }
      ],
      "sources": [
        { "url": "https://example-directory.com/...", "domain": "example-directory.com", "title": "Top Example Services", "site_name": "Example Directory", "position": 1, "is_inline_cited": true, "source_type": "chatgpt" }
      ],
      "query_fanouts": ["best example service example city"],
      "response_metadata": { "source": "chatgpt" },
      "sentiment_score": 0.72,
      "sentiment_label": "positive",
      "sentiment_confidence": 0.88,
      "sentiment_context": "Described as highly experienced..."
    }
  ],
  "pagination": { "limit": 100, "offset": 0, "total": 3120 }
}

Field notes:


6. GET /projects/:id/sources

Aggregated domain-level citation data for the date range. Supports pagination and date range.

{
  "project": { "id": "0d5f...", "business_name": "Example Client" },
  "date_range": { "start": "2026-07-13T00:00:00.000Z", "end": "2026-08-12T00:00:00.000Z" },
  "data": [
    {
      "domain": "example-directory.com",
      "citation_count": 184,
      "prompt_count": 41,
      "brand_mentioned_count": 22
    }
  ],
  "pagination": { "limit": 100, "offset": 0 }
}

Counts are distinct-aware: prompt_count is the number of unique prompts that cited the domain.


7. GET /projects/:id/urls

Same as sources but aggregated at the individual URL level. Supports pagination and date range.

{
  "project": { "id": "0d5f...", "business_name": "Example Client" },
  "date_range": { "start": "...", "end": "..." },
  "data": [
    {
      "url": "https://www.example-list.com/example-services/tx/example-city/",
      "domain": "example-list.com",
      "citation_count": 37,
      "prompt_count": 14
    }
  ],
  "pagination": { "limit": 100, "offset": 0 }
}

8. GET /projects/:id/stats

Daily visibility trend plus brand trend series for the date range. No pagination.

{
  "project": { "id": "0d5f...", "business_name": "Example Client" },
  "date_range": { "start": "...", "end": "..." },
  "daily_visibility": [
    { "date": "2026-08-11", "total_checks": 96, "mentions": 41, "visibility_pct": 42.7 }
  ],
  "brand_trends": [
    { "date": "2026-08-11", "brand_name": "Example Client", "mention_count": 41 }
  ]
}

Brand names are normalized server-side, so variants of your brand (e.g. "Example Client" and "Example Client Legal Group") are merged into one series.


8b. GET /projects/:id/visibility

Daily brand visibility broken down by model/source — the pre-aggregated series behind the "Model Visibility" chart. No pagination, one request covers the whole date range.

Query parameters: start_date, end_date, and optional source (comma-separated list to filter, e.g. source=chatgpt,perplexity). Both canonical values (chatgpt, google_ai_overview, etc.) and legacy internal labels (openai, google_ai_overviews) are accepted.

{
  "project": { "id": "0d5f...", "business_name": "Example Client" },
  "date_range": { "start": "...", "end": "..." },
  "overall": { "total_checks": 2880, "mentioned_checks": 601, "visibility_pct": 20.9 },
  "by_source": [
    { "source": "google_ai_overview", "total_checks": 620, "mentioned_checks": 197, "visibility_pct": 31.8 },
    { "source": "google_ai_mode", "total_checks": 601, "mentioned_checks": 170, "visibility_pct": 28.3 },
    { "source": "chatgpt", "total_checks": 615, "mentioned_checks": 122, "visibility_pct": 19.8 }
  ],
  "daily": [
    { "date": "2026-08-11", "source": "chatgpt", "total_checks": 21, "mentioned_checks": 4, "visibility_pct": 19.0 }
  ]
}

by_source gives the period totals (the horizontal bars); daily gives one row per date × source for time-series charts. Percentages are computed from the summed counts, not averaged across days.


9. GET /projects/:id/competitors

Competitor leaderboard for the date range. No pagination.

Brand-name variants are collapsed server-side (e.g. "Greene Broillet", "Greene Broillet & Wheeler" and "Greene Broillet & Wheeler, LLP" return as one canonical brand), matching the dashboard exactly. The same collapsing applies to brand_trends in /stats.

Each brand is returned as multiple bucketed rows: one for the current window, one for the previous comparison window, and one per AI source. To get the overall leaderboard, filter to bucket === "current". Do not sum across buckets or you will double- or triple-count mentions.

{
  "project": { "id": "0d5f...", "business_name": "Example Client" },
  "date_range": { "start": "...", "end": "..." },
  "competitors": [
    {
      "brand_name": "Example Client",
      "is_target": true,
      "bucket": "current",
      "mention_count": 412,
      "visibility_pct": 42.7,
      "avg_position": 2.1
    },
    {
      "brand_name": "Example Client",
      "is_target": true,
      "bucket": "previous",
      "mention_count": 380,
      "visibility_pct": 41.2,
      "avg_position": 2.2
    },
    {
      "brand_name": "Example Client",
      "is_target": true,
      "bucket": "src:chatgpt",
      "mention_count": 122,
      "visibility_pct": 19.8,
      "avg_position": 2.0
    },
    {
      "brand_name": "Competitor A",
      "is_target": false,
      "bucket": "current",
      "mention_count": 388,
      "visibility_pct": 40.2,
      "avg_position": 2.6
    }
  ]
}

Your own brand appears once per bucket with is_target: true; all detected variants are merged into that row. To reproduce the dashboard "Top Brands" table, use only rows where bucket is current.

Note on mention counts: the mention_count for your target brand in the current bucket should match the sum of mentions from /projects/:id/stats for the same date range. If you need a single "total mentions" number, use /stats; /competitors is meant for breakdowns and comparisons.


10. GET /projects/:id/prompts/stats

Per-prompt performance for the date range — the data behind the Tracked Prompts report. No pagination.

Query parameters: start_date, end_date, and optional source (defaults to all; accepts the canonical source values below). model is accepted as an alias for source.

{
  "project": { "id": "0d5f...", "business_name": "Example Client" },
  "date_range": { "start": "...", "end": "..." },
  "source": "all",
  "data": [
    {
      "prompt_id": "b91e...",
      "prompt_text": "best example service in example city",
      "tags": ["core", "local"],
      "excluded_from_visibility": false,
      "total_checks": 120,
      "mentions": 47,
      "excluded_checks": 0,
      "visibility_pct": 39,
      "trend": [{ "date": "2026-08-01", "mention_percent": 42 }],
      "top_competitors": [{ "brand_name": "Competitor A", "mentions": 61 }],
      "query_fanouts": ["example service near me"],
      "sources": ["chatgpt", "perplexity", "gemini"]
    }
  ]
}

11. GET /projects/:id/sentiment

Brand sentiment for the date range — overall, versus the previous equal-length period, per AI source, per day, per category and by theme. Only answers where your brand was mentioned and scored are included. No pagination.

This endpoint is served by the same shared sentiment summary that powers the dashboard, project sentiment page, web reports and PDF reports, so the numbers always match the product.

Query params: start_date, end_date, and optional source (chatgpt, perplexity, gemini, google_ai_overview, google_ai_mode, or all). model is accepted as an alias for source.

Scores range from -1 to 1. Mentions fall into four buckets: positive, negative, neutral and mixed (both clearly positive and clearly negative signals in the same answer). average_score excludes mixed and unclear mentions.

{
  "project": { "id": "0d5f...", "business_name": "Example Client" },
  "date_range": { "start": "...", "end": "..." },
  "source": "all",
  "summary": {
    "total": 261,
    "positive": 234,
    "neutral": 25,
    "negative": 1,
    "mixed": 1,
    "unclear": 0,
    "average_score": 0.6474,
    "positive_pct": 89.7,
    "negative_pct": 0.4,
    "neutral_pct": 9.6,
    "mixed_pct": 0.4
  },
  "previous_period": {
    "total": 240,
    "positive": 210,
    "neutral": 28,
    "negative": 2,
    "mixed": 0,
    "average_score": 0.6301
  },
  "change": {
    "total": 21,
    "positive": 24,
    "negative": -1,
    "average_score": 0.0173,
    "positive_pct": 2.2,
    "negative_pct": -0.4
  },
  "by_source": [
    {
      "source": "chatgpt",
      "total": 81,
      "positive": 74,
      "neutral": 7,
      "negative": 0,
      "mixed": 0,
      "average_score": 0.6785,
      "positive_pct": 91.4
    }
  ],
  "daily": [
    {
      "date": "2026-08-07",
      "total": 45,
      "positive": 39,
      "neutral": 6,
      "negative": 0,
      "mixed": 0,
      "average_score": 0.6369
    }
  ],
  "by_category": [
    { "category": "reputation", "total": 62, "positive": 58, "neutral": 4, "negative": 0, "mixed": 0, "average_score": 0.71 }
  ],
  "top_themes": [
    { "theme": "Strong client reviews", "total": 34, "positive": 33, "neutral": 1, "negative": 0, "mixed": 0, "average_score": 0.74 }
  ]
}

Errors

StatusMeaning
400Bad request — missing or invalid parameters
401Unauthorized — missing, invalid, expired, or revoked API key
403Forbidden — API access not enabled for this organization's plan
404Not found — unknown endpoint, or resource not in your organization
429Too many requests — daily rate limit exceeded
504Gateway timeout — query took too long; narrow the date range or retry
500Internal server error

All errors share the same shape:

{ "error": "Project not found or not accessible" }

Examples

cURL

curl -H "Authorization: Bearer $HOVERBOARD_API_KEY" \
  "https://auth.gethoverboard.com/functions/v1/api-gateway/projects?limit=50"

Date-filtered results

curl -H "Authorization: Bearer $HOVERBOARD_API_KEY" \
  "https://auth.gethoverboard.com/functions/v1/api-gateway/projects/PROJECT_ID/results?start_date=2026-07-01&end_date=2026-07-31&limit=500"

Node (fetch)

const BASE = "https://auth.gethoverboard.com/functions/v1/api-gateway";

async function hb(path, params = {}) {
  const url = new URL(BASE + path);
  Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, String(v)));
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.HOVERBOARD_API_KEY}` },
  });
  if (!res.ok) throw new Error((await res.json()).error);
  return res.json();
}

const { data: projects } = await hb("/projects");
const stats = await hb(`/projects/${projects[0].id}/stats`, {
  start_date: "2026-08-01",
});

Python

import os, requests

BASE = "https://auth.gethoverboard.com/functions/v1/api-gateway"
HEADERS = {"Authorization": f"Bearer {os.environ['HOVERBOARD_API_KEY']}"}

def hb(path, **params):
    r = requests.get(BASE + path, headers=HEADERS, params=params, timeout=60)
    r.raise_for_status()
    return r.json()

projects = hb("/projects")["data"]
sources = hb(f"/projects/{projects[0]['id']}/sources", limit=200)

Paginating everything

async function all(path, params = {}) {
  const out = [];
  let offset = 0;
  const limit = 1000;
  for (;;) {
    const page = await hb(path, { ...params, limit, offset });
    out.push(...page.data);
    if (page.data.length < limit) break;
    offset += limit;
  }
  return out;
}

Best practices