Partner API

Authenticated partner API guide

Retrieve completed visibility scores, review historical trends, or list the brands your partner token is authorized to access.

Versioning and compatibility

The partner endpoints use unversioned /api/partner/... paths. There is no path, header, or query parameter for selecting an API version. The OpenAPI document's info.version value, 1.0.0, is specification metadata; it does not version these request URLs or negotiate a version.

This guide and the OpenAPI document describe the current contract. No general backward-compatibility window or deprecation or sunset schedule is documented or promised.

Quickstart

Partner tokens use the partner_... format. Send your provisioned token in the Authorization header as Bearer <partner-token>, starting with the brand listing endpoint:

curl https://aisearchstackhub.ai/api/partner/brands \
  -H 'Authorization: Bearer <partner-token>'

JavaScript fetch

(async () => {
const apiOrigin = 'https://aisearchstackhub.ai';
const partnerToken = '<partner-token>';

const response = await fetch(apiOrigin + '/api/partner/brands', {
  headers: {
    Authorization: 'Bearer ' + partnerToken,
  },
});
const body = await response.json();

if (!response.ok) {
  throw new Error(body.error && body.error.message);
}

for (const brand of body.brands) {
  console.log(brand.brand_id, brand.name);
}
})().catch((error) => console.error(error.message));

Expected 200 OK response

{
  "brands": [
    {
      "brand_id": "123e4567-e89b-12d3-a456-426614174000",
      "name": "Example Brand",
      "monitoring_enabled": true,
      "latest_visibility_at": "2026-09-01T09:30:00.000Z"
    }
  ]
}

Use a returned brand_id with the visibility endpoints to retrieve the latest or historical visibility data.

Authentication

Every request must include an active partner token in an HTTP Authorization header using the Bearer scheme:

Authorization: Bearer <partner-token>

The token must be active, unrevoked, and unexpired. Visibility requests additionally require an explicit scope for the requested brand; the collection endpoint returns only brands explicitly scoped to that token. Query-string tokens, cookies, and Basic authentication are not accepted.

Troubleshooting authentication. A missing or unparsable Authorization: Bearer <token> header returns 401 partner_token_required. A parsed but malformed, unknown, inactive, revoked, or expired token returns 401 partner_token_invalid. Token lookup failures use that same response; there is no separate availability status for them. A valid token without access to the requested brand returns 403 brand_access_denied. After authentication, route failures return 500 internal_error; an authorized brand with no completed visibility report returns 404 visibility_not_found.

Keep tokens secure. Raw partner tokens are issued once when provisioned. Store the value in a secrets manager and never commit it, place it in a URL, or expose it in client-side code or logs.

Rate limits

The three partner operations—GET /api/partner/brands, GET /api/partner/brands/:brandId/visibility, and GET /api/partner/brands/:brandId/visibility/history—share one fixed-window bucket for each authenticated token. The production allowance is 60 requests per 60-second window per authenticated token. A burst can consume the whole window; requests beyond the allowance return 429 Too Many Requests before downstream route or database work runs.

The bucket is process-local and in-memory, not a deployment-wide or IP-based quota. Separate Node processes or instances can therefore have separate buckets, and restarting a process clears its bucket. Only requests that pass authentication reach the limiter; scoped visibility requests must also pass UUID validation first, so missing or invalid credentials and invalid scoped brand IDs do not consume a bucket entry.

A throttled response includes Retry-After and JSON error.retry_after. Both contain the runtime-calculated number of seconds until the current fixed window expires; wait at least that long before retrying. The 12-second value shown below is illustrative only, not a guaranteed response value. The implementation does not emit remaining-quota, absolute-reset, or X-RateLimit-* headers, and it provides no separate reset timestamp or reset endpoint.

Wait at least the indicated delay before retrying the same idempotent GET request. No retry behavior is documented for other statuses.

HTTP/1.1 429 Too Many Requests
Retry-After: 12

{
  "error": {
    "code": "rate_limited",
    "message": "Too many partner API requests",
    "retry_after": 12
  }
}

This example's 12 is illustrative; use the actual Retry-After header and error.retry_after value returned at runtime.

Request

List accessible brands

GET/api/partner/brands

This endpoint has no required path or query parameters and accepts no request body. It returns every brand explicitly scoped to the authenticated token in one response, ordered deterministically by brand_id. The response shape is { "brands": [...] }. It has no supported pagination parameters or pagination metadata, so callers do not make follow-up page requests.

curl https://aisearchstackhub.ai/api/partner/brands \
  -H 'Authorization: Bearer <partner-token>'

Expected 200 OK response

{
  "brands": [
    {
      "brand_id": "123e4567-e89b-12d3-a456-426614174000",
      "name": "Example Brand",
      "monitoring_enabled": true,
      "latest_visibility_at": "2026-09-01T09:30:00.000Z"
    }
  ]
}

Concise 4xx example

curl https://aisearchstackhub.ai/api/partner/brands \
  -H 'Authorization: Bearer invalid-token'

HTTP/1.1 401 Unauthorized
{
  "error": {
    "code": "partner_token_invalid",
    "message": "Partner token is invalid"
  }
}

Latest visibility

GET/api/partner/brands/:brandId/visibility

brandId is the required UUID path parameter for the brand. The endpoint accepts no request body or query parameters.

curl https://aisearchstackhub.ai/api/partner/brands/<brand-uuid>/visibility \
  -H 'Authorization: Bearer <partner-token>'

JavaScript fetch

(async () => {
const apiOrigin = 'https://aisearchstackhub.ai';
const partnerToken = '<partner-token>';
const brandId = '<brand-uuid>'; // Use a brand_id from the listing response.

// No request body or query parameters are required.
const response = await fetch(
  apiOrigin + '/api/partner/brands/' + encodeURIComponent(brandId) + '/visibility',
  {
    headers: {
      Authorization: 'Bearer ' + partnerToken,
    },
  }
);
const body = await response.json();

if (!response.ok) {
  throw new Error(body.error && body.error.message);
}

console.log(body.visibility_score, body.score_timestamp, body.score_week);
})().catch((error) => console.error(error.message));

One latest report. This endpoint returns a single object for the latest completed visibility_weekly report. It is not a paginated collection: there are no page, limit, cursor, or historical-range parameters.

Expected 200 OK response

{
  "brand_id": "123e4567-e89b-12d3-a456-426614174000",
  "visibility_score": 74,
  "score_timestamp": "2026-09-01T09:30:00.000Z",
  "score_week": "2026-08-31",
  "engine_breakdown": {
    "ChatGPT": {
      "visibility_pct": 80,
      "position_avg": 2,
      "share": { "rival.example": 20 }
    },
    "Claude": { "visibility_pct": 70, "position_avg": 3, "share": {} },
    "Perplexity": { "visibility_pct": 75, "position_avg": 2, "share": {} },
    "Gemini": { "visibility_pct": 71, "position_avg": 4, "share": {} }
  }
}

Concise 4xx example

curl https://aisearchstackhub.ai/api/partner/brands/not-a-uuid/visibility \
  -H 'Authorization: Bearer <partner-token>'

HTTP/1.1 400 Bad Request
{
  "error": {
    "code": "invalid_brand_id",
    "message": "brandId must be a UUID"
  }
}

Historical visibility

GET/api/partner/brands/:brandId/visibility/history

brandId is the required UUID path parameter for the brand. The endpoint returns a direct JSON array of completed visibility_weekly reports, ordered newest first. It accepts only the optional from, to, and limit filters. Inclusive from and to values must be real YYYY-MM-DD calendar dates with from on or before to. limit is a result cap, not a page size: it defaults to 12 and accepts integers from 1 through 100.

The success body has no page, offset, cursor, total-count, or next-page metadata, and there is no page-based continuation mechanism. To retrieve longer histories, make bounded, non-overlapping date-window requests small enough to fit the 100-result maximum, then combine the returned arrays. Because both date filters are inclusive, do not reuse a boundary date in adjacent windows.

curl 'https://aisearchstackhub.ai/api/partner/brands/<brand-uuid>/visibility/history?from=2026-01-01&to=2026-09-01&limit=12' \
  -H 'Authorization: Bearer <partner-token>'

Expected 200 OK response

[
  {
    "brand_id": "123e4567-e89b-12d3-a456-426614174000",
    "visibility_score": 74,
    "score_timestamp": "2026-09-01T09:30:00.000Z",
    "score_week": "2026-08-31",
    "engine_breakdown": { "ChatGPT": { "visibility_pct": 80, "position_avg": 2, "share": {} } }
  }
]

Concise 4xx example

curl 'https://aisearchstackhub.ai/api/partner/brands/<brand-uuid>/visibility/history?limit=0' \
  -H 'Authorization: Bearer <partner-token>'

HTTP/1.1 400 Bad Request
{
  "error": {
    "code": "invalid_limit",
    "message": "limit must be an integer between 1 and 100"
  }
}

Successful response

A successful response is 200 OK and contains these five fields:

brand_id

Type: string (UUID); always present. Example: 123e4567-e89b-12d3-a456-426614174000.

visibility_score

Type: integer from 0 to 100; always present. Example: 74.

score_timestamp

Type: UTC ISO 8601 string; always present. Example: 2026-09-01T09:30:00.000Z.

score_week

Type: string date in YYYY-MM-DD format; always present. Example: 2026-08-31.

engine_breakdown

Type: object keyed by engine name; always present. Example key: ChatGPT.

Engine fields

engine_breakdown is keyed by the engines included in the report—normally ChatGPT, Claude, Perplexity, and Gemini. Every engine item includes the first three fields below; position_delta is conditional.

visibility_pct

Type: integer from 0 to 100; always present. Example: 80.

position_avg

Type: integer or null; always present, and null when no integer positions are available. Example: 2 or null.

share

Type: object mapping competitor domains to numeric percentages; always present. Examples: {} or {"rival.example":20}.

position_delta

Type: optional integer; omitted unless current and prior position_avg are both integers. Examples: 1 (worsened) or -1 (improved).

{
  "brand_id": "123e4567-e89b-12d3-a456-426614174000",
  "visibility_score": 74,
  "score_timestamp": "2026-09-01T09:30:00.000Z",
  "score_week": "2026-08-31",
  "engine_breakdown": {
    "ChatGPT": {
      "visibility_pct": 80,
      "position_avg": 2,
      "position_delta": 1,
      "share": { "rival.example": 20 }
    },
    "Claude": { "visibility_pct": 70, "position_avg": 3, "position_delta": -1, "share": {} },
    "Perplexity": { "visibility_pct": 75, "position_avg": null, "share": {} },
    "Gemini": { "visibility_pct": 71, "position_avg": 4, "share": {} }
  }
}

Historical response

The history endpoint returns a direct array, including [] when the authorized brand has no matching reports. Each item uses the same visibility-report shape and engine fields as the latest endpoint, including nullable position_avg and conditional position_delta. Items are ordered by score_week, then timestamp, newest first:

[
  {
    "brand_id": "123e4567-e89b-12d3-a456-426614174000",
    "visibility_score": 74,
    "score_timestamp": "2026-09-01T09:30:00.000Z",
    "score_week": "2026-08-31",
    "engine_breakdown": { "ChatGPT": { "visibility_pct": 80, "position_avg": 2, "share": {} } }
  }
]

Accessible brand collection

A successful collection response is 200 OK with a stable brands envelope. Each item contains the scoped brand's identity and monitoring status:

brands

Type: array of brand objects; always present, including when empty. Example: [].

brand_id

Type: string (UUID); always present on each item. Example: 123e4567-e89b-12d3-a456-426614174000.

name

Type: string or null; always present and nullable. Example: null.

monitoring_enabled

Type: boolean; always present. Example: true.

latest_visibility_at

Type: UTC ISO 8601 string or null; always present, and null when no report exists. Example: 2026-09-01T09:30:00.000Z or null.

{
  "brands": [
    {
      "brand_id": "123e4567-e89b-12d3-a456-426614174000",
      "name": "Example Brand",
      "monitoring_enabled": true,
      "latest_visibility_at": "2026-09-01T09:30:00.000Z"
    },
    {
      "brand_id": "123e4567-e89b-12d3-a456-426614174001",
      "name": null,
      "monitoring_enabled": false,
      "latest_visibility_at": null
    }
  ]
}

A token with no scoped brands receives exactly { "brands": [] }.

Errors

Ordinary failures use { "error": { "code": "...", "message": "..." } }. Only 429 responses add error.retry_after and the Retry-After header; both contain the same runtime-calculated number of seconds until the current fixed window expires.

StatusCodeMessageApplies to and request guidance
400invalid_brand_idbrandId must be a UUIDLatest and history: change the path parameter to a UUID.
400invalid_from_datefrom must be a valid YYYY-MM-DD dateHistory: change from to a real calendar date in YYYY-MM-DD format.
400invalid_to_dateto must be a valid YYYY-MM-DD dateHistory: change to to a real calendar date in YYYY-MM-DD format.
400invalid_date_rangefrom must be on or before toHistory: change the inclusive range so from is not after to.
400invalid_limitlimit must be an integer between 1 and 100History: change limit to an integer from 1 through 100.
401partner_token_requiredAuthorization Bearer token requiredAll operations: send a valid Authorization: Bearer <token> header.
401partner_token_invalidPartner token is invalidAll operations: repair or replace the unknown, inactive, revoked, or expired token.
403brand_access_deniedPartner token cannot access this brandLatest and history: use a brand within the token's scope.
404visibility_not_foundNo completed weekly visibility report foundLatest only: the authorized brand has no completed weekly report yet.
429rate_limitedToo many partner API requestsAll operations: wait at least the runtime-calculated seconds in Retry-After and error.retry_after before retrying the same GET.
500internal_errorUnable to list accessible brandsList brands: the route encountered an unexpected server failure; no request change is defined.
500internal_errorUnable to fetch visibility dataLatest visibility: the route encountered an unexpected server failure; no request change is defined.
500internal_errorUnable to fetch visibility historyHistory: the route encountered an unexpected server failure; no request change is defined.

Request changes. Correct the request for 400, repair or replace credentials for 401, and verify brand scope for 403. A 404 means the authorized brand has no completed weekly report yet. For 429, wait for the returned delay before retrying the same GET. The API documents no client recovery behavior for 500.

401 Unauthorized — invalid credentials

HTTP/1.1 401 Unauthorized

{
  "error": {
    "code": "partner_token_invalid",
    "message": "Partner token is invalid"
  }
}

The credentials are missing, malformed, invalid, revoked, or expired. Repair or replace them before sending another request; do not retry unchanged credentials.

403 Forbidden — brand scope denied

HTTP/1.1 403 Forbidden

{
  "error": {
    "code": "brand_access_denied",
    "message": "Partner token cannot access this brand"
  }
}

The token is valid but is not scoped to the requested brand. Verify the requested brand scope before retrying.

404 Not Found — no completed report

HTTP/1.1 404 Not Found

{
  "error": {
    "code": "visibility_not_found",
    "message": "No completed weekly visibility report found"
  }
}

The brand is authorized, but no completed weekly report exists yet. Treat this as an authorized brand with no completed weekly report yet, not as an authentication or scope failure.

429 Too Many Requests — rate limited

HTTP/1.1 429 Too Many Requests
Retry-After: 12

{
  "error": {
    "code": "rate_limited",
    "message": "Too many partner API requests",
    "retry_after": 12
  }
}

The 12-second values are illustrative only. The implementation supplies the current runtime-calculated delay; it does not supply remaining-quota or absolute-reset metadata, any X-RateLimit-* headers, or a separate reset timestamp or endpoint.