# Signal API Documentation

Source: https://ceyo.ai/docs/signal

Complete documentation for provisioning, embedding, and integrating with the Signal API.

---

# Introduction

Source: https://ceyo.ai/docs/signal/guides-overview

### Overview

The Signal API lets you manage workspaces, packages, projects, locations, visibility data, prompts, competitors, actions, and listings through one API.

**Base URL:** `https://api.signal.ceyo.ai/v1`

Your API key selects the workspace automatically, so workspace IDs are not included in request paths.

### Quickstart

Add your API key to the Authorization header, then make a request to the resource you need.

#### 1\. Confirm your workspace

```
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/workspace' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

#### 2\. List projects

```
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects?page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

Successful requests return JSON. Continue with the API reference for endpoint parameters and response fields.

### Authentication

Send your platform API key with every request using either header:

```
Authorization: Bearer ceyo_platform_...
```

```
X-Api-Key: ceyo_platform_...
```

Keep API keys on your server and never include them in browser or mobile application code. Requests with an absent, invalid, expired, or revoked key return `401 Unauthorized`. Use only one authentication header; conflicting credentials are rejected.

---

# Using the API

Source: https://ceyo.ai/docs/signal/using-the-api

### API conventions

**JSON**

Request and response bodies use `application/json`.

**Identifiers**

Resource IDs are UUIDs. Project and location paths also accept configured external IDs.

**Dates**

Dates use `YYYY-MM-DD`. Timestamps use ISO 8601 in UTC.

**Pagination**

List endpoints use `page` and `per_page` and return a `pagination` object.

**Idempotency**

Endpoints that support safe retries accept an `Idempotency-Key` header.

### Site Audit IP allowlisting

Site Audit sends crawler requests from a stable production egress IP. If your firewall or bot protection restricts automated traffic, allowlist this address for `GET` and `HEAD` requests.

**Production IP**

`34.248.152.92`

**User agent**

`CeyoSiteAudit/1.0 (+https://www.ceyo.ai/features/site-audit)`

### Rate limits

Standard public API traffic is limited to 600 requests per minute per API key. Source-IP limits and lower limits for sensitive or write-heavy operations may also apply.

When a limit is reached, the API returns `429 Too Many Requests`. Wait for the number of seconds in the `Retry-After` header before retrying. Use exponential backoff and reuse the same `Idempotency-Key` when retrying an idempotent operation.

```
HTTP/1.1 429 Too Many Requests
Retry-After: 42

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests were made.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

### Errors

Every error uses one nested `error` envelope containing a stable code, readable message, details, and request identifier. `details` is always an object, array, or null.

```
{
  "error": {
    "code": "not_found",
    "message": "The requested resource was not found.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

---

# Platform setup

Source: https://ceyo.ai/docs/signal/platform-setup-guides

### Platform setup

Set up the resource structure and packages that customer data will use before adding prompts, competitors, or customer access.

### Understand the resource hierarchy

The API key selects one workspace. Packages, projects, and locations created with that key stay inside that workspace.

```
Workspace
├── Package (project)
│   └── Standard project
└── Locations-only project
    ├── Location → Package (location)
    └── Location → Package (location)
```

**Workspace**

The tenant boundary. It comes from the API key and never appears in public API paths.

**Package**

Defines visibility limits, models, cadence, enabled features, and customer access for one resource type.

**Project**

Represents a tracked brand or acts as the container for a location portfolio.

**Location**

Represents one local entity under a project and uses its own location package.

> **Stable partner identifiers**
>
> Set `external_id` from your own system when creating projects and locations. You can use it in supported lookup and path parameters instead of storing only Signal UUIDs.

### Choose project or location scope

Choose the scope from the data you need to track. This decision also determines where packages, prompts, competitors, and visibility data live.

**Standard project**

Use for one brand or website with project-level visibility. The project requires a project package and website.

**Locations-only project**

Use as a portfolio container when each location is tracked and packaged independently. The container has no project package.

**Location scope**

Use for one store, office, service area, or other local entity. Location API paths are always nested under their project.

> **Immutable choice**
>
> `project_mode` and package assignment cannot be changed after creation. Create a new resource if the hierarchy needs to change.

### Configure packages

Create packages before provisioning resources. A project package can be assigned only to standard projects. A location package can be assigned only to locations.

**Visibility**

Select a cadence, prompt limit, and at least one supported model.

**Features**

Enable diagnosis, agents, analytics, prompt volume, or fan-out only where the customer needs them.

**Customer access**

Set `pricing.frontend_delivery_enabled` for hosted Signal access and redirect login links.

**Listings**

Location packages include listings automatically. Do not send the derived `listings.enabled` field.

```json
{
  "package_type": "project",
  "configuration": {
    "visibility": {
      "cadence": "weekly",
      "prompt_limit": 100,
      "model_keys": ["chatgpt", "claude"]
    },
    "diagnosis": {
      "enabled": true
    },
    "pricing": {
      "frontend_delivery_enabled": true
    }
  }
}
```

> **Validate before creating**
>
> Send the package type and configuration to `POST /packages/preview`. After validation, create the package with `POST /packages`. To change a package configuration later, clone it and assign the new package to newly created resources.

### Provision a project

Use a workspace-scoped key with `projects:write`. Send a stable external ID so future synchronization does not depend on the project name.

#### Standard project

Supply `package_id` and `website`. Set `start: true` to begin automatic onboarding immediately after the project is created.

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Acme Europe",
    "external_id": "customer-acme-eu",
    "project_mode": "standard",
    "package_id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
    "website": "https://acme.example",
    "country_code": "NL",
    "language": "en",
    "start": true
  }'
```

#### Locations-only project

Omit `package_id`. The locations created under this project select their own location packages.

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Acme Locations",
    "external_id": "customer-acme-locations",
    "project_mode": "locations_only",
    "start": false
  }'
```

> **Start behavior**
>
> With `start: false`, automatic onboarding is skipped. The resource remains available for manual configuration through the supported settings, prompt, and competitor APIs.

### Provision a location portfolio

Create the locations-only project first, then create each location beneath its returned project ID or external ID.

**1\. Select a package**

Use an active `location` package from the same workspace.

**2\. Preserve your identity**

Set a unique location `external_id`, such as a store or account identifier.

**3\. Send local data**

Send a `google_place_id` when you have one. Otherwise, send your own local data; `city` and `country_code` are enough to use `start: true`.

**4\. Choose onboarding**

Set `start` independently for each location and poll the returned `onboarding_operation.status_url`.

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/locations' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Acme Amsterdam",
    "external_id": "store-nl-ams-01",
    "package_id": "66aeb6f8-a353-4acf-a914-e288ca0e4341",
    "website": "https://acme.example/amsterdam",
    "city": "Amsterdam",
    "country_code": "NL",
    "language": "nl",
    "start": true
  }'
```

Without a place ID, onboarding looks for a clear Google match. It keeps your supplied fields, and continues without listing analysis if no match is found. Repeat the request for each location, then use `GET /projects/{project_id}/locations` or the locations overview endpoint to reconcile the completed portfolio.

---

# Visibility workflows

Source: https://ceyo.ai/docs/signal/visibility-workflow-guides

### Visibility workflows

Configure what Signal monitors, review the resulting measurements, and inspect the underlying responses and citations.

### Set up visibility tracking

Decide whether visibility belongs to a standard project or to an individual location. A locations-only project is a container and does not have project-level visibility.

**1\. Configure the package**

Select the visibility cadence, prompt limit, and model keys before assigning the package to a project or location.

**2\. Choose automatic or manual setup**

Set `start: true` when creating the resource to queue automatic onboarding. Use `start: false` when you will create topics, prompts, and competitors through the API.

**3\. Use the correct scope**

Project paths start with `/projects/{project_id}`. Location paths add `/locations/{location_id}` before the visibility resource.

**4\. Grant only needed capabilities**

Use `prompts:read` and `prompts:write`, `competitors:read` and `competitors:write`, and `visibility:read` for the corresponding operations.

> **Automatic setup is asynchronous**
>
> Automatic onboarding creates topics, active prompts, and tracked competitors, then dispatches an initial visibility run. Do not assume the results are present when the create-resource response returns.

### Manage topics and prompts

Create a durable topic first, then add the questions you want to monitor. Each prompt belongs to one topic in the same visibility scope.

**Create topics**

Send a name to `POST /visibility/topics`. Use the returned topic ID when creating prompts.

**Create prompts**

`POST /visibility/prompts` creates an active prompt and starts a visibility run across the package's enabled models.

**Add prompts in bulk**

`POST /visibility/prompts/bulk_create` accepts 1–100 rows atomically. One invalid row or capacity failure rejects the request.

**Pause without deleting history**

Archive the prompt to stop active monitoring. Restore it with `POST /visibility/prompts/{prompt_id}/restore`; restoring does not start a visibility run.

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/prompts' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{
    "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
    "content": "Which platforms help measure visibility in AI answers?",
    "category": "general"
  }'
```

> **Permanent deletion is separate**
>
> `DELETE /visibility/prompts/{prompt_id}/permanent` starts irreversible background deletion. Prefer archive when the prompt may be needed again.

### Manage competitors and suggestions

Keep tracked competitors separate from candidates. Prompt suggestions also remain separate from active prompts until you explicitly track them.

**Tracked competitors**

List with `GET /competitors?state=tracked` or create one with `POST /competitors`. A tracked competitor requires a domain.

**Suggested competitors**

List with `GET /competitors?state=suggested`. Track one by patching `competitor_state` to `tracked`, or dismiss it with `DELETE /competitors/{competitor_id}`.

**Suggested prompts**

Read `GET /visibility/suggested_prompts`. Use the suggestion's `/track` or `/dismiss` action after review.

**Capacity checks**

Tracking a competitor requires an available tracked slot. Tracking a suggested prompt consumes active and daily prompt capacity and starts a visibility run.

> **Removal preserves stored results**
>
> Removing a competitor is a soft dismissal. Its stored visibility evidence remains available, and creating the same normalized domain or case-insensitive name later can reactivate it.

### Read visibility results

Start with the summary, then use competitor and model endpoints when you need a breakdown. All reads require `visibility:read`.

**Overall result**

`GET /visibility/summary` returns primary mentions, visibility percentage, average position, and citation count.

**Competitor comparison**

`GET /visibility/competitors` compares the primary entity with tracked competitors. Add `/timeseries` for dated mention and position series.

**Model trends**

`GET /visibility/model-trends` returns daily and range-level primary-brand visibility by model, plus an aggregate.

**Filter consistently**

Use `start`, `end`, `models`, `topic_ids`, or `prompt_id`. The default window is seven days and the maximum is three months.

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/summary?start=2026-07-24&end=2026-07-30&models=chatgpt,perplexity' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

> **Read percentages as 0–100 values**
>
> Visibility is the share of matching responses that mention the primary entity. Average position uses only primary mentions with a detected position; citation totals count occurrences.

### Work with responses and citations

Use response and citation endpoints to inspect the evidence behind aggregate visibility metrics.

**Inspect model responses**

`GET /prompts/{prompt_id}/responses` returns successful model responses, newest first, with brand presence, position, sentiment, entities, and a citation preview.

**Get all citations for a prompt**

`GET /prompts/{prompt_id}/citations` aggregates repeated citations across the selected date range. The separate responses endpoint previews at most three citations per model response.

**Review citations across the scope**

`GET /citations` can group results by page or domain and supports topic, model, search, and domain filters.

**Plot citation activity**

`GET /citations/timeseries` returns daily totals, per-model frequencies, and top citations.

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/prompts/{prompt_id}/responses?start=2026-07-24&end=2026-07-30' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

> **Use the same scope and filters**
>
> Response and citation paths sit under the selected project or location. Their date window defaults to seven days, can span up to three months, and can be narrowed with model keys.

---

# Customer access

Source: https://ceyo.ai/docs/signal/customer-access-guides

### Customer access

Give each customer the correct project or location scope through an embedded Signal application or a short-lived link to hosted Signal.

### Choose embedded or hosted access

Both access modes use an embedded identity and its project or location grants. Choose where the customer should work.

**Embedded access**

Mount Signal inside your product with a short-lived embed session. Your backend mints the token, and the browser receives only that token. Embed roles are `viewer` or `editor`.

**Hosted access**

Create a redirect login link when the customer should open the hosted Signal application. The resource package must enable `pricing.frontend_delivery_enabled`.

**Origin configuration**

For embeds, add each HTTPS host-page origin to the issuing API key's `allowed_origins`. The same allowlist validates an optional hosted-login `return_url`.

> **Keep the API key server-side**
>
> Create embed sessions and login links from your authenticated backend. Never place a Signal API key in browser code or loader options.

### Provision identities and access grants

Use a workspace-scoped key with `identities:manage`. The external user ID is unique within the workspace, and repeating the request updates the same identity.

```curl
# Upsert an identity using your stable user ID.
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-42' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{
    "email": "user-42@example.com",
    "name": "User 42"
  }'

# Grant access to one project.
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-42/projects/customer-project/access' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{ "role": "viewer" }'
```

**Project grant**

Grants project access. It also applies when issuing a location-scoped session beneath that project.

**Location grant**

Use the nested `/projects/{project_id}/locations/{location_id}/access` path to restrict access to one location.

**Granted-location portfolio**

Mint with `location_scope: "granted"` and no `location_id` to expose active direct location grants in one project. Signal resolves them live, preserves each location's role, and ignores project grants for this scope. Revoking one grant removes only that location. The embed exposes Overview and Locations, not project-wide routes.

**Roles**

Grants accept `viewer`, `editor`, or `admin`. Embed sessions expose only viewer or editor; an admin grant resolves to editor in the iframe.

**Lifecycle**

Set an identity to `disabled` to block new access and revoke its active embed sessions. Delete a grant to remove that resource scope.

> **Optional inline embed provisioning**
>
> `POST /embed/sessions` can atomically upsert an identity, create a viewer or editor grant, and issue its first session through the `provision` object. With granted scope, use `provision.location_grants` to ensure one to 100 direct grants atomically. This requires a workspace-scoped key with both `embed_sessions:create` and `identities:manage`.

```json
{
  "external_user_id": "regional-manager-42",
  "project_id": "customer-project",
  "location_scope": "granted",
  "provision": {
    "location_grants": [
      { "location_id": "store-amsterdam", "role": "editor" },
      { "location_id": "store-utrecht", "role": "viewer" }
    ]
  }
}
```

### Create redirect login links

Use a key with `login_links:manage`. The identity must be active and already have access to the requested project or location, and the assigned package must enable customer access.

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/login-links' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{
    "external_user_id": "customer-user-42",
    "project_id": "customer-project",
    "expires_in": 900
  }'
```

**Lifetime**

`expires_in` defaults to 900 seconds and accepts 60 to 3,600 seconds.

**One-time use**

Send the returned `login_link.url` directly to the intended customer. Redemption marks it used, so it cannot be redeemed again.

**Destination**

Omit `redirect_path` to open the selected resource. Custom values must be safe paths within hosted Signal, not absolute URLs.

**Return URL**

An optional `return_url` must use HTTPS and an origin listed in the issuing API key's `allowed_origins`.

> **Inspect or revoke**
>
> Use `GET /login-links/{login_link_id}` to inspect status. Use `DELETE` on the same path to revoke a link while it is still pending. Treat the returned URL as a secret.

### Manage session renewal and revocation

Embed sessions default to 3,600 seconds and accept a TTL from 60 to 86,400 seconds. Refresh them through your backend; never expose the API key to the browser.

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embed/sessions/refresh' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header "Idempotency-Key: ${UNIQUE_REQUEST_ID}" \
  --data '{
    "session_token": "${CURRENT_SESSION_TOKEN}",
    "ttl_seconds": 3600
  }'
```

**Renew before expiry**

Implement the loader's `onTokenExpired` callback. Return the replacement token, or call `embed.updateToken()` after your backend refreshes it.

**Rotation**

A successful refresh exchanges the current token for a replacement. The exchanged token no longer authorizes embed API requests.

**Access checks**

Refresh and embed requests recheck the identity, resource, grant, role, and issuing API key. If refresh is rejected, mint a new session only after your application confirms the user still has access.

**Immediate revocation**

Disable the identity, revoke or change its grant, or revoke the issuing API key. Active embed requests then fail their live access check.

> **Use unique idempotency keys**
>
> Session create and refresh requests require an `Idempotency-Key`. Reuse it only when retrying the same operation with the same request body.

---

# Integration patterns

Source: https://ceyo.ai/docs/signal/integration-pattern-guides

### Integration patterns

Keep partner data synchronized while handling retries, pagination, background work, and resource deletion predictably.

### Synchronize resources with external IDs

Assign each project and location the stable identifier from your own system. Project external IDs are unique within the workspace. Location external IDs are unique within their parent project.

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/by-external-id/customer-acme-eu' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

**Use exact lookups**

External-ID lookups are case-sensitive. URL-encode the identifier in `/projects/by-external-id/{external_id}` or the corresponding nested location path.

**Use IDs in resource paths**

Project and location path parameters accept either a Signal UUID or the configured external ID. A location identifier is resolved only inside its parent project.

**Reconcile before writing**

Look up the resource, create it after a `404`, and patch only fields that changed when it already exists.

> **Changing an external ID**
>
> Project and location updates can replace `external_id`. Save the returned value before the next synchronization because the previous external-ID lookup will no longer match.

### Build idempotent provisioning flows

Choose one idempotency key for one intended operation and persist it with the job that sends the request.

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/bulk' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: sync-run-2026-08-04-001' \
  --data '{
    "projects": [
      {
        "name": "Acme Europe",
        "external_id": "customer-acme-eu",
        "package_id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
        "website": "https://acme.example",
        "start": false
      }
    ]
  }'
```

**Native support**

Bulk project creation, bulk location creation, embed session creation, and embed session refresh require an `Idempotency-Key` header.

**Retry with the same key**

Repeating the same request returns the original operation or response. Reusing its key with a different request returns `409 idempotency_conflict`.

**Client-managed create**

Single project and location create endpoints do not provide native idempotency keys. Look up a stable external ID before creating; if concurrent creation returns `409`, look up that external ID again.

> **Do not rotate keys during an uncertain retry**
>
> A new key represents a new intended native-idempotent operation. Keep the original key when a timeout or connection failure leaves the first request's result unknown.

### Paginate and poll efficiently

List endpoints use 1-based offset pages. The default page size is 25, the maximum is 100, and every response includes `page`, `per_page`, `total`, and `total_pages`.

```javascript
let page = 1;

while (true) {
  const response = await fetch(
    `https://api.signal.ceyo.ai/v1/projects?page=${page}&per_page=100&sort=created_at&direction=asc`,
    { headers: { Authorization: `Bearer ${process.env.SIGNAL_API_KEY}` } }
  );
  if (!response.ok) throw new Error(`List failed: ${response.status}`);

  const { projects, pagination } = await response.json();
  await synchronizePage(projects);

  if (page >= pagination.total_pages) break;
  page += 1;
}
```

> **Use a deterministic sort**
>
> Project and location lists use the resource ID as an ascending final tie-breaker. Keep the same filters, sort, and direction throughout one scan. A page beyond the result set returns an empty array.

Bulk creates return `202 Accepted` with a `status_url`. Poll that URL until the operation reaches a terminal status, then inspect every item.

Project and location creates with `start: true` return an `onboarding_operation`. Poll its `status_url` until it succeeds, completes partially, fails, or is cancelled.

```javascript
const terminal = new Set([
  "completed",
  "completed_with_errors",
  "failed",
]);

let operation;
do {
  const response = await fetch(
    `https://api.signal.ceyo.ai${statusUrl}`,
    { headers: { Authorization: `Bearer ${process.env.SIGNAL_API_KEY}` } }
  );
  if (!response.ok) throw new Error(`Poll failed: ${response.status}`);

  ({ bulk_operation: operation } = await response.json());
  if (!terminal.has(operation.status)) await new Promise((r) => setTimeout(r, 2000));
} while (!terminal.has(operation.status));

for (const item of operation.items) {
  if (item.status === "failed") recordFailure(item.index, item.error);
}
```

**Continue polling**

`pending` and `running` are non-terminal operation statuses.

**Stop polling**

`completed`, `completed_with_errors`, and `failed` are terminal statuses.

**Handle partial success**

One failed bulk item does not roll back successful items. Store each returned resource ID and retry failed records deliberately.

### Handle deletion and lifecycle changes

Project and location deletion is asynchronous. A successful request returns `202 Accepted` with no response body.

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/projects/customer-acme-eu' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"

# HTTP/1.1 202 Accepted
# No response body
```

**Project deletion**

The project enters a deleting state and disappears from ordinary public project reads while its locations and scoped resources are purged.

**Location deletion**

The location is archived and its visibility scope is made inactive before its location-scoped resources are purged.

**Repeated requests**

Repeating a delete while the same deletion is active is accepted. After the resource has been purged, its path returns `404`.

> **Track deletion in your system**
>
> The delete response does not include an operation ID or completion status. Record that deletion was requested. Do not treat a project `404` as proof that purging has finished, and retry a conflicting recreation later.

> **Lifecycle fields are read-only**
>
> Project and location updates reject `status`. Use the supported delete endpoint for removal and treat returned lifecycle status as server-managed state.

---

# Optimization workflows

Source: https://ceyo.ai/docs/signal/optimization-workflow-guides

### Optimization workflows

Turn generated recommendations and listing observations into trackable work, then read later measurements without treating them as guaranteed outcomes.

### Process recommended actions

Actions are generated from current findings. Use a key with `actions:read` to build a queue for either a project or a location.

**1\. Build the active queue**

List with `status=active` to include `todo` and `in_progress` actions. Narrow the queue by priority, type, effort, source category, topic, or search text.

**2\. Prioritize in context**

Compare `priority`, `estimated_impact`, `effort_level`, and `feasibility`. These fields support prioritization; they are not delivery or outcome guarantees.

**3\. Inspect the evidence**

Get the action before assigning work. The detail response adds supporting findings and may include a structured guide with implementation, validation, rollback, and expected-timeline guidance.

**4\. Preserve the scope**

Use the project action paths for project work. For location work, insert `/locations/{location_id}` before `/actions`; an action is available only in its own scope.

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/actions?status=active&priority=high&page=1' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/actions/{action_id}' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

> **Generated guidance can be absent**
>
> The public response returns `guide: null` when no guide is available. Your workflow should still use the action's description, recommendation, target, topics, and findings.

### Track action status and impact

Use a key with `actions:write` to synchronize work status. Mark completion only after the implementation and its validation steps are finished, because `completed_at` anchors impact measurement.

**todo**

Can move to `in_progress`, `completed`, or `dismissed`.

**in\_progress**

Can move back to `todo`, or forward to `completed` or `dismissed`. The first start time is retained.

**completed or dismissed**

Can be reopened to `todo`. Reopening clears the corresponding completion or dismissal time.

**resolved**

Is system-managed when supporting findings resolve. It cannot be selected or transitioned through the public API.

```curl
curl --request PATCH \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/actions/{action_id}' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{"status":"completed"}'
```

Read impact with `actions:read`. The endpoint examines completed actions in the requested date window and compares eligible visibility runs immediately before and after completion.

**pending**

The action has no eligible baseline run, or is awaiting a post-completion run. Read the returned `reason` instead of assuming measurement is available immediately.

**measured**

The response includes baseline, comparison, and deltas for visibility, citations, sentiment, and average position where values are available.

**Outcome**

`improved`, `unchanged`, or `declined` is derived from the visibility change, not from every returned metric.

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/actions/impact?start_on=2026-05-01&end_on=2026-07-30' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

> **Interpret impact carefully**
>
> Before-and-after measurements show correlation. They do not prove that the completed action was the only cause of the observed change.

### Monitor listing health

Listings are read-only and available only for locations. A key needs `listings:read`, and scheduled analysis requires a configured Google place and a location package with listings enabled.

**1\. Read the current overview**

Get `/listings/profile` for the configured listing identity, newest retained profile, and latest scan summary. A successful response can contain null values when listing data is unavailable.

**2\. Track scan completion**

List `/listings/scans` newest first. Scan statuses are `pending`, `running`, `succeeded`, `failed`, or `skipped`; fetch one scan for its scored checks when a result is available.

**3\. Triage open findings**

List `/listings/findings?status=open`, then filter by severity, category, or search text. Findings expose the observed issue and recommendation but are not writable through the Listings API.

**4\. Confirm on a later scan**

Apply profile changes outside Signal, then use later scheduled scan data and finding status to assess whether the condition is still observed.

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/locations/{location_id}/listings/profile' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/locations/{location_id}/listings/findings?status=open&severity=high&page=1' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

> **No on-demand scan endpoint**
>
> Listing scans are created by scheduled analysis and cannot be started through the public Listings API. Detailed profile data is retained for 29 days; historical scores and checks remain available.

---

# Google Analytics

Source: https://ceyo.ai/docs/signal/google-analytics-guides

### Google Analytics guides

Connect GA4 and start reading traffic and conversion data.

### Connect with Google OAuth

Use a key with `analytics:write`. Start authorization, send the user to the returned URL, let Signal's public callback finish the exchange, then list accounts and properties and select a property.

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/analytics/connect' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

> **Required package setting**
>
> The scope's package must enable `advanced.google_analytics_enabled`.

### Use an external token source

Create a workspace token source when your platform manages Google access tokens. Attach it to a project or location with your external resource ID and GA4 property ID, then trigger a sync.

```curl
curl --request PUT \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/analytics/external_source' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --data '{"token_source_id":"{token_source_id}","external_resource_id":"customer-123","property_id":"properties/123456"}'
```

### Fetch analytics data

Use `analytics:read` to fetch the overview, referrals, conversions, conversion events, or landing pages. The same endpoints work for locations by inserting `/locations/{location_id}` before `/analytics`.

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/analytics?days=30' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}"
```

> **Sync is asynchronous**
>
> A sync request returns `202`. Poll the connection until `syncing_since` is null and `last_synced_at` changes.

---

# Workspace

Source: https://ceyo.ai/docs/signal/workspace

### Workspace

Read and update your workspace and manage immutable project and location packages.

### Get workspace

`GET /workspace`

Returns the workspace selected by the Bearer API key.

#### Response envelope

`workspace`:**Workspace**

The requested or updated workspace.

#### Workspace

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Workspace identifier. |
| `name` | string |  | Workspace display name. |
| `description` | string \| null |  | Workspace description. |
| `logo_url` | string \| null |  | Absolute URL of the workspace logo, when one is configured. |
| `status` | active \| suspended \| archived |  | Current workspace lifecycle status. |
| `created_at` | datetime |  | Workspace creation time. |
| `updated_at` | datetime |  | Most recent workspace update time. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/workspace' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "workspace": {
    "id": "87661f5e-5931-4535-9889-22192f943d27",
    "name": "Northstar Digital",
    "description": "AI visibility programs for customer brands.",
    "logo_url": "https://api.ceyo.ai/media/logos/opaque-logo-token",
    "status": "active",
    "created_at": "2026-01-12T09:30:00Z",
    "updated_at": "2026-07-30T14:20:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The Bearer API key was not provided or is invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Update workspace

`PATCH /workspace`

Updates only the supplied workspace fields. Omitted fields remain unchanged.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | New workspace display name. Maximum: 200 characters. |
| `description` | string \| null |  | New description. Maximum: 5,000 characters; use null to clear it. |

> **JSON only**
>
> Supply only the fields you want to change. Workspace logos are not accepted by this operation.

#### Response envelope

`workspace`:**Workspace**

The requested or updated workspace.

#### Workspace

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Workspace identifier. |
| `name` | string |  | Workspace display name. |
| `description` | string \| null |  | Workspace description. |
| `logo_url` | string \| null |  | Absolute URL of the workspace logo, when one is configured. |
| `status` | active \| suspended \| archived |  | Current workspace lifecycle status. |
| `created_at` | datetime |  | Workspace creation time. |
| `updated_at` | datetime |  | Most recent workspace update time. |

#### Request and response

```curl
curl --request PATCH \
  --url 'https://api.signal.ceyo.ai/v1/workspace' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Northstar Digital",
  "description": "AI visibility programs for customer brands."
}'
```

```json
{
  "workspace": {
    "id": "87661f5e-5931-4535-9889-22192f943d27",
    "name": "Northstar Digital",
    "description": "AI visibility programs for customer brands.",
    "logo_url": "https://api.ceyo.ai/media/logos/opaque-logo-token",
    "status": "active",
    "created_at": "2026-01-12T09:30:00Z",
    "updated_at": "2026-07-30T14:20:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body or a query parameter is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key was not provided or is invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `422` | validation\_failed |  | One or more request fields are invalid. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Packages

Source: https://ceyo.ai/docs/signal/packages

### Packages

Read and update your workspace and manage immutable project and location packages.

### List packages

`GET /packages`

Returns workspace packages in a fixed ascending order by package type, name, then ID.

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `q` | string | Optional | Search package names and descriptions. |
| `package_type` | project \| location | Optional | Return packages of one resource type. |
| `status` | active \| inactive \| archived | Optional | Return packages in one lifecycle status. When omitted, packages in all statuses are returned. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Number of packages per page. Maximum: 100. |

#### Response envelope

`packages`:**Package\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `packages` | Package\[\] |  | Packages matching the selected filters. |
| `pagination` | Pagination |  | Pagination metadata. |

#### Package

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Package identifier. |
| `name` | string |  | Package display name. |
| `description` | string \| null |  | Package description. |
| `package_type` | project \| location |  | Resource type that can use this package. |
| `status` | active \| inactive \| archived |  | Current package lifecycle status. Only active packages can be attached to resources or archived. |
| `configuration` | PackageConfiguration |  | Normalized feature and capacity configuration. |
| `created_at` | datetime |  | Package creation time. |
| `updated_at` | datetime |  | Most recent package update time. |

#### PackageConfiguration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility` | object |  | Cadence, prompt limit, selected models, model tiers, citations, and sentiment. |
| `diagnosis` | object |  | Diagnosis feature settings. |
| `agents` | object |  | Content and technical agent settings. |
| `advanced` | object |  | Prompt volume, Google Analytics, and fan-out settings. |
| `pricing` | object |  | Customer access setting. |

#### Visibility configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `cadence` | monthly \| weekly \| daily |  | Tracking schedule. |
| `prompt_limit` | integer |  | Maximum tracked prompts. |
| `model_keys` | string\[\] |  | Selected model keys. |

#### Feature configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `diagnosis.enabled` | boolean |  | Whether Diagnosis, Actions, and Site Audit are enabled. |
| `agents.content_enabled` | boolean |  | Whether the content agent is enabled. |
| `agents.technical_enabled` | boolean |  | Whether the technical agent is enabled. |
| `advanced.prompt_volume_enabled` | boolean |  | Whether prompt-volume analysis is enabled. |
| `advanced.google_analytics_enabled` | boolean |  | Whether Google Analytics integration is enabled. |
| `advanced.fanout_enabled` | boolean |  | Whether fan-out query analysis is enabled. |
| `pricing.frontend_delivery_enabled` | boolean |  | Whether Customer access to hosted Signal UI and redirect login links is enabled. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Number of records requested per page. |
| `total` | integer |  | Total matching packages. |
| `total_pages` | integer |  | Total available pages. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/packages?q=growth&package_type=project&status=active&page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "packages": [
    {
    "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
    "name": "Growth Weekly",
    "description": "Weekly visibility and diagnosis for growing brands.",
    "package_type": "project",
    "status": "active",
    "configuration": {
      "visibility": {
        "cadence": "weekly",
        "prompt_limit": 100,
        "model_keys": ["chatgpt", "claude", "perplexity", "gemini"]
      },
      "diagnosis": { "enabled": true },
      "agents": {
        "content_enabled": true,
        "technical_enabled": true
      },
      "advanced": {
        "prompt_volume_enabled": true,
        "google_analytics_enabled": false,
        "fanout_enabled": true
      },
      "pricing": {
        "frontend_delivery_enabled": true
      }
    },
    "created_at": "2026-07-30T15:05:00Z",
    "updated_at": "2026-07-30T15:05:00Z"
  }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body or a query parameter is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key was not provided or is invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Validate package configuration

`POST /packages/preview`

Validates and normalizes a proposed package configuration without creating a package.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `package_type` | project \| location |  | Required resource type. |
| `configuration` | PackageConfigurationInput |  | Required proposed configuration. |

#### PackageConfigurationInput

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility` | object |  | Required visibility tracking configuration. |
| `visibility.cadence` | monthly \| weekly \| daily |  | Tracking schedule. |
| `visibility.prompt_limit` | integer |  | Maximum tracked prompts. Minimum: 20. |
| `visibility.model_keys` | string\[\] |  | Non-empty list containing chatgpt, claude, perplexity, gemini, google\_ai\_mode, google\_ai\_overview, grok, or copilot. |
| `diagnosis.enabled` | boolean |  | Enable Diagnosis, Actions, and Site Audit. Defaults to false. |
| `agents.content_enabled` | boolean |  | Enable the content agent. Defaults to false and is normalized to false unless diagnosis.enabled is true. |
| `agents.technical_enabled` | boolean |  | Enable the technical agent. Defaults to false and is normalized to false unless diagnosis.enabled is true. |
| `listings.enabled` | derived |  | Derived from package\_type: true for location packages and false for project packages. Do not send this field. |
| `advanced.prompt_volume_enabled` | boolean |  | Enable prompt-volume analysis. Defaults to false. |
| `advanced.google_analytics_enabled` | boolean |  | Enable Google Analytics integration. Defaults to false. |
| `advanced.fanout_enabled` | boolean |  | Enable fan-out query analysis. Defaults to false. |
| `pricing.frontend_delivery_enabled` | boolean |  | Enable Customer access, including hosted Signal UI and redirect login links, and apply one 25% uplift. Defaults to false. |

#### Response envelope

`configuration`:**PackageConfiguration**

Normalized public configuration.

#### PackageConfiguration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility` | object |  | Cadence, prompt limit, selected models, model tiers, citations, and sentiment. |
| `diagnosis` | object |  | Diagnosis feature settings. |
| `agents` | object |  | Content and technical agent settings. |
| `advanced` | object |  | Prompt volume, Google Analytics, and fan-out settings. |
| `pricing` | object |  | Customer access setting. |

#### Visibility configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `cadence` | monthly \| weekly \| daily |  | Tracking schedule. |
| `prompt_limit` | integer |  | Maximum tracked prompts. |
| `model_keys` | string\[\] |  | Selected model keys. |

#### Feature configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `diagnosis.enabled` | boolean |  | Whether Diagnosis, Actions, and Site Audit are enabled. |
| `agents.content_enabled` | boolean |  | Whether the content agent is enabled. |
| `agents.technical_enabled` | boolean |  | Whether the technical agent is enabled. |
| `advanced.prompt_volume_enabled` | boolean |  | Whether prompt-volume analysis is enabled. |
| `advanced.google_analytics_enabled` | boolean |  | Whether Google Analytics integration is enabled. |
| `advanced.fanout_enabled` | boolean |  | Whether fan-out query analysis is enabled. |
| `pricing.frontend_delivery_enabled` | boolean |  | Whether Customer access to hosted Signal UI and redirect login links is enabled. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/packages/preview' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "package_type": "project",
  "configuration": {
    "visibility": {
      "cadence": "weekly",
      "prompt_limit": 100,
      "model_keys": ["chatgpt", "claude", "perplexity", "gemini"]
    },
    "diagnosis": { "enabled": true },
    "agents": {
      "content_enabled": true,
      "technical_enabled": true
    },
    "advanced": {
      "prompt_volume_enabled": true,
      "google_analytics_enabled": false,
      "fanout_enabled": true
    },
    "pricing": {
      "frontend_delivery_enabled": true
    }
  }
}'
```

```json
{
  "configuration": {
      "visibility": {
        "cadence": "weekly",
        "prompt_limit": 100,
        "model_keys": ["chatgpt", "claude", "perplexity", "gemini"]
      },
      "diagnosis": { "enabled": true },
      "agents": {
        "content_enabled": true,
        "technical_enabled": true
      },
      "advanced": {
        "prompt_volume_enabled": true,
        "google_analytics_enabled": false,
        "fanout_enabled": true
      },
      "pricing": {
        "frontend_delivery_enabled": true
      }
    }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body or a query parameter is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key was not provided or is invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `422` | validation\_failed |  | One or more request fields are invalid. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Create package

`POST /packages`

Creates an immutable package and returns its public configuration.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Required package name. Maximum: 200 characters. |
| `description` | string \| null |  | Optional description. Maximum: 5,000 characters. |
| `package_type` | project \| location |  | Required resource type. |
| `configuration` | PackageConfigurationInput |  | Required feature and capacity configuration. |

#### PackageConfigurationInput

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility` | object |  | Required visibility tracking configuration. |
| `visibility.cadence` | monthly \| weekly \| daily |  | Tracking schedule. |
| `visibility.prompt_limit` | integer |  | Maximum tracked prompts. Minimum: 20. |
| `visibility.model_keys` | string\[\] |  | Non-empty list containing chatgpt, claude, perplexity, gemini, google\_ai\_mode, google\_ai\_overview, grok, or copilot. |
| `diagnosis.enabled` | boolean |  | Enable Diagnosis, Actions, and Site Audit. Defaults to false. |
| `agents.content_enabled` | boolean |  | Enable the content agent. Defaults to false and is normalized to false unless diagnosis.enabled is true. |
| `agents.technical_enabled` | boolean |  | Enable the technical agent. Defaults to false and is normalized to false unless diagnosis.enabled is true. |
| `listings.enabled` | derived |  | Derived from package\_type: true for location packages and false for project packages. Do not send this field. |
| `advanced.prompt_volume_enabled` | boolean |  | Enable prompt-volume analysis. Defaults to false. |
| `advanced.google_analytics_enabled` | boolean |  | Enable Google Analytics integration. Defaults to false. |
| `advanced.fanout_enabled` | boolean |  | Enable fan-out query analysis. Defaults to false. |
| `pricing.frontend_delivery_enabled` | boolean |  | Enable Customer access, including hosted Signal UI and redirect login links, and apply one 25% uplift. Defaults to false. |

> **Location package example**
>
> Sending `package_type: "location"` derives `configuration.listings.enabled: true`. Listings is derived and must not be included in the request configuration.

#### Response envelope

`package`:**Package**

The requested or resulting package.

#### Package

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Package identifier. |
| `name` | string |  | Package display name. |
| `description` | string \| null |  | Package description. |
| `package_type` | project \| location |  | Resource type that can use this package. |
| `status` | active \| inactive \| archived |  | Current package lifecycle status. Only active packages can be attached to resources or archived. |
| `configuration` | PackageConfiguration |  | Normalized feature and capacity configuration. |
| `created_at` | datetime |  | Package creation time. |
| `updated_at` | datetime |  | Most recent package update time. |

#### PackageConfiguration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility` | object |  | Cadence, prompt limit, selected models, model tiers, citations, and sentiment. |
| `diagnosis` | object |  | Diagnosis feature settings. |
| `agents` | object |  | Content and technical agent settings. |
| `advanced` | object |  | Prompt volume, Google Analytics, and fan-out settings. |
| `pricing` | object |  | Customer access setting. |

#### Visibility configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `cadence` | monthly \| weekly \| daily |  | Tracking schedule. |
| `prompt_limit` | integer |  | Maximum tracked prompts. |
| `model_keys` | string\[\] |  | Selected model keys. |

#### Feature configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `diagnosis.enabled` | boolean |  | Whether Diagnosis, Actions, and Site Audit are enabled. |
| `agents.content_enabled` | boolean |  | Whether the content agent is enabled. |
| `agents.technical_enabled` | boolean |  | Whether the technical agent is enabled. |
| `advanced.prompt_volume_enabled` | boolean |  | Whether prompt-volume analysis is enabled. |
| `advanced.google_analytics_enabled` | boolean |  | Whether Google Analytics integration is enabled. |
| `advanced.fanout_enabled` | boolean |  | Whether fan-out query analysis is enabled. |
| `pricing.frontend_delivery_enabled` | boolean |  | Whether Customer access to hosted Signal UI and redirect login links is enabled. |

> **201 Created**
>
> A successful request returns HTTP `201`. Package names must be unique within their package type.

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/packages' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Growth Weekly",
  "description": "Weekly visibility and diagnosis for growing brands.",
  "package_type": "project",
  "configuration": {
    "visibility": {
      "cadence": "weekly",
      "prompt_limit": 100,
      "model_keys": ["chatgpt", "claude", "perplexity", "gemini"]
    },
    "diagnosis": { "enabled": true },
    "agents": {
      "content_enabled": true,
      "technical_enabled": true
    },
    "advanced": {
      "prompt_volume_enabled": true,
      "google_analytics_enabled": false,
      "fanout_enabled": true
    },
    "pricing": {
      "frontend_delivery_enabled": true
    }
  }
}'
```

```json
{
  "package": {
    "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
    "name": "Growth Weekly",
    "description": "Weekly visibility and diagnosis for growing brands.",
    "package_type": "project",
    "status": "active",
    "configuration": {
      "visibility": {
        "cadence": "weekly",
        "prompt_limit": 100,
        "model_keys": ["chatgpt", "claude", "perplexity", "gemini"]
      },
      "diagnosis": { "enabled": true },
      "agents": {
        "content_enabled": true,
        "technical_enabled": true
      },
      "advanced": {
        "prompt_volume_enabled": true,
        "google_analytics_enabled": false,
        "fanout_enabled": true
      },
      "pricing": {
        "frontend_delivery_enabled": true
      }
    },
    "created_at": "2026-07-30T15:05:00Z",
    "updated_at": "2026-07-30T15:05:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body or a query parameter is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key was not provided or is invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `422` | validation\_failed |  | One or more request fields are invalid. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get package

`GET /packages/{package_id}`

Returns one package from the workspace selected by the Bearer API key.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `package_id` | uuid | Required | Package identifier. |

#### Response envelope

`package`:**Package**

The requested or resulting package.

#### Package

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Package identifier. |
| `name` | string |  | Package display name. |
| `description` | string \| null |  | Package description. |
| `package_type` | project \| location |  | Resource type that can use this package. |
| `status` | active \| inactive \| archived |  | Current package lifecycle status. Only active packages can be attached to resources or archived. |
| `configuration` | PackageConfiguration |  | Normalized feature and capacity configuration. |
| `created_at` | datetime |  | Package creation time. |
| `updated_at` | datetime |  | Most recent package update time. |

#### PackageConfiguration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility` | object |  | Cadence, prompt limit, selected models, model tiers, citations, and sentiment. |
| `diagnosis` | object |  | Diagnosis feature settings. |
| `agents` | object |  | Content and technical agent settings. |
| `advanced` | object |  | Prompt volume, Google Analytics, and fan-out settings. |
| `pricing` | object |  | Customer access setting. |

#### Visibility configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `cadence` | monthly \| weekly \| daily |  | Tracking schedule. |
| `prompt_limit` | integer |  | Maximum tracked prompts. |
| `model_keys` | string\[\] |  | Selected model keys. |

#### Feature configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `diagnosis.enabled` | boolean |  | Whether Diagnosis, Actions, and Site Audit are enabled. |
| `agents.content_enabled` | boolean |  | Whether the content agent is enabled. |
| `agents.technical_enabled` | boolean |  | Whether the technical agent is enabled. |
| `advanced.prompt_volume_enabled` | boolean |  | Whether prompt-volume analysis is enabled. |
| `advanced.google_analytics_enabled` | boolean |  | Whether Google Analytics integration is enabled. |
| `advanced.fanout_enabled` | boolean |  | Whether fan-out query analysis is enabled. |
| `pricing.frontend_delivery_enabled` | boolean |  | Whether Customer access to hosted Signal UI and redirect login links is enabled. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/packages/92404fd7-f096-49e9-9ab0-5ed73517d9db' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "package": {
    "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
    "name": "Growth Weekly",
    "description": "Weekly visibility and diagnosis for growing brands.",
    "package_type": "project",
    "status": "active",
    "configuration": {
      "visibility": {
        "cadence": "weekly",
        "prompt_limit": 100,
        "model_keys": ["chatgpt", "claude", "perplexity", "gemini"]
      },
      "diagnosis": { "enabled": true },
      "agents": {
        "content_enabled": true,
        "technical_enabled": true
      },
      "advanced": {
        "prompt_volume_enabled": true,
        "google_analytics_enabled": false,
        "fanout_enabled": true
      },
      "pricing": {
        "frontend_delivery_enabled": true
      }
    },
    "created_at": "2026-07-30T15:05:00Z",
    "updated_at": "2026-07-30T15:05:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The Bearer API key was not provided or is invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested package was not found in this workspace. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Clone package

`POST /packages/{package_id}/clone`

Creates a new active package by copying the source package type and configuration.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `package_id` | uuid | Required | Package identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Required name for the cloned package. Maximum: 200 characters. |
| `description` | string \| null |  | Optional description. Maximum: 5,000 characters. When omitted, the source description is copied. |

> **201 Created**
>
> A successful request returns HTTP `201` with a new package ID and new creation and update timestamps. The clone copies `package_type` and `configuration`. Package type and configuration cannot be overridden in the request.

#### Response envelope

`package`:**Package**

The requested or resulting package.

#### Package

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Package identifier. |
| `name` | string |  | Package display name. |
| `description` | string \| null |  | Package description. |
| `package_type` | project \| location |  | Resource type that can use this package. |
| `status` | active \| inactive \| archived |  | Current package lifecycle status. Only active packages can be attached to resources or archived. |
| `configuration` | PackageConfiguration |  | Normalized feature and capacity configuration. |
| `created_at` | datetime |  | Package creation time. |
| `updated_at` | datetime |  | Most recent package update time. |

#### PackageConfiguration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility` | object |  | Cadence, prompt limit, selected models, model tiers, citations, and sentiment. |
| `diagnosis` | object |  | Diagnosis feature settings. |
| `agents` | object |  | Content and technical agent settings. |
| `advanced` | object |  | Prompt volume, Google Analytics, and fan-out settings. |
| `pricing` | object |  | Customer access setting. |

#### Visibility configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `cadence` | monthly \| weekly \| daily |  | Tracking schedule. |
| `prompt_limit` | integer |  | Maximum tracked prompts. |
| `model_keys` | string\[\] |  | Selected model keys. |

#### Feature configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `diagnosis.enabled` | boolean |  | Whether Diagnosis, Actions, and Site Audit are enabled. |
| `agents.content_enabled` | boolean |  | Whether the content agent is enabled. |
| `agents.technical_enabled` | boolean |  | Whether the technical agent is enabled. |
| `advanced.prompt_volume_enabled` | boolean |  | Whether prompt-volume analysis is enabled. |
| `advanced.google_analytics_enabled` | boolean |  | Whether Google Analytics integration is enabled. |
| `advanced.fanout_enabled` | boolean |  | Whether fan-out query analysis is enabled. |
| `pricing.frontend_delivery_enabled` | boolean |  | Whether Customer access to hosted Signal UI and redirect login links is enabled. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/packages/92404fd7-f096-49e9-9ab0-5ed73517d9db/clone' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Growth Weekly Plus",
  "description": "A new package based on Growth Weekly."
}'
```

```json
{
  "package": {
    "id": "1fd73876-e220-4962-924b-a74bdb32481d",
    "name": "Growth Weekly Plus",
    "description": "A new package based on Growth Weekly.",
    "package_type": "project",
    "status": "active",
    "configuration": {
      "visibility": {
        "cadence": "weekly",
        "prompt_limit": 100,
        "model_keys": ["chatgpt", "claude", "perplexity", "gemini"]
      },
      "diagnosis": { "enabled": true },
      "agents": {
        "content_enabled": true,
        "technical_enabled": true
      },
      "advanced": {
        "prompt_volume_enabled": true,
        "google_analytics_enabled": false,
        "fanout_enabled": true
      },
      "pricing": {
        "frontend_delivery_enabled": true
      }
    },
    "created_at": "2026-07-31T10:05:00Z",
    "updated_at": "2026-07-31T10:05:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body or a query parameter is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key was not provided or is invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested package was not found in this workspace. |
| `422` | validation\_failed |  | One or more request fields are invalid. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Archive package

`POST /packages/{package_id}/archive`

Archives an unattached active package. The request body must be an empty JSON object.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `package_id` | uuid | Required | Package identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `{}` | object |  | Required empty JSON request body. |

> **200 OK**
>
> A successful request returns HTTP `200`. Only an unattached active package can be archived. A package that is attached, already archived, or not active returns a conflict. An archived package cannot be restored.

#### Response envelope

`package`:**Package**

The requested or resulting package.

#### Package

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Package identifier. |
| `name` | string |  | Package display name. |
| `description` | string \| null |  | Package description. |
| `package_type` | project \| location |  | Resource type that can use this package. |
| `status` | active \| inactive \| archived |  | Current package lifecycle status. Only active packages can be attached to resources or archived. |
| `configuration` | PackageConfiguration |  | Normalized feature and capacity configuration. |
| `created_at` | datetime |  | Package creation time. |
| `updated_at` | datetime |  | Most recent package update time. |

#### PackageConfiguration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility` | object |  | Cadence, prompt limit, selected models, model tiers, citations, and sentiment. |
| `diagnosis` | object |  | Diagnosis feature settings. |
| `agents` | object |  | Content and technical agent settings. |
| `advanced` | object |  | Prompt volume, Google Analytics, and fan-out settings. |
| `pricing` | object |  | Customer access setting. |

#### Visibility configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `cadence` | monthly \| weekly \| daily |  | Tracking schedule. |
| `prompt_limit` | integer |  | Maximum tracked prompts. |
| `model_keys` | string\[\] |  | Selected model keys. |

#### Feature configuration

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `diagnosis.enabled` | boolean |  | Whether Diagnosis, Actions, and Site Audit are enabled. |
| `agents.content_enabled` | boolean |  | Whether the content agent is enabled. |
| `agents.technical_enabled` | boolean |  | Whether the technical agent is enabled. |
| `advanced.prompt_volume_enabled` | boolean |  | Whether prompt-volume analysis is enabled. |
| `advanced.google_analytics_enabled` | boolean |  | Whether Google Analytics integration is enabled. |
| `advanced.fanout_enabled` | boolean |  | Whether fan-out query analysis is enabled. |
| `pricing.frontend_delivery_enabled` | boolean |  | Whether Customer access to hosted Signal UI and redirect login links is enabled. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/packages/92404fd7-f096-49e9-9ab0-5ed73517d9db/archive' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{}'
```

```json
{
  "package": {
    "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
    "name": "Growth Weekly",
    "description": "Weekly visibility and diagnosis for growing brands.",
    "package_type": "project",
    "status": "archived",
    "configuration": {
      "visibility": {
        "cadence": "weekly",
        "prompt_limit": 100,
        "model_keys": ["chatgpt", "claude", "perplexity", "gemini"]
      },
      "diagnosis": { "enabled": true },
      "agents": {
        "content_enabled": true,
        "technical_enabled": true
      },
      "advanced": {
        "prompt_volume_enabled": true,
        "google_analytics_enabled": false,
        "fanout_enabled": true
      },
      "pricing": {
        "frontend_delivery_enabled": true
      }
    },
    "created_at": "2026-07-30T15:05:00Z",
    "updated_at": "2026-07-31T10:12:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body or a query parameter is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key was not provided or is invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested package was not found in this workspace. |
| `409` | package\_not\_active \| package\_in\_use |  | The package is attached to a resource, is already archived, or is not active. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Tags

Source: https://ceyo.ai/docs/signal/tags

### Tags

Create and manage workspace tags used to organize projects.

> **Authentication and scope**
>
> Send `Authorization: Bearer ceyo_platform_...` on every request. The API key selects the workspace, so paths never require a workspace identifier.

> **Project organization**
>
> Tags are workspace-scoped and assigned to projects through `tag_ids`. Deleting a tag also removes it from every assigned project.

### List tags

`GET /tags`

Returns the paginated workspace tag catalog used by project tag\_ids.

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer | Optional; Default: 1 | 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Tags per page. Minimum 1, maximum 100. |

#### Response envelope

`tags`:**Tag\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `tags` | Tag\[\] |  | Tags ordered case-insensitively by name, then ID. |
| `pagination` | Pagination |  | Pagination metadata for the tag catalog. |

#### Tag

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Tag identifier. |
| `name` | string |  | Tag display name. |
| `color` | string |  | Tag display color. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Number of records requested per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/tags?page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "tags": [
    {
      "id": "589dfbca-f4e2-420a-a288-f9b4c9ea1593",
      "name": "Retail",
      "color": "#0057FF"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Create tag

`POST /tags`

Creates a workspace tag for assignment to projects.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Required unique tag name. Maximum: 25 characters. |
| `color` | string |  | Optional supported tag color. Defaults to #0057FF. |

#### Response envelope

`tag`:**Tag**

The newly created tag.

#### Tag

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Tag identifier. |
| `name` | string |  | Tag display name. |
| `color` | string |  | Tag display color. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/tags' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Retail",
  "color": "#0057FF"
}'
```

```json
{
  "tag": {
    "id": "589dfbca-f4e2-420a-a288-f9b4c9ea1593",
    "name": "Retail",
    "color": "#0057FF"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Delete tag

`DELETE /tags/{tag_id}`

Deletes a workspace tag and removes it from assigned projects.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `tag_id` | uuid | Required | Workspace tag identifier. |

> **204 No Content**
>
> A successful deletion returns no response body.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Projects

Source: https://ceyo.ai/docs/signal/projects

### Projects

Provision and manage projects in the workspace selected by your API key.

> **Authentication and scope**
>
> Send `Authorization: Bearer ceyo_platform_...` on every request. The API key selects the workspace, so paths never require a workspace identifier.

> **Packages and project modes**
>
> A standard project selects an active project package with `package_id`. Every location selects an active location package. A `locations_only` project is a container for locations, has no project package, and is not itself a tracking scope. Package assignments cannot be changed through resource updates.

> **Pagination, filters, and ordering**
>
> List endpoints use 1-based `page` and `per_page`, return pagination metadata, and return an empty array when the page is beyond the result set. Filters combine with AND. Search is trimmed, case-insensitive, and limited to 200 characters. Sorts are stable and use resource ID as the final ascending tie-breaker.

### List projects

`GET /projects`

Returns projects visible to the API key. Filters combine with AND and the selected sort is deterministic.

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `q` | string | Optional | Case-insensitive search across project name, website, description, folder, and tag name. Maximum: 200 characters. |
| `folder` | string | Optional | Match one folder, case-insensitively. |
| `status` | active \| inactive \| archived | Optional | Return projects in one lifecycle status. |
| `project_mode` | standard \| locations\_only | Optional | Return projects in one mode. |
| `tag_id` | uuid | Optional | Return projects assigned to this workspace tag. |
| `sort` | created\_at \| updated\_at \| name | Optional; Default: created\_at | Field used for ordering. |
| `direction` | asc \| desc | Optional; Default: desc | Sort direction. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Number of records per page, from 1 through 100. Values outside this range return 422. |

#### Response envelope

`projects`:**Project\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `projects` | Project\[\] |  | Matching projects in the requested deterministic sort order. |
| `pagination` | Pagination |  | Pagination metadata. |

#### Project

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Project identifier. |
| `workspace_id` | uuid |  | Identifier of the workspace that owns the project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Project display name. |
| `description` | string \| null |  | Project description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS website URL. |
| `brand_aliases` | string\[\] |  | Normalized alternative brand names. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `project_mode` | standard \| locations\_only |  | Standard projects have their own tracking package. locations\_only projects act as containers for independently packaged locations. |
| `status` | active \| inactive \| archived |  | Current project lifecycle status. |
| `package` | PackageReference \| null |  | Assigned project package. Null when project\_mode is locations\_only. |
| `address` | string \| null |  | Normalized street address. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string |  | Normalized country name. |
| `country_code` | string |  | Uppercase ISO 3166-1 alpha-2 country code. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `google_place_id` | string \| null |  | Google place identifier when one is configured. |
| `language` | string |  | Lowercase ISO 639-1 content language. |
| `action_language` | string \| null |  | Lowercase ISO 639-1 language used for generated actions. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. |
| `local_mode` | boolean |  | Whether local geographic context is emphasized. |
| `local_context` | object |  | Resolved geographic context used by project processing. |
| `location_defaults` | object |  | Default language, action language, website, and parent-brand settings available to locations in this project. |
| `folder` | string \| null |  | Optional workspace organization folder. |
| `tags` | Tag\[\] |  | Up to three workspace tags assigned to the project. |
| `logo_url` | string \| null |  | Absolute project logo URL when configured. |
| `created_at` | datetime |  | Project creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent project update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Tag

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Tag identifier. |
| `name` | string |  | Tag display name. |
| `color` | string |  | Tag display color. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Number of records requested per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects?q=acme&status=active&sort=created_at&direction=desc&page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "projects": [
    {
      "id": "e6c96c98-d777-40e0-94ec-48931f57782f",
      "workspace_id": "b7dd886f-b144-4ab4-907b-8efde9df881a",
      "external_id": "partner-project-acme",
      "name": "Acme Europe",
      "description": "European visibility program for Acme.",
      "website": "https://acme.example/",
      "brand_aliases": [
        "acme",
        "acme europe"
      ],
      "competitors": [
        {
          "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
          "kind": "competitor",
          "name": "Example Rival",
          "domain": "example-rival.com",
          "aliases": [
            "rival"
          ],
          "status": "active",
          "competitor_state": "tracked",
          "created_at": "2026-07-02T11:20:00Z",
          "updated_at": "2026-07-30T09:10:00Z"
        }
      ],
      "project_mode": "standard",
      "status": "active",
      "package": {
        "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
        "name": "Growth Weekly"
      },
      "address": "1 Market Street",
      "city": "Amsterdam",
      "state": "North Holland",
      "postal_code": "1012 JS",
      "country": "Netherlands",
      "country_code": "NL",
      "latitude": 52.3728,
      "longitude": 4.8936,
      "google_place_id": null,
      "language": "en",
      "action_language": "en",
      "focus": "country",
      "local_mode": false,
      "local_context": {},
      "location_defaults": {
        "language": "en",
        "action_language": "en",
        "website": "https://acme.example/",
        "include_parent_brand": true
      },
      "folder": "Europe",
      "tags": [
        {
          "id": "30761d13-bc7e-45c8-8968-2147a37d6e54",
          "name": "Retail",
          "color": "#2563EB"
        }
      ],
      "logo_url": null,
      "created_at": "2026-07-31T08:00:00Z",
      "updated_at": "2026-07-31T08:00:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Create project

`POST /projects`

Synchronously provisions a project and returns it. Optionally starts onboarding after creation.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Required project name. Maximum: 200 characters. |
| `project_mode` | standard \| locations\_only |  | Optional project mode. Defaults to standard. |
| `package_id` | uuid |  | Required for standard projects and omitted for locations\_only projects. Must identify an active project package. |
| `external_id` | string \| null |  | Optional partner identifier, unique across projects. Maximum: 200 characters. |
| `website` | string |  | Required for standard projects. Valid HTTP or HTTPS URL; maximum 2,048 characters. |
| `description` | string \| null |  | Optional description. Maximum: 5,000 characters. |
| `brand_aliases` | string\[\] |  | Up to 10 alternative names, each at most 200 characters. Values are trimmed, lowercased, and deduplicated. |
| `country_code` | string |  | Uppercase ISO 3166-1 alpha-2 country code. Defaults to US. |
| `country` | string |  | Country name. Defaults to country\_code. |
| `city` | string \| null |  | City; required when focus is city. Maximum: 200 characters. |
| `state` | string \| null |  | Region or state. Maximum: 200 characters. |
| `address` | string \| null |  | Street address. Maximum: 500 characters. |
| `postal_code` | string \| null |  | Postal code. Maximum: 200 characters. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `google_place_id` | string \| null |  | Google place identifier. Maximum: 500 characters. |
| `language` | ISO 639-1 string |  | Content language. Defaults to en. |
| `action_language` | ISO 639-1 string \| null |  | Optional language for generated actions. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. Defaults to country. |
| `local_mode` | boolean |  | Enable local geographic context. Defaults to false. |
| `folder` | string \| null |  | Optional organization folder. Maximum: 30 characters. |
| `tag_ids` | uuid\[\] |  | Up to three tag identifiers from this workspace. |
| `tag_names` | string\[\] |  | Up to three tag names. Names are trimmed and matched case-insensitively; missing names are created. Each name is 1–25 characters and the workspace may contain at most 50 tags. Takes precedence over tag\_ids. |
| `start` | boolean |  | When true, starts onboarding immediately after synchronous provisioning succeeds. Must be false for locations\_only container projects. Defaults to false. |

> **Conditional requirements**
>
> For `project_mode: "standard"`, `package_id` and `website` are required. For `project_mode: "locations_only"`, omit `package_id`; locations created under the project select their own location packages.

#### Response envelope

`project`:**Project**`onboarding_operation`:**OnboardingOperation | null**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project` | Project |  | The provisioned project. |
| `onboarding_operation` | OnboardingOperation \| null |  | Polling operation when start is true; null when onboarding was not requested. |

#### Project

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Project identifier. |
| `workspace_id` | uuid |  | Identifier of the workspace that owns the project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Project display name. |
| `description` | string \| null |  | Project description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS website URL. |
| `brand_aliases` | string\[\] |  | Normalized alternative brand names. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `project_mode` | standard \| locations\_only |  | Standard projects have their own tracking package. locations\_only projects act as containers for independently packaged locations. |
| `status` | active \| inactive \| archived |  | Current project lifecycle status. |
| `package` | PackageReference \| null |  | Assigned project package. Null when project\_mode is locations\_only. |
| `address` | string \| null |  | Normalized street address. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string |  | Normalized country name. |
| `country_code` | string |  | Uppercase ISO 3166-1 alpha-2 country code. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `google_place_id` | string \| null |  | Google place identifier when one is configured. |
| `language` | string |  | Lowercase ISO 639-1 content language. |
| `action_language` | string \| null |  | Lowercase ISO 639-1 language used for generated actions. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. |
| `local_mode` | boolean |  | Whether local geographic context is emphasized. |
| `local_context` | object |  | Resolved geographic context used by project processing. |
| `location_defaults` | object |  | Default language, action language, website, and parent-brand settings available to locations in this project. |
| `folder` | string \| null |  | Optional workspace organization folder. |
| `tags` | Tag\[\] |  | Up to three workspace tags assigned to the project. |
| `logo_url` | string \| null |  | Absolute project logo URL when configured. |
| `created_at` | datetime |  | Project creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent project update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Tag

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Tag identifier. |
| `name` | string |  | Tag display name. |
| `color` | string |  | Tag display color. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Acme Europe",
  "external_id": "partner-project-acme",
  "website": "https://acme.example",
  "description": "European visibility program for Acme.",
  "package_id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
  "country": "Netherlands",
  "country_code": "NL",
  "language": "en",
  "focus": "country",
  "start": false
}'
```

```json
HTTP/1.1 201 Created

{
  "project": {
    "id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "workspace_id": "b7dd886f-b144-4ab4-907b-8efde9df881a",
    "external_id": "partner-project-acme",
    "name": "Acme Europe",
    "description": "European visibility program for Acme.",
    "website": "https://acme.example/",
    "brand_aliases": [
      "acme",
      "acme europe"
    ],
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "project_mode": "standard",
    "status": "active",
    "package": {
      "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
      "name": "Growth Weekly"
    },
    "address": "1 Market Street",
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": "NL",
    "latitude": 52.3728,
    "longitude": 4.8936,
    "google_place_id": null,
    "language": "en",
    "action_language": "en",
    "focus": "country",
    "local_mode": false,
    "local_context": {},
    "location_defaults": {
      "language": "en",
      "action_language": "en",
      "website": "https://acme.example/",
      "include_parent_brand": true
    },
    "folder": "Europe",
    "tags": [
      {
        "id": "30761d13-bc7e-45c8-8968-2147a37d6e54",
        "name": "Retail",
        "color": "#2563EB"
      }
    ],
    "logo_url": null,
    "created_at": "2026-07-31T08:00:00Z",
    "updated_at": "2026-07-31T08:00:00Z"
  },
  "onboarding_operation": null
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `409` | conflict |  | The external ID is already in use or the resource cannot accept this operation in its current state. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Bulk create projects

`POST /projects/bulk`

Accepts up to 100 project create records and provisions them asynchronously with controlled workspace concurrency.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `projects` | ProjectCreate\[\] |  | Between 1 and 100 records using the same fields and validation as Create project. |

> **Idempotency required**
>
> Send a unique `Idempotency-Key` header. Repeating the same request returns the existing operation. Reusing the key with a different body returns `409`.

> **Onboarding**
>
> Each record controls onboarding independently with `start`. `start: true` is rejected for a `locations_only` container; onboard its locations instead.

#### Response envelope

`bulk_operation`:**BulkOperation**

Accepted operation summary with an ID and status\_url for polling.

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/bulk' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Idempotency-Key: provision-2026-08-04-001' \
  --header 'Content-Type: application/json' \
  --data '{
  "projects": [
    {
      "name": "Acme Europe",
      "external_id": "partner-project-acme",
      "website": "https://acme.example",
      "description": "European visibility program for Acme.",
      "package_id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
      "country": "Netherlands",
      "country_code": "NL",
      "language": "en",
      "focus": "country",
      "start": false
    },
    {
      "name": "Acme locations",
      "external_id": "partner-project-acme-locations",
      "project_mode": "locations_only",
      "start": false
    }
  ]
}'
```

```json
HTTP/1.1 202 Accepted

{
  "bulk_operation": {
    "id": "f9bc15cc-e9c9-4e93-a93e-b713c92c7315",
    "type": "projects",
    "status": "pending",
    "parent_project_id": null,
    "total": 2,
    "pending": 2,
    "succeeded": 0,
    "failed": 0,
    "created_at": "2026-08-04T15:00:00Z",
    "started_at": null,
    "completed_at": null,
    "status_url": "/v1/bulk-operations/f9bc15cc-e9c9-4e93-a93e-b713c92c7315"
  }
}
```

Poll the shared [Get bulk operation](/docs/signal/bulk-operations#get-bulk-operation) endpoint for per-record results.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `409` | conflict |  | The external ID is already in use or the resource cannot accept this operation in its current state. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get project

`GET /projects/{project_id}`

Returns one project by its Ceyo UUID.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |

#### Response envelope

`project`:**Project**

The requested, created, or updated project. The envelope is identical for UUID and external-ID lookup.

#### Project

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Project identifier. |
| `workspace_id` | uuid |  | Identifier of the workspace that owns the project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Project display name. |
| `description` | string \| null |  | Project description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS website URL. |
| `brand_aliases` | string\[\] |  | Normalized alternative brand names. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `project_mode` | standard \| locations\_only |  | Standard projects have their own tracking package. locations\_only projects act as containers for independently packaged locations. |
| `status` | active \| inactive \| archived |  | Current project lifecycle status. |
| `package` | PackageReference \| null |  | Assigned project package. Null when project\_mode is locations\_only. |
| `address` | string \| null |  | Normalized street address. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string |  | Normalized country name. |
| `country_code` | string |  | Uppercase ISO 3166-1 alpha-2 country code. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `google_place_id` | string \| null |  | Google place identifier when one is configured. |
| `language` | string |  | Lowercase ISO 639-1 content language. |
| `action_language` | string \| null |  | Lowercase ISO 639-1 language used for generated actions. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. |
| `local_mode` | boolean |  | Whether local geographic context is emphasized. |
| `local_context` | object |  | Resolved geographic context used by project processing. |
| `location_defaults` | object |  | Default language, action language, website, and parent-brand settings available to locations in this project. |
| `folder` | string \| null |  | Optional workspace organization folder. |
| `tags` | Tag\[\] |  | Up to three workspace tags assigned to the project. |
| `logo_url` | string \| null |  | Absolute project logo URL when configured. |
| `created_at` | datetime |  | Project creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent project update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Tag

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Tag identifier. |
| `name` | string |  | Tag display name. |
| `color` | string |  | Tag display color. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project": {
    "id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "workspace_id": "b7dd886f-b144-4ab4-907b-8efde9df881a",
    "external_id": "partner-project-acme",
    "name": "Acme Europe",
    "description": "European visibility program for Acme.",
    "website": "https://acme.example/",
    "brand_aliases": [
      "acme",
      "acme europe"
    ],
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "project_mode": "standard",
    "status": "active",
    "package": {
      "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
      "name": "Growth Weekly"
    },
    "address": "1 Market Street",
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": "NL",
    "latitude": 52.3728,
    "longitude": 4.8936,
    "google_place_id": null,
    "language": "en",
    "action_language": "en",
    "focus": "country",
    "local_mode": false,
    "local_context": {},
    "location_defaults": {
      "language": "en",
      "action_language": "en",
      "website": "https://acme.example/",
      "include_parent_brand": true
    },
    "folder": "Europe",
    "tags": [
      {
        "id": "30761d13-bc7e-45c8-8968-2147a37d6e54",
        "name": "Retail",
        "color": "#2563EB"
      }
    ],
    "logo_url": null,
    "created_at": "2026-07-31T08:00:00Z",
    "updated_at": "2026-07-31T08:00:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get project overview

`GET /projects/{project_id}/overview`

Returns the project, a 30-day visibility summary, and location map markers.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |

#### Response envelope

`project`:**Project**`visibility_summary`:**VisibilitySummary**`map`:**LocationMap**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project` | Project |  | Current project resource. |
| `visibility_summary` | VisibilitySummary |  | Latest completed 30-day visibility summary across active locations. |
| `map` | LocationMap |  | Active location markers with valid coordinates. |

#### VisibilitySummary

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility_rate` | number \| null |  | Percentage of included responses that mentioned the brand; null when unavailable. |
| `avg_position` | number \| null |  | Average 1-based brand position when present; null when no ranked mention is available. |

#### LocationMap

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `markers` | LocationMarker\[\] |  | Markers ordered by location name, then location ID. Only active locations with both coordinates are eligible. |

#### LocationMarker

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Location identifier. |
| `external_id` | string \| null |  | Partner-supplied location identifier. |
| `name` | string |  | Location display name. |
| `formatted_address` | string \| null |  | Formatted address used in map labels. |
| `latitude` | number |  | Marker latitude. |
| `longitude` | number |  | Marker longitude. |
| `visibility_rate` | number \| null |  | Latest 30-day brand visibility percentage. |
| `avg_position` | number \| null |  | Latest 30-day average brand position. |

> **Aggregation and empty data**
>
> Visibility uses the latest completed 30-day window across active locations. A project without eligible data returns null rates and positions.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/overview' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project": {
    "id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "workspace_id": "b7dd886f-b144-4ab4-907b-8efde9df881a",
    "external_id": "partner-project-acme",
    "name": "Acme Europe",
    "description": "European visibility program for Acme.",
    "website": "https://acme.example/",
    "brand_aliases": [
      "acme",
      "acme europe"
    ],
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "project_mode": "standard",
    "status": "active",
    "package": {
      "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
      "name": "Growth Weekly"
    },
    "address": "1 Market Street",
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": "NL",
    "latitude": 52.3728,
    "longitude": 4.8936,
    "google_place_id": null,
    "language": "en",
    "action_language": "en",
    "focus": "country",
    "local_mode": false,
    "local_context": {},
    "location_defaults": {
      "language": "en",
      "action_language": "en",
      "website": "https://acme.example/",
      "include_parent_brand": true
    },
    "folder": "Europe",
    "tags": [
      {
        "id": "30761d13-bc7e-45c8-8968-2147a37d6e54",
        "name": "Retail",
        "color": "#2563EB"
      }
    ],
    "logo_url": null,
    "created_at": "2026-07-31T08:00:00Z",
    "updated_at": "2026-07-31T08:00:00Z"
  },
  "visibility_summary": {
    "visibility_rate": 68.4,
    "avg_position": 2.7
  },
  "map": {
    "markers": [
      {
        "id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
        "external_id": "partner-location-amsterdam",
        "name": "Acme Amsterdam",
        "formatted_address": "1 Market Street, 1012 JS Amsterdam, Netherlands",
        "latitude": 52.3728,
        "longitude": 4.8936,
        "visibility_rate": 68.4,
        "avg_position": 2.7
      }
    ]
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Find project by external ID

`GET /projects/by-external-id/{external_id}`

Returns the project whose external\_id exactly matches the URL-encoded path value.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_id` | string | Required | URL-encoded, case-sensitive external ID previously assigned to the resource. |

> **Exact lookup**
>
> Project external IDs are unique for the API key's workspace. Matching is case-sensitive. An empty or malformed path value returns `400`; an unknown value returns `404`.

#### Response envelope

`project`:**Project**

The requested, created, or updated project. The envelope is identical for UUID and external-ID lookup.

#### Project

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Project identifier. |
| `workspace_id` | uuid |  | Identifier of the workspace that owns the project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Project display name. |
| `description` | string \| null |  | Project description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS website URL. |
| `brand_aliases` | string\[\] |  | Normalized alternative brand names. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `project_mode` | standard \| locations\_only |  | Standard projects have their own tracking package. locations\_only projects act as containers for independently packaged locations. |
| `status` | active \| inactive \| archived |  | Current project lifecycle status. |
| `package` | PackageReference \| null |  | Assigned project package. Null when project\_mode is locations\_only. |
| `address` | string \| null |  | Normalized street address. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string |  | Normalized country name. |
| `country_code` | string |  | Uppercase ISO 3166-1 alpha-2 country code. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `google_place_id` | string \| null |  | Google place identifier when one is configured. |
| `language` | string |  | Lowercase ISO 639-1 content language. |
| `action_language` | string \| null |  | Lowercase ISO 639-1 language used for generated actions. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. |
| `local_mode` | boolean |  | Whether local geographic context is emphasized. |
| `local_context` | object |  | Resolved geographic context used by project processing. |
| `location_defaults` | object |  | Default language, action language, website, and parent-brand settings available to locations in this project. |
| `folder` | string \| null |  | Optional workspace organization folder. |
| `tags` | Tag\[\] |  | Up to three workspace tags assigned to the project. |
| `logo_url` | string \| null |  | Absolute project logo URL when configured. |
| `created_at` | datetime |  | Project creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent project update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Tag

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Tag identifier. |
| `name` | string |  | Tag display name. |
| `color` | string |  | Tag display color. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/by-external-id/partner-project-acme' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project": {
    "id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "workspace_id": "b7dd886f-b144-4ab4-907b-8efde9df881a",
    "external_id": "partner-project-acme",
    "name": "Acme Europe",
    "description": "European visibility program for Acme.",
    "website": "https://acme.example/",
    "brand_aliases": [
      "acme",
      "acme europe"
    ],
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "project_mode": "standard",
    "status": "active",
    "package": {
      "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
      "name": "Growth Weekly"
    },
    "address": "1 Market Street",
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": "NL",
    "latitude": 52.3728,
    "longitude": 4.8936,
    "google_place_id": null,
    "language": "en",
    "action_language": "en",
    "focus": "country",
    "local_mode": false,
    "local_context": {},
    "location_defaults": {
      "language": "en",
      "action_language": "en",
      "website": "https://acme.example/",
      "include_parent_brand": true
    },
    "folder": "Europe",
    "tags": [
      {
        "id": "30761d13-bc7e-45c8-8968-2147a37d6e54",
        "name": "Retail",
        "color": "#2563EB"
      }
    ],
    "logo_url": null,
    "created_at": "2026-07-31T08:00:00Z",
    "updated_at": "2026-07-31T08:00:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Update project

`PATCH /projects/{project_id}`

Updates only supplied project settings. Omitted fields remain unchanged.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |

#### JSON request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | New project name. Maximum: 200 characters. |
| `external_id` | string \| null |  | partner identifier, unique across projects. Maximum: 200 characters. |
| `description` | string \| null |  | description. Maximum: 5,000 characters. |
| `brand_aliases` | string\[\] |  | Up to 10 alternative names, each at most 200 characters. Values are trimmed, lowercased, and deduplicated. |
| `country_code` | string |  | Uppercase ISO 3166-1 alpha-2 country code. |
| `country` | string |  | Country name. |
| `city` | string \| null |  | City; required when focus is city. Maximum: 200 characters. |
| `state` | string \| null |  | Region or state. Maximum: 200 characters. |
| `address` | string \| null |  | Street address. Maximum: 500 characters. |
| `postal_code` | string \| null |  | Postal code. Maximum: 200 characters. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `google_place_id` | string \| null |  | Google place identifier. Maximum: 500 characters. |
| `language` | ISO 639-1 string |  | Content language. |
| `action_language` | ISO 639-1 string \| null |  | language for generated actions. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. |
| `local_mode` | boolean |  | Enable local geographic context. |
| `folder` | string \| null |  | organization folder. Maximum: 30 characters. |
| `tag_ids` | uuid\[\] |  | Up to three tag identifiers from this workspace. |
| `tag_names` | string\[\] |  | Up to three tag names. Names are trimmed and matched case-insensitively; missing names are created. Each name is 1–25 characters and the workspace may contain at most 50 tags. Takes precedence over tag\_ids. |
| `location_defaults` | object |  | Replacement defaults for location language, action\_language, website, and include\_parent\_brand. These affect effective location behavior without changing explicit location values. |
| `website` | string \| null |  | Replacement normalized HTTP or HTTPS URL, maximum 2,048 characters. Standard projects cannot clear this field; locations\_only projects may send null. |

#### Multipart request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | New project name. Maximum: 200 characters. |
| `external_id` | string \| null |  | partner identifier, unique across projects. Maximum: 200 characters. |
| `description` | string \| null |  | description. Maximum: 5,000 characters. |
| `brand_aliases` | string\[\] |  | Up to 10 alternative names, each at most 200 characters. Values are trimmed, lowercased, and deduplicated. |
| `country_code` | string |  | Uppercase ISO 3166-1 alpha-2 country code. |
| `country` | string |  | Country name. |
| `city` | string \| null |  | City; required when focus is city. Maximum: 200 characters. |
| `state` | string \| null |  | Region or state. Maximum: 200 characters. |
| `address` | string \| null |  | Street address. Maximum: 500 characters. |
| `postal_code` | string \| null |  | Postal code. Maximum: 200 characters. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `google_place_id` | string \| null |  | Google place identifier. Maximum: 500 characters. |
| `language` | ISO 639-1 string |  | Content language. |
| `action_language` | ISO 639-1 string \| null |  | language for generated actions. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. |
| `local_mode` | boolean |  | Enable local geographic context. |
| `folder` | string \| null |  | organization folder. Maximum: 30 characters. |
| `tag_ids` | uuid\[\] |  | Up to three tag identifiers from this workspace. |
| `tag_names` | string\[\] |  | Up to three tag names. Names are trimmed and matched case-insensitively; missing names are created. Each name is 1–25 characters and the workspace may contain at most 50 tags. Takes precedence over tag\_ids. |
| `location_defaults` | object |  | Replacement defaults for location language, action\_language, website, and include\_parent\_brand. These affect effective location behavior without changing explicit location values. |
| `website` | string \| null |  | Replacement normalized HTTP or HTTPS URL, maximum 2,048 characters. Standard projects cannot clear this field; locations\_only projects may send null. |
| `logo` | binary |  | JPEG, PNG, or WebP image, maximum 5 MB. Replaces the current logo after validation. |
| `remove_logo` | boolean |  | Set true to remove the current logo. If logo is also supplied, the uploaded logo takes precedence and remove\_logo is ignored. |

> **Replacement fields and precedence**
>
> `project_mode` and package assignment are immutable. Set nullable JSON fields to `null` to clear them. Empty alias and tag arrays clear their assignments. If both `tag_names` and `tag_ids` are supplied, `tag_names` wins. Missing tag names are created atomically; if the three-assignment or 50-tag workspace limit would be exceeded, no tags are changed.

> **Logo upload and removal**
>
> Use `application/json` when no file is involved. Use `multipart/form-data` to upload `logo` or set `remove_logo=true`; other fields retain the same validation and replacement semantics. Repeated `tag_ids[]`, `tag_names[]`, and `brand_aliases[]` parts represent arrays. A supplied logo takes precedence over `remove_logo`. Successful removal returns `logo_url: null`.

#### Response envelope

`project`:**Project**

The requested, created, or updated project. The envelope is identical for UUID and external-ID lookup.

#### Project

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Project identifier. |
| `workspace_id` | uuid |  | Identifier of the workspace that owns the project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Project display name. |
| `description` | string \| null |  | Project description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS website URL. |
| `brand_aliases` | string\[\] |  | Normalized alternative brand names. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `project_mode` | standard \| locations\_only |  | Standard projects have their own tracking package. locations\_only projects act as containers for independently packaged locations. |
| `status` | active \| inactive \| archived |  | Current project lifecycle status. |
| `package` | PackageReference \| null |  | Assigned project package. Null when project\_mode is locations\_only. |
| `address` | string \| null |  | Normalized street address. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string |  | Normalized country name. |
| `country_code` | string |  | Uppercase ISO 3166-1 alpha-2 country code. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `google_place_id` | string \| null |  | Google place identifier when one is configured. |
| `language` | string |  | Lowercase ISO 639-1 content language. |
| `action_language` | string \| null |  | Lowercase ISO 639-1 language used for generated actions. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. |
| `local_mode` | boolean |  | Whether local geographic context is emphasized. |
| `local_context` | object |  | Resolved geographic context used by project processing. |
| `location_defaults` | object |  | Default language, action language, website, and parent-brand settings available to locations in this project. |
| `folder` | string \| null |  | Optional workspace organization folder. |
| `tags` | Tag\[\] |  | Up to three workspace tags assigned to the project. |
| `logo_url` | string \| null |  | Absolute project logo URL when configured. |
| `created_at` | datetime |  | Project creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent project update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Tag

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Tag identifier. |
| `name` | string |  | Tag display name. |
| `color` | string |  | Tag display color. |

#### Request and response

```curl
curl --request PATCH \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Acme Europe & UK",
  "external_id": "partner-project-acme-eu",
  "brand_aliases": [
    "acme",
    "acme europe",
    "acme uk"
  ],
  "tag_names": [
    "Retail",
    "Europe"
  ]
}'
```

```json
{
  "project": {
    "id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "workspace_id": "b7dd886f-b144-4ab4-907b-8efde9df881a",
    "external_id": "partner-project-acme-eu",
    "name": "Acme Europe & UK",
    "description": "European visibility program for Acme.",
    "website": "https://acme.example/",
    "brand_aliases": [
      "acme",
      "acme europe",
      "acme uk"
    ],
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "project_mode": "standard",
    "status": "active",
    "package": {
      "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
      "name": "Growth Weekly"
    },
    "address": "1 Market Street",
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": "NL",
    "latitude": 52.3728,
    "longitude": 4.8936,
    "google_place_id": null,
    "language": "en",
    "action_language": "en",
    "focus": "country",
    "local_mode": false,
    "local_context": {},
    "location_defaults": {
      "language": "en",
      "action_language": "en",
      "website": "https://acme.example/",
      "include_parent_brand": true
    },
    "folder": "Europe",
    "tags": [
      {
        "id": "30761d13-bc7e-45c8-8968-2147a37d6e54",
        "name": "Retail",
        "color": "#2563EB"
      },
      {
        "id": "b4e8bc5c-8282-4969-9d50-d8cf7723705a",
        "name": "Europe",
        "color": "#7C3AED"
      }
    ],
    "logo_url": null,
    "created_at": "2026-07-31T08:00:00Z",
    "updated_at": "2026-07-31T12:00:00Z"
  }
}
```

#### Request and response

```curl
curl --request PATCH \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --form 'tag_names[]=Retail' \
  --form 'tag_names[]=Europe' \
  --form 'logo=@./acme-logo.webp;type=image/webp' \
  --form 'remove_logo=true'
```

```json
{
  "project": {
    "id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "workspace_id": "b7dd886f-b144-4ab4-907b-8efde9df881a",
    "external_id": "partner-project-acme",
    "name": "Acme Europe",
    "description": "European visibility program for Acme.",
    "website": "https://acme.example/",
    "brand_aliases": [
      "acme",
      "acme europe"
    ],
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "project_mode": "standard",
    "status": "active",
    "package": {
      "id": "92404fd7-f096-49e9-9ab0-5ed73517d9db",
      "name": "Growth Weekly"
    },
    "address": "1 Market Street",
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": "NL",
    "latitude": 52.3728,
    "longitude": 4.8936,
    "google_place_id": null,
    "language": "en",
    "action_language": "en",
    "focus": "country",
    "local_mode": false,
    "local_context": {},
    "location_defaults": {
      "language": "en",
      "action_language": "en",
      "website": "https://acme.example/",
      "include_parent_brand": true
    },
    "folder": "Europe",
    "tags": [
      {
        "id": "30761d13-bc7e-45c8-8968-2147a37d6e54",
        "name": "Retail",
        "color": "#2563EB"
      },
      {
        "id": "b4e8bc5c-8282-4969-9d50-d8cf7723705a",
        "name": "Europe",
        "color": "#7C3AED"
      }
    ],
    "logo_url": "https://api.ceyo.ai/media/logos/opaque-logo-token",
    "created_at": "2026-07-31T08:00:00Z",
    "updated_at": "2026-07-31T12:05:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `409` | conflict |  | The external ID is already in use or the resource cannot accept this operation in its current state. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Delete project

`DELETE /projects/{project_id}`

Schedules asynchronous deletion of a project and all resources contained by it.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |

> **202 Accepted**
>
> Deletion runs asynchronously and the response has no body.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 202 Accepted
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Locations

Source: https://ceyo.ai/docs/signal/locations

### Locations

Provision and manage locations in the selected project.

> **Authentication and scope**
>
> Send `Authorization: Bearer ceyo_platform_...` on every request. The API key selects the workspace, so paths never require a workspace identifier.

> **Packages and project modes**
>
> A standard project selects an active project package with `package_id`. Every location selects an active location package. A `locations_only` project is a container for locations, has no project package, and is not itself a tracking scope. Package assignments cannot be changed through resource updates.

> **Pagination, filters, and ordering**
>
> List endpoints use 1-based `page` and `per_page`, return pagination metadata, and return an empty array when the page is beyond the result set. Filters combine with AND. Search is trimmed, case-insensitive, and limited to 200 characters. Sorts are stable and use resource ID as the final ascending tie-breaker.

### List locations

`GET /projects/{project_id}/locations`

Returns locations in a project. Filters combine with AND and the selected sort is deterministic.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `q` | string | Optional | Case-insensitive search across name, external\_id, formatted address, city, state, postal code, country, phone, and email. Maximum: 200 characters. |
| `status` | active \| inactive \| archived | Optional | Return locations in one lifecycle status. |
| `country_code` | ISO 3166-1 alpha-2 string | Optional | Match the location’s explicit country code. |
| `sort` | created\_at \| updated\_at \| name | Optional; Default: created\_at | Field used for ordering. |
| `direction` | asc \| desc | Optional; Default: desc | Sort direction. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Number of records per page, from 1 through 100. Values outside this range return 422. |

#### Response envelope

`locations`:**Location\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `locations` | Location\[\] |  | Matching locations in the requested deterministic sort order. |
| `pagination` | Pagination |  | Pagination metadata. |

#### Location

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Location identifier. |
| `project_id` | uuid |  | Identifier of the containing project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Location display name. |
| `description` | string \| null |  | Location description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS location website. |
| `brand_aliases` | string\[\] |  | Normalized alternative names for the location brand. |
| `language` | ISO 639-1 string \| null |  | Location content language, or null when not overridden. |
| `action_language` | ISO 639-1 string \| null |  | Location action language, or null when not overridden. |
| `include_parent_brand` | boolean \| null |  | Whether the parent project brand is included. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `phone` | string \| null |  | Partner-supplied contact phone number. |
| `email` | string \| null |  | Normalized contact email address. |
| `metadata` | object |  | Partner-owned JSON metadata. Keys and values are returned without interpretation. |
| `local_context` | object |  | Partner-supplied local facts used to contextualize processing. |
| `focus` | global \| country \| region \| city |  | Configured geographic targeting focus. |
| `google_place_id` | string \| null |  | Google place identifier. |
| `google_place_source` | provided \| discovered \| null |  | Whether the Google place was supplied by the customer or matched during onboarding. |
| `google_place_name` | string \| null |  | Business name associated with the Google place. |
| `google_maps_url` | string \| null |  | Google Maps URL for the location. |
| `formatted_address` | string \| null |  | Formatted physical address. |
| `address_line_2` | string \| null |  | Optional second address line. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string \| null |  | Normalized country name. |
| `country_code` | string \| null |  | Explicit uppercase ISO 3166-1 alpha-2 country code, or null when not configured. Locations do not inherit the project country. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `status` | active \| inactive \| archived |  | Current location lifecycle status. |
| `package` | PackageReference |  | Assigned location package. |
| `created_at` | datetime |  | Location creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent location update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Metadata and local context object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `additional properties` | JSON value |  | Arbitrary partner-owned keys with string, number, boolean, null, object, or array values. |
| `maximum size` | 16 KB |  | Limit measured after JSON serialization. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Number of records requested per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations?q=amsterdam&status=active&sort=name&direction=asc&page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "locations": [
    {
      "id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
      "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
      "external_id": "partner-location-amsterdam",
      "name": "Acme Amsterdam",
      "description": "Acme flagship store in Amsterdam.",
      "website": "https://acme.example/amsterdam",
      "brand_aliases": [
        "acme amsterdam"
      ],
      "language": "en",
      "action_language": "en",
      "include_parent_brand": true,
      "competitors": [
        {
          "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
          "kind": "competitor",
          "name": "Example Rival",
          "domain": "example-rival.com",
          "aliases": [
            "rival"
          ],
          "status": "active",
          "competitor_state": "tracked",
          "created_at": "2026-07-02T11:20:00Z",
          "updated_at": "2026-07-30T09:10:00Z"
        }
      ],
      "phone": "+31 20 555 0100",
      "email": "amsterdam@acme.example",
      "metadata": {
        "partner_region_id": "nl-west"
      },
      "local_context": {
        "neighborhood": "Centrum",
        "service_area": "Amsterdam"
      },
      "focus": "city",
      "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
      "google_place_source": "provided",
      "google_place_name": "Acme Amsterdam",
      "google_maps_url": "https://maps.google.com/?cid=123456789",
      "formatted_address": "1 Market Street, 1012 JS Amsterdam, Netherlands",
      "address_line_2": null,
      "city": "Amsterdam",
      "state": "North Holland",
      "postal_code": "1012 JS",
      "country": "Netherlands",
      "country_code": null,
      "latitude": 52.3728,
      "longitude": 4.8936,
      "status": "active",
      "package": {
        "id": "8ae92d3f-18fb-4899-aef0-11f50b8bd0a7",
        "name": "Local Growth Weekly"
      },
      "created_at": "2026-07-31T08:10:00Z",
      "updated_at": "2026-07-31T08:10:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Create location

`POST /projects/{project_id}/locations`

Synchronously provisions a location under a project and returns it. Optionally starts onboarding after creation.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Required location name. Maximum: 200 characters. |
| `package_id` | uuid |  | Required active location package identifier from the same workspace. |
| `external_id` | string \| null |  | Optional partner identifier, unique among locations in this project. |
| `description` | string \| null |  | Optional description. Maximum: 5,000 characters. |
| `website` | string \| null |  | Valid HTTP or HTTPS URL; maximum 2,048 characters. |
| `brand_aliases` | string\[\] |  | Up to 10 alternative names, each at most 200 characters. Values are normalized and deduplicated. |
| `language` | ISO 639-1 string \| null |  | Optional lowercase content-language override. Null uses the project location default, then en. |
| `action_language` | ISO 639-1 string \| null |  | Optional lowercase action-language override. Null uses the project location default, then the effective content language. |
| `include_parent_brand` | boolean \| null |  | Optional parent-brand override. Null uses the project location default. |
| `phone` | string \| null |  | Contact phone number; maximum 50 characters. |
| `email` | string \| null |  | Valid contact email; maximum 320 characters. |
| `metadata` | object |  | Partner-owned JSON object, maximum serialized size 16 KB. Defaults to {}. |
| `local_context` | object |  | Local context JSON object, maximum serialized size 16 KB. Defaults to {}. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. Defaults to city. |
| `google_place_id` | string \| null |  | Google place identifier, unique in the project. When supplied, Ceyo validates it and resolves canonical place details. Maximum: 500 characters. |
| `google_place_name` | string \| null |  | Google place business name. Maximum: 200 characters. |
| `google_maps_url` | string \| null |  | Google Maps URL. Maximum: 2,048 characters. |
| `formatted_address` | string \| null |  | Formatted physical address. Maximum: 500 characters. |
| `address_line_2` | string \| null |  | Optional suite, unit, or floor. Preserved separately from the canonical address. Maximum: 200 characters. |
| `city` | string \| null |  | City. Maximum: 200 characters. |
| `state` | string \| null |  | Region or state. Maximum: 200 characters. |
| `postal_code` | string \| null |  | Postal code. Maximum: 200 characters. |
| `country` | string \| null |  | Country name. Maximum: 200 characters. |
| `country_code` | string \| null |  | ISO 3166-1 alpha-2 code. Null leaves the location country code unset; locations do not inherit the project country. |
| `latitude` | number \| null |  | Latitude from -90 through 90. Latitude and longitude must be supplied together. |
| `longitude` | number \| null |  | Longitude from -180 through 180. Latitude and longitude must be supplied together. |
| `start` | boolean |  | When true, starts onboarding after provisioning. Supply either google\_place\_id or both city and country\_code. Defaults to false. |

> **Location package**
>
> `package_id` must identify an active location package in the same workspace. The location can provide its own `country_code`. If omitted, the location country code remains unset; it does not inherit the project country.

> **Automatic or manual place data**
>
> With `google_place_id`, Ceyo validates the place and fills canonical place details. Without it, provide your own location data. For `start: true`, `city` and `country_code` are enough; onboarding then tries to find a high-confidence Google match in the background.

> **Best-effort listing match**
>
> Customer-supplied fields are kept when a match is found. If no clear match exists, onboarding continues and listing analysis is skipped.

#### Response envelope

`location`:**Location**`onboarding_operation`:**OnboardingOperation | null**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `location` | Location |  | The provisioned location. |
| `onboarding_operation` | OnboardingOperation \| null |  | Polling operation when start is true; null when onboarding was not requested. |

#### Location

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Location identifier. |
| `project_id` | uuid |  | Identifier of the containing project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Location display name. |
| `description` | string \| null |  | Location description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS location website. |
| `brand_aliases` | string\[\] |  | Normalized alternative names for the location brand. |
| `language` | ISO 639-1 string \| null |  | Location content language, or null when not overridden. |
| `action_language` | ISO 639-1 string \| null |  | Location action language, or null when not overridden. |
| `include_parent_brand` | boolean \| null |  | Whether the parent project brand is included. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `phone` | string \| null |  | Partner-supplied contact phone number. |
| `email` | string \| null |  | Normalized contact email address. |
| `metadata` | object |  | Partner-owned JSON metadata. Keys and values are returned without interpretation. |
| `local_context` | object |  | Partner-supplied local facts used to contextualize processing. |
| `focus` | global \| country \| region \| city |  | Configured geographic targeting focus. |
| `google_place_id` | string \| null |  | Google place identifier. |
| `google_place_source` | provided \| discovered \| null |  | Whether the Google place was supplied by the customer or matched during onboarding. |
| `google_place_name` | string \| null |  | Business name associated with the Google place. |
| `google_maps_url` | string \| null |  | Google Maps URL for the location. |
| `formatted_address` | string \| null |  | Formatted physical address. |
| `address_line_2` | string \| null |  | Optional second address line. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string \| null |  | Normalized country name. |
| `country_code` | string \| null |  | Explicit uppercase ISO 3166-1 alpha-2 country code, or null when not configured. Locations do not inherit the project country. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `status` | active \| inactive \| archived |  | Current location lifecycle status. |
| `package` | PackageReference |  | Assigned location package. |
| `created_at` | datetime |  | Location creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent location update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Metadata and local context object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `additional properties` | JSON value |  | Arbitrary partner-owned keys with string, number, boolean, null, object, or array values. |
| `maximum size` | 16 KB |  | Limit measured after JSON serialization. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Acme Amsterdam",
  "external_id": "partner-location-amsterdam",
  "package_id": "8ae92d3f-18fb-4899-aef0-11f50b8bd0a7",
  "website": "https://acme.example/amsterdam",
  "language": "nl",
  "action_language": "en",
  "include_parent_brand": true,
  "phone": "+31 20 555 0100",
  "email": "amsterdam@acme.example",
  "metadata": {
    "partner_region_id": "nl-west"
  },
  "local_context": {
    "neighborhood": "Centrum",
    "service_area": "Amsterdam"
  },
  "focus": "city",
  "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
  "address_line_2": "Suite 4",
  "start": true
}'
```

```json
HTTP/1.1 201 Created

{
  "location": {
    "id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
    "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "external_id": "partner-location-amsterdam",
    "name": "Acme Amsterdam",
    "description": "Acme flagship store in Amsterdam.",
    "website": "https://acme.example/amsterdam",
    "brand_aliases": [
      "acme amsterdam"
    ],
    "language": "en",
    "action_language": "en",
    "include_parent_brand": true,
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "phone": "+31 20 555 0100",
    "email": "amsterdam@acme.example",
    "metadata": {
      "partner_region_id": "nl-west"
    },
    "local_context": {
      "neighborhood": "Centrum",
      "service_area": "Amsterdam"
    },
    "focus": "city",
    "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
    "google_place_source": "provided",
    "google_place_name": "Acme Amsterdam",
    "google_maps_url": "https://maps.google.com/?cid=123456789",
    "formatted_address": "1 Market Street, 1012 JS Amsterdam, Netherlands",
    "address_line_2": "Suite 4",
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": null,
    "latitude": 52.3728,
    "longitude": 4.8936,
    "status": "active",
    "package": {
      "id": "8ae92d3f-18fb-4899-aef0-11f50b8bd0a7",
      "name": "Local Growth Weekly"
    },
    "created_at": "2026-07-31T08:10:00Z",
    "updated_at": "2026-07-31T08:10:00Z"
  },
  "onboarding_operation": {
    "id": "1c07ea43-a8fe-4d07-8741-9c624d67b466",
    "status": "queued",
    "resource_type": "location",
    "resource_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
    "progress": {
      "completed": 0,
      "total": 6
    },
    "message": "Onboarding is queued.",
    "steps": [
      {
        "key": "enrichment",
        "status": "pending"
      },
      {
        "key": "topics",
        "status": "pending"
      },
      {
        "key": "prompts",
        "status": "pending"
      },
      {
        "key": "competitors",
        "status": "pending"
      },
      {
        "key": "visibility",
        "status": "pending"
      },
      {
        "key": "diagnosis",
        "status": "pending"
      }
    ],
    "status_url": "/v1/onboarding-operations/1c07ea43-a8fe-4d07-8741-9c624d67b466",
    "created_at": "2026-08-04T15:00:00Z",
    "updated_at": "2026-08-04T15:00:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `409` | conflict |  | The external ID is already in use or the resource cannot accept this operation in its current state. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |
| `503` | place\_details\_unavailable |  | A supplied Google Place ID could not be resolved because place details are temporarily unavailable. |

### Bulk create locations

`POST /projects/{project_id}/locations/bulk`

Accepts up to 100 location create records and provisions them asynchronously under one project.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `locations` | LocationCreate\[\] |  | Between 1 and 100 records using the same fields and validation as Create location. |

> **Idempotency required**
>
> Send a unique `Idempotency-Key` header. Repeating the same request returns the existing operation. Reusing the key with a different body returns `409`.

> **Independent onboarding**
>
> Set `start` separately on each record. Successful records with `start: true` enqueue location onboarding; one failed record does not roll back the others.

#### Response envelope

`bulk_operation`:**BulkOperation**

Accepted operation summary with an ID and status\_url for polling.

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/bulk' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Idempotency-Key: provision-2026-08-04-001' \
  --header 'Content-Type: application/json' \
  --data '{
  "locations": [
    {
      "name": "Acme Amsterdam",
      "external_id": "partner-location-amsterdam",
      "package_id": "8ae92d3f-18fb-4899-aef0-11f50b8bd0a7",
      "website": "https://acme.example/amsterdam",
      "language": "nl",
      "action_language": "en",
      "include_parent_brand": true,
      "phone": "+31 20 555 0100",
      "email": "amsterdam@acme.example",
      "metadata": {
        "partner_region_id": "nl-west"
      },
      "local_context": {
        "neighborhood": "Centrum",
        "service_area": "Amsterdam"
      },
      "focus": "city",
      "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
      "address_line_2": "Suite 4",
      "start": true
    },
    {
      "name": "Acme Rotterdam",
      "external_id": "partner-location-rotterdam",
      "package_id": "8ae92d3f-18fb-4899-aef0-11f50b8bd0a7",
      "website": "https://acme.example/amsterdam",
      "language": "nl",
      "action_language": "en",
      "include_parent_brand": true,
      "phone": "+31 20 555 0100",
      "email": "amsterdam@acme.example",
      "metadata": {
        "partner_region_id": "nl-west"
      },
      "local_context": {
        "neighborhood": "Centrum",
        "service_area": "Amsterdam"
      },
      "focus": "city",
      "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
      "address_line_2": "Suite 4",
      "start": true,
      "city": "Rotterdam"
    }
  ]
}'
```

```json
HTTP/1.1 202 Accepted

{
  "bulk_operation": {
    "id": "f9bc15cc-e9c9-4e93-a93e-b713c92c7315",
    "type": "locations",
    "status": "pending",
    "parent_project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "total": 2,
    "pending": 2,
    "succeeded": 0,
    "failed": 0,
    "created_at": "2026-08-04T15:00:00Z",
    "started_at": null,
    "completed_at": null,
    "status_url": "/v1/bulk-operations/f9bc15cc-e9c9-4e93-a93e-b713c92c7315"
  }
}
```

Poll the shared [Get bulk operation](/docs/signal/bulk-operations#get-bulk-operation) endpoint for per-record results.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `409` | conflict |  | The external ID is already in use or the resource cannot accept this operation in its current state. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get location

`GET /projects/{project_id}/locations/{location_id}`

Returns one location by its Ceyo UUID within the selected project.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |
| `location_id` | location UUID \| location external ID | Required | Ceyo location UUID or configured partner external ID belonging to the project. |

#### Response envelope

`location`:**Location**

The requested, created, or updated location. The envelope is identical for UUID and external-ID lookup.

#### Location

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Location identifier. |
| `project_id` | uuid |  | Identifier of the containing project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Location display name. |
| `description` | string \| null |  | Location description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS location website. |
| `brand_aliases` | string\[\] |  | Normalized alternative names for the location brand. |
| `language` | ISO 639-1 string \| null |  | Location content language, or null when not overridden. |
| `action_language` | ISO 639-1 string \| null |  | Location action language, or null when not overridden. |
| `include_parent_brand` | boolean \| null |  | Whether the parent project brand is included. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `phone` | string \| null |  | Partner-supplied contact phone number. |
| `email` | string \| null |  | Normalized contact email address. |
| `metadata` | object |  | Partner-owned JSON metadata. Keys and values are returned without interpretation. |
| `local_context` | object |  | Partner-supplied local facts used to contextualize processing. |
| `focus` | global \| country \| region \| city |  | Configured geographic targeting focus. |
| `google_place_id` | string \| null |  | Google place identifier. |
| `google_place_source` | provided \| discovered \| null |  | Whether the Google place was supplied by the customer or matched during onboarding. |
| `google_place_name` | string \| null |  | Business name associated with the Google place. |
| `google_maps_url` | string \| null |  | Google Maps URL for the location. |
| `formatted_address` | string \| null |  | Formatted physical address. |
| `address_line_2` | string \| null |  | Optional second address line. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string \| null |  | Normalized country name. |
| `country_code` | string \| null |  | Explicit uppercase ISO 3166-1 alpha-2 country code, or null when not configured. Locations do not inherit the project country. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `status` | active \| inactive \| archived |  | Current location lifecycle status. |
| `package` | PackageReference |  | Assigned location package. |
| `created_at` | datetime |  | Location creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent location update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Metadata and local context object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `additional properties` | JSON value |  | Arbitrary partner-owned keys with string, number, boolean, null, object, or array values. |
| `maximum size` | 16 KB |  | Limit measured after JSON serialization. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/a1308d14-149c-4dd7-a4c5-295ac9090f58' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "location": {
    "id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
    "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "external_id": "partner-location-amsterdam",
    "name": "Acme Amsterdam",
    "description": "Acme flagship store in Amsterdam.",
    "website": "https://acme.example/amsterdam",
    "brand_aliases": [
      "acme amsterdam"
    ],
    "language": "en",
    "action_language": "en",
    "include_parent_brand": true,
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "phone": "+31 20 555 0100",
    "email": "amsterdam@acme.example",
    "metadata": {
      "partner_region_id": "nl-west"
    },
    "local_context": {
      "neighborhood": "Centrum",
      "service_area": "Amsterdam"
    },
    "focus": "city",
    "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
    "google_place_source": "provided",
    "google_place_name": "Acme Amsterdam",
    "google_maps_url": "https://maps.google.com/?cid=123456789",
    "formatted_address": "1 Market Street, 1012 JS Amsterdam, Netherlands",
    "address_line_2": null,
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": null,
    "latitude": 52.3728,
    "longitude": 4.8936,
    "status": "active",
    "package": {
      "id": "8ae92d3f-18fb-4899-aef0-11f50b8bd0a7",
      "name": "Local Growth Weekly"
    },
    "created_at": "2026-07-31T08:10:00Z",
    "updated_at": "2026-07-31T08:10:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get locations overview

`GET /projects/{project_id}/locations/overview`

Returns a paginated location comparison view and map markers for the filtered result set.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `q` | string | Optional | Case-insensitive search across location name, external\_id, formatted address, city, state, postal code, and country. Maximum: 200 characters. |
| `status` | active \| inactive | Optional | Return one non-archived lifecycle status. Archived locations are excluded. |
| `country_code` | ISO 3166-1 alpha-2 string | Optional | Match the location’s explicit country code. |
| `sort` | name \| visibility\_rate \| avg\_position | Optional; Default: name | Field used for ordering. Null metrics sort after non-null values in either direction. |
| `direction` | asc \| desc | Optional; Default: asc | Sort direction. When sort is not name and direction is omitted, the default is desc. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Number of records per page, from 1 through 100. Values outside this range return 422. |

#### Response envelope

`locations`:**LocationOverview\[\]**`pagination`:**Pagination**`map`:**LocationMap**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `locations` | LocationOverview\[\] |  | The requested page of matching locations. |
| `pagination` | Pagination |  | Pagination over the filtered and sorted location set. |
| `map` | LocationMap |  | Markers for the complete filtered set. |

#### LocationOverview

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Location identifier. |
| `external_id` | string \| null |  | Partner-supplied location identifier. |
| `name` | string |  | Location display name. |
| `status` | active \| inactive |  | Current non-archived lifecycle status. |
| `formatted_address` | string \| null |  | Formatted physical address. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `country_code` | string |  | Effective uppercase ISO 3166-1 alpha-2 country code. |
| `latitude` | number \| null |  | Latitude, or null when unavailable. |
| `longitude` | number \| null |  | Longitude, or null when unavailable. |
| `visibility_summary` | VisibilitySummary |  | Latest completed 30-day location visibility summary. |

#### VisibilitySummary

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility_rate` | number \| null |  | Percentage of included responses that mentioned the brand; null when unavailable. |
| `avg_position` | number \| null |  | Average 1-based brand position when present; null when no ranked mention is available. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Number of records requested per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

#### LocationMap

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `markers` | LocationMarker\[\] |  | Markers ordered by location name, then location ID. Only active locations with both coordinates are eligible. |

#### LocationMarker

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Location identifier. |
| `external_id` | string \| null |  | Partner-supplied location identifier. |
| `name` | string |  | Location display name. |
| `formatted_address` | string \| null |  | Formatted address used in map labels. |
| `latitude` | number |  | Marker latitude. |
| `longitude` | number |  | Marker longitude. |
| `visibility_rate` | number \| null |  | Latest 30-day brand visibility percentage. |
| `avg_position` | number \| null |  | Latest 30-day average brand position. |

> **Metrics, map, and sorting**
>
> Visibility fields use the latest completed 30-day window for each location. The page and marker set use the same search and filters. Pagination affects `locations` only. Every sort uses name and then location ID as ascending tie-breakers.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/overview?q=amsterdam&status=active&sort=visibility_rate&direction=desc&page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "locations": [
    {
      "id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
      "external_id": "partner-location-amsterdam",
      "name": "Acme Amsterdam",
      "status": "active",
      "formatted_address": "1 Market Street, 1012 JS Amsterdam, Netherlands",
      "city": "Amsterdam",
      "state": "North Holland",
      "country_code": "NL",
      "latitude": 52.3728,
      "longitude": 4.8936,
      "visibility_summary": {
        "visibility_rate": 68.4,
        "avg_position": 2.7
      }
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  },
  "map": {
    "markers": [
      {
        "id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
        "external_id": "partner-location-amsterdam",
        "name": "Acme Amsterdam",
        "formatted_address": "1 Market Street, 1012 JS Amsterdam, Netherlands",
        "latitude": 52.3728,
        "longitude": 4.8936,
        "visibility_rate": 68.4,
        "avg_position": 2.7
      }
    ]
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Find location by external ID

`GET /projects/{project_id}/locations/by-external-id/{external_id}`

Returns the location whose external\_id exactly matches the URL-encoded value within the selected project.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |
| `external_id` | string | Required | URL-encoded, case-sensitive external ID previously assigned to the resource. |

> **Project-scoped lookup**
>
> Location external IDs are unique within a project and matching is case-sensitive. An empty or malformed path value returns `400`; an unknown value or a value assigned in another project returns `404`.

#### Response envelope

`location`:**Location**

The requested, created, or updated location. The envelope is identical for UUID and external-ID lookup.

#### Location

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Location identifier. |
| `project_id` | uuid |  | Identifier of the containing project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Location display name. |
| `description` | string \| null |  | Location description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS location website. |
| `brand_aliases` | string\[\] |  | Normalized alternative names for the location brand. |
| `language` | ISO 639-1 string \| null |  | Location content language, or null when not overridden. |
| `action_language` | ISO 639-1 string \| null |  | Location action language, or null when not overridden. |
| `include_parent_brand` | boolean \| null |  | Whether the parent project brand is included. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `phone` | string \| null |  | Partner-supplied contact phone number. |
| `email` | string \| null |  | Normalized contact email address. |
| `metadata` | object |  | Partner-owned JSON metadata. Keys and values are returned without interpretation. |
| `local_context` | object |  | Partner-supplied local facts used to contextualize processing. |
| `focus` | global \| country \| region \| city |  | Configured geographic targeting focus. |
| `google_place_id` | string \| null |  | Google place identifier. |
| `google_place_source` | provided \| discovered \| null |  | Whether the Google place was supplied by the customer or matched during onboarding. |
| `google_place_name` | string \| null |  | Business name associated with the Google place. |
| `google_maps_url` | string \| null |  | Google Maps URL for the location. |
| `formatted_address` | string \| null |  | Formatted physical address. |
| `address_line_2` | string \| null |  | Optional second address line. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string \| null |  | Normalized country name. |
| `country_code` | string \| null |  | Explicit uppercase ISO 3166-1 alpha-2 country code, or null when not configured. Locations do not inherit the project country. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `status` | active \| inactive \| archived |  | Current location lifecycle status. |
| `package` | PackageReference |  | Assigned location package. |
| `created_at` | datetime |  | Location creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent location update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Metadata and local context object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `additional properties` | JSON value |  | Arbitrary partner-owned keys with string, number, boolean, null, object, or array values. |
| `maximum size` | 16 KB |  | Limit measured after JSON serialization. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/by-external-id/partner-location-amsterdam' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "location": {
    "id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
    "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "external_id": "partner-location-amsterdam",
    "name": "Acme Amsterdam",
    "description": "Acme flagship store in Amsterdam.",
    "website": "https://acme.example/amsterdam",
    "brand_aliases": [
      "acme amsterdam"
    ],
    "language": "en",
    "action_language": "en",
    "include_parent_brand": true,
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "phone": "+31 20 555 0100",
    "email": "amsterdam@acme.example",
    "metadata": {
      "partner_region_id": "nl-west"
    },
    "local_context": {
      "neighborhood": "Centrum",
      "service_area": "Amsterdam"
    },
    "focus": "city",
    "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
    "google_place_source": "provided",
    "google_place_name": "Acme Amsterdam",
    "google_maps_url": "https://maps.google.com/?cid=123456789",
    "formatted_address": "1 Market Street, 1012 JS Amsterdam, Netherlands",
    "address_line_2": null,
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": null,
    "latitude": 52.3728,
    "longitude": 4.8936,
    "status": "active",
    "package": {
      "id": "8ae92d3f-18fb-4899-aef0-11f50b8bd0a7",
      "name": "Local Growth Weekly"
    },
    "created_at": "2026-07-31T08:10:00Z",
    "updated_at": "2026-07-31T08:10:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Update location

`PATCH /projects/{project_id}/locations/{location_id}`

Updates only supplied location settings. Omitted fields remain unchanged.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |
| `location_id` | location UUID \| location external ID | Required | Ceyo location UUID or configured partner external ID belonging to the project. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | New location name. Maximum: 200 characters. |
| `external_id` | string \| null |  | partner identifier, unique among locations in this project. |
| `description` | string \| null |  | description. Maximum: 5,000 characters. |
| `website` | string \| null |  | Valid HTTP or HTTPS URL; maximum 2,048 characters. |
| `brand_aliases` | string\[\] |  | Up to 10 alternative names, each at most 200 characters. Values are normalized and deduplicated. |
| `language` | ISO 639-1 string \| null |  | lowercase content-language override. Null uses the project location default, then en. |
| `action_language` | ISO 639-1 string \| null |  | lowercase action-language override. Null uses the project location default, then the effective content language. |
| `include_parent_brand` | boolean \| null |  | parent-brand override. Null uses the project location default. |
| `phone` | string \| null |  | Contact phone number; maximum 50 characters. |
| `email` | string \| null |  | Valid contact email; maximum 320 characters. |
| `metadata` | object |  | Partner-owned JSON object, maximum serialized size 16 KB. |
| `local_context` | object |  | Local context JSON object, maximum serialized size 16 KB. |
| `focus` | global \| country \| region \| city |  | Geographic targeting focus. |
| `google_place_id` | string \| null |  | Google place identifier, unique in the project. When supplied, Ceyo validates it and resolves canonical place details. Maximum: 500 characters. |
| `google_place_name` | string \| null |  | Google place business name. Maximum: 200 characters. |
| `google_maps_url` | string \| null |  | Google Maps URL. Maximum: 2,048 characters. |
| `formatted_address` | string \| null |  | Formatted physical address. Maximum: 500 characters. |
| `address_line_2` | string \| null |  | suite, unit, or floor. Preserved separately from the canonical address. Maximum: 200 characters. |
| `city` | string \| null |  | City. Maximum: 200 characters. |
| `state` | string \| null |  | Region or state. Maximum: 200 characters. |
| `postal_code` | string \| null |  | Postal code. Maximum: 200 characters. |
| `country` | string \| null |  | Country name. Maximum: 200 characters. |
| `country_code` | string \| null |  | ISO 3166-1 alpha-2 code. Null leaves the location country code unset; locations do not inherit the project country. |
| `latitude` | number \| null |  | Latitude from -90 through 90. Latitude and longitude must be supplied together. |
| `longitude` | number \| null |  | Longitude from -180 through 180. Latitude and longitude must be supplied together. |
| `address_line_2` | string \| null |  | Replacement optional second address line. |

> **Replacement and clearing behavior**
>
> Project membership and package assignment are immutable. Set nullable fields to `null` to clear them. `metadata` and `local_context` replace their complete objects; send `{}` to clear their keys. An empty `brand_aliases` array removes all aliases.

#### Response envelope

`location`:**Location**

The requested, created, or updated location. The envelope is identical for UUID and external-ID lookup.

#### Location

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Location identifier. |
| `project_id` | uuid |  | Identifier of the containing project. |
| `external_id` | string \| null |  | Case-sensitive identifier supplied by the partner. |
| `name` | string |  | Location display name. |
| `description` | string \| null |  | Location description. |
| `website` | string \| null |  | Normalized HTTP or HTTPS location website. |
| `brand_aliases` | string\[\] |  | Normalized alternative names for the location brand. |
| `language` | ISO 639-1 string \| null |  | Location content language, or null when not overridden. |
| `action_language` | ISO 639-1 string \| null |  | Location action language, or null when not overridden. |
| `include_parent_brand` | boolean \| null |  | Whether the parent project brand is included. |
| `competitors` | Competitor\[\] |  | Active tracked competitors. Compatible tracked projection of the dedicated Competitors contract; suggested and dismissed records are excluded. |
| `phone` | string \| null |  | Partner-supplied contact phone number. |
| `email` | string \| null |  | Normalized contact email address. |
| `metadata` | object |  | Partner-owned JSON metadata. Keys and values are returned without interpretation. |
| `local_context` | object |  | Partner-supplied local facts used to contextualize processing. |
| `focus` | global \| country \| region \| city |  | Configured geographic targeting focus. |
| `google_place_id` | string \| null |  | Google place identifier. |
| `google_place_source` | provided \| discovered \| null |  | Whether the Google place was supplied by the customer or matched during onboarding. |
| `google_place_name` | string \| null |  | Business name associated with the Google place. |
| `google_maps_url` | string \| null |  | Google Maps URL for the location. |
| `formatted_address` | string \| null |  | Formatted physical address. |
| `address_line_2` | string \| null |  | Optional second address line. |
| `city` | string \| null |  | Normalized city. |
| `state` | string \| null |  | Normalized region or state. |
| `postal_code` | string \| null |  | Normalized postal code. |
| `country` | string \| null |  | Normalized country name. |
| `country_code` | string \| null |  | Explicit uppercase ISO 3166-1 alpha-2 country code, or null when not configured. Locations do not inherit the project country. |
| `latitude` | number \| null |  | Latitude from -90 through 90. |
| `longitude` | number \| null |  | Longitude from -180 through 180. |
| `status` | active \| inactive \| archived |  | Current location lifecycle status. |
| `package` | PackageReference |  | Assigned location package. |
| `created_at` | datetime |  | Location creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent location update time in ISO 8601 format. |

#### PackageReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Assigned package identifier. |
| `name` | string |  | Assigned package name. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Competitor identifier. |
| `kind` | competitor |  | Entity role. Always competitor in this projection. |
| `name` | string |  | Competitor display name. |
| `domain` | string |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. Required for tracked competitors. |
| `aliases` | string\[\] |  | Additional names recognized for the competitor. |
| `status` | active |  | Tracked competitors always participate in current processing. |
| `competitor_state` | tracked |  | Management lifecycle state. This projection contains tracked competitors only. |
| `created_at` | datetime |  | Competitor creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Most recent competitor update time in ISO 8601 format. |

#### Metadata and local context object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `additional properties` | JSON value |  | Arbitrary partner-owned keys with string, number, boolean, null, object, or array values. |
| `maximum size` | 16 KB |  | Limit measured after JSON serialization. |

#### Request and response

```curl
curl --request PATCH \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/a1308d14-149c-4dd7-a4c5-295ac9090f58' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Acme Amsterdam Centrum",
  "description": "Acme flagship store in central Amsterdam.",
  "brand_aliases": [
    "acme amsterdam",
    "acme centrum"
  ],
  "phone": "+31 20 555 0199",
  "email": "centrum@acme.example",
  "metadata": {
    "partner_region_id": "nl-central"
  },
  "local_context": {
    "neighborhood": "Centrum",
    "service_area": "Amsterdam city center"
  },
  "focus": "city"
}'
```

```json
{
  "location": {
    "id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
    "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "external_id": "partner-location-amsterdam",
    "name": "Acme Amsterdam Centrum",
    "description": "Acme flagship store in central Amsterdam.",
    "website": "https://acme.example/amsterdam",
    "brand_aliases": [
      "acme amsterdam",
      "acme centrum"
    ],
    "language": "en",
    "action_language": "en",
    "include_parent_brand": true,
    "competitors": [
      {
        "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
        "kind": "competitor",
        "name": "Example Rival",
        "domain": "example-rival.com",
        "aliases": [
          "rival"
        ],
        "status": "active",
        "competitor_state": "tracked",
        "created_at": "2026-07-02T11:20:00Z",
        "updated_at": "2026-07-30T09:10:00Z"
      }
    ],
    "phone": "+31 20 555 0199",
    "email": "centrum@acme.example",
    "metadata": {
      "partner_region_id": "nl-central"
    },
    "local_context": {
      "neighborhood": "Centrum",
      "service_area": "Amsterdam city center"
    },
    "focus": "city",
    "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
    "google_place_source": "provided",
    "google_place_name": "Acme Amsterdam",
    "google_maps_url": "https://maps.google.com/?cid=123456789",
    "formatted_address": "1 Market Street, 1012 JS Amsterdam, Netherlands",
    "address_line_2": null,
    "city": "Amsterdam",
    "state": "North Holland",
    "postal_code": "1012 JS",
    "country": "Netherlands",
    "country_code": null,
    "latitude": 52.3728,
    "longitude": 4.8936,
    "status": "active",
    "package": {
      "id": "8ae92d3f-18fb-4899-aef0-11f50b8bd0a7",
      "name": "Local Growth Weekly"
    },
    "created_at": "2026-07-31T08:10:00Z",
    "updated_at": "2026-07-31T12:10:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `409` | conflict |  | The external ID is already in use or the resource cannot accept this operation in its current state. |
| `422` | validation\_failed |  | One or more fields are invalid, or package\_id does not identify an active package of the required type. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Delete location

`DELETE /projects/{project_id}/locations/{location_id}`

Schedules asynchronous deletion of a location and its location-scoped resources.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. |
| `location_id` | location UUID \| location external ID | Required | Ceyo location UUID or configured partner external ID belonging to the project. |

> **202 Accepted**
>
> Deletion runs asynchronously and the response has no body.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/a1308d14-149c-4dd7-a4c5-295ac9090f58' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 202 Accepted
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "name": [
        "must be present"
      ]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | The requested project or location was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Bulk operations

Source: https://ceyo.ai/docs/signal/bulk-operations

### Bulk operations

Poll asynchronous project and location creation and inspect the result of every submitted record.

### Get bulk operation

`GET /bulk-operations/{id}`

Returns the current state and item results for one bulk create operation.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid | Required | Bulk operation identifier returned by a bulk create request. |

#### BulkOperation response envelope

`id`:**uuid**`type`:**projects | locations**`status`:**pending | running | completed | completed\_with\_errors | failed**`parent_project_id`:**uuid | null**`total`:**integer**`pending`:**integer**`succeeded`:**integer**`failed`:**integer**`status_url`:**string**`items`:**BulkOperationItem\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Bulk operation identifier. |
| `type` | projects \| locations |  | Resource type created by the operation. |
| `status` | pending \| running \| completed \| completed\_with\_errors \| failed |  | Current operation state. |
| `parent_project_id` | uuid \| null |  | Parent project for a location operation; null for a project operation. |
| `total` | integer |  | Submitted record count. |
| `pending` | integer |  | Records that have not reached a terminal state. |
| `succeeded` | integer |  | Records created successfully. |
| `failed` | integer |  | Records that failed validation or processing. |
| `status_url` | string |  | Relative URL for polling this operation. |
| `items` | BulkOperationItem\[\] |  | One result for each submitted record, ordered by input index. |

#### BulkOperationItem

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `index` | integer |  | Zero-based position in the submitted array. |
| `status` | pending \| queued \| running \| succeeded \| failed |  | Current item state. |
| `resource_id` | uuid |  | Created project or location identifier when successful. |
| `external_id` | string \| null |  | Partner identifier copied from the created resource. |
| `onboarding_started` | boolean |  | Whether this item created an onboarding run from start: true. |
| `onboarding_operation` | OnboardingOperation |  | Onboarding status and polling URL when onboarding\_started is true. |
| `error` | object |  | Stable error code, message, and field details for a failed item. |

> **Polling**
>
> Poll the returned `status_url` until the operation is `completed`, `completed_with_errors`, or `failed`. Item failures do not roll back successful records.

> **Safe errors**
>
> Item errors contain stable public codes and sanitized field details. Unexpected internal failures return `processing_failed` without provider names, exception messages, or implementation details.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/bulk-operations/f9bc15cc-e9c9-4e93-a93e-b713c92c7315' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "bulk_operation": {
    "id": "f9bc15cc-e9c9-4e93-a93e-b713c92c7315",
    "type": "projects",
    "status": "completed_with_errors",
    "parent_project_id": null,
    "total": 2,
    "pending": 0,
    "succeeded": 1,
    "failed": 1,
    "created_at": "2026-08-04T15:00:00Z",
    "started_at": "2026-08-04T15:00:01Z",
    "completed_at": "2026-08-04T15:00:03Z",
    "status_url": "/v1/bulk-operations/f9bc15cc-e9c9-4e93-a93e-b713c92c7315",
    "items": [
      {
        "index": 0,
        "status": "succeeded",
        "resource_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
        "external_id": "partner-project-acme",
        "onboarding_started": true,
        "onboarding_operation": {
          "id": "1c07ea43-a8fe-4d07-8741-9c624d67b466",
          "status": "queued",
          "resource_type": "project",
          "resource_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
          "progress": {
            "completed": 0,
            "total": 6
          },
          "message": "Onboarding is queued.",
          "steps": [
            {
              "key": "enrichment",
              "status": "pending"
            },
            {
              "key": "topics",
              "status": "pending"
            },
            {
              "key": "prompts",
              "status": "pending"
            },
            {
              "key": "competitors",
              "status": "pending"
            },
            {
              "key": "visibility",
              "status": "pending"
            },
            {
              "key": "diagnosis",
              "status": "pending"
            }
          ],
          "status_url": "/v1/onboarding-operations/1c07ea43-a8fe-4d07-8741-9c624d67b466"
        },
        "started_at": "2026-08-04T15:00:01Z",
        "completed_at": "2026-08-04T15:00:02Z"
      },
      {
        "index": 1,
        "status": "failed",
        "error": {
          "code": "validation_failed",
          "message": "The record failed validation.",
          "details": [
            {
              "field": "package_id",
              "message": "is invalid"
            }
          ]
        },
        "started_at": "2026-08-04T15:00:01Z",
        "completed_at": "2026-08-04T15:00:03Z"
      }
    ]
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent, invalid, expired, or revoked. |
| `404` | bulk\_operation\_not\_found |  | The operation does not exist or is outside the API key scope. |

---

# Onboarding operations

Source: https://ceyo.ai/docs/signal/onboarding-operations

### Onboarding operations

Poll automatic project and location onboarding with one shared operation endpoint.

### Get onboarding operation

`GET /onboarding-operations/{id}`

Returns safe progress for one project or location onboarding operation.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid | Required | Onboarding operation identifier returned when a project or location is created with start: true. |

#### OnboardingOperation response envelope

`id`:**uuid**`status`:**queued | running | waiting\_visibility | waiting\_diagnosis | succeeded | partial | failed | cancelled**`resource_type`:**project | location**`resource_id`:**uuid**`current_step`:**string | null**`progress`:**object**`message`:**string**`error`:**object | null**`steps`:**OnboardingStep\[\]**`status_url`:**string**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Onboarding operation identifier. |
| `status` | queued \| running \| waiting\_visibility \| waiting\_diagnosis \| succeeded \| partial \| failed \| cancelled |  | Current operation state. |
| `resource_type` | project \| location |  | Resource being onboarded. |
| `resource_id` | uuid |  | Project or location identifier. |
| `current_step` | string \| null |  | Current onboarding step, or null before work begins. |
| `progress` | object |  | Completed and total step counts. |
| `message` | string |  | Stable, customer-safe status message. |
| `error` | object \| null |  | Stable public error code and message for failed or partial operations. |
| `steps` | OnboardingStep\[\] |  | Public status for each onboarding step. |
| `status_url` | string |  | Relative URL for polling this operation. |

> **Polling**
>
> Poll `status_url` until the status is `succeeded`, `partial`, `failed`, or `cancelled`.

> **Safe errors**
>
> Status and step errors use fixed public codes and messages. Provider names, exception text, and internal metadata are never returned.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/onboarding-operations/1c07ea43-a8fe-4d07-8741-9c624d67b466' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "onboarding_operation": {
    "id": "1c07ea43-a8fe-4d07-8741-9c624d67b466",
    "status": "running",
    "resource_type": "location",
    "resource_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
    "current_step": "topics",
    "progress": {
      "completed": 1,
      "total": 6
    },
    "message": "Onboarding is in progress.",
    "steps": [
      {
        "key": "enrichment",
        "status": "succeeded"
      },
      {
        "key": "topics",
        "status": "running"
      },
      {
        "key": "prompts",
        "status": "pending"
      },
      {
        "key": "competitors",
        "status": "pending"
      },
      {
        "key": "visibility",
        "status": "pending"
      },
      {
        "key": "diagnosis",
        "status": "pending"
      }
    ],
    "status_url": "/v1/onboarding-operations/1c07ea43-a8fe-4d07-8741-9c624d67b466",
    "created_at": "2026-08-04T15:00:00Z",
    "started_at": "2026-08-04T15:00:01Z",
    "updated_at": "2026-08-04T15:00:03Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The operation ID is not a UUID. |
| `401` | invalid\_api\_key |  | The API key is absent, invalid, expired, or revoked. |
| `403` | forbidden |  | The API key lacks read or write access for the resource being onboarded. |
| `404` | not\_found |  | The operation does not exist or is outside the API key scope. |

---

# Topics

Source: https://ceyo.ai/docs/signal/topics

### Topics

Create and manage topic groups used to organize prompts for the selected project or location.

**Scope:** Project

### List topics

`GET /projects/{project_id}/visibility/topics`

Returns paginated topics in the selected project, ordered deterministically by created\_at, then id.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer | Optional; Default: 1 | 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Topics per page. Maximum: 100. |

#### Response envelope

`project_id`:**uuid**`topics`:**Topic\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `topics` | Topic\[\] |  | Topics ordered by created\_at, then id. |
| `pagination` | Pagination |  | Topic pagination metadata. |

#### Topic

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique topic identifier. |
| `name` | string |  | Customer-facing topic name; maximum 200 characters and case-insensitively unique within this visibility scope. |
| `created_at` | datetime |  | Topic creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Time the topic was last updated. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/topics?page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "topics": [
    {
      "id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
      "name": "AI visibility platforms",
      "created_at": "2026-07-20T08:30:00Z",
      "updated_at": "2026-07-20T08:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}
```

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Records requested per page. |
| `total` | integer |  | Total records matching the filters. |
| `total_pages` | integer |  | Total available pages. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or topic was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Create topic

`POST /projects/{project_id}/visibility/topics`

Creates a topic in the selected project. Topic names should represent a durable customer question area rather than an individual query.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Required topic name; maximum 200 characters and case-insensitively unique within the scope. |

#### Response envelope

`project_id`:**uuid**`topic`:**Topic**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `topic` | Topic |  | Created topic. |

#### Topic

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique topic identifier. |
| `name` | string |  | Customer-facing topic name; maximum 200 characters and case-insensitively unique within this visibility scope. |
| `created_at` | datetime |  | Topic creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Time the topic was last updated. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/topics' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{"name":"AI visibility platforms"}'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "topic": {
    "id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
    "name": "AI visibility platforms",
    "created_at": "2026-07-20T08:30:00Z",
    "updated_at": "2026-07-20T08:30:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or topic was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get topic

`GET /projects/{project_id}/visibility/topics/{topic_id}`

Returns one topic from the selected project. Use the prompt list with topic\_ids to retrieve its prompts and metrics.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `topic_id` | uuid | Required | Topic identifier. |

#### Response envelope

`project_id`:**uuid**`topic`:**Topic**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `topic` | Topic |  | Requested topic. |

#### Topic

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique topic identifier. |
| `name` | string |  | Customer-facing topic name; maximum 200 characters and case-insensitively unique within this visibility scope. |
| `created_at` | datetime |  | Topic creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Time the topic was last updated. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/topics/{topic_id}' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "topic": {
    "id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
    "name": "AI visibility platforms",
    "created_at": "2026-07-20T08:30:00Z",
    "updated_at": "2026-07-20T08:30:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or topic was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Delete topic

`DELETE /projects/{project_id}/visibility/topics/{topic_id}`

Starts irreversible asynchronous deletion of a topic and its dependent visibility data. This is not an archive operation.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `topic_id` | uuid | Required | Topic identifier. |

> **Irreversible**
>
> Topic deletion is asynchronous and permanent. Archive individual prompts when they may need to be restored later.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/topics/{topic_id}' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 202 Accepted
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or topic was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Prompts

Source: https://ceyo.ai/docs/signal/prompts

### Prompts

Create and manage the monitored questions used for visibility tracking.

**Scope:** Project

### List prompts

`GET /projects/{project_id}/visibility/prompts`

Returns paginated prompts and visibility metrics for the selected project.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First metrics date in YYYY-MM-DD format. The inclusive range may span at most 3 months. |
| `end` | date | Optional; Default: Today | Last metrics date in YYYY-MM-DD format. The inclusive range may span at most 3 months. |
| `models` | string | Optional; Default: All enabled models | Comma-separated or repeated model keys. |
| `topic_ids` | string | Optional | Comma-separated or repeated topic UUIDs. |
| `competitor_ids` | string | Optional | Tracked competitor UUIDs that must be mentioned. |
| `sentiments` | string | Optional | Comma-separated negative, neutral, or positive labels. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison | Optional | Restrict prompts to one category. |
| `q` | string | Optional | Search prompt content and topic names. |
| `status` | active \| archived | Optional; Default: active | Select active prompts or archived prompts. |
| `sort` | created\_at \| prompt \| topic \| visibility \| position \| volume | Optional; Default: created\_at | Field used to order the result. |
| `sort_direction` | asc \| desc | Optional; Default: desc | Sort direction. |
| `page` | integer | Optional; Default: 1 | 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Prompts per page. Maximum: 100. |

#### Response envelope

`project_id`:**uuid**`prompts`:**Prompt\[\]**`start`:**date**`end`:**date**`models`:**string\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `prompts` | Prompt\[\] |  | Paginated prompts matching the filters. |
| `start` | date |  | Resolved metrics start date. |
| `end` | date |  | Resolved metrics end date. |
| `models` | string\[\] |  | Model keys included in metric calculations. |
| `pagination` | Pagination |  | Prompt pagination metadata. |

#### Prompt

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique prompt identifier. |
| `topic_id` | uuid |  | Topic that contains the prompt. |
| `topic_name` | string |  | Current name of the containing topic. |
| `content` | string |  | Question sent to configured AI models. Maximum 20,000 characters. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Stable snake\_case intent category. |
| `city_override` | string \| null |  | Prompt-specific city. Null means the city is inherited from the selected scope. |
| `country_override` | string \| null |  | Prompt-specific country. Null means the country is inherited from the selected scope; present together with country\_code\_override. |
| `country_code_override` | string \| null |  | Prompt-specific uppercase ISO 3166-1 alpha-2 country code. Null means the code is inherited; present together with country\_override. |
| `archived_at` | datetime \| null |  | Archive time. Null means the prompt is active. |
| `created_at` | datetime |  | Prompt creation time in ISO 8601 format. |
| `metrics` | PromptMetrics \| null |  | Visibility metrics for the requested date and model filters. |

#### PromptMetrics

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `primary_mentions` | integer |  | Responses that mentioned the primary tracked brand. |
| `visibility_percentage` | number |  | Share of responses that mentioned the primary tracked brand. |
| `visibility_trend_pp` | number \| null |  | Percentage-point change from the preceding equal-length period. |
| `average_position` | number \| null |  | Average 1-based primary-brand position when mentioned. |
| `position_trend` | number \| null |  | Average-position change from the preceding period. |
| `average_position_trend` | number \| null |  | Change in average position from the preceding comparison window. |
| `average_sentiment` | number \| null |  | Average primary-brand sentiment score. |
| `sentiment_counts` | SentimentCounts |  | Response counts keyed by negative, neutral, and positive. |
| `citations` | integer |  | Citation occurrences across matching responses. |
| `competitor_mentions` | Record<string, integer> |  | Mention counts keyed by tracked competitor name. |

#### SentimentCounts

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `negative` | integer |  | Negative responses. |
| `neutral` | integer |  | Neutral responses. |
| `positive` | integer |  | Positive responses. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Records requested per page. |
| `total` | integer |  | Total records matching the filters. |
| `total_pages` | integer |  | Total available pages. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/prompts?start=2026-07-24&end=2026-07-30&models=chatgpt,perplexity&status=active&page=1' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "prompts": [
    {
      "id": "4de5e484-ce6a-4e45-ad7c-bd48db2549af",
      "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
      "topic_name": "AI visibility platforms",
      "content": "Which platforms help brands measure visibility in AI answers?",
      "category": "organic_search",
      "city_override": null,
      "country_override": null,
      "country_code_override": null,
      "archived_at": null,
      "created_at": "2026-07-20T08:35:00Z",
      "metrics": {
        "primary_mentions": 8,
        "visibility_percentage": 57.14,
        "visibility_trend_pp": 7.14,
        "average_position": 2.25,
        "position_trend": -0.5,
        "average_position_trend": -0.5,
        "average_sentiment": 7.6,
        "sentiment_counts": {
          "negative": 1,
          "neutral": 4,
          "positive": 9
        },
        "citations": 22,
        "competitor_mentions": {
          "Example competitor": 6
        }
      }
    }
  ],
  "start": "2026-07-24",
  "end": "2026-07-30",
  "models": [
    "chatgpt",
    "perplexity"
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 36,
    "total_pages": 2
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Create prompt

`POST /projects/{project_id}/visibility/prompts`

Creates and activates one prompt for visibility tracking across the enabled models.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `topic_id` | uuid |  | Topic in this scope. Required when creating a prompt. |
| `content` | string |  | Prompt question. Required; maximum 20,000 characters. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Prompt category. Defaults to general when creating. |
| `city_override` | string \| null |  | Optional create-only city override; maximum 200 characters. Omit or send null to inherit the scope city. |
| `country_override` | string \| null |  | Country name for a location override; must be provided with country\_code\_override. Maximum 200 characters. |
| `country_code_override` | string \| null |  | Uppercase two-letter code for a location override; must be provided with country\_override. |

> **Location override**
>
> country\_override and country\_code\_override must be supplied together. city\_override is optional. Effective geography combines these values with the scope defaults. Language comes from the selected scope.

#### Response envelope

`project_id`:**uuid**`prompt`:**Prompt**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `prompt` | Prompt |  | Created and activated prompt. |

#### Prompt

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique prompt identifier. |
| `topic_id` | uuid |  | Topic that contains the prompt. |
| `topic_name` | string |  | Current name of the containing topic. |
| `content` | string |  | Question sent to configured AI models. Maximum 20,000 characters. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Stable snake\_case intent category. |
| `city_override` | string \| null |  | Prompt-specific city. Null means the city is inherited from the selected scope. |
| `country_override` | string \| null |  | Prompt-specific country. Null means the country is inherited from the selected scope; present together with country\_code\_override. |
| `country_code_override` | string \| null |  | Prompt-specific uppercase ISO 3166-1 alpha-2 country code. Null means the code is inherited; present together with country\_override. |
| `archived_at` | datetime \| null |  | Archive time. Null means the prompt is active. |
| `created_at` | datetime |  | Prompt creation time in ISO 8601 format. |
| `metrics` | PromptMetrics \| null |  | Visibility metrics for the requested date and model filters. |

#### PromptMetrics

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `primary_mentions` | integer |  | Responses that mentioned the primary tracked brand. |
| `visibility_percentage` | number |  | Share of responses that mentioned the primary tracked brand. |
| `visibility_trend_pp` | number \| null |  | Percentage-point change from the preceding equal-length period. |
| `average_position` | number \| null |  | Average 1-based primary-brand position when mentioned. |
| `position_trend` | number \| null |  | Average-position change from the preceding period. |
| `average_position_trend` | number \| null |  | Change in average position from the preceding comparison window. |
| `average_sentiment` | number \| null |  | Average primary-brand sentiment score. |
| `sentiment_counts` | SentimentCounts |  | Response counts keyed by negative, neutral, and positive. |
| `citations` | integer |  | Citation occurrences across matching responses. |
| `competitor_mentions` | Record<string, integer> |  | Mention counts keyed by tracked competitor name. |

#### SentimentCounts

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `negative` | integer |  | Negative responses. |
| `neutral` | integer |  | Neutral responses. |
| `positive` | integer |  | Positive responses. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/prompts' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{"topic_id":"ab20526b-6bb2-436c-8c93-5bf77ea43848","content":"Which platforms help brands measure visibility in AI answers?","category":"organic_search"}'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "prompt": {
    "id": "4de5e484-ce6a-4e45-ad7c-bd48db2549af",
    "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
    "topic_name": "AI visibility platforms",
    "content": "Which platforms help brands measure visibility in AI answers?",
    "category": "organic_search",
    "city_override": null,
    "country_override": null,
    "country_code_override": null,
    "archived_at": null,
    "created_at": "2026-07-20T08:35:00Z",
    "metrics": null
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Bulk create prompts

`POST /projects/{project_id}/visibility/prompts/bulk_create`

Creates and activates up to 100 prompts atomically. Any invalid row or capacity failure rejects the entire request.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `prompts` | PromptInput\[\] |  | Required array containing 1–100 prompt rows. |

#### PromptInput

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `topic_id` | uuid |  | Topic in this scope. Required when creating a prompt. |
| `content` | string |  | Prompt question. Required; maximum 20,000 characters. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Prompt category. Defaults to general when creating. |
| `city_override` | string \| null |  | Optional create-only city override; maximum 200 characters. Omit or send null to inherit the scope city. |
| `country_override` | string \| null |  | Country name for a location override; must be provided with country\_code\_override. Maximum 200 characters. |
| `country_code_override` | string \| null |  | Uppercase two-letter code for a location override; must be provided with country\_override. |

> **Location override**
>
> Each row must supply country\_override and country\_code\_override together. city\_override is optional. Effective geography combines these values with the scope defaults; language is inherited.

#### Response envelope

`project_id`:**uuid**`prompts`:**Prompt\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `prompts` | Prompt\[\] |  | Created and activated prompts in request order. |

#### Prompt

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique prompt identifier. |
| `topic_id` | uuid |  | Topic that contains the prompt. |
| `topic_name` | string |  | Current name of the containing topic. |
| `content` | string |  | Question sent to configured AI models. Maximum 20,000 characters. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Stable snake\_case intent category. |
| `city_override` | string \| null |  | Prompt-specific city. Null means the city is inherited from the selected scope. |
| `country_override` | string \| null |  | Prompt-specific country. Null means the country is inherited from the selected scope; present together with country\_code\_override. |
| `country_code_override` | string \| null |  | Prompt-specific uppercase ISO 3166-1 alpha-2 country code. Null means the code is inherited; present together with country\_override. |
| `archived_at` | datetime \| null |  | Archive time. Null means the prompt is active. |
| `created_at` | datetime |  | Prompt creation time in ISO 8601 format. |
| `metrics` | PromptMetrics \| null |  | Visibility metrics for the requested date and model filters. |

#### PromptMetrics

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `primary_mentions` | integer |  | Responses that mentioned the primary tracked brand. |
| `visibility_percentage` | number |  | Share of responses that mentioned the primary tracked brand. |
| `visibility_trend_pp` | number \| null |  | Percentage-point change from the preceding equal-length period. |
| `average_position` | number \| null |  | Average 1-based primary-brand position when mentioned. |
| `position_trend` | number \| null |  | Average-position change from the preceding period. |
| `average_position_trend` | number \| null |  | Change in average position from the preceding comparison window. |
| `average_sentiment` | number \| null |  | Average primary-brand sentiment score. |
| `sentiment_counts` | SentimentCounts |  | Response counts keyed by negative, neutral, and positive. |
| `citations` | integer |  | Citation occurrences across matching responses. |
| `competitor_mentions` | Record<string, integer> |  | Mention counts keyed by tracked competitor name. |

#### SentimentCounts

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `negative` | integer |  | Negative responses. |
| `neutral` | integer |  | Neutral responses. |
| `positive` | integer |  | Positive responses. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/prompts/bulk_create' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{"prompts":[{"topic_id":"ab20526b-6bb2-436c-8c93-5bf77ea43848","content":"Which platforms help brands measure visibility in AI answers?","category":"organic_search"},{"topic_id":"ab20526b-6bb2-436c-8c93-5bf77ea43848","content":"How do AI visibility platforms compare?","category":"competitor_comparison"}]}'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "prompts": [
    {
      "id": "4de5e484-ce6a-4e45-ad7c-bd48db2549af",
      "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
      "topic_name": "AI visibility platforms",
      "content": "Which platforms help brands measure visibility in AI answers?",
      "category": "organic_search",
      "city_override": null,
      "country_override": null,
      "country_code_override": null,
      "archived_at": null,
      "created_at": "2026-07-20T08:35:00Z",
      "metrics": null
    },
    {
      "id": "ab7d6157-ca42-4d43-b02c-edeea5911475",
      "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
      "topic_name": "AI visibility platforms",
      "content": "How do AI visibility platforms compare?",
      "category": "competitor_comparison",
      "city_override": null,
      "country_override": null,
      "country_code_override": null,
      "archived_at": null,
      "created_at": "2026-07-20T08:35:00Z",
      "metrics": null
    }
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get prompt

`GET /projects/{project_id}/visibility/prompts/{prompt_id}`

Returns one active or archived prompt in the selected project, with metrics for the requested period and enabled-model context.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `prompt_id` | uuid | Required | Prompt identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First metrics date in YYYY-MM-DD format. The inclusive range may span at most 3 months. |
| `end` | date | Optional; Default: Today | Last metrics date in YYYY-MM-DD format. The inclusive range may span at most 3 months. |
| `models` | string | Optional; Default: All enabled models | Comma-separated or repeated model keys. |

#### Response envelope

`project_id`:**uuid**`prompt`:**Prompt**`start`:**date**`end`:**date**`models`:**string\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `prompt` | Prompt |  | Requested prompt and metrics. |
| `start` | date |  | Resolved metrics start. |
| `end` | date |  | Resolved metrics end. |
| `models` | string\[\] |  | Models included in metrics. |

#### Prompt

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique prompt identifier. |
| `topic_id` | uuid |  | Topic that contains the prompt. |
| `topic_name` | string |  | Current name of the containing topic. |
| `content` | string |  | Question sent to configured AI models. Maximum 20,000 characters. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Stable snake\_case intent category. |
| `city_override` | string \| null |  | Prompt-specific city. Null means the city is inherited from the selected scope. |
| `country_override` | string \| null |  | Prompt-specific country. Null means the country is inherited from the selected scope; present together with country\_code\_override. |
| `country_code_override` | string \| null |  | Prompt-specific uppercase ISO 3166-1 alpha-2 country code. Null means the code is inherited; present together with country\_override. |
| `archived_at` | datetime \| null |  | Archive time. Null means the prompt is active. |
| `created_at` | datetime |  | Prompt creation time in ISO 8601 format. |
| `metrics` | PromptMetrics \| null |  | Visibility metrics for the requested date and model filters. |

#### PromptMetrics

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `primary_mentions` | integer |  | Responses that mentioned the primary tracked brand. |
| `visibility_percentage` | number |  | Share of responses that mentioned the primary tracked brand. |
| `visibility_trend_pp` | number \| null |  | Percentage-point change from the preceding equal-length period. |
| `average_position` | number \| null |  | Average 1-based primary-brand position when mentioned. |
| `position_trend` | number \| null |  | Average-position change from the preceding period. |
| `average_position_trend` | number \| null |  | Change in average position from the preceding comparison window. |
| `average_sentiment` | number \| null |  | Average primary-brand sentiment score. |
| `sentiment_counts` | SentimentCounts |  | Response counts keyed by negative, neutral, and positive. |
| `citations` | integer |  | Citation occurrences across matching responses. |
| `competitor_mentions` | Record<string, integer> |  | Mention counts keyed by tracked competitor name. |

#### SentimentCounts

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `negative` | integer |  | Negative responses. |
| `neutral` | integer |  | Neutral responses. |
| `positive` | integer |  | Positive responses. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/prompts/{prompt_id}?start=2026-07-24&end=2026-07-30' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "prompt": {
    "id": "4de5e484-ce6a-4e45-ad7c-bd48db2549af",
    "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
    "topic_name": "AI visibility platforms",
    "content": "Which platforms help brands measure visibility in AI answers?",
    "category": "organic_search",
    "city_override": null,
    "country_override": null,
    "country_code_override": null,
    "archived_at": null,
    "created_at": "2026-07-20T08:35:00Z",
    "metrics": {
      "primary_mentions": 8,
      "visibility_percentage": 57.14,
      "visibility_trend_pp": 7.14,
      "average_position": 2.25,
      "position_trend": -0.5,
      "average_position_trend": -0.5,
      "average_sentiment": 7.6,
      "sentiment_counts": {
        "negative": 1,
        "neutral": 4,
        "positive": 9
      },
      "citations": 22,
      "competitor_mentions": {
        "Example competitor": 6
      }
    }
  },
  "start": "2026-07-24",
  "end": "2026-07-30",
  "models": [
    "chatgpt",
    "perplexity"
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Archive prompt

`DELETE /projects/{project_id}/visibility/prompts/{prompt_id}`

Archives an active prompt and stops it consuming active capacity. Historical response data remains available. A successful archive has no response body.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `prompt_id` | uuid | Required | Prompt identifier. |

#### Response

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `204` | No Content |  | Prompt was archived successfully. |

> **Reversible archive**
>
> Archive hides a prompt from active monitoring but preserves its data. Use restore to activate it again. Permanent deletion is a separate, irreversible endpoint.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/prompts/{prompt_id}' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 204 No Content
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Restore prompt

`POST /projects/{project_id}/visibility/prompts/{prompt_id}/restore`

Restores and reactivates an archived prompt after checking active capacity only. Restore does not consume daily activations and does not schedule a visibility run.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `prompt_id` | uuid | Required | Prompt identifier. |

#### Response envelope

`project_id`:**uuid**`prompt`:**Prompt**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `prompt` | Prompt |  | Restored and reactivated prompt. |

#### Prompt

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique prompt identifier. |
| `topic_id` | uuid |  | Topic that contains the prompt. |
| `topic_name` | string |  | Current name of the containing topic. |
| `content` | string |  | Question sent to configured AI models. Maximum 20,000 characters. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Stable snake\_case intent category. |
| `city_override` | string \| null |  | Prompt-specific city. Null means the city is inherited from the selected scope. |
| `country_override` | string \| null |  | Prompt-specific country. Null means the country is inherited from the selected scope; present together with country\_code\_override. |
| `country_code_override` | string \| null |  | Prompt-specific uppercase ISO 3166-1 alpha-2 country code. Null means the code is inherited; present together with country\_override. |
| `archived_at` | datetime \| null |  | Archive time. Null means the prompt is active. |
| `created_at` | datetime |  | Prompt creation time in ISO 8601 format. |
| `metrics` | PromptMetrics \| null |  | Visibility metrics for the requested date and model filters. |

#### PromptMetrics

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `primary_mentions` | integer |  | Responses that mentioned the primary tracked brand. |
| `visibility_percentage` | number |  | Share of responses that mentioned the primary tracked brand. |
| `visibility_trend_pp` | number \| null |  | Percentage-point change from the preceding equal-length period. |
| `average_position` | number \| null |  | Average 1-based primary-brand position when mentioned. |
| `position_trend` | number \| null |  | Average-position change from the preceding period. |
| `average_position_trend` | number \| null |  | Change in average position from the preceding comparison window. |
| `average_sentiment` | number \| null |  | Average primary-brand sentiment score. |
| `sentiment_counts` | SentimentCounts |  | Response counts keyed by negative, neutral, and positive. |
| `citations` | integer |  | Citation occurrences across matching responses. |
| `competitor_mentions` | Record<string, integer> |  | Mention counts keyed by tracked competitor name. |

#### SentimentCounts

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `negative` | integer |  | Negative responses. |
| `neutral` | integer |  | Neutral responses. |
| `positive` | integer |  | Positive responses. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/prompts/{prompt_id}/restore' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "prompt": {
    "id": "4de5e484-ce6a-4e45-ad7c-bd48db2549af",
    "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
    "topic_name": "AI visibility platforms",
    "content": "Which platforms help brands measure visibility in AI answers?",
    "category": "organic_search",
    "city_override": null,
    "country_override": null,
    "country_code_override": null,
    "archived_at": null,
    "created_at": "2026-07-20T08:35:00Z",
    "metrics": null
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Delete prompt permanently

`DELETE /projects/{project_id}/visibility/prompts/{prompt_id}/permanent`

Starts irreversible asynchronous deletion of a prompt and its dependent visibility data. Archive the prompt instead if it may be needed again.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `prompt_id` | uuid | Required | Prompt identifier. |

> **Irreversible delete**
>
> The request returns before background deletion completes.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/prompts/{prompt_id}/permanent' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 202 Accepted
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Suggested prompts

Source: https://ceyo.ai/docs/signal/suggested-prompts

### Suggested prompts

Review, track, or dismiss prompt suggestions generated from visibility evidence.

**Scope:** Project

### List suggested prompts

`GET /projects/{project_id}/visibility/suggested_prompts`

Returns paginated diagnosis- and fanout-sourced prompt suggestions for the selected project. Suggestions are separate from tracked prompts and consume no capacity.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `q` | string | Optional | Search suggestion content, reason, and topic name. |
| `status` | suggested \| tracked \| dismissed \| expired | Optional; Default: suggested | Suggestion lifecycle state to return. |
| `page` | integer | Optional; Default: 1 | 1-based page number. |
| `per_page` | integer | Optional; Default: 20 | Suggestions per page. Maximum: 100. |

#### Response envelope

`project_id`:**uuid**`suggested_prompts`:**SuggestedPrompt\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `suggested_prompts` | SuggestedPrompt\[\] |  | Suggestions matching the selected status and search. |
| `pagination` | Pagination |  | Suggestion pagination metadata. |

#### SuggestedPrompt

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Suggested prompt identifier. |
| `topic_id` | uuid |  | Existing topic selected for the suggestion. |
| `topic_name` | string |  | Current name of the selected topic. |
| `content` | string |  | Suggested prompt question. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Prompt intent category. |
| `reason` | string \| null |  | Evidence-based reason the prompt was suggested. |
| `status` | suggested \| tracked \| dismissed \| expired |  | Suggestion lifecycle state. |
| `tracked_prompt_id` | uuid \| null |  | Prompt created when this suggestion was tracked. |
| `created_at` | datetime |  | Suggestion creation time. |
| `tracked_at` | datetime \| null |  | Time the suggestion became a tracked prompt. |
| `dismissed_at` | datetime \| null |  | Time the suggestion was dismissed. |
| `expired_at` | datetime \| null |  | Time the suggestion was automatically expired. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Records requested per page. |
| `total` | integer |  | Total records matching the filters. |
| `total_pages` | integer |  | Total available pages. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/suggested_prompts?status=suggested&page=1&per_page=20' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "suggested_prompts": [
    {
      "id": "5c297c06-6609-4e2f-ab59-3297e10be39f",
      "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
      "topic_name": "AI visibility platforms",
      "content": "What are the best ways to improve brand visibility in AI answers?",
      "category": "general",
      "reason": "Diagnosis found low visibility for non-branded discovery questions.",
      "status": "suggested",
      "tracked_prompt_id": null,
      "created_at": "2026-07-31T08:00:00Z",
      "tracked_at": null,
      "dismissed_at": null,
      "expired_at": null
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 5,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or suggested prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Track suggested prompt

`POST /projects/{project_id}/visibility/suggested_prompts/{suggested_prompt_id}/track`

Converts one visible suggestion into an active prompt and schedules a visibility run. The operation consumes active and daily activation capacity.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `suggested_prompt_id` | uuid | Required | Suggested Prompt identifier. |

#### SuggestedPrompt

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Suggested prompt identifier. |
| `topic_id` | uuid |  | Existing topic selected for the suggestion. |
| `topic_name` | string |  | Current name of the selected topic. |
| `content` | string |  | Suggested prompt question. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Prompt intent category. |
| `reason` | string \| null |  | Evidence-based reason the prompt was suggested. |
| `status` | suggested \| tracked \| dismissed \| expired |  | Suggestion lifecycle state. |
| `tracked_prompt_id` | uuid \| null |  | Prompt created when this suggestion was tracked. |
| `created_at` | datetime |  | Suggestion creation time. |
| `tracked_at` | datetime \| null |  | Time the suggestion became a tracked prompt. |
| `dismissed_at` | datetime \| null |  | Time the suggestion was dismissed. |
| `expired_at` | datetime \| null |  | Time the suggestion was automatically expired. |

#### Response envelope

`project_id`:**uuid**`suggested_prompts`:**SuggestedPrompt\[\]**`prompts`:**Prompt\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `suggested_prompts` | SuggestedPrompt\[\] |  | Suggestions transitioned to tracked. |
| `prompts` | Prompt\[\] |  | Prompts created from the suggestions. |

#### Prompt

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique prompt identifier. |
| `topic_id` | uuid |  | Topic that contains the prompt. |
| `topic_name` | string |  | Current name of the containing topic. |
| `content` | string |  | Question sent to configured AI models. Maximum 20,000 characters. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Stable snake\_case intent category. |
| `city_override` | string \| null |  | Prompt-specific city. Null means the city is inherited from the selected scope. |
| `country_override` | string \| null |  | Prompt-specific country. Null means the country is inherited from the selected scope; present together with country\_code\_override. |
| `country_code_override` | string \| null |  | Prompt-specific uppercase ISO 3166-1 alpha-2 country code. Null means the code is inherited; present together with country\_override. |
| `archived_at` | datetime \| null |  | Archive time. Null means the prompt is active. |
| `created_at` | datetime |  | Prompt creation time in ISO 8601 format. |
| `metrics` | PromptMetrics \| null |  | Visibility metrics for the requested date and model filters. |

#### PromptMetrics

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `primary_mentions` | integer |  | Responses that mentioned the primary tracked brand. |
| `visibility_percentage` | number |  | Share of responses that mentioned the primary tracked brand. |
| `visibility_trend_pp` | number \| null |  | Percentage-point change from the preceding equal-length period. |
| `average_position` | number \| null |  | Average 1-based primary-brand position when mentioned. |
| `position_trend` | number \| null |  | Average-position change from the preceding period. |
| `average_position_trend` | number \| null |  | Change in average position from the preceding comparison window. |
| `average_sentiment` | number \| null |  | Average primary-brand sentiment score. |
| `sentiment_counts` | SentimentCounts |  | Response counts keyed by negative, neutral, and positive. |
| `citations` | integer |  | Citation occurrences across matching responses. |
| `competitor_mentions` | Record<string, integer> |  | Mention counts keyed by tracked competitor name. |

#### SentimentCounts

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `negative` | integer |  | Negative responses. |
| `neutral` | integer |  | Neutral responses. |
| `positive` | integer |  | Positive responses. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/suggested_prompts/{suggested_prompt_id}/track' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "suggested_prompts": [
    {
      "id": "5c297c06-6609-4e2f-ab59-3297e10be39f",
      "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
      "topic_name": "AI visibility platforms",
      "content": "What are the best ways to improve brand visibility in AI answers?",
      "category": "general",
      "reason": "Diagnosis found low visibility for non-branded discovery questions.",
      "status": "tracked",
      "tracked_prompt_id": "4de5e484-ce6a-4e45-ad7c-bd48db2549af",
      "created_at": "2026-07-31T08:00:00Z",
      "tracked_at": "2026-07-31T10:10:00Z",
      "dismissed_at": null,
      "expired_at": null
    }
  ],
  "prompts": [
    {
      "id": "4de5e484-ce6a-4e45-ad7c-bd48db2549af",
      "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
      "topic_name": "AI visibility platforms",
      "content": "Which platforms help brands measure visibility in AI answers?",
      "category": "organic_search",
      "city_override": null,
      "country_override": null,
      "country_code_override": null,
      "archived_at": null,
      "created_at": "2026-07-20T08:35:00Z",
      "metrics": null
    }
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or suggested prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Dismiss suggested prompt

`POST /projects/{project_id}/visibility/suggested_prompts/{suggested_prompt_id}/dismiss`

Dismisses one visible suggestion without creating a prompt. Dismissed content remains fingerprinted to prevent the same suggestion from reappearing.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `suggested_prompt_id` | uuid | Required | Suggested Prompt identifier. |

#### SuggestedPrompt

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Suggested prompt identifier. |
| `topic_id` | uuid |  | Existing topic selected for the suggestion. |
| `topic_name` | string |  | Current name of the selected topic. |
| `content` | string |  | Suggested prompt question. |
| `category` | general \| organic\_search \| brand\_sentiment \| competitor\_comparison |  | Prompt intent category. |
| `reason` | string \| null |  | Evidence-based reason the prompt was suggested. |
| `status` | suggested \| tracked \| dismissed \| expired |  | Suggestion lifecycle state. |
| `tracked_prompt_id` | uuid \| null |  | Prompt created when this suggestion was tracked. |
| `created_at` | datetime |  | Suggestion creation time. |
| `tracked_at` | datetime \| null |  | Time the suggestion became a tracked prompt. |
| `dismissed_at` | datetime \| null |  | Time the suggestion was dismissed. |
| `expired_at` | datetime \| null |  | Time the suggestion was automatically expired. |

#### Response envelope

`project_id`:**uuid**`suggested_prompts`:**SuggestedPrompt\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `suggested_prompts` | SuggestedPrompt\[\] |  | Suggestions transitioned to dismissed. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/suggested_prompts/{suggested_prompt_id}/dismiss' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "suggested_prompts": [
    {
      "id": "5c297c06-6609-4e2f-ab59-3297e10be39f",
      "topic_id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
      "topic_name": "AI visibility platforms",
      "content": "What are the best ways to improve brand visibility in AI answers?",
      "category": "general",
      "reason": "Diagnosis found low visibility for non-branded discovery questions.",
      "status": "dismissed",
      "tracked_prompt_id": null,
      "created_at": "2026-07-31T08:00:00Z",
      "tracked_at": null,
      "dismissed_at": "2026-07-31T10:12:00Z",
      "expired_at": null
    }
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request \| invalid\_date\_range \| range\_too\_large |  | A prompt filter, model, identifier, or metrics date range is invalid, or the range exceeds three months. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage this visibility scope. |
| `404` | not\_found |  | The project, location, or suggested prompt was not found. |
| `409` | conflict |  | The requested lifecycle operation conflicts with current state. |
| `422` | validation\_failed \| prompt\_limit\_reached \| daily\_prompt\_activation\_limit\_reached |  | Input is invalid, a suggested-prompt status is unsupported, capacity is exhausted, or a bulk request exceeds 100 rows. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Competitors

Source: https://ceyo.ai/docs/signal/competitors

### Competitors

Manage tracked and suggested competitors for a project or location, including merges, bulk dismissal, and brand claims.

**Scope:** Project

### List competitors

`GET /projects/{project_id}/competitors?state=tracked`

Returns tracked competitors for the selected project, together with the primary brand and pagination.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `state` | tracked \| suggested | Optional; Default: tracked | Return competitors in one lifecycle state. |
| `q` | string | Optional | Case-insensitive search across name, domain, and aliases. |
| `domain` | all \| with-domain \| without-domain | Optional; Default: all | Return all competitors or filter by whether a domain is present. |
| `sort` | name-asc \| name-desc \| newest \| visibility \| mentions | Optional; Default: name-asc | Sort by name, creation time, visibility rate, or mention count. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Number of competitors per page. Minimum: 1. Maximum: 50; this matches the maximum suggested dataset size. |

#### List response envelope

`project_id`:**uuid**`primary`:**Competitor**`competitors`:**Competitor\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `primary` | Competitor |  | Primary tracked brand for comparison. |
| `competitors` | Competitor\[\] |  | Competitors matching the filters. |
| `pagination` | Pagination |  | Pagination metadata for competitors. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique competitor identifier. |
| `kind` | primary \| competitor |  | Entity role. Items in competitors are competitor; primary describes the tracked brand. |
| `name` | string |  | Display name used in prompts, results, and reports. |
| `domain` | string \| null |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. |
| `aliases` | string\[\] |  | Additional names recognized as this entity. Maximum: 10. |
| `status` | active \| inactive |  | Processing status. Competitors are active exactly when tracked and inactive when suggested or dismissed. |
| `competitor_state` | tracked \| suggested \| dismissed \| null |  | Competitor management lifecycle state. Null for the primary brand. |
| `created_at` | datetime |  | Competitor creation time. |
| `updated_at` | datetime |  | Last competitor update time. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page number. |
| `per_page` | integer |  | Requested page size. Minimum: 1. Maximum: 50. |
| `total` | integer |  | Total competitors matching the applied filters. |
| `total_pages` | integer |  | Total pages at the current per\_page value. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors?state=tracked&sort=visibility&domain=all&page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "primary": {
    "id": "10cf756e-ad42-43d7-beda-e8e9cc96cd41",
    "kind": "primary",
    "name": "Acme",
    "domain": "acme.example",
    "aliases": ["Acme AI"],
    "status": "active",
    "competitor_state": null,
    "created_at": "2026-04-12T08:00:00Z",
    "updated_at": "2026-07-28T10:30:00Z"
  },
  "competitors": [
    {
      "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
      "kind": "competitor",
      "name": "Northstar",
      "domain": "northstar.example",
      "aliases": ["Northstar AI"],
      "status": "active",
      "competitor_state": "tracked",
      "created_at": "2026-07-02T11:20:00Z",
      "updated_at": "2026-07-30T09:10:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `422` | invalid\_filter \| validation\_failed |  | A state, sort, or domain filter is invalid, or a pagination value is outside the supported range. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Create competitor

`POST /projects/{project_id}/competitors`

Creates a tracked competitor and returns 201 Created. If the normalized domain or case-insensitive name matches an existing dismissed competitor, that record is reactivated and returned with 200 OK instead of creating a duplicate. A tracked competitor requires a domain; aliases accept at most 10 unique values.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Competitor display name. Required. |
| `domain` | string |  | Competitor domain. Required for a tracked competitor and normalized before matching. |
| `aliases` | string\[\] |  | Optional alternate names. Maximum: 10 unique values. |

#### Create competitor response envelope

`project_id`:**uuid**`competitor`:**Competitor**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `competitor` | Competitor |  | Created or updated competitor. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique competitor identifier. |
| `kind` | primary \| competitor |  | Entity role. Items in competitors are competitor; primary describes the tracked brand. |
| `name` | string |  | Display name used in prompts, results, and reports. |
| `domain` | string \| null |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. |
| `aliases` | string\[\] |  | Additional names recognized as this entity. Maximum: 10. |
| `status` | active \| inactive |  | Processing status. Competitors are active exactly when tracked and inactive when suggested or dismissed. |
| `competitor_state` | tracked \| suggested \| dismissed \| null |  | Competitor management lifecycle state. Null for the primary brand. |
| `created_at` | datetime |  | Competitor creation time. |
| `updated_at` | datetime |  | Last competitor update time. |

> **Matching and response status**
>
> Input such as `https://www.northstar.example/pricing` is stored as `northstar.example`. A new record returns `201 Created`; reactivation by normalized domain or case-insensitive name returns `200 OK`.

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{"name":"Northstar","domain":"https://www.northstar.example/pricing","aliases":["Northstar AI"]}'
```

```json
HTTP/1.1 201 Created

{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "competitor": {
      "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
      "kind": "competitor",
      "name": "Northstar",
      "domain": "northstar.example",
      "aliases": ["Northstar AI"],
      "status": "active",
      "competitor_state": "tracked",
      "created_at": "2026-07-02T11:20:00Z",
      "updated_at": "2026-07-30T09:10:00Z"
    }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `409` | domain\_conflict \| own\_domain \| invalid\_state |  | The normalized domain belongs to another active competitor or to the primary brand. |
| `422` | validation\_failed \| limit\_exceeded |  | Required data is absent, aliases exceed 10, or tracked capacity would be exceeded. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Update competitor

`PATCH /projects/{project_id}/competitors/{competitor_id}`

Updates a competitor's name, normalized domain, aliases, or competitor\_state. Omitted fields remain unchanged. Status is derived from competitor\_state and cannot be submitted.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `competitor_id` | uuid | Required | Competitor identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Mutable display name. Maximum: 200 characters. |
| `domain` | string |  | Mutable normalized domain. Required whenever competitor\_state is tracked. |
| `aliases` | string\[\] |  | Mutable replacement list of up to 10 unique alternate names. |
| `competitor_state` | tracked \| suggested |  | Mutable lifecycle state. PATCH accepts only tracked or suggested; dismissed is set only by a dismissal or merge operation. |

#### Update competitor response envelope

`project_id`:**uuid**`competitor`:**Competitor**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `competitor` | Competitor |  | Created or updated competitor. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique competitor identifier. |
| `kind` | primary \| competitor |  | Entity role. Items in competitors are competitor; primary describes the tracked brand. |
| `name` | string |  | Display name used in prompts, results, and reports. |
| `domain` | string \| null |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. |
| `aliases` | string\[\] |  | Additional names recognized as this entity. Maximum: 10. |
| `status` | active \| inactive |  | Processing status. Competitors are active exactly when tracked and inactive when suggested or dismissed. |
| `competitor_state` | tracked \| suggested \| dismissed \| null |  | Competitor management lifecycle state. Null for the primary brand. |
| `created_at` | datetime |  | Competitor creation time. |
| `updated_at` | datetime |  | Last competitor update time. |

> **State restrictions**
>
> PATCH accepts exactly these mutable fields: `name`, `domain`, `aliases`, and `competitor_state`. A tracked competitor requires a domain. Status is set internally to active for tracked competitors and inactive for suggested competitors. Setting the state to suggested frees a tracked slot. Dismissed cannot be supplied to PATCH.

#### Request and response

```curl
curl --request PATCH \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors/{competitor_id}' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{"name":"Northstar AI","aliases":["Northstar","North Star"],"competitor_state":"suggested"}'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "competitor": {
      "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
      "kind": "competitor",
      "name": "Northstar AI",
      "domain": "northstar.example",
      "aliases": ["Northstar", "North Star"],
      "status": "inactive",
      "competitor_state": "suggested",
      "created_at": "2026-07-02T11:20:00Z",
      "updated_at": "2026-07-30T09:10:00Z"
    }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `409` | domain\_conflict \| own\_domain \| invalid\_state |  | The normalized domain belongs to another active competitor or to the primary brand. |
| `422` | validation\_failed \| limit\_exceeded |  | Required data is absent, aliases exceed 10, or tracked capacity would be exceeded. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Remove competitor

`DELETE /projects/{project_id}/competitors/{competitor_id}`

Soft-dismisses a competitor while retaining its mentions, rankings, citations, and visibility results.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `competitor_id` | uuid | Required | Competitor identifier. |

> **Result continuity**
>
> Removal does not delete stored results. A later create request with the same normalized domain or case-insensitive name can reactivate the dismissed record.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors/{competitor_id}' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 204 No Content
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `409` | invalid\_state |  | The competitor cannot be dismissed from its current state. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Merge competitors

`POST /projects/{project_id}/competitors/merge`

Combines duplicate competitors into one canonical target. Select 2–5 competitors, all in the same lifecycle state.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `primary_id` | uuid |  | Canonical competitor to retain. |
| `duplicate_ids` | uuid\[\] |  | One to four unique duplicate IDs. Together with primary\_id, the selection must contain 2–5 competitors in the same state. |

#### Merge competitors response envelope

`project_id`:**uuid**`competitor`:**Competitor**`merged_ids`:**uuid\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `competitor` | Competitor |  | Created or updated competitor. |
| `merged_ids` | uuid\[\] |  | Records now pointing to the canonical competitor. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique competitor identifier. |
| `kind` | primary \| competitor |  | Entity role. Items in competitors are competitor; primary describes the tracked brand. |
| `name` | string |  | Display name used in prompts, results, and reports. |
| `domain` | string \| null |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. |
| `aliases` | string\[\] |  | Additional names recognized as this entity. Maximum: 10. |
| `status` | active \| inactive |  | Processing status. Competitors are active exactly when tracked and inactive when suggested or dismissed. |
| `competitor_state` | tracked \| suggested \| dismissed \| null |  | Competitor management lifecycle state. Null for the primary brand. |
| `created_at` | datetime |  | Competitor creation time. |
| `updated_at` | datetime |  | Last competitor update time. |

> **Merge behavior**
>
> Aliases and retained results are associated with the canonical target. Source records remain dismissed after their aliases are moved to the canonical competitor.

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors/merge' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{"primary_id":"63ec8dad-c12f-43c8-89e4-06eb629d0977","duplicate_ids":["d732c038-d411-449c-a6cb-351b6d8a2961"]}'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "competitor": {
      "id": "63ec8dad-c12f-43c8-89e4-06eb629d0977",
      "kind": "competitor",
      "name": "Northstar",
      "domain": "northstar.example",
      "aliases": ["Northstar AI"],
      "status": "active",
      "competitor_state": "tracked",
      "created_at": "2026-07-02T11:20:00Z",
      "updated_at": "2026-07-30T09:10:00Z"
    },
  "merged_ids": ["d732c038-d411-449c-a6cb-351b6d8a2961"]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `409` | mixed\_states \| invalid\_target |  | The selected competitors do not share a state or the primary competitor is invalid. |
| `422` | invalid\_selection |  | Select between 2 and 5 unique competitor IDs. |
| `422` | validation\_failed |  | The merged aliases exceed the supported limit. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Bulk remove competitors

`POST /projects/{project_id}/competitors/bulk_destroy`

Soft-dismisses up to 25 competitors in one request, retains their stored results, and returns the dismissed IDs with 200 OK.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `ids` | uuid\[\] |  | One to 25 unique competitor IDs to dismiss. |

#### Bulk remove competitors response envelope

`project_id`:**uuid**`dismissed_ids`:**uuid\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `dismissed_ids` | uuid\[\] |  | Competitor IDs dismissed by this request. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors/bulk_destroy' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{"ids":["63ec8dad-c12f-43c8-89e4-06eb629d0977","d732c038-d411-449c-a6cb-351b6d8a2961"]}'
```

```json
HTTP/1.1 200 OK

{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "dismissed_ids": ["63ec8dad-c12f-43c8-89e4-06eb629d0977", "d732c038-d411-449c-a6cb-351b6d8a2961"]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `422` | invalid\_selection |  | Supply between 1 and 25 unique competitor IDs. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Claim competitor as brand

`POST /projects/{project_id}/competitors/{competitor_id}/claim_as_brand`

Promotes a competitor identity to the primary brand. Its name and aliases are added to the brand aliases, then the competitor is soft-dismissed.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `competitor_id` | uuid | Required | Competitor identifier. |

> **Brand identity update**
>
> Claiming retains stored results, updates the primary brand aliases, and dismisses the competitor so it no longer consumes a tracked slot.

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors/{competitor_id}/claim_as_brand' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 204 No Content
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `409` | invalid\_state |  | The competitor cannot be claimed from its current state. |
| `422` | alias\_limit\_exceeded |  | Claiming the competitor would exceed the maximum of 10 brand aliases. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### List suggested competitors

`GET /projects/{project_id}/competitors?state=suggested`

Returns the current suggested competitor dataset for the selected project, capped at 50 items. Suggested items may include visibility-rate and average-rank metrics.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `state` | tracked \| suggested | Optional; Default: suggested | Return competitors in one lifecycle state. |
| `q` | string | Optional | Case-insensitive search across name, domain, and aliases. |
| `domain` | all \| with-domain \| without-domain | Optional; Default: all | Return all competitors or filter by whether a domain is present. |
| `sort` | name-asc \| name-desc \| newest \| visibility \| mentions | Optional; Default: name-asc | Sort by name, creation time, visibility rate, or mention count. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Number of competitors per page. Minimum: 1. Maximum: 50; this matches the maximum suggested dataset size. |

#### List response envelope

`project_id`:**uuid**`primary`:**Competitor**`competitors`:**Competitor\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `primary` | Competitor |  | Primary tracked brand for comparison. |
| `competitors` | Competitor\[\] |  | Competitors matching the filters. |
| `pagination` | Pagination |  | Pagination metadata for competitors. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique competitor identifier. |
| `kind` | primary \| competitor |  | Entity role. Items in competitors are competitor; primary describes the tracked brand. |
| `name` | string |  | Display name used in prompts, results, and reports. |
| `domain` | string \| null |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. |
| `aliases` | string\[\] |  | Additional names recognized as this entity. Maximum: 10. |
| `status` | active \| inactive |  | Processing status. Competitors are active exactly when tracked and inactive when suggested or dismissed. |
| `competitor_state` | tracked \| suggested \| dismissed \| null |  | Competitor management lifecycle state. Null for the primary brand. |
| `created_at` | datetime |  | Competitor creation time. |
| `updated_at` | datetime |  | Last competitor update time. |
| `metrics` | CompetitorMetrics (optional) |  | Included only on suggested-list items with qualifying responses; otherwise omitted. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page number. |
| `per_page` | integer |  | Requested page size. Minimum: 1. Maximum: 50. |
| `total` | integer |  | Total competitors matching the applied filters. |
| `total_pages` | integer |  | Total pages at the current per\_page value. |

#### CompetitorMetrics

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility_rate` | number |  | Percentage of eligible responses that mentioned this entity, from 0 through 100. |
| `avg_rank` | number \| null |  | Average 1-based rank when a ranked result included the entity. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors?state=suggested&sort=visibility&domain=all&page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "primary": {
    "id": "10cf756e-ad42-43d7-beda-e8e9cc96cd41",
    "kind": "primary",
    "name": "Acme",
    "domain": "acme.example",
    "aliases": ["Acme AI"],
    "status": "active",
    "competitor_state": null,
    "created_at": "2026-04-12T08:00:00Z",
    "updated_at": "2026-07-28T10:30:00Z"
  },
  "competitors": [
    {
      "id": "a8223436-70dc-4bc8-89fc-99139366711c",
      "kind": "competitor",
      "name": "Orbit Labs",
      "domain": "orbitlabs.example",
      "aliases": [],
      "status": "inactive",
      "competitor_state": "suggested",
      "created_at": "2026-07-21T07:15:00Z",
      "updated_at": "2026-07-30T09:10:00Z",
      "metrics": {
        "visibility_rate": 24.17,
        "avg_rank": 4.2
      }
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `422` | invalid\_filter \| validation\_failed |  | A state, sort, or domain filter is invalid, or a pagination value is outside the supported range. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Track suggested competitor

`PATCH /projects/{project_id}/competitors/{competitor_id}`

Moves a suggested competitor into the tracked set. The suggestion must have a valid normalized domain and a tracked slot must be available.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `competitor_id` | uuid | Required | Competitor identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Mutable display name. Maximum: 200 characters. |
| `domain` | string |  | Mutable normalized domain. Required whenever competitor\_state is tracked. |
| `aliases` | string\[\] |  | Mutable replacement list of up to 10 unique alternate names. |
| `competitor_state` | tracked \| suggested |  | Mutable lifecycle state. PATCH accepts only tracked or suggested; dismissed is set only by a dismissal or merge operation. |

> **Required tracking state**
>
> Set `competitor_state` to `tracked`. Status is set to active internally. Any other mutable fields may be updated in the same request.

#### Track suggested competitor response envelope

`project_id`:**uuid**`competitor`:**Competitor**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `competitor` | Competitor |  | Created or updated competitor. |

#### Competitor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique competitor identifier. |
| `kind` | primary \| competitor |  | Entity role. Items in competitors are competitor; primary describes the tracked brand. |
| `name` | string |  | Display name used in prompts, results, and reports. |
| `domain` | string \| null |  | Normalized hostname without a scheme, path, query, leading www, or trailing dot. |
| `aliases` | string\[\] |  | Additional names recognized as this entity. Maximum: 10. |
| `status` | active \| inactive |  | Processing status. Competitors are active exactly when tracked and inactive when suggested or dismissed. |
| `competitor_state` | tracked \| suggested \| dismissed \| null |  | Competitor management lifecycle state. Null for the primary brand. |
| `created_at` | datetime |  | Competitor creation time. |
| `updated_at` | datetime |  | Last competitor update time. |

#### Request and response

```curl
curl --request PATCH \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors/{competitor_id}' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{"competitor_state":"tracked"}'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "competitor": {
      "id": "a8223436-70dc-4bc8-89fc-99139366711c",
      "kind": "competitor",
      "name": "Orbit Labs",
      "domain": "orbitlabs.example",
      "aliases": [],
      "status": "active",
      "competitor_state": "tracked",
      "created_at": "2026-07-21T07:15:00Z",
      "updated_at": "2026-07-30T09:10:00Z"
    }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `409` | domain\_conflict \| own\_domain \| invalid\_state |  | The normalized domain belongs to another active competitor or to the primary brand. |
| `422` | validation\_failed \| limit\_exceeded |  | Required data is absent, aliases exceed 10, or tracked capacity would be exceeded. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Dismiss suggested competitor

`DELETE /projects/{project_id}/competitors/{competitor_id}`

Soft-dismisses a suggested competitor so it is excluded from the active suggestion queue while retaining its discovery evidence.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `competitor_id` | uuid | Required | Competitor identifier. |

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/competitors/{competitor_id}' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 204 No Content
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [{ "field": "domain", "message": "must be a valid hostname" }],
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | Authorization bearer token is absent or invalid. |
| `403` | forbidden |  | The API key lacks the required competitor capability or cannot access this scope. |
| `404` | not\_found |  | Project, location, or competitor was not found. |
| `409` | invalid\_state |  | The competitor cannot be dismissed from its current state. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Visibility analytics

Source: https://ceyo.ai/docs/signal/visibility

### Visibility

Measure brand visibility, compare competitors, and track performance trends across AI models for a project or location.

**Scope:** Project

> **Percentage metrics**
>
> Visibility, coverage, and share of voice are percentages from 0 to 100. Competitor timeseries are keyed by tracked entity, with selected model keys returned separately in `models`.

### Get visibility overview

`GET /projects/{project_id}/visibility/summary`

Returns aggregate visibility and citation metrics for matching responses in the selected project.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First run date to include, in YYYY-MM-DD format. |
| `end` | date | Optional; Default: Today | Last run date to include, in YYYY-MM-DD format. |
| `models` | string | Optional; Default: All model keys present in matching responses | Comma-separated or repeated model keys, such as chatgpt,claude,perplexity. |
| `topic_ids` | string | Optional | Comma-separated or repeated topic UUIDs. |
| `topic_id` | uuid | Optional | Alias for topic\_ids when filtering by one topic UUID. |
| `prompt_id` | uuid | Optional | Restrict results to one prompt in the selected scope. |

#### Response envelope

`start`:**date**`end`:**date**`models`:**string\[\]**`summary`:**object**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date |  | Resolved start date. |
| `end` | date |  | Resolved end date. |
| `models` | string\[\] |  | Model keys included in the calculation. When omitted from the request, this contains every model key present in matching responses. |
| `summary` | object |  | Aggregate visibility metrics. |

#### Summary object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `primary_mentions` | integer |  | Responses in which the primary tracked entity was mentioned. |
| `visibility_percentage` | number |  | Share of responses that mentioned the primary tracked entity, rounded to two decimals. |
| `average_position` | number \| null |  | Average 1-based position for primary-entity mentions with a detected position. |
| `citations` | integer |  | Total citation occurrences across matching responses. |

> **Metric definitions**
>
> `visibility_percentage` is the share of matching responses that mention the primary entity. `average_position` uses only primary mentions with a detected position, and `citations` counts citation occurrences.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/summary?start=2026-07-24&end=2026-07-30&models=chatgpt,perplexity' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "start": "2026-07-24",
  "end": "2026-07-30",
  "models": ["chatgpt", "perplexity"],
  "summary": {
    "primary_mentions": 17,
    "visibility_percentage": 42.5,
    "average_position": 2.82,
    "citations": 31
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_date\_range \| range\_too\_large |  | A date is invalid or the requested window exceeds three months. |
| `401` | invalid\_api\_key |  | The Bearer API key is missing or invalid. |
| `403` | forbidden |  | The API key is valid but is not allowed to access this resource. |
| `404` | not\_found |  | The project, location, or visibility scope was not found. |
| `422` | visibility\_unavailable |  | Project-level visibility is unavailable for a locations-only container; use a location scope. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Compare competitors

`GET /projects/{project_id}/visibility/competitors`

Compares the primary entity plus tracked competitors for the selected project, ordered with primary first, then by share\_of\_voice descending and name.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First run date to include, in YYYY-MM-DD format. |
| `end` | date | Optional; Default: Today | Last run date to include, in YYYY-MM-DD format. |
| `models` | string | Optional; Default: All model keys present in matching responses | Comma-separated or repeated model keys, such as chatgpt,claude,perplexity. |
| `topic_ids` | string | Optional | Comma-separated or repeated topic UUIDs. |
| `topic_id` | uuid | Optional | Alias for topic\_ids when filtering by one topic UUID. |
| `prompt_id` | uuid | Optional | Restrict results to one prompt in the selected scope. |

#### Response envelope

`start`:**date**`end`:**date**`models`:**string\[\]**`total_mentions`:**integer**`entities`:**object\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date |  | Resolved start date. |
| `end` | date |  | Resolved end date. |
| `models` | string\[\] |  | Model keys included in the calculation. When omitted from the request, this contains every model key present in matching responses. |
| `total_mentions` | integer |  | Mentions across all returned entities. |
| `entities` | object\[\] |  | The primary entity plus tracked competitors, ordered with primary first, then by share\_of\_voice descending and name. |

#### Entity

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `tracked_entity_id` | uuid |  | Tracked primary brand or competitor identifier. |
| `kind` | primary \| competitor |  | Entity relationship to the selected scope. |
| `name` | string |  | Tracked entity name. |
| `domain` | string \| null |  | Normalized entity domain when configured. |
| `status` | active |  | Current tracked-entity status. Only active entities are returned. |
| `mentions` | integer |  | Responses in which this entity was mentioned. |
| `coverage_percentage` | number |  | Share of matching responses that mentioned the entity. |
| `share_of_voice` | number |  | Entity mentions divided by mentions across all returned entities, multiplied by 100. |
| `average_position` | number \| null |  | Average detected 1-based position when the entity was mentioned. |

> **Metric definitions**
>
> Coverage uses all matching responses as its denominator. Share of voice uses mentions across the returned primary entity and tracked competitors. Results place the primary entity first, followed by competitors ordered by `share_of_voice` descending and then name. When there are no mentions, both percentages are `0.0` and average position is `null`.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/competitors?start=2026-07-24&end=2026-07-30&topic_ids=ab20526b-6bb2-436c-8c93-5bf77ea43848' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "start": "2026-07-24",
  "end": "2026-07-30",
  "models": ["chatgpt", "perplexity"],
  "total_mentions": 33,
  "entities": [
    {
      "tracked_entity_id": "f3385d92-8f24-49f2-9819-18412e93427f",
      "kind": "primary",
      "name": "Ceyo",
      "domain": "ceyo.ai",
      "status": "active",
      "mentions": 17,
      "coverage_percentage": 42.5,
      "share_of_voice": 51.52,
      "average_position": 2.82
    },
    {
      "tracked_entity_id": "2e230599-4918-49a7-bb08-b30711836aa8",
      "kind": "competitor",
      "name": "Example Competitor",
      "domain": "competitor.example",
      "status": "active",
      "mentions": 16,
      "coverage_percentage": 40.0,
      "share_of_voice": 48.48,
      "average_position": 3.13
    }
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_date\_range \| range\_too\_large |  | A date is invalid or the requested window exceeds three months. |
| `401` | invalid\_api\_key |  | The Bearer API key is missing or invalid. |
| `403` | forbidden |  | The API key is valid but is not allowed to access this resource. |
| `404` | not\_found |  | The project, location, or visibility scope was not found. |
| `422` | visibility\_unavailable |  | Project-level visibility is unavailable for a locations-only container; use a location scope. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get competitor trends

`GET /projects/{project_id}/visibility/competitors/timeseries`

Returns dated coverage, mention, and position series for the primary entity and competitors in the selected project.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First run date to include, in YYYY-MM-DD format. |
| `end` | date | Optional; Default: Today | Last run date to include, in YYYY-MM-DD format. |
| `models` | string | Optional; Default: All model keys present in matching responses | Comma-separated or repeated model keys, such as chatgpt,claude,perplexity. |
| `topic_ids` | string | Optional | Comma-separated or repeated topic UUIDs. |
| `topic_id` | uuid | Optional | Alias for topic\_ids when filtering by one topic UUID. |
| `prompt_id` | uuid | Optional | Restrict results to one prompt in the selected scope. |

#### Response envelope

`start`:**date**`end`:**date**`models`:**string\[\]**`entities`:**object\[\]**`points`:**object\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date |  | Resolved start date. |
| `end` | date |  | Resolved end date. |
| `models` | string\[\] |  | Model keys included in the calculation. When omitted from the request, this contains every model key present in matching responses. |
| `entities` | object\[\] |  | Metadata for each keyed series. |
| `points` | object\[\] |  | One point for each date containing matching responses. |

#### Timeseries entity

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `key` | string |  | Stable series key in the form entity:{tracked\_entity\_id}. |
| `tracked_entity_id` | uuid |  | Tracked entity identifier. |
| `kind` | primary \| competitor |  | Entity relationship to the selected scope. |
| `name` | string |  | Tracked entity name. |
| `domain` | string \| null |  | Normalized entity domain when configured. |
| `aliases` | string\[\] |  | Additional names recognized as mentions of this entity. |

#### Timeseries point

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `date` | date |  | Visibility measurement date. |
| `mentions` | object |  | Mention count keyed by entity series key. |
| `average_positions` | object |  | Average 1-based position or null, keyed by entity series key. |

> **Series keys and dates**
>
> Join each object in `mentions` and `average_positions` to entity metadata through `key`. Dates without matching responses are omitted; returned entity values are zero-filled for dates that have responses.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/competitors/timeseries?start=2026-07-24&end=2026-07-30&models=chatgpt,perplexity' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "start": "2026-07-24",
  "end": "2026-07-30",
  "models": ["chatgpt", "perplexity"],
  "entities": [
    {
      "key": "entity:f3385d92-8f24-49f2-9819-18412e93427f",
      "tracked_entity_id": "f3385d92-8f24-49f2-9819-18412e93427f",
      "kind": "primary",
      "name": "Ceyo",
      "domain": "ceyo.ai",
      "aliases": ["Ceyo AI"]
    }
  ],
  "points": [
    {
      "date": "2026-07-30",
      "mentions": {
        "entity:f3385d92-8f24-49f2-9819-18412e93427f": 4
      },
      "average_positions": {
        "entity:f3385d92-8f24-49f2-9819-18412e93427f": 2.5
      }
    }
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_date\_range \| range\_too\_large |  | A date is invalid or the requested window exceeds three months. |
| `401` | invalid\_api\_key |  | The Bearer API key is missing or invalid. |
| `403` | forbidden |  | The API key is valid but is not allowed to access this resource. |
| `404` | not\_found |  | The project, location, or visibility scope was not found. |
| `422` | visibility\_unavailable |  | Project-level visibility is unavailable for a locations-only container; use a location scope. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get model visibility trends

`GET /projects/{project_id}/visibility/model-trends`

Returns daily and range-level primary-brand visibility by model, plus an aggregate across models, for the selected project.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First run date to include, in YYYY-MM-DD format. |
| `end` | date | Optional; Default: Today | Last run date to include, in YYYY-MM-DD format. |
| `models` | string | Optional; Default: All model keys present in matching responses | Comma-separated or repeated model keys, such as chatgpt,claude,perplexity. |
| `topic_ids` | string | Optional | Comma-separated or repeated topic UUIDs. |
| `topic_id` | uuid | Optional | Alias for topic\_ids when filtering by one topic UUID. |
| `prompt_id` | uuid | Optional | Restrict results to one prompt in the selected scope. |

#### Response envelope

`start`:**date**`end`:**date**`models`:**string\[\]**`aggregate`:**object**`model_summaries`:**object\[\]**`days`:**object\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date |  | Resolved start date. |
| `end` | date |  | Resolved end date. |
| `models` | string\[\] |  | Model keys included in the calculation. When omitted from the request, this contains every model key present in matching responses. |
| `aggregate` | object |  | Primary-brand visibility across all returned models. |
| `model_summaries` | object\[\] |  | Range-level primary-brand visibility for each returned model, ordered by model key. |
| `days` | object\[\] |  | Daily primary-brand visibility in ascending date order. Dates without matching responses are omitted. |

#### Aggregate object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `mentions` | integer |  | Responses in which the primary tracked brand was mentioned across all returned models. |
| `visibility_percentage` | number |  | Share of responses that mentioned the primary tracked brand, rounded to two decimals. |
| `comparison` | object \| null |  | Comparison with the immediately preceding window of the same length, or null when that window has no matching responses. |

#### Model summary object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `model` | string |  | Stable model key. |
| `mentions` | integer |  | Responses for this model in which the primary tracked brand was mentioned. |
| `visibility_percentage` | number |  | Share of this model’s responses that mentioned the primary tracked brand, rounded to two decimals. |
| `comparison` | object \| null |  | Comparison with the same model in the immediately preceding window, or null when that model has no matching responses in the comparison window. |

#### Daily object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `date` | date |  | Visibility measurement date in YYYY-MM-DD format. |
| `aggregate` | object |  | Primary-brand visibility across all returned models for this date. |
| `models` | object\[\] |  | Primary-brand visibility for each returned model on this date, ordered by model key. |

#### Daily aggregate object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `mentions` | integer |  | Responses across all returned models that mentioned the primary tracked brand on this date. |
| `visibility_percentage` | number |  | Share of responses that mentioned the primary tracked brand on this date. |

#### Daily model object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `model` | string |  | Stable model key. |
| `mentions` | integer |  | Responses for this model that mentioned the primary tracked brand on this date. |
| `visibility_percentage` | number |  | Share of this model’s responses that mentioned the primary tracked brand on this date. |

#### Comparison object

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date |  | First date of the immediately preceding comparison window. |
| `end` | date |  | Last date of the immediately preceding comparison window. |
| `mentions` | integer |  | Responses that mentioned the primary tracked brand in the comparison window. |
| `visibility_percentage` | number |  | Primary-brand visibility in the comparison window, expressed as a percentage from 0 to 100. |
| `delta_percentage_points` | number |  | Selected-window visibility\_percentage minus comparison-window visibility\_percentage, in percentage points. |

> **Ranges, percentages, and comparisons**
>
> The default range is seven days and the maximum range is three months. Every `visibility_percentage` is a value from 0 to 100 calculated as the share of responses that mention the primary brand. Range-level comparisons use the immediately preceding window of equal length. A positive `delta_percentage_points` means visibility increased. Dates without matching responses are omitted.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/visibility/model-trends?start=2026-07-24&end=2026-07-30&models=chatgpt,perplexity' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "start": "2026-07-24",
  "end": "2026-07-30",
  "models": ["chatgpt", "perplexity"],
  "aggregate": {
    "mentions": 17,
    "visibility_percentage": 42.5,
    "comparison": {
      "start": "2026-07-17",
      "end": "2026-07-23",
      "mentions": 14,
      "visibility_percentage": 36.84,
      "delta_percentage_points": 5.66
    }
  },
  "model_summaries": [
    {
      "model": "chatgpt",
      "mentions": 10,
      "visibility_percentage": 50.0,
      "comparison": {
        "start": "2026-07-17",
        "end": "2026-07-23",
        "mentions": 8,
        "visibility_percentage": 42.11,
        "delta_percentage_points": 7.89
      }
    },
    {
      "model": "perplexity",
      "mentions": 7,
      "visibility_percentage": 35.0,
      "comparison": {
        "start": "2026-07-17",
        "end": "2026-07-23",
        "mentions": 6,
        "visibility_percentage": 31.58,
        "delta_percentage_points": 3.42
      }
    }
  ],
  "days": [
    {
      "date": "2026-07-30",
      "aggregate": {
        "mentions": 4,
        "visibility_percentage": 50.0
      },
      "models": [
        {
          "model": "chatgpt",
          "mentions": 3,
          "visibility_percentage": 75.0
        },
        {
          "model": "perplexity",
          "mentions": 1,
          "visibility_percentage": 25.0
        }
      ]
    }
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_date\_range \| range\_too\_large |  | A date is invalid or the requested window exceeds three months. |
| `401` | invalid\_api\_key |  | The Bearer API key is missing or invalid. |
| `403` | forbidden |  | The API key is valid but is not allowed to access this resource. |
| `404` | not\_found |  | The project, location, or visibility scope was not found. |
| `422` | visibility\_unavailable |  | Project-level visibility is unavailable for a locations-only container; use a location scope. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Responses

Source: https://ceyo.ai/docs/signal/responses

### Responses

Fetch successful model responses and the sources cited across a project or location.

**Scope:** Project

### List prompt responses

`GET /projects/{project_id}/prompts/{prompt_id}/responses`

Returns one row per successful model response for a prompt in the selected project. Responses are ordered by run date and completion time, newest first.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `prompt_id` | uuid | Required | Prompt identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First run date to include, in YYYY-MM-DD format. |
| `end` | date | Optional; Default: Today | Last run date to include, in YYYY-MM-DD format. |
| `models` | string | Optional; Default: All enabled models | Comma-separated or repeated model keys. For example: chatgpt,claude,perplexity. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 15 | Number of records per page. Maximum: 50. |

#### Response envelope

`project_id`:**uuid**`prompt_id`:**uuid**`start`:**date**`end`:**date**`models`:**string\[\]**`rows`:**Response\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `prompt_id` | uuid |  | Prompt represented by the response. |
| `start` | date |  | Resolved start date. |
| `end` | date |  | Resolved end date. |
| `models` | string\[\] |  | Model keys included in the response. |
| `rows` | Response\[\] |  | Paginated model responses. |
| `pagination` | Pagination |  | Pagination metadata. |

#### Response

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `run_on` | date |  | Calendar date assigned to the visibility run. |
| `model_key` | string |  | Model that generated the response. |
| `status` | string |  | Response status. Returned rows have succeeded. |
| `response_text` | string \| null |  | Full stored response text. |
| `brand_present` | boolean |  | Whether the primary tracked brand was mentioned. |
| `position` | integer \| null |  | 1-based brand position when a ranked list was detected. |
| `sentiment` | Sentiment |  | Sentiment status, score, and label for the primary brand. |
| `entities` | EntityResult\[\] |  | Mention and position results for the brand and competitors. |
| `citations_count` | integer |  | Total citations attached to the response. |
| `citations` | CitationPreview\[\] |  | Preview of up to three citations. Use citations endpoints for all. |
| `finished_at` | datetime \| null |  | Time response processing completed. |

#### Sentiment

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `status` | string |  | not\_requested, pending, running, succeeded, failed, or skipped. |
| `score` | number \| null |  | Sentiment score from 0 to 10. |
| `label` | string \| null |  | negative, neutral, or positive. |

#### EntityResult

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `tracked_entity_id` | uuid |  | Tracked brand or competitor identifier. |
| `name` | string |  | Entity name captured when the response was processed. |
| `kind` | primary \| competitor |  | Relationship of the entity to the scope. |
| `mentioned` | boolean |  | Whether the entity appeared in the response. |
| `position` | integer \| null |  | 1-based entity position when a ranked list was detected. |

#### CitationPreview

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `domain` | string |  | Normalized citation hostname. |
| `url` | string |  | Citation URL. |
| `position` | integer |  | 1-based citation order in the model response. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page number. |
| `per_page` | integer |  | Requested number of records per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

> **Citation preview**
>
> The `citations` array contains at most three entries. Use the prompt citations endpoint to retrieve the complete set.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/prompts/{prompt_id}/responses?start=2026-07-24&end=2026-07-30&models=chatgpt,perplexity' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "prompt_id": "4de5e484-ce6a-4e45-ad7c-bd48db2549af",
  "start": "2026-07-24",
  "end": "2026-07-30",
  "models": ["chatgpt", "perplexity"],
  "rows": [
    {
      "run_on": "2026-07-30",
      "model_key": "chatgpt",
      "status": "succeeded",
      "response_text": "Ceyo is an AI visibility platform...",
      "brand_present": true,
      "position": 2,
      "sentiment": {
        "status": "succeeded",
        "score": 8.2,
        "label": "positive"
      },
      "entities": [
        {
          "tracked_entity_id": "f3385d92-8f24-49f2-9819-18412e93427f",
          "name": "Ceyo",
          "kind": "primary",
          "mentioned": true,
          "position": 2
        }
      ],
      "citations_count": 2,
      "citations": [
        {
          "domain": "example.com",
          "url": "https://example.com/ai-visibility",
          "position": 1
        }
      ],
      "finished_at": "2026-07-30T09:42:16Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 15,
    "total": 14,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_date_range",
    "message": "The requested date range is invalid.",
    "details": null,
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_date\_range \| range\_too\_large \| invalid\_filter |  | The requested filters or date range are invalid. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot access this resource. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `422` | visibility\_unavailable |  | Visibility is unavailable for the selected scope. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Citations

Source: https://ceyo.ai/docs/signal/citations

### Citations

Fetch successful model responses and the sources cited across a project or location.

**Scope:** Project

### List prompt citations

`GET /projects/{project_id}/prompts/{prompt_id}/citations`

Aggregates every citation attached to responses for one prompt in the selected project. Repeated citations are combined and counted across the requested date range.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `prompt_id` | uuid | Required | Prompt identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First run date to include, in YYYY-MM-DD format. |
| `end` | date | Optional; Default: Today | Last run date to include, in YYYY-MM-DD format. |
| `models` | string | Optional; Default: All enabled models | Comma-separated or repeated model keys. For example: chatgpt,claude,perplexity. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 15 | Number of records per page. Maximum: 50. |

#### Response envelope

`project_id`:**uuid**`prompt_id`:**uuid**`start`:**date**`end`:**date**`models`:**string\[\]**`rows`:**Citation\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `prompt_id` | uuid |  | Prompt represented by the response. |
| `start` | date |  | Resolved start date. |
| `end` | date |  | Resolved end date. |
| `models` | string\[\] |  | Model keys included in the response. |
| `rows` | Citation\[\] |  | Paginated citation pages. |
| `pagination` | Pagination |  | Pagination metadata. |

#### Citation

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `domain` | string |  | Normalized citation hostname. |
| `page` | string |  | Normalized page value used for grouping and display. |
| `url` | string |  | Canonical citation URL. |
| `title` | string \| null |  | Stored page title when supplied by the response provider. |
| `frequency` | integer |  | Citation occurrences across the selected responses. |
| `model_keys` | string\[\] |  | Models that cited the page at least once. |
| `model_frequencies` | Record<string, integer> |  | Citation frequency keyed by model. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page number. |
| `per_page` | integer |  | Requested number of records per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/prompts/{prompt_id}/citations?start=2026-07-24&end=2026-07-30&models=chatgpt,perplexity' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "prompt_id": "4de5e484-ce6a-4e45-ad7c-bd48db2549af",
  "start": "2026-07-24",
  "end": "2026-07-30",
  "models": ["chatgpt", "perplexity"],
  "rows": [
    {
      "domain": "example.com",
      "page": "example.com/ai-visibility",
      "url": "https://example.com/ai-visibility",
      "title": "A guide to AI visibility",
      "frequency": 6,
      "model_keys": ["chatgpt", "perplexity"],
      "model_frequencies": {
        "chatgpt": 4,
        "perplexity": 2
      }
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 15,
    "total": 8,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_date_range",
    "message": "The requested date range is invalid.",
    "details": null,
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_date\_range \| range\_too\_large \| invalid\_filter |  | The requested filters or date range are invalid. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot access this resource. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `422` | visibility\_unavailable |  | Visibility is unavailable for the selected scope. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### List project citations

`GET /projects/{project_id}/citations`

Returns citation frequency across every matching prompt response in the selected project. Results can be grouped by individual page or by domain.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First run date to include, in YYYY-MM-DD format. |
| `end` | date | Optional; Default: Today | Last run date to include, in YYYY-MM-DD format. |
| `models` | string | Optional; Default: All enabled models | Comma-separated or repeated model keys. For example: chatgpt,claude,perplexity. |
| `topic_ids` | string | Optional | Comma-separated topic UUIDs. |
| `group` | page \| domain | Optional; Default: page | Return page rows or domain groups containing their top pages. |
| `q` | string | Optional | Search citation domains, URLs, and titles. |
| `domain` | string | Optional | Restrict results to one normalized domain. |
| `exclude_competitors` | boolean | Optional; Default: true | Exclude tracked competitor domains and subdomains. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Number of records per page. Maximum: 100. |

#### Response envelope

`project_id`:**uuid**`start`:**date**`end`:**date**`models`:**string\[\]**`group`:**page | domain**`rows`:**Citation\[\]**`groups`:**CitationDomainGroup\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `start` | date |  | Resolved start date. |
| `end` | date |  | Resolved end date. |
| `models` | string\[\] |  | Model keys included in the response. |
| `group` | page \| domain |  | Resolved grouping mode. |
| `rows` | Citation\[\] |  | Page citations when group is page; otherwise empty. |
| `groups` | CitationDomainGroup\[\] |  | Domain groups when group is domain; otherwise empty. |
| `pagination` | Pagination |  | Pagination metadata for the selected grouping. |

#### Citation

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `domain` | string |  | Normalized citation hostname. |
| `page` | string |  | Normalized page value used for grouping and display. |
| `url` | string |  | Canonical citation URL. |
| `title` | string \| null |  | Stored page title when supplied by the response provider. |
| `frequency` | integer |  | Citation occurrences across the selected responses. |
| `model_keys` | string\[\] |  | Models that cited the page at least once. |
| `model_frequencies` | Record<string, integer> |  | Citation frequency keyed by model. |

#### CitationDomainGroup

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `domain` | string |  | Normalized citation hostname. |
| `frequency` | integer |  | Citation occurrences across every page in the domain. |
| `model_keys` | string\[\] |  | Models that cited the domain. |
| `model_frequencies` | Record<string, integer> |  | Domain citation frequency keyed by model. |
| `pages` | Citation\[\] |  | Highest-frequency pages within the domain. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page number. |
| `per_page` | integer |  | Requested number of records per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

> **Grouping**
>
> When `group=page`, results are returned in `rows`. When `group=domain`, `rows` is empty and domain results are returned in `groups`.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/citations?start=2026-07-01&end=2026-07-30&group=domain' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "start": "2026-07-01",
  "end": "2026-07-30",
  "models": ["chatgpt", "perplexity"],
  "group": "domain",
  "rows": [],
  "groups": [
    {
      "domain": "example.com",
      "frequency": 19,
      "model_keys": ["chatgpt", "perplexity"],
      "model_frequencies": {
        "chatgpt": 12,
        "perplexity": 7
      },
      "pages": [
        {
          "domain": "example.com",
          "page": "example.com/ai-visibility",
          "url": "https://example.com/ai-visibility",
          "title": "A guide to AI visibility",
          "frequency": 11,
          "model_keys": ["chatgpt", "perplexity"],
          "model_frequencies": {
            "chatgpt": 7,
            "perplexity": 4
          }
        }
      ]
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 12,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_date_range",
    "message": "The requested date range is invalid.",
    "details": null,
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_date\_range \| range\_too\_large \| invalid\_filter |  | The requested filters or date range are invalid. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot access this resource. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `422` | visibility\_unavailable |  | Visibility is unavailable for the selected scope. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get citation trends

`GET /projects/{project_id}/citations/timeseries`

Returns daily citation totals for the selected project, including a count for each requested model. Dates with no citations are returned with zero values.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start` | date | Optional; Default: 6 days before end | First run date to include, in YYYY-MM-DD format. |
| `end` | date | Optional; Default: Today | Last run date to include, in YYYY-MM-DD format. |
| `models` | string | Optional; Default: All enabled models | Comma-separated or repeated model keys. For example: chatgpt,claude,perplexity. |
| `topic_ids` | string | Optional | Comma-separated topic UUIDs. |
| `exclude_competitors` | boolean | Optional; Default: true | Exclude tracked competitor domains and subdomains. |
| `group` | domain \| page | Optional; Default: domain | Control whether top citations represent domains or pages. |

#### Response envelope

`project_id`:**uuid**`start`:**date**`end`:**date**`models`:**string\[\]**`points`:**CitationTimeseriesPoint\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `start` | date |  | Resolved start date. |
| `end` | date |  | Resolved end date. |
| `models` | string\[\] |  | Model keys included in the response. |
| `points` | CitationTimeseriesPoint\[\] |  | One zero-filled point for each date in the range. |

#### CitationTimeseriesPoint

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `date` | date |  | Calendar date represented by the point. |
| `totals` | Record<string, integer> |  | Citation count keyed by model. |
| `total` | integer |  | Total citations across the selected models. |
| `top_citations` | TopCitation\[\] |  | Up to five highest-frequency domains or pages. |

#### TopCitation

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `domain` | string |  | Normalized citation hostname. |
| `url` | string \| null |  | Citation URL when grouping by page. |
| `title` | string \| null |  | Stored page title when grouping by page. |
| `frequency` | integer |  | Citation occurrences for this date. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/citations/timeseries?start=2026-07-24&end=2026-07-30&models=chatgpt,perplexity' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "start": "2026-07-24",
  "end": "2026-07-30",
  "models": ["chatgpt", "perplexity"],
  "points": [
    {
      "date": "2026-07-30",
      "totals": {
        "chatgpt": 12,
        "perplexity": 8
      },
      "total": 20,
      "top_citations": [
        {
          "domain": "example.com",
          "url": "https://example.com/ai-visibility",
          "title": "A guide to AI visibility",
          "frequency": 7
        }
      ]
    }
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_date_range",
    "message": "The requested date range is invalid.",
    "details": null,
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_date\_range \| range\_too\_large \| invalid\_filter |  | The requested filters or date range are invalid. |
| `401` | invalid\_api\_key |  | Authorization is absent or invalid. |
| `403` | forbidden |  | The API key cannot access this resource. |
| `404` | not\_found |  | The project, location, or prompt was not found. |
| `422` | visibility\_unavailable |  | Visibility is unavailable for the selected scope. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Actions

Source: https://ceyo.ai/docs/signal/actions

### Actions

Retrieve prioritized recommendations, supporting findings, implementation guides, status, and measured outcomes.

**Scope:** Project

### List actions

`GET /projects/{project_id}/actions`

Returns actions for the selected project, with server-side filtering and pagination.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `q` | string | Optional | Search titles, descriptions, recommendations, and targets. |
| `status` | string | Optional; Default: active | active, todo, in\_progress, completed, dismissed, or resolved. active includes todo and in\_progress. |
| `priority` | string | Optional | low, medium, high, or critical. |
| `action_type` | string | Optional | Restrict results to one action type. |
| `effort_level` | low \| medium \| high | Optional | Restrict results by estimated effort. |
| `source_category` | string | Optional | Restrict results by work area. |
| `topic_id` | uuid | Optional | Return actions linked to one topic. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Number of actions per page. Maximum: 100. |

#### Response envelope

`project_id`:**uuid**`actions`:**Action\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `actions` | Action\[\] |  | Actions matching the selected filters. |
| `pagination` | Pagination |  | Pagination metadata. |

#### Action

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique action identifier. |
| `status` | string |  | todo, in\_progress, completed, dismissed, or resolved. |
| `priority` | string |  | low, medium, high, or critical. |
| `estimated_impact` | integer |  | Relative expected impact from 0 to 100. |
| `effort_level` | low \| medium \| high |  | Estimated implementation effort. |
| `feasibility` | low \| medium \| high |  | Estimated ability to complete the recommendation. |
| `source_category` | string |  | owned, earned, reputation, local, competitive, operational, or general. |
| `content_format` | string \| null |  | Recommended deliverable format when relevant. |
| `action_type` | string |  | technical, content, visibility, competitor, listing, earned\_editorial, earned\_ugc, earned\_reference, sentiment\_correction, or general. |
| `title` | string |  | Short action title. |
| `description` | string \| null |  | Problem or opportunity addressed by the action. |
| `recommendation` | string \| null |  | Recommended outcome or approach. |
| `target_kind` | string |  | Type of resource targeted by the action. |
| `target` | string \| null |  | Target URL, prompt reference, profile, or scope value. |
| `guide` | Guide \| null |  | Structured implementation guide when available. |
| `topics` | TopicReference\[\] |  | Topics connected through supporting prompts and findings. |
| `started_at` | datetime \| null |  | When the action first entered in\_progress. |
| `completed_at` | datetime \| null |  | When the action was completed. |
| `dismissed_at` | datetime \| null |  | When the action was dismissed. |
| `resolved_at` | datetime \| null |  | When supporting findings were automatically resolved. |
| `created_at` | datetime |  | Action creation time. |
| `updated_at` | datetime |  | Last action update time. |

#### Guide

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `diagnosis` | string |  | Concise explanation of the diagnosed issue. |
| `gap_analysis` | string |  | Difference between the current and desired state. |
| `action_steps` | string\[\] |  | Ordered implementation steps. |
| `validation_steps` | string\[\] |  | Checks used to confirm completion. |
| `rollback_notes` | string |  | Recovery guidance when a change must be reverted. |
| `impact_prediction` | string |  | Expected outcome after implementation. |
| `impact_timeline_days` | integer |  | Estimated days before an outcome may become measurable. |

#### TopicReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Topic identifier. |
| `name` | string |  | Topic display name. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Number of records requested per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/actions?status=active&priority=high&page=1' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "actions": [
    {
      "id": "8ec60fe5-9c0b-41ea-98ce-9c98f846466f",
      "status": "in_progress",
      "priority": "high",
      "estimated_impact": 78,
      "effort_level": "medium",
      "feasibility": "high",
      "source_category": "owned",
      "content_format": "comparison_page",
      "action_type": "content",
      "title": "Create a focused comparison page",
      "description": "Competitors are cited for high-intent comparison prompts.",
      "recommendation": "Publish a factual comparison addressing the observed gaps.",
      "target_kind": "site",
      "target": "https://example.com",
      "topics": [
        {
          "id": "ab20526b-6bb2-436c-8c93-5bf77ea43848",
          "name": "AI visibility platforms"
        }
      ],
      "started_at": "2026-07-30T10:05:00Z",
      "completed_at": null,
      "dismissed_at": null,
      "resolved_at": null,
      "created_at": "2026-07-20T08:30:00Z",
      "updated_at": "2026-07-30T10:05:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 42,
    "total_pages": 2
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request parameters are invalid.",
    "details": { "status": ["is not supported"] },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | Project, location, or action was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get action

`GET /projects/{project_id}/actions/{action_id}`

Returns one action, its structured implementation guide, and all supporting findings in the selected project.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `action_id` | uuid | Required | Action identifier. |

#### Action response envelope

`project_id`:**uuid**`action`:**Action**`findings`:**Finding\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `action` | Action |  | Requested action and its implementation guide. |
| `findings` | Finding\[\] |  | Supporting findings linked to the action. |

#### Action

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique action identifier. |
| `status` | string |  | todo, in\_progress, completed, dismissed, or resolved. |
| `priority` | string |  | low, medium, high, or critical. |
| `estimated_impact` | integer |  | Relative expected impact from 0 to 100. |
| `effort_level` | low \| medium \| high |  | Estimated implementation effort. |
| `feasibility` | low \| medium \| high |  | Estimated ability to complete the recommendation. |
| `source_category` | string |  | owned, earned, reputation, local, competitive, operational, or general. |
| `content_format` | string \| null |  | Recommended deliverable format when relevant. |
| `action_type` | string |  | technical, content, visibility, competitor, listing, earned\_editorial, earned\_ugc, earned\_reference, sentiment\_correction, or general. |
| `title` | string |  | Short action title. |
| `description` | string \| null |  | Problem or opportunity addressed by the action. |
| `recommendation` | string \| null |  | Recommended outcome or approach. |
| `target_kind` | string |  | Type of resource targeted by the action. |
| `target` | string \| null |  | Target URL, prompt reference, profile, or scope value. |
| `guide` | Guide \| null |  | Structured implementation guide when available. |
| `topics` | TopicReference\[\] |  | Topics connected through supporting prompts and findings. |
| `started_at` | datetime \| null |  | When the action first entered in\_progress. |
| `completed_at` | datetime \| null |  | When the action was completed. |
| `dismissed_at` | datetime \| null |  | When the action was dismissed. |
| `resolved_at` | datetime \| null |  | When supporting findings were automatically resolved. |
| `created_at` | datetime |  | Action creation time. |
| `updated_at` | datetime |  | Last action update time. |

#### Guide

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `diagnosis` | string |  | Concise explanation of the diagnosed issue. |
| `gap_analysis` | string |  | Difference between the current and desired state. |
| `action_steps` | string\[\] |  | Ordered implementation steps. |
| `validation_steps` | string\[\] |  | Checks used to confirm completion. |
| `rollback_notes` | string |  | Recovery guidance when a change must be reverted. |
| `impact_prediction` | string |  | Expected outcome after implementation. |
| `impact_timeline_days` | integer |  | Estimated days before an outcome may become measurable. |

#### TopicReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Topic identifier. |
| `name` | string |  | Topic display name. |

#### Finding

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `source` | string |  | Analysis source that produced the finding. |
| `category` | string |  | Finding category. |
| `severity` | string |  | Finding severity. |
| `status` | string |  | Current finding lifecycle status. |
| `target_kind` | string |  | Type of resource that produced the signal. |
| `target` | string \| null |  | Target value when available. |
| `title` | string |  | Finding title. |
| `description` | string \| null |  | Evidence-backed finding description. |
| `recommendation` | string \| null |  | Recommended response to the finding. |
| `first_seen_at` | datetime |  | First detection time. |
| `last_seen_at` | datetime |  | Most recent detection time. |
| `resolved_at` | datetime \| null |  | Resolution time, when resolved. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/actions/{action_id}' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "action": {
    "id": "8ec60fe5-9c0b-41ea-98ce-9c98f846466f",
    "status": "in_progress",
    "priority": "high",
    "estimated_impact": 78,
    "effort_level": "medium",
    "feasibility": "high",
    "source_category": "owned",
    "content_format": "comparison_page",
    "action_type": "content",
    "title": "Create a focused comparison page",
    "description": "Competitors are cited for high-intent comparison prompts.",
    "recommendation": "Publish a factual comparison addressing the observed gaps.",
    "target_kind": "site",
    "target": "https://example.com",
    "guide": {
      "diagnosis": "Competitors own the strongest comparison citations.",
      "gap_analysis": "The site does not answer the comparison intent directly.",
      "action_steps": [
        "Collect product evidence for each comparison criterion.",
        "Publish a transparent comparison page."
      ],
      "validation_steps": [
        "Confirm every claim has a source.",
        "Request indexing after publication."
      ],
      "rollback_notes": "Remove unsupported claims if evidence changes.",
      "impact_prediction": "Improved relevance for comparison prompts.",
      "impact_timeline_days": 30
    },
    "topics": [],
    "started_at": "2026-07-30T10:05:00Z",
    "completed_at": null,
    "dismissed_at": null,
    "resolved_at": null,
    "created_at": "2026-07-20T08:30:00Z",
    "updated_at": "2026-07-30T10:05:00Z"
  },
  "findings": [
    {
      "source": "visibility",
      "category": "citation_gap",
      "severity": "high",
      "status": "open",
      "target_kind": "site",
      "target": "example.com",
      "title": "Competitors own comparison citations",
      "description": "Competitor pages are repeatedly cited.",
      "recommendation": "Address the observed comparison intent.",
      "first_seen_at": "2026-07-10T07:30:00Z",
      "last_seen_at": "2026-07-30T09:30:00Z",
      "resolved_at": null
    }
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request parameters are invalid.",
    "details": { "status": ["is not supported"] },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | Project, location, or action was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Update action status

`PATCH /projects/{project_id}/actions/{action_id}`

Updates the customer-managed lifecycle status of an action. Resolved actions are controlled automatically by their supporting findings.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `action_id` | uuid | Required | Action identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `status` | todo \| in\_progress \| completed \| dismissed |  | New customer-managed action status. |

#### Allowed transitions

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `todo` | → in\_progress, completed, dismissed |  | A new action can be started, completed, or dismissed. |
| `in_progress` | → todo, completed, dismissed |  | Work can be paused, completed, or dismissed. |
| `completed` | → todo |  | A completed action can be reopened. |
| `dismissed` | → todo |  | A dismissed action can be reopened. |

#### Action response envelope

`project_id`:**uuid**`action`:**Action**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `action` | Action |  | Updated action. |

#### Action

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Unique action identifier. |
| `status` | string |  | todo, in\_progress, completed, dismissed, or resolved. |
| `priority` | string |  | low, medium, high, or critical. |
| `estimated_impact` | integer |  | Relative expected impact from 0 to 100. |
| `effort_level` | low \| medium \| high |  | Estimated implementation effort. |
| `feasibility` | low \| medium \| high |  | Estimated ability to complete the recommendation. |
| `source_category` | string |  | owned, earned, reputation, local, competitive, operational, or general. |
| `content_format` | string \| null |  | Recommended deliverable format when relevant. |
| `action_type` | string |  | technical, content, visibility, competitor, listing, earned\_editorial, earned\_ugc, earned\_reference, sentiment\_correction, or general. |
| `title` | string |  | Short action title. |
| `description` | string \| null |  | Problem or opportunity addressed by the action. |
| `recommendation` | string \| null |  | Recommended outcome or approach. |
| `target_kind` | string |  | Type of resource targeted by the action. |
| `target` | string \| null |  | Target URL, prompt reference, profile, or scope value. |
| `guide` | Guide \| null |  | Structured implementation guide when available. |
| `topics` | TopicReference\[\] |  | Topics connected through supporting prompts and findings. |
| `started_at` | datetime \| null |  | When the action first entered in\_progress. |
| `completed_at` | datetime \| null |  | When the action was completed. |
| `dismissed_at` | datetime \| null |  | When the action was dismissed. |
| `resolved_at` | datetime \| null |  | When supporting findings were automatically resolved. |
| `created_at` | datetime |  | Action creation time. |
| `updated_at` | datetime |  | Last action update time. |

#### Guide

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `diagnosis` | string |  | Concise explanation of the diagnosed issue. |
| `gap_analysis` | string |  | Difference between the current and desired state. |
| `action_steps` | string\[\] |  | Ordered implementation steps. |
| `validation_steps` | string\[\] |  | Checks used to confirm completion. |
| `rollback_notes` | string |  | Recovery guidance when a change must be reverted. |
| `impact_prediction` | string |  | Expected outcome after implementation. |
| `impact_timeline_days` | integer |  | Estimated days before an outcome may become measurable. |

#### TopicReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Topic identifier. |
| `name` | string |  | Topic display name. |

#### Request and response

```curl
curl --request PATCH \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/actions/{action_id}' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{"status":"completed"}'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "action": {
    "id": "8ec60fe5-9c0b-41ea-98ce-9c98f846466f",
    "status": "completed",
    "completed_at": "2026-07-31T10:15:00Z",
    "updated_at": "2026-07-31T10:15:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request parameters are invalid.",
    "details": { "status": ["is not supported"] },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | Project, location, or action was not found. |
| `409` | invalid\_transition |  | The requested status transition is not allowed. |
| `422` | invalid\_status |  | The submitted status is not writable. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get action impact

`GET /projects/{project_id}/actions/impact`

Compares visibility signals before and after completed actions in the selected project. Actions remain pending until both baseline and comparison runs are available.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start_on` | date | Optional; Default: 90 days before end\_on | First completion date to include. |
| `end_on` | date | Optional; Default: Today | Last completion date to include. |
| `topic_id` | uuid | Optional | Measure actions connected to one topic. |
| `action_type` | string | Optional | Measure one action type. |

#### Response envelope

`project_id`:**uuid**`period`:**ImpactPeriod**`summary`:**ImpactSummary**`timeline`:**ImpactTimelinePoint\[\]**`actions`:**ActionMeasurement\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved Ceyo project identifier. |
| `period` | ImpactPeriod |  | Resolved start\_on and end\_on. |
| `summary` | ImpactSummary |  | Completed, measured, pending, and outcome totals. |
| `timeline` | ImpactTimelinePoint\[\] |  | Weekly completion and visibility outcome series. |
| `actions` | ActionMeasurement\[\] |  | Measurements for up to 100 completed actions. |

#### ImpactPeriod

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `start_on` | date |  | First completion date included in the measurement window. |
| `end_on` | date |  | Last completion date included in the measurement window. |

#### ImpactSummary

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `completed_actions` | integer |  | Completed actions included in the measurement window. |
| `measured_actions` | integer |  | Actions with both baseline and comparison signals. |
| `pending_actions` | integer |  | Actions awaiting an eligible baseline or comparison signal. |
| `improved` | integer |  | Measured actions classified as improved. |
| `unchanged` | integer |  | Measured actions classified as unchanged. |
| `declined` | integer |  | Measured actions classified as declined. |
| `average_visibility_delta` | number \| null |  | Mean visibility percentage-point change, or null when no action is measured. |
| `average_citation_delta` | number \| null |  | Mean citation-count change, or null when no action is measured. |
| `average_sentiment_delta` | number \| null |  | Mean sentiment-score change, or null when no comparable values exist. |

#### ImpactTimelinePoint

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `date` | date |  | Start date of the represented week. |
| `completed` | integer |  | Actions completed during the represented week. |
| `measured` | integer |  | Completed actions with an available measurement. |
| `average_visibility_delta` | number \| null |  | Mean visibility percentage-point change, or null when the week has no measurements. |

#### ActionMeasurement

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Action identifier. |
| `title` | string |  | Action title. |
| `action_type` | string |  | Action type. |
| `completed_at` | datetime |  | Action completion time. |
| `topics` | TopicReference\[\] |  | Topics used to scope the measurement. |
| `status` | measured \| pending |  | Measurement availability. |
| `reason` | no\_baseline\_run \| awaiting\_post\_completion\_run \| null |  | Why a pending action cannot yet be measured. |
| `outcome` | improved \| unchanged \| declined \| null |  | Outcome derived from visibility change when measured. |
| `baseline_at` | datetime \| null |  | Baseline run completion time. |
| `comparison_at` | datetime \| null |  | Comparison run completion time. |
| `baseline` | ImpactSnapshot \| null |  | Signals immediately before completion when measured. |
| `comparison` | ImpactSnapshot \| null |  | Latest eligible signals after completion when measured. |
| `delta` | ActionMeasurementDelta \| null |  | Signal changes when the action is measured. |

#### ActionMeasurementDelta

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility_percentage_points` | number \| null |  | Comparison visibility minus baseline visibility. |
| `citation_count` | integer \| null |  | Comparison citation count minus baseline citation count. |
| `sentiment_score` | number \| null |  | Comparison sentiment score minus baseline sentiment score. |
| `average_position` | number \| null |  | Improvement in average position; positive values indicate movement toward position one. |

#### ImpactSnapshot

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `visibility_percentage` | number |  | Primary brand visibility percentage. |
| `citation_count` | integer |  | Distinct cited pages. |
| `sentiment_score` | number \| null |  | Average primary-brand sentiment score. |
| `average_position` | number \| null |  | Average primary-brand position. |

#### TopicReference

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Topic identifier. |
| `name` | string |  | Topic display name. |

> **Outcome interpretation**
>
> Measurements show correlated before-and-after signals. They do not establish that an action was the only cause of a change.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/actions/impact?start_on=2026-05-01&end_on=2026-07-30' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
  "period": {
    "start_on": "2026-05-01",
    "end_on": "2026-07-30"
  },
  "summary": {
    "completed_actions": 12,
    "measured_actions": 9,
    "pending_actions": 3,
    "improved": 6,
    "unchanged": 2,
    "declined": 1,
    "average_visibility_delta": 4.8,
    "average_citation_delta": 3.2,
    "average_sentiment_delta": 0.4
  },
  "timeline": [
    {
      "date": "2026-07-27",
      "completed": 3,
      "measured": 2,
      "average_visibility_delta": 5.1
    }
  ],
  "actions": [
    {
      "id": "8ec60fe5-9c0b-41ea-98ce-9c98f846466f",
      "title": "Create a focused comparison page",
      "action_type": "content",
      "completed_at": "2026-07-10T10:15:00Z",
      "topics": [],
      "status": "measured",
      "outcome": "improved",
      "baseline_at": "2026-07-09T07:30:00Z",
      "comparison_at": "2026-07-30T09:30:00Z",
      "baseline": {
        "visibility_percentage": 35.0,
        "citation_count": 8,
        "sentiment_score": 6.8,
        "average_position": 3.4
      },
      "comparison": {
        "visibility_percentage": 42.5,
        "citation_count": 13,
        "sentiment_score": 7.3,
        "average_position": 2.8
      },
      "delta": {
        "visibility_percentage_points": 7.5,
        "citation_count": 5,
        "sentiment_score": 0.5,
        "average_position": 0.6
      }
    }
  ]
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request parameters are invalid.",
    "details": { "status": ["is not supported"] },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | not\_found |  | Project, location, or action was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Listings

Source: https://ceyo.ai/docs/signal/listings

### Listings

Read a location's configured listing identity, observed profile, scheduled scan records, and listing findings.

> **Authentication**
>
> Send a platform API key in the `Authorization` header as `Bearer ceyo_platform_...`, or in `X-Api-Key`. The key requires `listings:read` and access to the requested project and location. Project and location path identifiers accept Ceyo UUIDs or partner external IDs.

### Get listing profile

`GET /projects/{project_id}/locations/{location_id}/listings/profile`

Returns the listing identity, newest retained profile, and latest scan summary for a location.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Response envelope

`listing`:**ConfiguredListing | null**`profile`:**ListingProfile | null**`latest_scan`:**ScanSummary | null**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `listing` | ConfiguredListing \| null |  | Configured listing identity, or null when listings are disabled or the location has no configured place. |
| `profile` | ListingProfile \| null |  | Profile from the newest succeeded scan with available profile data, or null when unavailable. |
| `latest_scan` | ScanSummary \| null |  | Newest listing scan summary, or null before any scan. |

#### ConfiguredListing

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string |  | Configured listing or location name. |
| `address` | string \| null |  | Configured location address. |
| `google_maps_url` | string \| null |  | Public Google Maps URL for the configured place. |

#### ListingProfile

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `place_id` | string |  | Place identifier represented by this profile. |
| `name` | string \| null |  | Public business name. |
| `formatted_address` | string \| null |  | Publicly formatted business address. |
| `website_url` | string \| null |  | Public website linked from the listing. |
| `national_phone_number` | string \| null |  | Phone number formatted for the listing country. |
| `international_phone_number` | string \| null |  | Phone number in international format. |
| `google_maps_url` | string \| null |  | Public Google Maps URL. |
| `business_status` | string \| null |  | Current public operating status. |
| `primary_type` | string \| null |  | Machine-readable primary business type. |
| `primary_type_display_name` | string \| null |  | Display name for the primary business type. |
| `types` | string\[\] |  | Normalized business type keys associated with the place. |
| `rating` | number \| null |  | Public average review rating. |
| `review_count` | integer \| null |  | Number of public ratings represented by the average. |
| `regular_opening_hours` | OpeningHours \| null |  | Normalized weekly opening schedule and special-day notices. |
| `editorial_summary` | string \| null |  | Public editorial description of the business. |
| `reviews` | Review\[\] |  | Up to the 50 most recent public reviews available with the profile. |
| `attributes` | ListingAttributes |  | Normalized service, accessibility, parking, payment, dining, and amenity attributes. |

#### OpeningHours

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `open_now` | boolean \| null |  | Whether the business is open at the profile observation time. |
| `periods` | OpeningPeriod\[\] |  | Weekly periods containing open and, when applicable, close day and time values. |
| `weekday_descriptions` | string\[\] |  | Human-readable hours for each represented weekday. |
| `special_days` | SpecialDay\[\] |  | Special-day entries with an ISO date and an exceptional\_hours flag. |

#### OpeningPeriod

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `open` | OpeningPoint |  | Opening point with day, hour, minute, and optional date. |
| `close` | OpeningPoint \| null |  | Closing point when the period has a defined close time. |

#### OpeningPoint

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `day` | integer |  | Day of week from 0 (Sunday) through 6 (Saturday). |
| `hour` | integer |  | Hour from 0 through 23. |
| `minute` | integer |  | Minute from 0 through 59. |
| `date` | date |  | Calendar date when the point represents a dated schedule. Omitted otherwise. |

#### SpecialDay

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `date` | ISO date |  | Calendar date in YYYY-MM-DD format. |
| `exceptional_hours` | boolean |  | Whether the date uses hours that differ from the regular schedule. |

#### Review

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `rating` | number \| null |  | Review rating. |
| `text` | string \| null |  | Public review text. |
| `publish_time` | datetime \| null |  | Review publication time. |
| `relative_publish_time` | string \| null |  | Human-readable relative publication time. |
| `google_maps_url` | string \| null |  | Public Google Maps URL for the review. |
| `author` | ReviewAuthor \| null |  | Public reviewer attribution when available. |

#### ReviewAuthor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `display_name` | string \| null |  | Public display name of the reviewer. |
| `uri` | string \| null |  | Public reviewer profile URL. |
| `photo_uri` | string \| null |  | Public reviewer profile image URL. |

#### ListingAttributes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `accessibility_options` | BooleanAttributeMap |  | Normalized boolean accessibility options keyed by snake\_case attribute names. |
| `payment_options` | BooleanAttributeMap |  | Normalized boolean payment options keyed by snake\_case attribute names. |
| `parking_options` | BooleanAttributeMap |  | Normalized boolean parking options keyed by snake\_case attribute names. |
| `delivery` | boolean \| null |  | Whether delivery is offered. |
| `dine_in` | boolean \| null |  | Whether dine-in service is offered. |
| `takeout` | boolean \| null |  | Whether takeout is offered. |
| `reservable` | boolean \| null |  | Whether reservations are accepted. |
| `serves_breakfast` | boolean \| null |  | Whether breakfast is served. |
| `serves_lunch` | boolean \| null |  | Whether lunch is served. |
| `serves_dinner` | boolean \| null |  | Whether dinner is served. |
| `serves_beer` | boolean \| null |  | Whether beer is served. |
| `serves_wine` | boolean \| null |  | Whether wine is served. |
| `serves_vegetarian_food` | boolean \| null |  | Whether vegetarian food is served. |

#### BooleanAttributeMap

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `{attribute_name}` | boolean |  | Boolean value keyed by a snake\_case accessibility, payment, or parking attribute name. |

#### ScanSummary

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Listing scan identifier. |
| `status` | pending \| running \| succeeded \| failed \| skipped |  | Current or terminal scan status. |
| `score` | integer \| null |  | Listing quality score from 0 to 100 when available. |
| `grade` | strong \| good \| needs\_attention \| weak \| null |  | Quality grade derived from the score. |
| `error_message` | string \| null |  | Customer-safe explanation when the scan failed. |
| `completed_at` | datetime \| null |  | Time scan processing reached a terminal status. |
| `created_at` | datetime |  | Time the scan was created. |

> **Provider field availability**
>
> Profile, review, author, and attribute objects are sparse. Provider fields are omitted when unavailable; fields explicitly documented as nullable may be returned as `null`.

> **Profile availability**
>
> `profile` is selected from the newest succeeded scan with a non-empty retained profile. It is `null` when no such profile is available. Detailed profile data is retained for 29 days; historical scan scores and checks remain available through the scan endpoints.

> **Disabled or unconfigured listings**
>
> This endpoint returns `200` when no listing data is available. In that case, `listing`, `profile`, and `latest_scan` are null.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/locations/{location_id}/listings/profile' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "listing": {
    "name": "Harbour Coffee",
    "address": "12 Market Street, Dublin 2, Ireland",
    "google_maps_url": "https://maps.google.com/?cid=123456789"
  },
  "profile": {
    "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
    "name": "Harbour Coffee",
    "formatted_address": "12 Market Street, Dublin 2, Ireland",
    "website_url": "https://harbourcoffee.example",
    "national_phone_number": "01 555 0142",
    "international_phone_number": "+353 1 555 0142",
    "google_maps_url": "https://maps.google.com/?cid=123456789",
    "business_status": "OPERATIONAL",
    "primary_type": "coffee_shop",
    "primary_type_display_name": "Coffee shop",
    "types": ["coffee_shop", "cafe", "food"],
    "rating": 4.6,
    "review_count": 187,
    "regular_opening_hours": {
      "open_now": true,
      "periods": [
        {
          "open": {"day": 1, "hour": 7, "minute": 30},
          "close": {"day": 1, "hour": 18, "minute": 0}
        }
      ],
      "weekday_descriptions": [
        "Monday: 7:30 AM – 6:00 PM",
        "Tuesday: 7:30 AM – 6:00 PM"
      ],
      "special_days": [
        {
          "date": "2026-08-03",
          "exceptional_hours": true
        }
      ]
    },
    "editorial_summary": "Independent coffee shop serving seasonal drinks.",
    "reviews": [
      {
        "rating": 5,
        "text": "Friendly team and excellent coffee.",
        "publish_time": "2026-07-25T11:14:00Z",
        "relative_publish_time": "a week ago",
        "google_maps_url": "https://maps.google.com/reviews/example",
        "author": {
          "display_name": "A. Customer",
          "uri": "https://maps.google.com/maps/contrib/456",
          "photo_uri": "https://lh3.googleusercontent.com/a/reviewer"
        }
      }
    ],
    "attributes": {
      "accessibility_options": {"wheelchair_accessible_entrance": true},
      "payment_options": {"accepts_credit_cards": true},
      "parking_options": {"street_parking": true},
      "delivery": false,
      "dine_in": true,
      "takeout": true,
      "reservable": false,
      "serves_breakfast": true,
      "serves_lunch": true,
      "serves_dinner": false,
      "serves_beer": false,
      "serves_wine": false,
      "serves_vegetarian_food": true
    }
  },
  "latest_scan": {
    "id": "ea0c4cdd-430e-444e-8446-cbe8d47edb56",
    "status": "succeeded",
    "score": 82,
    "grade": "good",
    "error_message": null,
    "completed_at": "2026-07-30T09:40:03Z",
    "created_at": "2026-07-30T09:39:58Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "not_found",
    "message": "The requested resource was not found.",
    "details": null,
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent, invalid, expired, or revoked. |
| `403` | forbidden |  | The API key lacks listings:read or cannot access the requested location. |
| `404` | not\_found |  | The project, location, or visibility scope was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### List listing scans

`GET /projects/{project_id}/locations/{location_id}/listings/scans`

Returns listing scan summaries for a location, ordered newest first.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 20 | Number of scans per page. Maximum: 50. |

#### Response envelope

`scans`:**ScanSummary\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `scans` | ScanSummary\[\] |  | Scan summaries ordered newest first. |
| `pagination` | Pagination |  | Pagination metadata. |

#### ScanSummary

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Listing scan identifier. |
| `status` | pending \| running \| succeeded \| failed \| skipped |  | Current or terminal scan status. |
| `score` | integer \| null |  | Listing quality score from 0 to 100 when available. |
| `grade` | strong \| good \| needs\_attention \| weak \| null |  | Quality grade derived from the score. |
| `error_message` | string \| null |  | Customer-safe explanation when the scan failed. |
| `completed_at` | datetime \| null |  | Time scan processing reached a terminal status. |
| `created_at` | datetime |  | Time the scan was created. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Number of records requested per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

> **Scheduled scans**
>
> This endpoint reports scan records. Scans are created by scheduled listing analysis and cannot be started from the Listings API.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/locations/{location_id}/listings/scans?page=1&per_page=20' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "scans": [
    {
      "id": "ea0c4cdd-430e-444e-8446-cbe8d47edb56",
      "status": "succeeded",
      "score": 82,
      "grade": "good",
      "error_message": null,
      "completed_at": "2026-07-30T09:40:03Z",
      "created_at": "2026-07-30T09:39:58Z"
    },
    {
      "id": "04af8634-f960-47cb-9522-98714f87ff36",
      "status": "failed",
      "score": null,
      "grade": null,
      "error_message": "Listing scan did not complete.",
      "completed_at": "2026-07-23T09:40:08Z",
      "created_at": "2026-07-23T09:39:58Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 9,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "page must be positive and per_page must be between 1 and 50.",
    "details": null,
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent, invalid, expired, or revoked. |
| `403` | forbidden |  | The API key lacks listings:read or cannot access the requested location. |
| `404` | not\_found |  | The project, location, or visibility scope was not found. |
| `422` | validation\_failed |  | page or per\_page is outside the supported range. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get listing scan

`GET /projects/{project_id}/locations/{location_id}/listings/scans/{scan_id}`

Returns one listing scan with its complete scored result and checks.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |
| `scan_id` | uuid | Required | Listing scan identifier. |

#### Response envelope

`scan`:**ListingScan**

Requested listing scan and its complete result.

#### ListingScan

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Listing scan identifier. |
| `status` | pending \| running \| succeeded \| failed \| skipped |  | Current or terminal scan status. |
| `score` | integer \| null |  | Listing quality score from 0 to 100 when available. |
| `grade` | strong \| good \| needs\_attention \| weak \| null |  | Quality grade derived from the score. |
| `error_message` | string \| null |  | Customer-safe explanation when the scan failed. |
| `completed_at` | datetime \| null |  | Time scan processing reached a terminal status. |
| `created_at` | datetime |  | Time the scan was created. |
| `result` | ScanResult \| null |  | Complete scored result on the single-scan endpoint. Null for unscored, failed, or skipped scans. |

#### ScanResult

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `score` | integer |  | Overall listing quality score from 0 to 100. |
| `grade` | strong \| good \| needs\_attention \| weak |  | Overall grade derived from the score. |
| `summary` | string |  | Short interpretation of the listing assessment. |
| `profile` | ListingProfile \| null |  | Profile observed for this scan, or null when detailed profile data is no longer retained. |
| `checks` | ListingCheck\[\] |  | Scored checks that contributed to the result. |

#### ListingCheck

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `label` | string |  | Human-readable check label. |
| `status` | passed \| warning \| failed |  | Check outcome. |
| `score` | integer |  | Points awarded by this check. |
| `severity` | low \| medium \| high |  | Importance of the observed condition. |
| `category` | string |  | Category used to group related checks. |
| `message` | string |  | Explanation of the observed condition. |
| `recommendation` | string |  | Recommended response to the check outcome. |

#### ListingProfile

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `place_id` | string |  | Place identifier represented by this profile. |
| `name` | string \| null |  | Public business name. |
| `formatted_address` | string \| null |  | Publicly formatted business address. |
| `website_url` | string \| null |  | Public website linked from the listing. |
| `national_phone_number` | string \| null |  | Phone number formatted for the listing country. |
| `international_phone_number` | string \| null |  | Phone number in international format. |
| `google_maps_url` | string \| null |  | Public Google Maps URL. |
| `business_status` | string \| null |  | Current public operating status. |
| `primary_type` | string \| null |  | Machine-readable primary business type. |
| `primary_type_display_name` | string \| null |  | Display name for the primary business type. |
| `types` | string\[\] |  | Normalized business type keys associated with the place. |
| `rating` | number \| null |  | Public average review rating. |
| `review_count` | integer \| null |  | Number of public ratings represented by the average. |
| `regular_opening_hours` | OpeningHours \| null |  | Normalized weekly opening schedule and special-day notices. |
| `editorial_summary` | string \| null |  | Public editorial description of the business. |
| `reviews` | Review\[\] |  | Up to the 50 most recent public reviews available with the profile. |
| `attributes` | ListingAttributes |  | Normalized service, accessibility, parking, payment, dining, and amenity attributes. |

#### OpeningHours

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `open_now` | boolean \| null |  | Whether the business is open at the profile observation time. |
| `periods` | OpeningPeriod\[\] |  | Weekly periods containing open and, when applicable, close day and time values. |
| `weekday_descriptions` | string\[\] |  | Human-readable hours for each represented weekday. |
| `special_days` | SpecialDay\[\] |  | Special-day entries with an ISO date and an exceptional\_hours flag. |

#### OpeningPeriod

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `open` | OpeningPoint |  | Opening point with day, hour, minute, and optional date. |
| `close` | OpeningPoint \| null |  | Closing point when the period has a defined close time. |

#### OpeningPoint

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `day` | integer |  | Day of week from 0 (Sunday) through 6 (Saturday). |
| `hour` | integer |  | Hour from 0 through 23. |
| `minute` | integer |  | Minute from 0 through 59. |
| `date` | date |  | Calendar date when the point represents a dated schedule. Omitted otherwise. |

#### SpecialDay

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `date` | ISO date |  | Calendar date in YYYY-MM-DD format. |
| `exceptional_hours` | boolean |  | Whether the date uses hours that differ from the regular schedule. |

#### Review

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `rating` | number \| null |  | Review rating. |
| `text` | string \| null |  | Public review text. |
| `publish_time` | datetime \| null |  | Review publication time. |
| `relative_publish_time` | string \| null |  | Human-readable relative publication time. |
| `google_maps_url` | string \| null |  | Public Google Maps URL for the review. |
| `author` | ReviewAuthor \| null |  | Public reviewer attribution when available. |

#### ReviewAuthor

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `display_name` | string \| null |  | Public display name of the reviewer. |
| `uri` | string \| null |  | Public reviewer profile URL. |
| `photo_uri` | string \| null |  | Public reviewer profile image URL. |

#### ListingAttributes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `accessibility_options` | BooleanAttributeMap |  | Normalized boolean accessibility options keyed by snake\_case attribute names. |
| `payment_options` | BooleanAttributeMap |  | Normalized boolean payment options keyed by snake\_case attribute names. |
| `parking_options` | BooleanAttributeMap |  | Normalized boolean parking options keyed by snake\_case attribute names. |
| `delivery` | boolean \| null |  | Whether delivery is offered. |
| `dine_in` | boolean \| null |  | Whether dine-in service is offered. |
| `takeout` | boolean \| null |  | Whether takeout is offered. |
| `reservable` | boolean \| null |  | Whether reservations are accepted. |
| `serves_breakfast` | boolean \| null |  | Whether breakfast is served. |
| `serves_lunch` | boolean \| null |  | Whether lunch is served. |
| `serves_dinner` | boolean \| null |  | Whether dinner is served. |
| `serves_beer` | boolean \| null |  | Whether beer is served. |
| `serves_wine` | boolean \| null |  | Whether wine is served. |
| `serves_vegetarian_food` | boolean \| null |  | Whether vegetarian food is served. |

#### BooleanAttributeMap

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `{attribute_name}` | boolean |  | Boolean value keyed by a snake\_case accessibility, payment, or parking attribute name. |

> **Provider field availability**
>
> Profile, review, author, and attribute objects are sparse. Provider fields are omitted when unavailable; fields explicitly documented as nullable may be returned as `null`.

> **Retained results**
>
> The score, grade, summary, and checks remain available for a completed scan. Detailed profile data is retained for 29 days, so the result's `profile` is `null` after that period while scores and checks persist.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/locations/{location_id}/listings/scans/{scan_id}' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "scan": {
    "id": "ea0c4cdd-430e-444e-8446-cbe8d47edb56",
    "status": "succeeded",
    "score": 82,
    "grade": "good",
    "result": {
      "score": 82,
      "grade": "good",
      "summary": "The profile is healthy, with a few worthwhile improvements.",
      "profile": {
        "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
        "name": "Harbour Coffee",
        "formatted_address": "12 Market Street, Dublin 2, Ireland",
        "website_url": "https://harbourcoffee.example",
        "national_phone_number": "01 555 0142",
        "international_phone_number": "+353 1 555 0142",
        "google_maps_url": "https://maps.google.com/?cid=123456789",
        "business_status": "OPERATIONAL",
        "primary_type": "coffee_shop",
        "primary_type_display_name": "Coffee shop",
        "types": ["coffee_shop", "cafe", "food"],
        "rating": 4.6,
        "review_count": 187,
        "regular_opening_hours": {
          "open_now": true,
          "periods": [],
          "weekday_descriptions": ["Monday: 7:30 AM – 6:00 PM"],
          "special_days": [
            {
              "date": "2026-08-03",
              "exceptional_hours": true
            }
          ]
        },
        "editorial_summary": "Independent coffee shop serving seasonal drinks.",
        "reviews": [],
        "attributes": {
          "delivery": false,
          "dine_in": true,
          "takeout": true
        }
      },
      "checks": [
        {
          "label": "Business name",
          "status": "passed",
          "score": 10,
          "severity": "medium",
          "category": "identity_consistency",
          "message": "The profile name matches the configured location.",
          "recommendation": "Keep the configured and public names aligned."
        },
        {
          "label": "Business hours",
          "status": "warning",
          "score": 8,
          "severity": "medium",
          "category": "profile_completeness",
          "message": "The profile exposes only partial business hours.",
          "recommendation": "Add complete regular and special hours."
        }
      ]
    },
    "error_message": null,
    "completed_at": "2026-07-30T09:40:03Z",
    "created_at": "2026-07-30T09:39:58Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "not_found",
    "message": "The requested resource was not found.",
    "details": null,
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent, invalid, expired, or revoked. |
| `403` | forbidden |  | The API key lacks listings:read or cannot access the requested location. |
| `404` | not\_found |  | The project, location, visibility scope, or listing scan was not found. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get listing findings

`GET /projects/{project_id}/locations/{location_id}/listings/findings`

Returns listing findings for a location with filtering and pagination.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `status` | open \| resolved \| ignored \| all | Optional; Default: open | Restrict findings by lifecycle status. Use all to include every status. |
| `severity` | info \| low \| medium \| high \| critical | Optional | Restrict findings to one severity. |
| `category` | string | Optional | Restrict findings to one category. |
| `q` | string | Optional | Search finding titles, descriptions, recommendations, and targets. Maximum: 200 characters. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 20 | Number of findings per page. Maximum: 50. |

#### Response envelope

`findings`:**ListingFinding\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `findings` | ListingFinding\[\] |  | Findings matching the selected filters. |
| `pagination` | Pagination |  | Pagination metadata. |

#### ListingFinding

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `category` | string |  | Category used to group related findings. |
| `severity` | info \| low \| medium \| high \| critical |  | Finding importance. |
| `status` | open \| resolved \| ignored |  | Current finding lifecycle status. |
| `target` | string \| null |  | Public target value when available. |
| `title` | string |  | Short finding title. |
| `description` | string \| null |  | Evidence-backed explanation of the finding. |
| `recommendation` | string \| null |  | Recommended response to the finding. |
| `first_seen_at` | datetime |  | Time the finding was first observed. |
| `last_seen_at` | datetime |  | Time the finding was most recently observed. |
| `resolved_at` | datetime \| null |  | Time the finding was resolved. |
| `created_at` | datetime |  | Time the finding was created. |
| `updated_at` | datetime |  | Time the finding was most recently updated. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Number of records requested per page. |
| `total` | integer |  | Total records matching the request. |
| `total_pages` | integer |  | Total available pages. |

> **Ordering**
>
> Findings are returned by most recent `last_seen_at`, with a stable identifier tie-breaker.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/projects/{project_id}/locations/{location_id}/listings/findings?status=open&severity=medium&page=1&per_page=20' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "findings": [
    {
      "category": "profile_completeness",
      "severity": "medium",
      "status": "open",
      "target": "ChIJN1t_tDeuEmsRUsoyG83frY4",
      "title": "Business hours",
      "description": "The profile exposes only partial business hours.",
      "recommendation": "Add complete regular and special hours.",
      "first_seen_at": "2026-07-16T09:40:03Z",
      "last_seen_at": "2026-07-30T09:40:03Z",
      "resolved_at": null,
      "created_at": "2026-07-16T09:40:03Z",
      "updated_at": "2026-07-30T09:40:03Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_filter",
    "message": "status is not a valid listing finding filter.",
    "details": {
      "status": ["is not supported"]
    },
    "request_id": "req_01K1JQY1RQQ7N3C5H1K6J0P8AT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_filter |  | A finding filter is invalid or q is longer than 200 characters. |
| `401` | invalid\_api\_key |  | The API key is absent, invalid, expired, or revoked. |
| `403` | forbidden |  | The API key lacks listings:read or cannot access the requested location. |
| `404` | not\_found |  | The project, location, or visibility scope was not found. |
| `422` | validation\_failed |  | page or per\_page is outside the supported range. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Google Analytics

Source: https://ceyo.ai/docs/signal/google-analytics

### Google Analytics

Connect GA4 and read referral, conversion, and landing-page analytics.

> **Package and capabilities**
>
> The assigned package must set `advanced.google_analytics_enabled` to `true`. Analytics reads require `analytics:read`; connection and configuration operations require `analytics:write`.

**Base URL:** `https://api.signal.ceyo.ai/v1`

> **Project and location scope**
>
> Use `/projects/{project_id}/analytics` for a standard project or `/projects/{project_id}/locations/{location_id}/analytics` for a location. IDs may be Signal UUIDs or partner external IDs.

**1\. Start authorization**

Call `POST /analytics/connect` at the selected scope and send the user to the returned `authorization_url`.

**2\. Complete the callback**

Google returns to Signal's public callback, which validates the API key and scope stored in state and exchanges the code.

**3\. Select a property**

List accounts and properties, then call `PUT /analytics/select_property`. To use partner-managed credentials instead, configure a token source and call `PUT /analytics/external_source`.

**4\. Sync**

Queue collection with `POST /analytics/sync`.

### Get analytics overview

`GET {scope}/analytics`

Returns the GA4 overview for a project or location.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `days` | integer | Optional; Default: 30 | Reporting window. Values are clamped to 7–90 days. |

#### Response body

`summary`:**object**`daily_series`:**object\[\]**`platforms`:**object\[\]**`conversion_summary`:**object**`action_markers`:**object\[\]**`connection`:**object**`days`:**integer**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `summary` | object |  | Traffic and conversion totals with trends. |
| `daily_series` | object\[\] |  | Daily analytics points. |
| `platforms` | object\[\] |  | AI platform breakdown. |
| `conversion_summary` | object |  | Conversion totals. |
| `action_markers` | object\[\] |  | Completed action markers in the window. |
| `connection` | object |  | Current GA4 connection summary. |
| `days` | integer |  | Applied reporting window. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Get analytics referrals

`GET {scope}/analytics/referrals`

Returns earned referral sources and their GA4 traffic.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `days` | integer | Optional; Default: 30 | Reporting window. Values are clamped to 7–90 days. |
| `page` | integer | Optional; Default: 1 | 1-based page number. Each page contains 25 records. |

#### Response body

`sources`:**object\[\]**`sources_pagination`:**Pagination**`hero`:**object**`summary`:**string | null**`days`:**integer**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `sources` | object\[\] |  | Referral sources for the requested page. |
| `sources_pagination` | Pagination |  | 25-item pagination metadata. |
| `hero` | object |  | Headline referral metrics. |
| `summary` | string \| null |  | Generated referral summary when available. |
| `days` | integer |  | Applied reporting window. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Get analytics conversions

`GET {scope}/analytics/conversions`

Returns conversion performance, optionally filtered by GA4 key event.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `days` | integer | Optional; Default: 30 | Reporting window. Values are clamped to 7–90 days. |
| `event_names` | string \| string\[\] | Optional | Comma-separated or repeated key event names. Maximum: 50. |

#### Response body

`hero`:**object**`rate_by_source`:**object\[\]**`by_platform`:**object\[\]**`summary`:**string | null**`days`:**integer**`applied_event_names`:**string\[\]**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `hero` | object |  | Headline conversion metrics. |
| `rate_by_source` | object\[\] |  | Conversion rates by source. |
| `by_platform` | object\[\] |  | Conversion metrics by channel. |
| `summary` | string \| null |  | Generated conversion summary when available. |
| `days` | integer |  | Applied reporting window. |
| `applied_event_names` | string\[\] |  | Normalized event filter. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Get analytics conversion events

`GET {scope}/analytics/conversion_events`

Lists observed GA4 key events for conversion filtering.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `days` | integer | Optional; Default: 90 | Reporting window. Values are clamped to 7–180 days. |

#### Response body

`events`:**{ name, total\_key\_events }\[\]**`days`:**integer**`limit`:**integer**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `events` | { name, total\_key\_events }\[\] |  | Up to 200 key events, ordered by count. |
| `days` | integer |  | Applied reporting window. |
| `limit` | integer |  | Maximum returned event count. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Get analytics landing pages

`GET {scope}/analytics/landing_pages`

Returns AI landing-page performance and platform distribution.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `days` | integer | Optional; Default: 30 | Reporting window. Values are clamped to 7–90 days. |
| `page` | integer | Optional; Default: 1 | 1-based page number. Each page contains 25 records. |
| `matrix_page` | integer | Optional; Default: 1 | 1-based page for the 25-item platform matrix. |

#### Response body

`pages`:**object\[\]**`pages_pagination`:**Pagination**`hero`:**object**`platform_matrix`:**object**`matrix_pagination`:**Pagination**`days`:**integer**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `pages` | object\[\] |  | Landing-page metrics. |
| `pages_pagination` | Pagination |  | Landing-page pagination. |
| `hero` | object |  | Headline landing-page metrics. |
| `platform_matrix` | object |  | Platform names, colors, and paged rows. |
| `matrix_pagination` | Pagination |  | Platform matrix pagination. |
| `days` | integer |  | Applied reporting window. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Get analytics connection

`GET {scope}/analytics/connection`

Returns the current connection for a project or location.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Response body

`project_id`:**uuid**`location_id`:**uuid**`connection`:**Connection**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Resolved project ID. |
| `location_id` | uuid |  | Resolved location ID when location-scoped. |
| `connection` | Connection |  | Connection details, or disconnected status. |

#### Connection

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Connection identifier. |
| `status` | connected \| disconnected \| revoked \| error |  | Current connection status. |
| `auth_strategy` | ceyo\_oauth \| external\_token\_source |  | Credential strategy. |
| `account_id` | string \| null |  | Selected GA4 account. |
| `property_id` | string \| null |  | Selected GA4 property. |
| `last_synced_at` | datetime \| null |  | Most recent completed sync. |
| `syncing_since` | datetime \| null |  | Start time of the active sync. |
| `error_message` | string \| null |  | Most recent connection error. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Connect Google Analytics

`POST {scope}/analytics/connect`

Creates a short-lived Google OAuth authorization URL.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Response body

`authorization_url`:**URL**

Google authorization URL.

> **Callback**
>
> Google returns to `/v1/google_analytics/oauth/callback`, which completes the exchange. Poll the connection endpoint after the browser reports success.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Exchange authorization code

`POST {scope}/analytics/exchange`

Exchanges a Google authorization code for a scoped connection.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string | Required | Authorization code returned by Google. |
| `state` | string | Required | Unmodified state from the authorization flow. |

#### Response body

`connected`:**true**`connection_id`:**uuid**`status`:**string**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `connected` | true |  | Confirms that credentials were stored. |
| `connection_id` | uuid |  | Connection identifier. |
| `status` | string |  | Current connection status. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### List analytics accounts

`GET {scope}/analytics/accounts`

Lists Google Analytics accounts available to the connection.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Response body

`accounts`:**object\[\]**

Available GA4 account summaries.

> **Capability**
>
> This connection setup endpoint requires `analytics:write`.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### List analytics properties

`GET {scope}/analytics/properties`

Lists GA4 properties for an available account.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `account_id` | string | Required | Google Analytics account identifier. |

#### Response body

`properties`:**object\[\]**

GA4 properties in the account.

> **Capability**
>
> This connection setup endpoint requires `analytics:write`.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Select analytics property

`PUT {scope}/analytics/select_property`

Selects the GA4 property and queues an initial sync.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `property_id` | string | Required | GA4 property identifier. |
| `property_name` | string | Optional | Property display name. |
| `account_id` | string | Required | Parent account identifier. |
| `account_name` | string | Optional | Parent account display name. |

#### Response body

`selected`:**true**`connection`:**Connection**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `selected` | true |  | Confirms the selection. |
| `connection` | Connection |  | Updated connection. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Set external analytics source

`PUT {scope}/analytics/external_source`

Connects a scope through a workspace token source.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `token_source_id` | uuid | Required | Enabled workspace token source. |
| `external_resource_id` | string | Required | Resource ID sent to the partner endpoint. |
| `property_id` | string | Optional | GA4 property ID. Required before syncing. |
| `property_name` | string | Optional | GA4 property display name. |
| `account_id` | string | Optional | GA4 account ID. |
| `account_name` | string | Optional | GA4 account display name. |

#### Response body

`connected`:**true**`connection`:**Connection**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `connected` | true |  | Confirms the external connection. |
| `connection` | Connection |  | Updated connection. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Sync Google Analytics

`POST {scope}/analytics/sync`

Queues GA4 collection for the selected property.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `force_full` | JSON boolean | Optional; Default: false | Requests a full refresh instead of an incremental sync; strings are rejected. |

#### 202 response body

`queued`:**true**

Returned with 202 Accepted.

> **Concurrency**
>
> A request made during an active sync returns `409 sync_in_progress`.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `409` | sync\_in\_progress |  | A Google Analytics sync is already in progress. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### Disconnect Google Analytics

`DELETE {scope}/analytics/connection`

Disconnects GA4 and removes all stored analytics for the scope.

#### Project path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |

#### Location path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | project UUID \| project external ID | Required | Project identifier. |
| `location_id` | location UUID \| location external ID | Required | Location identifier belonging to the project. |

> **Destructive operation**
>
> Disconnecting clears credentials, the selected property, and collected daily and event metrics.

#### Response body

`disconnected`:**true**

Confirms the connection was removed.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden \| analytics\_unavailable |  | The key lacks access or GA4 is not enabled by the package. |
| `404` | not\_found |  | The scope or requested analytics resource was not found. |
| `422` | validation\_failed \| analytics\_not\_connected |  | Input is invalid or the connection is not ready. |
| `429` | google\_rate\_limited |  | Google Analytics temporarily rate limited the request. |
| `502` | google\_unavailable \| external\_token\_unavailable \| external\_token\_unauthorized |  | Google or an external token provider is temporarily unavailable. |

### List Google Analytics token sources

`GET /google_analytics_token_sources`

Lists external token sources in the API key workspace.

#### Response body

`token_sources`:**TokenSource\[\]**

Workspace token sources ordered by name.

#### TokenSource

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Token source identifier. |
| `name` | string |  | Workspace-unique display name. |
| `strategy` | external\_token\_endpoint |  | Credential strategy. |
| `enabled` | boolean |  | Whether new token requests are allowed. |
| `endpoint_url` | URL |  | Partner endpoint that returns a Google access token. |
| `http_method` | GET \| POST |  | HTTP method used to request a token. |
| `service_key_header` | string |  | Header used to send the service key. |
| `has_service_key` | boolean |  | Whether an encrypted service key is stored. |
| `header_names` | string\[\] |  | Configured custom header names; values are never returned. |
| `request_resource_id_key` | string |  | Request field used for the external resource identifier. |
| `attached_scope_count` | integer |  | Number of attached project or location scopes. |

> **Capability**
>
> All token source endpoints, including this list, require workspace-scoped `analytics:write`.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden |  | The key lacks workspace analytics:write access. |
| `404` | not\_found |  | The token source was not found in this workspace. |
| `422` | validation\_failed \| limit\_reached |  | Input is invalid or the workspace limit was reached. |

### Create Google Analytics token source

`POST /google_analytics_token_sources`

Creates an external token endpoint configuration.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string | Required | Workspace-unique display name. |
| `strategy` | external\_token\_endpoint | Required | Token source strategy. |
| `endpoint_url` | URL | Required | HTTPS endpoint that returns a Google access token. |
| `http_method` | GET \| POST | Required | Method used to call the endpoint. |
| `service_key_header` | string | Required | Header name for the service key. |
| `service_key` | string | Optional | Optional secret value. It is encrypted and never returned. |
| `request_resource_id_key` | string | Required | Request field for external\_resource\_id. |
| `enabled` | boolean | Required | Enables or disables the source. |
| `headers` | object | Optional | Optional request headers with scalar values. |

#### 201 response body

`token_source`:**TokenSource**

Created token source. Returns 201.

#### TokenSource

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Token source identifier. |
| `name` | string |  | Workspace-unique display name. |
| `strategy` | external\_token\_endpoint |  | Credential strategy. |
| `enabled` | boolean |  | Whether new token requests are allowed. |
| `endpoint_url` | URL |  | Partner endpoint that returns a Google access token. |
| `http_method` | GET \| POST |  | HTTP method used to request a token. |
| `service_key_header` | string |  | Header used to send the service key. |
| `has_service_key` | boolean |  | Whether an encrypted service key is stored. |
| `header_names` | string\[\] |  | Configured custom header names; values are never returned. |
| `request_resource_id_key` | string |  | Request field used for the external resource identifier. |
| `attached_scope_count` | integer |  | Number of attached project or location scopes. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden |  | The key lacks workspace analytics:write access. |
| `404` | not\_found |  | The token source was not found in this workspace. |
| `422` | validation\_failed \| limit\_reached |  | Input is invalid or the workspace limit was reached. |

### Update Google Analytics token source

`PATCH /google_analytics_token_sources/{id}`

Updates a workspace token source.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid | Required | Token source identifier. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `name` | string | Optional | Workspace-unique display name. |
| `strategy` | external\_token\_endpoint | Optional | Token source strategy. |
| `endpoint_url` | URL | Optional | HTTPS endpoint that returns a Google access token. |
| `http_method` | GET \| POST | Optional | Method used to call the endpoint. |
| `service_key_header` | string | Optional | Header name for the service key. |
| `service_key` | string | Optional | Replacement secret. Omit or send blank to keep the existing value. |
| `request_resource_id_key` | string | Optional | Request field for external\_resource\_id. |
| `enabled` | boolean | Optional | Enables or disables the source. |
| `headers` | object | Optional | Optional request headers with scalar values. |

#### Response body

`token_source`:**TokenSource**

Updated token source.

#### TokenSource

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Token source identifier. |
| `name` | string |  | Workspace-unique display name. |
| `strategy` | external\_token\_endpoint |  | Credential strategy. |
| `enabled` | boolean |  | Whether new token requests are allowed. |
| `endpoint_url` | URL |  | Partner endpoint that returns a Google access token. |
| `http_method` | GET \| POST |  | HTTP method used to request a token. |
| `service_key_header` | string |  | Header used to send the service key. |
| `has_service_key` | boolean |  | Whether an encrypted service key is stored. |
| `header_names` | string\[\] |  | Configured custom header names; values are never returned. |
| `request_resource_id_key` | string |  | Request field used for the external resource identifier. |
| `attached_scope_count` | integer |  | Number of attached project or location scopes. |

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden |  | The key lacks workspace analytics:write access. |
| `404` | not\_found |  | The token source was not found in this workspace. |
| `422` | validation\_failed \| limit\_reached |  | Input is invalid or the workspace limit was reached. |

### Delete Google Analytics token source

`DELETE /google_analytics_token_sources/{id}`

Deletes an unused workspace token source.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid | Required | Token source identifier. |

#### Response body

`deleted`:**true**

Confirms deletion.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": [
      {
        "field": "name",
        "message": "must be present"
      }
    ],
    "request_id": "req_01K1GP6J8QQFZ4D2B6C5A9V3TS"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `401` | invalid\_api\_key |  | The API key is absent or invalid. |
| `403` | forbidden |  | The key lacks workspace analytics:write access. |
| `404` | not\_found |  | The token source was not found in this workspace. |
| `409` | token\_source\_in\_use |  | Attached analytics scopes must be disconnected first. |
| `422` | validation\_failed \| limit\_reached |  | Input is invalid or the workspace limit was reached. |

---

# Embedded identities

Source: https://ceyo.ai/docs/signal/embedded-identities

### Embedded identities

Provision partner-managed identities for embedded experiences or redirect login, then assign project or location access by your stable `external_user_id`.

> **Role behavior by access method**
>
> `viewer` and `editor` apply to both embedded and hosted access. `admin` enables project or location settings and partner-managed user administration after redirect login. Embed sessions never receive admin privileges; an admin grant is capped to `editor` in the embeddable.

> **Authentication and workspace scope**
>
> Use a workspace-scoped API key with `identities:manage`. Send it as `Authorization: Bearer ceyo_platform_...` on every request. The key selects the workspace, so paths do not include a workspace identifier. Keep the key on your server.

> **Stable external IDs**
>
> Choose an immutable ID from your own system, not an email address. Matching is case-sensitive. URL-encode it in every resource path; for example, `customer/user 42` becomes `customer%2Fuser%2042`.

### List embedded identities

`GET /embedded-identities`

Returns embedded identities in the workspace selected by the API key, including their direct project and location grants.

#### Query parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `q` | string | Optional | Case-insensitive search across external\_user\_id, email, and name. |
| `status` | active \| disabled | Optional | Return identities in one lifecycle status. |
| `project_id` | project UUID \| project external ID | Optional | Return identities with direct access to this project or one of its locations. |
| `location_id` | location UUID \| location external ID | Optional | Return identities with direct access to this location. When location\_id is an external ID, project\_id is required. |
| `role` | viewer \| editor \| admin | Optional | Return identities with at least one matching direct project or location grant. |
| `page` | integer | Optional; Default: 1 | The 1-based page number. |
| `per_page` | integer | Optional; Default: 25 | Number of identities per page. Minimum: 1; maximum: 100. |

#### Response envelope

`embedded_identities`:**EmbeddedIdentity\[\]**`pagination`:**Pagination**

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `embedded_identities` | EmbeddedIdentity\[\] |  | Matching identities ordered by creation time newest first. |
| `pagination` | Pagination |  | Pagination metadata. |

#### EmbeddedIdentity

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string |  | Stable, case-sensitive identifier supplied by your application. |
| `email` | string \| null |  | Optional contact or display email. It is profile data and is not used to sign in. |
| `name` | string \| null |  | Optional display name. |
| `avatar_url` | string \| null |  | Optional absolute HTTPS URL for a display avatar. |
| `metadata` | object |  | Partner-defined JSON object. Values are returned as supplied and must not contain credentials or secrets. |
| `status` | active \| disabled |  | Active identities can receive access and use embed sessions or redirect login. Disabling an identity revokes active access sessions and pending login links. |
| `project_access` | ProjectAccess\[\] |  | Direct project grants for the identity. |
| `location_access` | LocationAccess\[\] |  | Direct location grants for the identity. |
| `created_at` | datetime |  | Identity creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Time the identity profile or status was most recently updated, in ISO 8601 format. |

#### ProjectAccess

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Ceyo UUID of the granted project. |
| `role` | viewer \| editor \| admin |  | Role granted across the project and all of its locations. |
| `granted_at` | datetime |  | Time the direct project grant was first created, in ISO 8601 format. |

#### LocationAccess

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Ceyo UUID of the project containing the location. |
| `location_id` | uuid |  | Ceyo UUID of the granted location. |
| `role` | viewer \| editor \| admin |  | Role granted for this location. |
| `granted_at` | datetime |  | Time the direct location grant was first created, in ISO 8601 format. |

#### Pagination

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `page` | integer |  | Current 1-based page. |
| `per_page` | integer |  | Number of identities requested per page. |
| `total` | integer |  | Total identities matching the request. |
| `total_pages` | integer |  | Total available pages. |

> **Access filters**
>
> `project_id` matches direct project grants and direct grants for locations in that project. `location_id` matches direct location grants only. Combine filters to narrow the same result set; an identity must satisfy every supplied filter.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities?q=avery&status=active&project_id=e6c96c98-d777-40e0-94ec-48931f57782f&role=viewer&page=1&per_page=25' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "embedded_identities": [
    {
      "external_user_id": "customer-user-4821",
      "email": "avery.quinn@example.com",
      "name": "Avery Quinn",
      "avatar_url": "https://cdn.example.com/avatars/customer-user-4821.png",
      "metadata": {
        "account_tier": "enterprise",
        "region": "emea"
      },
      "status": "active",
      "project_access": [
        {
          "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
          "role": "editor",
          "granted_at": "2026-07-31T09:30:00Z"
        }
      ],
      "location_access": [
        {
          "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
          "location_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
          "role": "viewer",
          "granted_at": "2026-07-31T09:35:00Z"
        }
      ],
      "created_at": "2026-07-31T09:20:00Z",
      "updated_at": "2026-07-31T09:35:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "total_pages": 1
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `422` | validation\_failed |  | One or more fields are invalid, the role is unsupported, or the location does not belong to the selected project. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Upsert embedded identity

`POST /embedded-identities/{external_user_id}`

Creates an embedded identity for external\_user\_id or updates the supplied profile fields on the existing identity.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string | Required | Your stable, case-sensitive identifier for the embedded identity. URL-encode the value before placing it in the path. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `email` | string \| null |  | Optional valid email address. Send null to clear it. The address is profile data only. |
| `name` | string \| null |  | Optional display name, up to 200 characters. Send null to clear it. |
| `avatar_url` | string \| null |  | Optional absolute HTTPS avatar URL, up to 2,048 characters. Send null to clear it. |
| `metadata` | object |  | Optional partner-defined JSON object. When supplied, it replaces the complete metadata object; send {} to clear it. |
| `status` | active \| disabled |  | Optional lifecycle status. New identities default to active. |

> **Idempotent upsert**
>
> Repeating the same request produces the same identity state without creating a duplicate. New identities return `201 Created`; existing identities return `200 OK`. Omitted fields stay unchanged on an existing identity and use their documented defaults on creation. The path value cannot be changed.

> **Status changes**
>
> Setting `status` to `disabled` preserves project and location grants while preventing new embedded sessions and revoking active embedded sessions. Set it back to `active` before minting another session.

#### Response envelope

`embedded_identity`:**EmbeddedIdentity**

The requested or resulting embedded identity.

#### EmbeddedIdentity

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string |  | Stable, case-sensitive identifier supplied by your application. |
| `email` | string \| null |  | Optional contact or display email. It is profile data and is not used to sign in. |
| `name` | string \| null |  | Optional display name. |
| `avatar_url` | string \| null |  | Optional absolute HTTPS URL for a display avatar. |
| `metadata` | object |  | Partner-defined JSON object. Values are returned as supplied and must not contain credentials or secrets. |
| `status` | active \| disabled |  | Active identities can receive access and use embed sessions or redirect login. Disabling an identity revokes active access sessions and pending login links. |
| `project_access` | ProjectAccess\[\] |  | Direct project grants for the identity. |
| `location_access` | LocationAccess\[\] |  | Direct location grants for the identity. |
| `created_at` | datetime |  | Identity creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Time the identity profile or status was most recently updated, in ISO 8601 format. |

#### ProjectAccess

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Ceyo UUID of the granted project. |
| `role` | viewer \| editor \| admin |  | Role granted across the project and all of its locations. |
| `granted_at` | datetime |  | Time the direct project grant was first created, in ISO 8601 format. |

#### LocationAccess

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Ceyo UUID of the project containing the location. |
| `location_id` | uuid |  | Ceyo UUID of the granted location. |
| `role` | viewer \| editor \| admin |  | Role granted for this location. |
| `granted_at` | datetime |  | Time the direct location grant was first created, in ISO 8601 format. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-4821' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "email": "avery.quinn@example.com",
  "name": "Avery Quinn",
  "avatar_url": "https://cdn.example.com/avatars/customer-user-4821.png",
  "metadata": {
    "account_tier": "enterprise",
    "region": "emea"
  },
  "status": "active"
}'
```

```json
HTTP/1.1 201 Created

{
  "embedded_identity": {
    "external_user_id": "customer-user-4821",
    "email": "avery.quinn@example.com",
    "name": "Avery Quinn",
    "avatar_url": "https://cdn.example.com/avatars/customer-user-4821.png",
    "metadata": {
      "account_tier": "enterprise",
      "region": "emea"
    },
    "status": "active",
    "project_access": [],
    "location_access": [],
    "created_at": "2026-07-31T09:20:00Z",
    "updated_at": "2026-07-31T09:20:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `409` | conflict |  | The requested change conflicts with the current identity or resource state. |
| `422` | validation\_failed |  | One or more fields are invalid, the role is unsupported, or the location does not belong to the selected project. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Get embedded identity

`GET /embedded-identities/{external_user_id}`

Returns one embedded identity and all of its direct project and location grants.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string | Required | Your stable, case-sensitive identifier for the embedded identity. URL-encode the value before placing it in the path. |

#### Response envelope

`embedded_identity`:**EmbeddedIdentity**

The requested or resulting embedded identity.

#### EmbeddedIdentity

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string |  | Stable, case-sensitive identifier supplied by your application. |
| `email` | string \| null |  | Optional contact or display email. It is profile data and is not used to sign in. |
| `name` | string \| null |  | Optional display name. |
| `avatar_url` | string \| null |  | Optional absolute HTTPS URL for a display avatar. |
| `metadata` | object |  | Partner-defined JSON object. Values are returned as supplied and must not contain credentials or secrets. |
| `status` | active \| disabled |  | Active identities can receive access and use embed sessions or redirect login. Disabling an identity revokes active access sessions and pending login links. |
| `project_access` | ProjectAccess\[\] |  | Direct project grants for the identity. |
| `location_access` | LocationAccess\[\] |  | Direct location grants for the identity. |
| `created_at` | datetime |  | Identity creation time in ISO 8601 format. |
| `updated_at` | datetime |  | Time the identity profile or status was most recently updated, in ISO 8601 format. |

#### ProjectAccess

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Ceyo UUID of the granted project. |
| `role` | viewer \| editor \| admin |  | Role granted across the project and all of its locations. |
| `granted_at` | datetime |  | Time the direct project grant was first created, in ISO 8601 format. |

#### LocationAccess

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Ceyo UUID of the project containing the location. |
| `location_id` | uuid |  | Ceyo UUID of the granted location. |
| `role` | viewer \| editor \| admin |  | Role granted for this location. |
| `granted_at` | datetime |  | Time the direct location grant was first created, in ISO 8601 format. |

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-4821' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "embedded_identity": {
    "external_user_id": "customer-user-4821",
    "email": "avery.quinn@example.com",
    "name": "Avery Quinn",
    "avatar_url": "https://cdn.example.com/avatars/customer-user-4821.png",
    "metadata": {
      "account_tier": "enterprise",
      "region": "emea"
    },
    "status": "active",
    "project_access": [
      {
        "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
        "role": "editor",
        "granted_at": "2026-07-31T09:30:00Z"
      }
    ],
    "location_access": [
      {
        "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
        "location_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
        "role": "viewer",
        "granted_at": "2026-07-31T09:35:00Z"
      }
    ],
    "created_at": "2026-07-31T09:20:00Z",
    "updated_at": "2026-07-31T09:35:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | embedded\_identity\_not\_found |  | No embedded identity has the supplied external\_user\_id in this workspace. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Delete embedded identity

`DELETE /embedded-identities/{external_user_id}`

Permanently deletes an embedded identity and removes every direct project and location grant assigned to it.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string | Required | Your stable, case-sensitive identifier for the embedded identity. URL-encode the value before placing it in the path. |

> **Deletion effects**
>
> Deletion is synchronous and returns `204 No Content`. Project and location resources are unchanged, but every access grant for this identity is removed. Active embedded sessions are revoked, and subsequent session creation or identity lookup fails until you upsert the external\_user\_id again.

> **Re-creating the identity**
>
> Upserting the same `external_user_id` after deletion creates a new identity with no project or location access. Grant each required scope again before minting a session.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-4821' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 204 No Content
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | embedded\_identity\_not\_found |  | No embedded identity has the supplied external\_user\_id in this workspace. |
| `409` | conflict |  | The requested change conflicts with the current identity or resource state. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Access grants

Source: https://ceyo.ai/docs/signal/access-grants

### Access grants

Grant or revoke viewer, editor, or hosted admin access to projects and locations for an existing embedded identity.

> **Role behavior by access method**
>
> `viewer` and `editor` apply to both embedded and hosted access. `admin` enables project or location settings and partner-managed user administration after redirect login. Embed sessions never receive admin privileges; an admin grant is capped to `editor` in the embeddable.

> **Authentication and workspace scope**
>
> Use a workspace-scoped API key with `identities:manage`. Send it as `Authorization: Bearer ceyo_platform_...` on every request. The key selects the workspace, so paths do not include a workspace identifier. Keep the key on your server.

> **Grant identifiers**
>
> `external_user_id` identifies an existing embedded identity and is case-sensitive. Project and location path values accept either Signal UUIDs or your configured external IDs.

> **Granted-location embed scope**
>
> Create a session with `location_scope: "granted"` to expose the identity's active direct location grants in one project. This live scope preserves each location's role; adding, changing, or revoking a direct grant updates access, while a project grant never broadens it. Only Overview and Locations are available at project navigation level.

### Grant project access

`POST /embedded-identities/{external_user_id}/projects/{project_id}/access`

Creates or updates a direct project grant for an active embedded identity.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string | Required | Your stable, case-sensitive identifier for the embedded identity. URL-encode the value before placing it in the path. |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID in the workspace selected by the API key. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `role` | viewer \| editor \| admin |  | Required role. viewer is read-only; editor permits supported operational changes; admin additionally manages hosted project or location settings and users. |

#### Response envelope

`project_access`:**ProjectAccess**

The requested or resulting direct project grant.

#### ProjectAccess

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Ceyo UUID of the granted project. |
| `role` | viewer \| editor \| admin |  | Role granted across the project and all of its locations. |
| `granted_at` | datetime |  | Time the direct project grant was first created, in ISO 8601 format. |

> **Idempotent grant**
>
> A new grant returns `201 Created`. Repeating the request with the same role returns `200 OK`, preserves `granted_at`, and makes no duplicate. Sending a different role updates the existing direct grant and also preserves `granted_at`.

> **Project scope**
>
> Project access applies to the project and all of its locations. Direct location grants may coexist with a project grant, but they do not reduce the effective project role.

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-4821/projects/e6c96c98-d777-40e0-94ec-48931f57782f/access' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "role": "editor"
}'
```

```json
HTTP/1.1 201 Created

{
  "project_access": {
    "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "role": "editor",
    "granted_at": "2026-07-31T09:30:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | resource\_not\_found |  | The embedded identity, project, or location was not found in this workspace. |
| `409` | conflict |  | The requested change conflicts with the current identity or resource state. |
| `422` | validation\_failed |  | One or more fields are invalid, the role is unsupported, or the location does not belong to the selected project. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Revoke project access

`DELETE /embedded-identities/{external_user_id}/projects/{project_id}/access`

Removes the direct project grant for an embedded identity without deleting the identity or its direct location grants.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string | Required | Your stable, case-sensitive identifier for the embedded identity. URL-encode the value before placing it in the path. |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID in the workspace selected by the API key. |

> **Idempotent revoke**
>
> The endpoint returns `204 No Content` whether the direct project grant existed or was already absent. The identity and project remain unchanged.

> **Remaining location access**
>
> Revoking project access does not remove direct location grants in the project. The identity can continue to reach those locations with their location roles. Revoke them separately when access to the entire project must end.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-4821/projects/e6c96c98-d777-40e0-94ec-48931f57782f/access' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 204 No Content
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | resource\_not\_found |  | The embedded identity, project, or location was not found in this workspace. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Grant location access

`POST /embedded-identities/{external_user_id}/projects/{project_id}/locations/{location_id}/access`

Creates or updates a direct grant to one location for an active embedded identity.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string | Required | Your stable, case-sensitive identifier for the embedded identity. URL-encode the value before placing it in the path. |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID in the workspace selected by the API key. |
| `location_id` | location UUID \| location external ID | Required | Ceyo location UUID or configured partner external ID belonging to the selected project. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `role` | viewer \| editor \| admin |  | Required role. viewer is read-only; editor permits supported operational changes; admin additionally manages hosted project or location settings and users. |

#### Response envelope

`location_access`:**LocationAccess**

The requested or resulting direct location grant.

#### LocationAccess

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `project_id` | uuid |  | Ceyo UUID of the project containing the location. |
| `location_id` | uuid |  | Ceyo UUID of the granted location. |
| `role` | viewer \| editor \| admin |  | Role granted for this location. |
| `granted_at` | datetime |  | Time the direct location grant was first created, in ISO 8601 format. |

> **Idempotent grant**
>
> A new grant returns `201 Created`. Repeating the request with the same role returns `200 OK`, preserves `granted_at`, and makes no duplicate. Sending a different role updates the existing direct grant and also preserves `granted_at`.

> **Location scope**
>
> The grant applies only to the selected location. The location must belong to the project in the path. A direct project grant may provide broader or stronger effective access without changing this location grant.

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-4821/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/a1308d14-149c-4dd7-a4c5-295ac9090f58/access' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "role": "viewer"
}'
```

```json
HTTP/1.1 201 Created

{
  "location_access": {
    "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "location_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
    "role": "viewer",
    "granted_at": "2026-07-31T09:35:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | resource\_not\_found |  | The embedded identity, project, or location was not found in this workspace. |
| `409` | conflict |  | The requested change conflicts with the current identity or resource state. |
| `422` | validation\_failed |  | One or more fields are invalid, the role is unsupported, or the location does not belong to the selected project. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Revoke location access

`DELETE /embedded-identities/{external_user_id}/projects/{project_id}/locations/{location_id}/access`

Removes the direct location grant for an embedded identity without changing project access or grants to other locations.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string | Required | Your stable, case-sensitive identifier for the embedded identity. URL-encode the value before placing it in the path. |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID in the workspace selected by the API key. |
| `location_id` | location UUID \| location external ID | Required | Ceyo location UUID or configured partner external ID belonging to the selected project. |

> **Idempotent revoke**
>
> The endpoint returns `204 No Content` whether the direct location grant existed or was already absent. The identity, project, and location remain unchanged.

> **Effective access**
>
> Revoking a location grant does not remove a project grant. If the identity still has project access, it can continue to reach this location through that broader scope.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/embedded-identities/customer-user-4821/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/a1308d14-149c-4dd7-a4c5-295ac9090f58/access' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
HTTP/1.1 204 No Content
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | A path value, query parameter, or JSON body is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot perform this operation. |
| `404` | resource\_not\_found |  | The embedded identity, project, or location was not found in this workspace. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Embed sessions

Source: https://ceyo.ai/docs/signal/embed-sessions

### Embed sessions

Issue short-lived browser credentials for an embedded user, then rotate them without changing the user’s effective access.

> **Provisioning options**
>
> By default, the embedded identity and its project or location grants must already exist. For a first-login flow, send `provision` to create or update the identity and ensure the scope grant or a batch of location grants atomically before issuing the session.

> **Server-to-server authentication**
>
> Call both endpoints from your backend with `SIGNAL_API_KEY` as the bearer credential. The key requires the `embed_sessions:create` capability. Inline provisioning additionally requires a workspace-scoped key with `identities:manage`. Send only the returned embed session token to the browser. Never expose the API key in JavaScript, HTML, logs, URLs, or mobile application code.

### Create embed session

`POST /embed/sessions`

Creates a short-lived embed session using existing access or optional atomic first-login provisioning.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string | Required | Stable, case-sensitive identifier for the signed-in user in your system. It must already exist unless provision is supplied. |
| `project_id` | project UUID \| project external ID | Required | Ceyo project UUID or configured partner external ID. The project must belong to the workspace selected by the API key. |
| `location_id` | location UUID \| location external ID | Optional | Restricts the session to one location in the project. The identity must have access unless provision ensures the grant. Omit for project scope. |
| `location_scope` | granted | Optional | Exposes only the identity’s active direct location grants in this project. Mutually exclusive with location\_id. |
| `ttl_seconds` | integer | Optional; Default: 3600 | Session lifetime in seconds. Minimum: 60. Maximum: 86400. Prefer the shortest lifetime suitable for your integration. |
| `expires_in` | integer | Optional | Compatibility alias for ttl\_seconds. Do not send both unless their values are identical. |
| `provision` | ProvisionInput | Optional | Optionally creates or updates the identity and ensures one project/location grant, or a granted-scope batch of location grants, before issuing the session. |

#### ProvisionInput

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `role` | viewer \| editor |  | Required for project or single-location provisioning. Do not combine with location\_grants. |
| `identity` | ProvisionIdentityInput |  | Optional identity fields to set. Omitted fields remain unchanged on an existing identity. |
| `location_grants` | ProvisionLocationGrantInput\[\] |  | One to 100 direct location grants. Allowed only with location\_scope granted and mutually exclusive with role. |

#### ProvisionIdentityInput

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `email` | string \| null |  | Optional normalized contact email. |
| `name` | string \| null |  | Optional display name. |
| `avatar_url` | HTTPS URL \| null |  | Optional absolute HTTPS avatar URL. |
| `metadata` | object |  | Optional partner metadata object, up to 16 KB. |

#### ProvisionLocationGrantInput

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `location_id` | location UUID \| location external ID |  | Required location in the selected project. |
| `role` | viewer \| editor |  | Required role for this direct location grant. Roles may differ between locations. |

> **Access validation**
>
> With neither scope selector, the identity must have active project access. With `location_id`, the identity must have access to that location. With `location_scope: "granted"`, only active direct location grants in the project are visible; a project grant does not expand the result. Grants are checked live, and each location keeps its own role. Sessions support only `viewer` and `editor`; hosted `admin` grants are capped to `editor`.

#### EmbedSessionResponse

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `session.token` | string |  | Opaque, short-lived credential for the embedded browser component. Treat it as a secret. |
| `session.token_type` | Bearer |  | Authentication scheme used by the embed session token. |
| `session.expires_at` | datetime |  | Token expiration time in ISO 8601 UTC format. |
| `session.expires_in` | integer |  | Session lifetime in seconds. |
| `session.project_id` | uuid |  | Compatibility alias for session.scope.project\_id. |
| `session.location_id` | uuid \| null |  | Compatibility alias for session.scope.location\_id. |
| `session.location_scope` | full \| location \| granted |  | Session scope mode. granted resolves the identity’s active direct location grants live. |
| `session.external_user_id` | string |  | Compatibility alias for session.identity.external\_user\_id. |
| `session.identity.external_user_id` | string |  | Partner identifier of the embedded user represented by the session. |
| `session.scope.type` | granted |  | Present for granted-location sessions. |
| `session.scope.project_id` | uuid |  | Canonical Ceyo UUID of the project available to the session. |
| `session.scope.location_id` | uuid \| null |  | Canonical Ceyo UUID of the restricted location, or null for project scope. |
| `session.scope.role` | viewer \| editor \| null |  | Effective role for project or single-location scope. Granted sessions use each location’s own role. |

```
type EmbedSessionResponse = {
  session: {
    token: string;
    token_type: "Bearer";
    expires_at: string;
    expires_in: number;
    project_id: string;
    location_id: string | null;
    location_scope: "full" | "location" | "granted";
    external_user_id: string;
    identity: {
      external_user_id: string;
    };
    scope: {
      type?: "granted";
      project_id: string;
      location_id: string | null;
      role: "viewer" | "editor" | null;
    };
  };
};
```

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: embed-create-user-42-20260731T123300Z' \
  --data '{
  "external_user_id": "user-42",
  "project_id": "partner-project-acme",
  "ttl_seconds": 3600
}'
```

```json
HTTP/1.1 201 Created

{
  "session": {
    "token": "ceyo_embed_eyJhbGciOi...",
    "token_type": "Bearer",
    "expires_at": "2026-07-31T13:33:00Z",
    "expires_in": 3600,
    "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "location_id": null,
    "location_scope": "full",
    "external_user_id": "user-42",
    "identity": {
      "external_user_id": "user-42"
    },
    "scope": {
      "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
      "location_id": null,
      "role": "viewer"
    }
  }
}
```

#### Location-scoped request

```
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: embed-create-franchisee-804-20260731T125000Z' \
  --data '{
  "external_user_id": "franchisee-804",
  "project_id": "partner-project-acme",
  "location_id": "partner-location-amsterdam",
  "ttl_seconds": 1800
}'
```

#### Pre-granted location portfolio

```
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: embed-create-region-42-20260805T120000Z' \
  --data '{
  "external_user_id": "regional-manager-42",
  "project_id": "partner-project-acme",
  "location_scope": "granted",
  "ttl_seconds": 3600
}'
```

This session's scope type is `granted`. It exposes Overview and Locations only; project-wide prompts, citations, competitors, settings, and users are unavailable. Single-location auto-open behavior still applies.

#### First-login provisioning

```
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: embed-provision-new-user-42' \
  --data '{
  "external_user_id": "new-user-42",
  "project_id": "partner-project-acme",
  "ttl_seconds": 3600,
  "provision": {
    "role": "viewer",
    "identity": {
      "email": "new-user-42@example.com",
      "name": "New User"
    }
  }
}'
```

#### Atomic location batch provisioning

```
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embed/sessions' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: embed-provision-region-42' \
  --data '{
  "external_user_id": "regional-manager-42",
  "project_id": "partner-project-acme",
  "location_scope": "granted",
  "provision": {
    "identity": {
      "email": "manager-42@example.com",
      "name": "Regional Manager"
    },
    "location_grants": [
      {
        "location_id": "store-amsterdam",
        "role": "editor"
      },
      {
        "location_id": "store-utrecht",
        "role": "viewer"
      }
    ]
  }
}'
```

Supply one to 100 grants. The batch atomically ensures the listed direct grants; it does not add unlisted locations or make a project grant part of this scope.

> **Idempotency and retries**
>
> A unique `Idempotency-Key` header is required for each intended session. Repeating the same request with the same key returns the original response and does not issue another token. Reusing that key with different body fields returns `409`. Use a new key when you intentionally need a separate session.

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request could not be processed.",
    "details": {
      "field": "ttl_seconds"
    },
    "request_id": "req_01JEXAMPLE7J8Q2Y4K6M9"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body is malformed, required fields are absent, or a request object or TTL is invalid. |
| `401` | invalid\_api\_key |  | The platform API key is absent, invalid, expired, or revoked. |
| `403` | forbidden \| access\_denied |  | The API key lacks the required capability, or the identity is disabled or has no active access grant. |
| `404` | identity\_not\_found \| project\_not\_found \| location\_not\_found |  | The requested identity, project, or location was not found. |
| `409` | idempotency\_conflict |  | The idempotency key was reused with a different request. |
| `422` | validation\_failed |  | The inline identity, grant role, or access grant failed validation. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made; follow the Retry-After header. |

### Refresh embed session

`POST /embed/sessions/refresh`

Atomically exchanges a current embed session token for a replacement with the same identity and scope.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `session_token` | string | Required | Current embed session token. It may be active or expired by no more than five minutes. The token is exchanged and cannot be used again after a successful refresh. |
| `ttl_seconds` | integer | Optional; Default: 3600 | Lifetime of the replacement token in seconds. Minimum: 60. Maximum: 86400. |
| `expires_in` | integer | Optional | Compatibility alias for ttl\_seconds. Do not send both unless their values are identical. |

> **Scope is preserved**
>
> Refresh accepts no identity, project, location, location scope, or role fields. The replacement inherits the current scope after the API revalidates access. A granted replacement continues to resolve direct location grants live. If required access was revoked, refresh fails; create a new session only after reconciling access.

> **Rotation and replay protection**
>
> A unique `Idempotency-Key` header is required. A successful exchange invalidates the submitted session token. Subsequent exchanges of that token fail unless they repeat the completed request with the same `Idempotency-Key`, in which case the original replacement response is returned. An active token may be refreshed at any time and an expired token has a five-minute refresh window.

#### EmbedSessionResponse

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `session.token` | string |  | Opaque, short-lived credential for the embedded browser component. Treat it as a secret. |
| `session.token_type` | Bearer |  | Authentication scheme used by the embed session token. |
| `session.expires_at` | datetime |  | Token expiration time in ISO 8601 UTC format. |
| `session.expires_in` | integer |  | Session lifetime in seconds. |
| `session.project_id` | uuid |  | Compatibility alias for session.scope.project\_id. |
| `session.location_id` | uuid \| null |  | Compatibility alias for session.scope.location\_id. |
| `session.location_scope` | full \| location \| granted |  | Session scope mode. granted resolves the identity’s active direct location grants live. |
| `session.external_user_id` | string |  | Compatibility alias for session.identity.external\_user\_id. |
| `session.identity.external_user_id` | string |  | Partner identifier of the embedded user represented by the session. |
| `session.scope.type` | granted |  | Present for granted-location sessions. |
| `session.scope.project_id` | uuid |  | Canonical Ceyo UUID of the project available to the session. |
| `session.scope.location_id` | uuid \| null |  | Canonical Ceyo UUID of the restricted location, or null for project scope. |
| `session.scope.role` | viewer \| editor \| null |  | Effective role for project or single-location scope. Granted sessions use each location’s own role. |

```
type EmbedSessionResponse = {
  session: {
    token: string;
    token_type: "Bearer";
    expires_at: string;
    expires_in: number;
    project_id: string;
    location_id: string | null;
    location_scope: "full" | "location" | "granted";
    external_user_id: string;
    identity: {
      external_user_id: string;
    };
    scope: {
      type?: "granted";
      project_id: string;
      location_id: string | null;
      role: "viewer" | "editor" | null;
    };
  };
};
```

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/embed/sessions/refresh' \
  --header "Authorization: Bearer ${SIGNAL_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: embed-refresh-session-7f42a-1' \
  --data '{
  "session_token": "ceyo_embed_eyJhbGciOi...current",
  "ttl_seconds": 3600
}'
```

```json
HTTP/1.1 200 OK

{
  "session": {
    "token": "ceyo_embed_eyJhbGciOi...replacement",
    "token_type": "Bearer",
    "expires_at": "2026-07-31T14:20:00Z",
    "expires_in": 3600,
    "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
    "location_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
    "location_scope": "location",
    "external_user_id": "franchisee-804",
    "identity": {
      "external_user_id": "franchisee-804"
    },
    "scope": {
      "project_id": "e6c96c98-d777-40e0-94ec-48931f57782f",
      "location_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
      "role": "editor"
    }
  }
}
```

#### Backend refresh endpoint

```
// Authenticate your application user before this handler.
// Your platform API key remains on the server.
app.post('/api/signal-session/refresh', async (req, res) => {
  const response = await fetch(
    'https://api.signal.ceyo.ai/v1/embed/sessions/refresh',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.SIGNAL_API_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': req.get('Idempotency-Key') || crypto.randomUUID(),
      },
      body: JSON.stringify({
        session_token: req.body.session_token,
        ttl_seconds: 3600,
      }),
    },
  );

  const body = await response.json();
  if (!response.ok) return res.status(response.status).json(body);
  res.json({
    token: body.session.token,
    expires_at: body.session.expires_at,
  });
});
```

#### Browser token lifecycle

Keep the active token and its `expiresAt` value in memory. Before expiration, send the current token to your authenticated backend, atomically replace the in-memory record, and call `embed.updateToken()`. The expiry callback should exchange your in-memory current token and return the replacement token. Returning `null` leaves the application locked.

```
let currentSession = {
  token: initialToken,
  expiresAt: initialExpiresAt,
};

async function refreshSession(
  currentToken: string,
  updateHandle: boolean,
): Promise<string | null> {
  const response = await fetch("/api/signal-session/refresh", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({ session_token: currentToken }),
  });
  if (!response.ok) return null;

  const session = await response.json();
  currentSession = {
    token: session.token,
    expiresAt: session.expires_at,
  };
  if (updateHandle) embed.updateToken(currentSession.token);
  return currentSession.token;
}

const embed = Ceyo.mount("#signal-visibility", {
  sessionToken: currentSession.token,
  onTokenExpired() {
    return refreshSession(currentSession.token, false);
  },
});

// Schedule this before currentSession.expiresAt.
await refreshSession(currentSession.token, true);
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request could not be processed.",
    "details": {
      "field": "ttl_seconds"
    },
    "request_id": "req_01JEXAMPLE7J8Q2Y4K6M9"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body is malformed, required fields are absent, or a request object or TTL is invalid. |
| `401` | invalid\_api\_key \| invalid\_session\_token |  | The API key or session token is invalid, revoked, expired, or outside the refresh window. |
| `403` | session\_workspace\_mismatch \| access\_denied |  | The session belongs to another workspace or its identity no longer has access. |
| `409` | idempotency\_conflict |  | The idempotency key was reused with a different request. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made; follow the Retry-After header. |

### Security checklist

**Authenticate your own user**

Mint or refresh a session only after validating the user’s session in your application. Do not accept an arbitrary `external_user_id` from an unauthenticated browser.

**Keep the platform key server-side**

The platform key can operate across its workspace. The browser receives only the short-lived, narrowly scoped embed token.

**Protect session tokens**

Send tokens only over HTTPS, keep them out of URLs and analytics, and avoid persistent browser storage. Hold them in memory when possible.

**Use short lifetimes**

The default is 3600 seconds and the maximum is 86400 seconds. Refresh shortly before expiration or during the five-minute post-expiry exchange window.

**Retry deliberately**

Use one idempotency key per intended create or refresh operation. Never retry a refresh with a new key after its outcome is unknown; first retry with the original key.

---

# Redirect login links

Source: https://ceyo.ai/docs/signal/redirect-login-links

### Redirect login links

Send an embedded identity into hosted Signal with a short-lived, one-time URL.

> **Hosted roles**
>
> The identity’s grant controls authorization after sign-in. `viewer` is read-only, `editor` manages supported operational features, and `admin` additionally manages settings and partner-managed users within the granted project or location. A login link authenticates the identity and never elevates its role.

> **Server-side only**
>
> Create, inspect, and revoke login links from your backend with a Signal API key. Never expose an API key in browser code, logs, or a public URL. Treat each returned login URL as a temporary credential.

> **Managed identity account**
>
> The supplied `external_user_id` resolves an embedded identity in the API key’s workspace. On first use, Signal provisions a dedicated partner-managed hosted profile for that workspace and external identifier. It has no password and never matches, links, or signs into an existing Ceyo account by email.

> **Access and destination checks**
>
> The API key needs `login_links:manage`, and the requested project or location package must enable Customer access through `pricing.frontend_delivery_enabled`. This single entitlement enables both hosted Signal UI and redirect login links. Signal verifies the package entitlement and embedded access both when the link is created and when it is used. Return URLs must use an exact HTTPS origin in the API key’s `allowed_origins`; redirect paths stay within hosted Signal.

### Create redirect login link

`POST /login-links`

Creates a one-time hosted Signal login URL for an embedded identity and a project or location it can access.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string |  | Required external identifier of the embedded identity that will sign in through its dedicated partner-managed account. Maximum: 200 characters. |
| `project_id` | project UUID \| project external ID |  | Required project identifier. The embedded identity must already have access to this project. |
| `location_id` | location UUID \| location external ID \| null |  | Optional location within the project. When supplied, the embedded identity must have access to this location. |
| `redirect_path` | string \| null |  | Optional relative path to open in hosted Signal after sign-in. Must begin with one slash and cannot contain a scheme, host, backslash, or protocol-relative URL. Defaults to the selected project or location home. |
| `return_url` | https URL \| null |  | Optional URL shown as the safe return destination from Signal. Its origin must be registered for the API key; fragments and embedded credentials are rejected. |
| `expires_in` | integer |  | Optional lifetime in seconds. Defaults to 900 (15 minutes); minimum: 60; maximum: 3,600 (1 hour). |

> **Safe navigation**
>
> `redirect_path` controls the first page opened inside Signal. `return_url` controls where the user may return afterward. Signal does not accept arbitrary origins, JavaScript URLs, protocol-relative URLs, or URLs containing credentials. After sign-in, hosted Signal displays a Return to partner action only when a validated `return_url` was supplied.

#### Response envelope

`login_link`:**LoginLink**

The requested or resulting login link.

#### LoginLink

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Stable login link identifier used for lifecycle requests. |
| `url` | string \| null |  | Hosted Signal sign-in URL. Returned only when the link is created; lifecycle responses return null. |
| `status` | pending \| used \| expired \| revoked |  | Current one-time login link status. |
| `expires_at` | datetime |  | Time the pending link expires, in ISO 8601 format. |
| `used_at` | datetime \| null |  | Time the link was successfully used, or null if it was not used. |
| `created_at` | datetime |  | Time the link was created, in ISO 8601 format. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/login-links' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "external_user_id": "customer-user-4821",
  "project_id": "partner-project-acme",
  "location_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
  "redirect_path": "/workspaces/{workspace_id}/projects/e6c96c98-d777-40e0-94ec-48931f57782f/visibility",
  "return_url": "https://portal.partner.example/customers/4821",
  "expires_in": 900
}'
```

```json
HTTP/1.1 201 Created

{
  "login_link": {
    "id": "7a73ed11-d5dc-4b6a-8681-d9e571d2a991",
    "url": "https://signal.ceyo.ai/login-links/7a73ed11-d5dc-4b6a-8681-d9e571d2a991?token=ceyo_login_...",
    "status": "pending",
    "expires_at": "2026-07-31T10:15:00Z",
    "used_at": null,
    "created_at": "2026-07-31T10:00:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body was not provided, is malformed, or contains an unknown field. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden \| customer\_access\_not\_enabled |  | The API key lacks login\_links:manage, the package does not enable Customer access, or the embedded identity lacks access to the requested scope. |
| `404` | not\_found |  | The embedded identity, project, or location was not found for this API key. |
| `422` | validation\_failed |  | An identifier, expiry, redirect path, or return URL does not meet the documented constraints. |
| `429` | rate\_limit\_exceeded |  | Too many login links were created or too many requests were made. |

### Get login link

`GET /login-links/{login_link_id}`

Returns the current state of a login link without changing it.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `login_link_id` | uuid | Required | Login link identifier returned by the create endpoint. |

#### Response envelope

`login_link`:**LoginLink**

The requested or resulting login link.

#### LoginLink

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Stable login link identifier used for lifecycle requests. |
| `url` | string \| null |  | Hosted Signal sign-in URL. Returned only when the link is created; lifecycle responses return null. |
| `status` | pending \| used \| expired \| revoked |  | Current one-time login link status. |
| `expires_at` | datetime |  | Time the pending link expires, in ISO 8601 format. |
| `used_at` | datetime \| null |  | Time the link was successfully used, or null if it was not used. |
| `created_at` | datetime |  | Time the link was created, in ISO 8601 format. |

> **Read-only status**
>
> This request returns `pending`, `used`, `expired`, or `revoked` and never changes the link. Used, expired, and revoked are terminal states. A terminal link cannot be used, revoked, or reactivated.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/login-links/7a73ed11-d5dc-4b6a-8681-d9e571d2a991' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "login_link": {
    "id": "7a73ed11-d5dc-4b6a-8681-d9e571d2a991",
    "url": null,
    "status": "pending",
    "expires_at": "2026-07-31T10:15:00Z",
    "used_at": null,
    "created_at": "2026-07-31T10:00:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The login\_link\_id is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage login links. |
| `404` | not\_found |  | The login link was not found for this API key. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Revoke login link

`DELETE /login-links/{login_link_id}`

Revokes a pending login link and returns its terminal revoked state.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `login_link_id` | uuid | Required | Login link identifier returned by the create endpoint. |

#### Response envelope

`login_link`:**LoginLink**

The requested or resulting login link.

#### LoginLink

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Stable login link identifier used for lifecycle requests. |
| `url` | string \| null |  | Hosted Signal sign-in URL. Returned only when the link is created; lifecycle responses return null. |
| `status` | pending \| used \| expired \| revoked |  | Current one-time login link status. |
| `expires_at` | datetime |  | Time the pending link expires, in ISO 8601 format. |
| `used_at` | datetime \| null |  | Time the link was successfully used, or null if it was not used. |
| `created_at` | datetime |  | Time the link was created, in ISO 8601 format. |

> **Terminal-state behavior**
>
> A pending link changes to `revoked` and returns `200 OK`. Used, expired, and revoked links remain unchanged and return `409 login_link_not_pending`. Revoked links cannot be used or reactivated.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/login-links/7a73ed11-d5dc-4b6a-8681-d9e571d2a991' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "login_link": {
    "id": "7a73ed11-d5dc-4b6a-8681-d9e571d2a991",
    "url": null,
    "status": "revoked",
    "expires_at": "2026-07-31T10:15:00Z",
    "used_at": null,
    "created_at": "2026-07-31T10:00:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The login\_link\_id is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage login links. |
| `404` | not\_found |  | The login link was not found for this API key. |
| `409` | login\_link\_not\_pending |  | The link is used, expired, or revoked and remains in that terminal state. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

---

# Getting started

Source: https://ceyo.ai/docs/signal/embedding-overview

### Embedding overview

Signal provides one complete embedded application, mounted once inside your product. Its internal navigation includes the permitted overview, locations, prompts, citations, competitors, and settings screens; those screens are not mounted as separate components. A short-lived session determines the project, end user, and access available in the application. Standard projects open at project-level visibility pages. Locations-only projects retain the location portfolio and map experience.

**Session endpoint:** `https://api.signal.ceyo.ai/v1/embed/sessions`

### How embedding works

**Partner backend**

Authenticates the signed-in user and uses the Signal API key to mint a short-lived session for that user and project.

**Browser**

Requests only the session token from your backend, loads the loader script, and calls `Ceyo.mount()` once.

**Embedded application**

Runs in a sandboxed iframe and renders the data permitted by the session. Theme, settings, and lifecycle callbacks are passed at mount time.

The Signal API key stays on your backend. The browser receives only a short-lived, user-scoped session token.

Embed sessions expose only viewer or editor access. Hosted admin privileges—including partner-managed user administration—are available only through redirect login and are never passed into the iframe.

### Provisioning and scope

Every session represents one external user and a project, one location, or that identity's directly granted locations in a project. Create projects and locations, choose their packages, and start onboarding through the public API before loading the embeddable. The iframe does not create or delete either resource. You can provision the identity and grant explicitly, then mint sessions as needed:

```
# 1. Upsert the partner-managed identity.
POST /v1/embedded-identities/user-42
{
  "email": "user-42@example.com",
  "name": "User 42"
}

# 2. Grant access for the scope this session will use.
POST /v1/embedded-identities/user-42/projects/partner-project/access
{
  "role": "editor"
}

# 3. Mint the short-lived browser session.
POST /v1/embed/sessions
Idempotency-Key: <unique-value>
{
  "external_user_id": "user-42",
  "project_id": "partner-project",
  "ttl_seconds": 3600
}
```

For first login, the session endpoint can perform those first two steps atomically through `provision`, as shown in the quickstart. Later calls without `provision` reuse the existing identity and grant.

**Project manager**

Grant project access and omit `location_id`. The session opens project-level prompts, citations, and competitors for a standard project, or its permitted locations for a locations-only project.

**Location user**

Grant direct location access and include `location_id` when minting. The session is restricted to that location.

**Granted-location portfolio**

Grant locations directly, then send `location_scope: "granted"` and omit `location_id`. The `granted_locations` browser scope exposes only Overview and Locations. It resolves active direct grants live, preserves each location's role, and ignores any project grant. Project-wide routes are unavailable.

#### Pre-granted identity

```
POST /v1/embed/sessions
{
  "external_user_id": "regional-manager-42",
  "project_id": "partner-project",
  "location_scope": "granted",
  "ttl_seconds": 3600
}
```

#### Atomic first-login location grants

```
POST /v1/embed/sessions
{
  "external_user_id": "regional-manager-42",
  "project_id": "partner-project",
  "location_scope": "granted",
  "provision": {
    "identity": {
      "email": "manager@example.com",
      "name": "Regional Manager"
    },
    "location_grants": [
      { "location_id": "store-amsterdam", "role": "editor" },
      { "location_id": "store-utrecht", "role": "viewer" }
    ]
  }
}
```

Batch provisioning accepts one to 100 direct location grants. Revoking a grant removes that location from the live portfolio; when one location remains, configured single-location auto-open still applies.

### Embedding quickstart

#### 1\. Mint a session on your backend

Send the signed-in user's stable external ID and the target project to the embed session endpoint. The example uses inline provisioning so the same atomic request can create the identity, ensure its grant, and issue the session.

```
// Runs on your server. Never send the API key to the browser.
app.post("/api/signal-session", async (req, res) => {
  const response = await fetch("https://api.signal.ceyo.ai/v1/embed/sessions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SIGNAL_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({
      project_id: process.env.SIGNAL_PROJECT_ID,
      external_user_id: req.user.id,
      ttl_seconds: 3600,
      provision: {
        role: req.user.canEditSignal ? "editor" : "viewer",
        identity: {
          email: req.user.email,
          name: req.user.name,
        },
      },
    }),
  });

  if (!response.ok) {
    return res.status(response.status).json(await response.json());
  }

  const { session } = await response.json();
  res.json({ token: session.token, expires_at: session.expires_at });
});

app.post("/api/signal-session/refresh", async (req, res) => {
  const response = await fetch("https://api.signal.ceyo.ai/v1/embed/sessions/refresh", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SIGNAL_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": req.get("Idempotency-Key") || crypto.randomUUID(),
    },
    body: JSON.stringify({
      session_token: req.body.session_token,
      ttl_seconds: 3600,
    }),
  });

  const body = await response.json();
  if (!response.ok) return res.status(response.status).json(body);
  res.json({ token: body.session.token, expires_at: body.session.expires_at });
});
```

#### 2\. Load and mount in the browser

```
<div id="signal-visibility"></div>
<script src="https://cdn.signal.ceyo.ai/embed/v1.js"></script>
<script>
  async function createSession() {
    const response = await fetch("/api/signal-session", { method: "POST" });
    if (!response.ok) throw new Error("Unable to create embed session");
    const session = await response.json();
    return { token: session.token, expiresAt: session.expires_at };
  }

  (async () => {
    let currentSession = await createSession(); // Keep tokens in memory.
    let embed;
    let refreshInFlight;

    async function refreshSession(currentToken, updateHandle) {
      if (currentSession.token !== currentToken) return currentSession.token;
      if (refreshInFlight) return refreshInFlight;

      refreshInFlight = (async () => {
        const response = await fetch("/api/signal-session/refresh", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Idempotency-Key": crypto.randomUUID(),
          },
          body: JSON.stringify({ session_token: currentToken }),
        });
        const session = response.ok
          ? await response.json()
          : await createSession();
        if (currentSession.token !== currentToken) return currentSession.token;
        currentSession = {
          token: session.token,
          expiresAt: session.expiresAt || session.expires_at,
        };
        if (updateHandle) embed.updateToken(currentSession.token);
        return currentSession.token;
      })().finally(() => {
        refreshInFlight = null;
      });

      return refreshInFlight;
    }

    embed = Ceyo.mount("#signal-visibility", {
      sessionToken: currentSession.token,
      height: "780px",
      onTokenExpired() {
        return refreshSession(currentSession.token, false);
      },
    });
    window.signalEmbed = embed;
  })();
</script>
```

---

# Integration

Source: https://ceyo.ai/docs/signal/embedding-integration

### Install the application

Include the loader once on the page that hosts the embedded application.

```
<script src="https://cdn.signal.ceyo.ai/embed/v1.js"></script>
```

The loader registers `Ceyo.mount()`. Mount after the target element exists in the document.

### Mount the application

Pass a CSS selector or DOM element as the target. The application fills the container width, while `height` controls its height.

```
const embed = Ceyo.mount("#signal-visibility", {
  sessionToken,
  height: "780px",
  title: "AI visibility",
  theme: {
    colors: { primary: "#0F766E" },
  },
  settings: {
    title: "AI Visibility",
    showProjectName: false,
  },
  onReady: () => console.log("Signal is ready"),
  onError: (error) => console.error("Signal error:", error),
});
```

#### Complete browser flow

```
<div id="signal-visibility"></div>
<script src="https://cdn.signal.ceyo.ai/embed/v1.js"></script>
<script>
  async function createSession() {
    const response = await fetch("/api/signal-session", { method: "POST" });
    if (!response.ok) throw new Error("Unable to create embed session");
    const session = await response.json();
    return { token: session.token, expiresAt: session.expires_at };
  }

  (async () => {
    let currentSession = await createSession(); // Keep tokens in memory.
    let embed;
    let refreshInFlight;

    async function refreshSession(currentToken, updateHandle) {
      if (currentSession.token !== currentToken) return currentSession.token;
      if (refreshInFlight) return refreshInFlight;

      refreshInFlight = (async () => {
        const response = await fetch("/api/signal-session/refresh", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Idempotency-Key": crypto.randomUUID(),
          },
          body: JSON.stringify({ session_token: currentToken }),
        });
        const session = response.ok
          ? await response.json()
          : await createSession();
        if (currentSession.token !== currentToken) return currentSession.token;
        currentSession = {
          token: session.token,
          expiresAt: session.expiresAt || session.expires_at,
        };
        if (updateHandle) embed.updateToken(currentSession.token);
        return currentSession.token;
      })().finally(() => {
        refreshInFlight = null;
      });

      return refreshInFlight;
    }

    embed = Ceyo.mount("#signal-visibility", {
      sessionToken: currentSession.token,
      height: "780px",
      onTokenExpired() {
        return refreshSession(currentSession.token, false);
      },
    });
    window.signalEmbed = embed;
  })();
</script>
```

### Mount options

Only `sessionToken` is required. All appearance, behavior, and callback options are optional.

```
type MountOptions = {
  sessionToken: string;
  height?: string;                 // default: "720px"
  minWidth?: string;               // default: "360px"
  title?: string;                  // iframe title; default: "Visibility"
  embedUrl?: string;               // staging/testing override only
  theme?: ThemeOptions;
  settings?: SettingsOptions;
  localization?: LocalizationOptions;
  onTokenExpired?: () => string | null | Promise<string | null>;
  onReady?: () => void;
  onError?: (message: string) => void;
};

type SettingsOptions = {
  title?: string;                   // heading; default: "AI Visibility"
  subtitle?: string;                // optional text below heading; default: ""
  showProjectName?: boolean;        // project label; default: true
  showProjectAdminTabs?: boolean;   // Settings + read-only Users; default: false
  showLocationAdminTabs?: boolean;  // Settings + read-only Users; default: false
  enablePromptDetail?: boolean;     // prompt navigation; default: true
  autoOpenSingleLocation?: boolean; // locations-only projects; default: true
};

type LocalizationOptions = {
  locale: string;                   // BCP 47, for example "nl-NL"
  messages?: TranslationMessages;   // required and complete unless locale is English
};

type TranslationMessages = Record<MessageKey, string>;

type ThemeOptions = {
  colors?: Partial<ThemeColors>;
  typography?: Partial<ThemeTypography>;
  shape?: Partial<ThemeShape>;
  spacing?: Partial<ThemeSpacing>;
  density?: ThemeDensity;           // default: "compact"
};

type ThemeColors = {
  primary: string;        // default: "#16181d"
  primaryText: string;    // default: "#ffffff"
  background: string;     // default: "transparent"
  surface: string;        // default: "#ffffff"
  text: string;           // default: "#16181d"
  muted: string;          // default: "#6b7280"
  border: string;         // default: "#e5e7eb"
  success: string;        // default: "#047857"
  successSurface: string; // default: "#ecfdf5"
  warning: string;        // default: "#b45309"
  warningSurface: string; // default: "#fffbeb"
  danger: string;         // default: "#b91c1c"
  dangerSurface: string;  // default: "#fef2f2"
};

type ThemeTypography = {
  fontFamily: string;  // default: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
  baseSize: string;    // default: "14px"
  smallSize: string;   // default: "12.5px"
  titleSize: string;   // default: "22px"
  titleWeight: string; // default: "700"
  bodyWeight: string;  // default: "400"
};

type ThemeShape = {
  radius: string;        // default: "10px"
  cardRadius: string;    // default: "10px"
  controlRadius: string; // default: "10px"
  pillRadius: string;    // default: "7px"
};

type ThemeSpacing = {
  pagePadding: string;   // default: "20px"
  panelPadding: string;  // default: "16px"
  cardPadding: string;   // default: "16px 18px"
  gap: string;           // default: "12px"
  controlHeight: string; // default: "34px"
};

type ThemeDensity = "compact" | "comfortable";
```

Leave `embedUrl` unset in production so the loader uses the matching Signal application. The override exists for Signal-provided staging and local test builds.

#### Returned handle

`Ceyo.mount()` returns the managed iframe and methods for replacing its token or removing it cleanly.

```
type EmbedHandle = {
  readonly iframe: HTMLIFrameElement;
  updateToken(token: string): void;
  unmount(): void;
};

const embed: EmbedHandle = Ceyo.mount("#signal-visibility", options);
```

### Token refresh

Keep the current token in memory. The iframe invokes `onTokenExpired` about one minute before expiration and after a recoverable expired, invalid, or revoked-session response. Exchange the current token through your authenticated backend. If that exchange is no longer valid, mint a new session for the signed-in user. Return the replacement token, or `null` when access should remain locked.

```
let currentSession = {
  token: initialToken,
  expiresAt: initialExpiresAt,
};

async function refreshSession(
  currentToken: string,
  updateHandle: boolean,
): Promise<string | null> {
  const response = await fetch("/api/signal-session/refresh", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({ session_token: currentToken }),
  });
  const replacement = response.ok
    ? response
    : await fetch("/api/signal-session", { method: "POST" });
  if (!replacement.ok) return null;

  const session = await replacement.json();
  currentSession = {
    token: session.token,
    expiresAt: session.expires_at,
  };
  if (updateHandle) embed.updateToken(currentSession.token);
  return currentSession.token;
}

const embed = Ceyo.mount("#signal-visibility", {
  sessionToken: currentSession.token,
  onTokenExpired() {
    return refreshSession(currentSession.token, false);
  },
});
```

---

# Customization

Source: https://ceyo.ai/docs/signal/embedding-customization

### Theme and appearance

Pass partial theme tokens to match your product. Unspecified values use the defaults listed in the type reference.

```
theme: {
  colors: {
    primary: "#0F766E",
    primaryText: "#FFFFFF",
    background: "transparent",
    surface: "#FFFFFF",
    text: "#16181D",
    muted: "#6B7280",
    border: "#E5E7EB",
    success: "#047857",
    successSurface: "#ECFDF5",
    warning: "#B45309",
    warningSurface: "#FFFBEB",
    danger: "#B91C1C",
    dangerSurface: "#FEF2F2",
  },
  typography: {
    fontFamily: "'Inter', sans-serif",
    baseSize: "14px",
    smallSize: "12.5px",
    titleSize: "22px",
    titleWeight: "700",
    bodyWeight: "400",
  },
  shape: {
    radius: "10px",
    cardRadius: "10px",
    controlRadius: "10px",
    pillRadius: "999px",
  },
  spacing: {
    pagePadding: "20px",
    panelPadding: "16px",
    cardPadding: "16px 18px",
    gap: "12px",
    controlHeight: "34px",
  },
  density: "compact",
}
```

**Colors**

Brand, background, surface, text, border, and status colors.

**Typography**

Font family, text sizes, and title and body weights.

**Shape**

Base, card, control, and pill corner radii.

**Spacing**

Page, panel, card, gap, and control-height overrides. Explicit spacing values take precedence over the selected density.

**Density**

Use `compact` or `comfortable` spacing.

Product settings may set the heading and subtitle, project-name visibility, detail navigation, and single-location behavior. `showProjectAdminTabs` and `showLocationAdminTabs` reveal the applicable Settings and read-only Users tabs; despite their names, they never grant admin access. The session role still controls every action.

Theme and settings are mount-time options. To change either after mounting, call `unmount()` and mount the application again with the new values. Use `updateToken()` only for session replacement.

### Localization

Signal includes an immutable English catalog. To render another language, pass one active BCP 47 locale and a complete translated message catalog at mount time. The UI locale is independent from the language configured on a project or location.

```
// Start from the complete catalog and translate every value.
const nlMessages = Ceyo.translationTemplate();

Object.assign(nlMessages, {
  "app.defaultTitle": "AI-zichtbaarheid",
  "frame.title": "AI-zichtbaarheid",
  "common.saveChanges": "Wijzigingen opslaan",
  "common.cancel": "Annuleren",
  "navigation.overview": "Overzicht",
  "navigation.locations": "Locaties",
  "navigation.prompts": "Prompts",
  // Continue until every value in the returned catalog is translated.
});

const embed = Ceyo.mount("#signal-visibility", {
  sessionToken,
  localization: {
    locale: "nl-NL",
    messages: nlMessages,
  },
  onError(message) {
    reportEmbedError(message);
  },
});
```

**Complete template**

Call `Ceyo.translationTemplate()` to receive every required key with its English source value. Translate every value and preserve the keys.

**Validation**

The iframe checks the locale, required keys, non-empty values, and interpolation variables. English catalogs cannot be overridden.

**Safe fallback**

An invalid or incomplete catalog falls back to English and reports an `invalid_localization` message through `onError` and the browser console.

**Formatting**

Numbers and dates use the selected locale. Labels, placeholders, empty states, accessibility text, toasts, and errors come from the supplied catalog.

> **Mount-time option**
>
> Pass only the locale needed by the current user. To switch languages, unmount and mount again with the next locale and catalog.

### Client events

`onReady` fires once the component can be used. `onError` reports an error message. `onTokenExpired` applies a returned replacement token. Keep the returned handle when the host page needs to update or unmount the application later.

```
const embed = Ceyo.mount("#signal-visibility", {
  sessionToken,
  onReady() {
    document.querySelector("#signal-loading")?.remove();
  },
  onError(message) {
    reportEmbedError(message);
  },
  async onTokenExpired() {
    return refreshSession(currentSession.token, false);
  },
});

// The returned handle supports lifecycle changes outside callbacks.
embed.updateToken(freshToken);
embed.unmount();
```

---

# Reference

Source: https://ceyo.ai/docs/signal/embedding-reference

### Security

**Server-side key**

Store the Signal API key in backend secrets. Never place it in HTML, browser code, logs, or client bundles.

**Short-lived token**

Give the browser only a short-lived embed session token and renew it through your authenticated backend.

**User scope**

Mint each session for the signed-in user and intended project. Validate authorization before creating the session.

**HTTPS**

Serve the host application over HTTPS in production.

**Allowed host origins**

Add each HTTPS origin that hosts the embeddable to every API key used to create or refresh its sessions. Those keys must use the same `allowed_origins`, for example `https://app.partner.example`. Configure only origins you control; Signal service origins are handled automatically.

**Cleanup**

Call `unmount()` when a single-page application removes the host view or signs the user out.

### Troubleshooting

**Target not found**

Mount after the container exists, or pass the DOM element directly.

**Session rejected**

Confirm the backend used the correct project and external user, and that the user has access to the requested scope.

**Locked session**

Implement `onTokenExpired` and ensure it returns a fresh token from your backend.

**Empty view**

Verify that the project contains data and the session user can access the expected project or locations.

**Layout issues**

Give the host container enough width and set an explicit component height.

**Localization fallback**

An `invalid_localization` error lists missing, empty, or invalid message keys. Start from `Ceyo.translationTemplate()`, preserve interpolation variables such as `{{count}}`, and mount again with the corrected catalog.

**Handshake debugging**

The iframe sends `ceyo:ready`, `ceyo:token_expired`, and `ceyo:error`. The loader replies with `ceyo:init` and replacement `ceyo:token` messages. Both sides validate the message source and origin; use the loader instead of posting messages directly.

**Runtime error**

`onError` receives one human-readable message string. Capture that message, then inspect the browser console and failed network response for structured API details and a request ID.
