# Locations

Source: https://preview.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.

> **Model availability**
>
> The selected country must support every visibility model in the package. This is validated on create, country updates, and each bulk item. Unsupported combinations return `422 unsupported_model_country_combination` with the unsupported model keys in the error details.

> **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, email, folder, and tag. Maximum: 200 characters. |
| `folder` | string | Optional | Match one folder, case-insensitively. |
| `tag` | string | Optional | Match one tag, case-insensitively. |
| `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

| 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. |
| `folder` | string \| null |  | Optional organization folder. |
| `tags` | string\[\] |  | Up to three organization tags. |
| `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. |
| `effective_action_language` | ISO 639-1 string |  | Final action language after applying inheritance. |
| `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. |
| `listing_management` | signal\_managed \| customer\_managed |  | Who supplies the Google listing profile. signal\_managed: Signal reads it from Google. customer\_managed: Signal uses the profile you submit. |
| `google_listing_profile_observed_at` | datetime \| null |  | When the submitted customer-managed listing profile was observed. Null for signal-managed locations. |
| `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. |
| `google_place_resolution` | object |  | Place resolution with state (resolved, ambiguous, unresolved, mismatch, or no\_listing), source (customer or automatic), customer confirmation time, and ambiguous candidates containing only place\_id, name, address, and google\_maps\_url. |
| `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",
      "folder": "Europe",
      "tags": [
        "Flagship",
        "Airport"
      ],
      "description": "Acme flagship store in Amsterdam.",
      "website": "https://acme.example/amsterdam",
      "brand_aliases": [
        "acme amsterdam"
      ],
      "language": "en",
      "action_language": "en",
      "effective_action_language": "en",
      "include_parent_brand": true,
      "listing_management": "signal_managed",
      "google_listing_profile_observed_at": null,
      "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",
      "google_place_resolution": {
        "state": "resolved",
        "source": "customer",
        "confirmed_at": "2026-07-31T08:10:00Z",
        "candidates": []
      },
      "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. If omitted, Signal infers it from a clear country\_code; 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, Signal validates it and resolves canonical place details. Required for customer\_managed listings, where it is not looked up with Google. Maximum: 500 characters. |
| `google_place_name` | string \| null |  | Google place business name. Maximum: 200 characters. |
| `listing_management` | signal\_managed \| customer\_managed |  | Who supplies the Google listing profile. With customer\_managed, Signal never calls Google for this location and scans the submitted google\_listing\_profile instead. Defaults to signal\_managed. |
| `google_listing_profile` | GoogleListingProfileInput |  | Complete Google listing profile. Accepts every field of the ListingProfile object from the Listings API, in the same shape, plus the GoogleListingProfileInput fields below. If place\_id is sent, it must equal google\_place\_id. Required when listing\_management is customer\_managed and rejected otherwise. Maximum: 512 KB. |
| `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. |
| `folder` | string \| null |  | Optional organization folder. Maximum: 30 characters. |
| `tags` | string\[\] |  | Up to three organization tags. Each tag is 1–25 characters. |
| `start` | boolean |  | When true, starts onboarding after provisioning. Supply either google\_place\_id or both city and country\_code. customer\_managed locations always need city and country\_code. Defaults to false. |
| `use_full_prompt_limit` | boolean |  | When start is true, creates 100% instead of 80% of the package prompt limit during onboarding. Both modes are capped at 100 prompts. Defaults to false. |
| `onboarding_topics` | OnboardingTopics \| null |  | Optional topic choices for automatic onboarding. Used only when start is true. |

#### OnboardingTopics

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `topics` | string\[\] |  | Up to five topic names. Each must contain 1–120 characters and be unique and brand-safe. |
| `fill_remaining` | boolean |  | When true, Signal suggests topics for the remaining slots. Defaults to true; false requires at least one supplied topic. |

#### GoogleListingProfileInput

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `observed_at` | datetime |  | Required. ISO 8601 time at which you read this profile from Google. |
| `latitude` | number \| null |  | Listing latitude. |
| `longitude` | number \| null |  | Listing longitude. |
| `current_opening_hours` | OpeningHours \| null |  | Opening hours for the current week, including special days. |
| `attributes` | ListingAttributes |  | Besides the documented ListingAttributes fields, also accepts the booleans serves\_brunch, serves\_cocktails, outdoor\_seating, live\_music, good\_for\_children, good\_for\_groups, and good\_for\_watching\_sports. |
| `photos` | object\[\] |  | Listing photos, each with name, width\_px, height\_px, and google\_maps\_url. Used by the photo coverage check. |

> **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.

> **Customer-managed listings**
>
> If you manage the Google listing yourself, send `listing_management: "customer_managed"`, `google_place_id`, and the full `google_listing_profile`. Send `city`, `state`, `postal_code`, and `country_code` directly; they are not derived from the profile. `city` and `country_code` are required to start onboarding. Signal never calls Google for this location and never overwrites your profile. Onboarding and scheduled listing scans use the submitted profile, and scans still produce scores and findings. The profile stays in use until you replace it; its age is visible through `google_listing_profile_observed_at`.

> **Topics and prompt count**
>
> With `start: true`, optionally supply up to five topic names in `onboarding_topics`. Set `fill_remaining: false` to use only the supplied topics. Onboarding creates 80% of the package prompt limit by default. Set `use_full_prompt_limit: true` to create 100%. The maximum is always 100 prompts.

#### Response envelope

| 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. |
| `folder` | string \| null |  | Optional organization folder. |
| `tags` | string\[\] |  | Up to three organization tags. |
| `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. |
| `effective_action_language` | ISO 639-1 string |  | Final action language after applying inheritance. |
| `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. |
| `listing_management` | signal\_managed \| customer\_managed |  | Who supplies the Google listing profile. signal\_managed: Signal reads it from Google. customer\_managed: Signal uses the profile you submit. |
| `google_listing_profile_observed_at` | datetime \| null |  | When the submitted customer-managed listing profile was observed. Null for signal-managed locations. |
| `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. |
| `google_place_resolution` | object |  | Place resolution with state (resolved, ambiguous, unresolved, mismatch, or no\_listing), source (customer or automatic), customer confirmation time, and ambiguous candidates containing only place\_id, name, address, and google\_maps\_url. |
| `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,
  "use_full_prompt_limit": true,
  "onboarding_topics": {
    "topics": [
      "Emergency dental care",
      "Cosmetic dentistry"
    ],
    "fill_remaining": 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",
    "folder": "Europe",
    "tags": [
      "Flagship",
      "Airport"
    ],
    "description": "Acme flagship store in Amsterdam.",
    "website": "https://acme.example/amsterdam",
    "brand_aliases": [
      "acme amsterdam"
    ],
    "language": "en",
    "action_language": "en",
    "effective_action_language": "en",
    "include_parent_brand": true,
    "listing_management": "signal_managed",
    "google_listing_profile_observed_at": null,
    "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",
    "google_place_resolution": {
      "state": "resolved",
      "source": "customer",
      "confirmed_at": "2026-07-31T08:10:00Z",
      "candidates": []
    },
    "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. Each started record may set `use_full_prompt_limit: true` and `onboarding_topics`.

#### Response envelope

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,
      "use_full_prompt_limit": true,
      "onboarding_topics": {
        "topics": [
          "Emergency dental care",
          "Cosmetic dentistry"
        ],
        "fill_remaining": 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,
      "use_full_prompt_limit": true,
      "onboarding_topics": {
        "topics": [
          "Emergency dental care",
          "Cosmetic dentistry"
        ],
        "fill_remaining": 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

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. |
| `folder` | string \| null |  | Optional organization folder. |
| `tags` | string\[\] |  | Up to three organization tags. |
| `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. |
| `effective_action_language` | ISO 639-1 string |  | Final action language after applying inheritance. |
| `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. |
| `listing_management` | signal\_managed \| customer\_managed |  | Who supplies the Google listing profile. signal\_managed: Signal reads it from Google. customer\_managed: Signal uses the profile you submit. |
| `google_listing_profile_observed_at` | datetime \| null |  | When the submitted customer-managed listing profile was observed. Null for signal-managed locations. |
| `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. |
| `google_place_resolution` | object |  | Place resolution with state (resolved, ambiguous, unresolved, mismatch, or no\_listing), source (customer or automatic), customer confirmation time, and ambiguous candidates containing only place\_id, name, address, and google\_maps\_url. |
| `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",
    "folder": "Europe",
    "tags": [
      "Flagship",
      "Airport"
    ],
    "description": "Acme flagship store in Amsterdam.",
    "website": "https://acme.example/amsterdam",
    "brand_aliases": [
      "acme amsterdam"
    ],
    "language": "en",
    "action_language": "en",
    "effective_action_language": "en",
    "include_parent_brand": true,
    "listing_management": "signal_managed",
    "google_listing_profile_observed_at": null,
    "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",
    "google_place_resolution": {
      "state": "resolved",
      "source": "customer",
      "confirmed_at": "2026-07-31T08:10:00Z",
      "candidates": []
    },
    "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. |

### List business facts

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

Returns the business facts Signal has reconciled for a location, including source details.

#### 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

Facts ordered with disputed values first.

#### BusinessFact

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `claim_type` | string |  | Fact type, such as name, phone, website, address, hours, reservations, takeout, delivery, menu\_url, booking\_url, or meal service. |
| `qualifier` | string \| null |  | Lowercase weekday for hours; null for other fact types. |
| `value` | JSON value |  | Current reconciled value. |
| `verification_state` | verified \| supported \| disputed \| unverified \| customer\_confirmed |  | How the available sources currently support the fact. |
| `publishable` | boolean |  | Whether Signal may use the value in generated recommendations. |
| `observations` | BusinessFactObservation\[\] |  | Source observations used to reconcile the fact. |
| `updated_at` | datetime |  | Most recent reconciliation time. |

#### BusinessFactObservation

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `source_kind` | string |  | Stable source category. |
| `source_label` | string |  | Customer-readable source name. |
| `source_host` | string \| null |  | Third-party host when relevant. |
| `value` | JSON value |  | Value reported by this source. |
| `stance` | assert \| dispute |  | Whether the source supports or disputes the value. |
| `observed_at` | datetime |  | When the value was observed. |
| `expires_at` | datetime \| null |  | When a customer confirmation expires. |
| `page_url` | string \| null |  | Public evidence page when available. |
| `reason` | string \| null |  | Customer-supplied dispute reason when available. |

#### 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/facts' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "facts": [
    {
      "claim_type": "phone",
      "qualifier": null,
      "value": "+31 20 555 0100",
      "verification_state": "disputed",
      "publishable": false,
      "observations": [
        {
          "source_kind": "official_page",
          "source_label": "Your website",
          "value": "+31 20 555 0100",
          "stance": "assert",
          "observed_at": "2026-09-07T10:00:00Z",
          "page_url": "https://acme.example/amsterdam"
        },
        {
          "source_kind": "google_verified",
          "source_label": "Google Business Profile",
          "value": "+31 20 555 0199",
          "stance": "assert",
          "observed_at": "2026-09-07T10:05:00Z"
        }
      ],
      "updated_at": "2026-09-07T10: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. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Confirm business fact

`POST /projects/{project_id}/locations/{location_id}/facts/{claim_type}/confirm`

Confirms the correct value for one location business fact and immediately reconciles dependent findings.

#### 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. |
| `claim_type` | string | Required | Supported business fact type. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `value` | string \| boolean \| object |  | Required value to confirm or dispute. Hours use opens/closes or periods. |
| `qualifier` | weekday |  | Required for hours and not accepted for other fact types. |
| `reason` | string |  | Optional explanation, up to 500 characters. |

#### Response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `fact` | BusinessFact |  | Updated fact. |
| `reconciled` | boolean |  | Whether dependent findings were reconciled immediately. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/a1308d14-149c-4dd7-a4c5-295ac9090f58/facts/phone/confirm' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "value": "+31 20 555 0100"
}'
```

```json
{
  "fact": {
    "claim_type": "phone",
    "qualifier": null,
    "value": "+31 20 555 0100",
    "verification_state": "customer_confirmed",
    "publishable": true,
    "observations": [],
    "updated_at": "2026-09-07T10:10:00Z"
  },
  "reconciled": 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": "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. |

### Dispute business fact

`POST /projects/{project_id}/locations/{location_id}/facts/{claim_type}/dispute`

Records that a reported value is incorrect and immediately reconciles dependent findings.

#### 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. |
| `claim_type` | string | Required | Supported business fact type. |

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `value` | string \| boolean \| object |  | Required value to confirm or dispute. Hours use opens/closes or periods. |
| `qualifier` | weekday |  | Required for hours and not accepted for other fact types. |
| `reason` | string |  | Optional explanation, up to 500 characters. |

#### Response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `fact` | BusinessFact |  | Updated fact. |
| `reconciled` | boolean |  | Whether dependent findings were reconciled immediately. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/a1308d14-149c-4dd7-a4c5-295ac9090f58/facts/phone/dispute' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "value": "+31 20 555 0199",
  "reason": "This number belongs to another branch."
}'
```

```json
{
  "fact": {
    "claim_type": "phone",
    "qualifier": null,
    "value": "+31 20 555 0199",
    "verification_state": "unverified",
    "publishable": false,
    "observations": [],
    "updated_at": "2026-09-07T10:10:00Z"
  },
  "reconciled": 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": "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. |

### 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

| 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

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. |
| `folder` | string \| null |  | Optional organization folder. |
| `tags` | string\[\] |  | Up to three organization tags. |
| `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. |
| `effective_action_language` | ISO 639-1 string |  | Final action language after applying inheritance. |
| `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. |
| `listing_management` | signal\_managed \| customer\_managed |  | Who supplies the Google listing profile. signal\_managed: Signal reads it from Google. customer\_managed: Signal uses the profile you submit. |
| `google_listing_profile_observed_at` | datetime \| null |  | When the submitted customer-managed listing profile was observed. Null for signal-managed locations. |
| `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. |
| `google_place_resolution` | object |  | Place resolution with state (resolved, ambiguous, unresolved, mismatch, or no\_listing), source (customer or automatic), customer confirmation time, and ambiguous candidates containing only place\_id, name, address, and google\_maps\_url. |
| `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",
    "folder": "Europe",
    "tags": [
      "Flagship",
      "Airport"
    ],
    "description": "Acme flagship store in Amsterdam.",
    "website": "https://acme.example/amsterdam",
    "brand_aliases": [
      "acme amsterdam"
    ],
    "language": "en",
    "action_language": "en",
    "effective_action_language": "en",
    "include_parent_brand": true,
    "listing_management": "signal_managed",
    "google_listing_profile_observed_at": null,
    "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",
    "google_place_resolution": {
      "state": "resolved",
      "source": "customer",
      "confirmed_at": "2026-07-31T08:10:00Z",
      "candidates": []
    },
    "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. If omitted, Signal infers it from a clear country\_code; 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 to select. Set to null to return the location to unresolved. |
| `google_place_name` | string \| null |  | Google place business name. Maximum: 200 characters. |
| `listing_management` | signal\_managed \| customer\_managed |  | Who supplies the Google listing profile. With customer\_managed, Signal never calls Google for this location and scans the submitted google\_listing\_profile instead. |
| `google_listing_profile` | GoogleListingProfileInput |  | Complete Google listing profile. Accepts every field of the ListingProfile object from the Listings API, in the same shape, plus the GoogleListingProfileInput fields below. If place\_id is sent, it must equal google\_place\_id. Required when listing\_management is customer\_managed and rejected otherwise. Maximum: 512 KB. |
| `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. |
| `folder` | string \| null |  | organization folder. Maximum: 30 characters. |
| `tags` | string\[\] |  | Up to three organization tags. Each tag is 1–25 characters. |
| `google_listing_absent` | boolean |  | Set to true to mark the location as having no Google listing. |
| `address_line_2` | string \| null |  | Replacement optional second address line. |

#### GoogleListingProfileInput

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `observed_at` | datetime |  | Required. ISO 8601 time at which you read this profile from Google. |
| `latitude` | number \| null |  | Listing latitude. |
| `longitude` | number \| null |  | Listing longitude. |
| `current_opening_hours` | OpeningHours \| null |  | Opening hours for the current week, including special days. |
| `attributes` | ListingAttributes |  | Besides the documented ListingAttributes fields, also accepts the booleans serves\_brunch, serves\_cocktails, outdoor\_seating, live\_music, good\_for\_children, good\_for\_groups, and good\_for\_watching\_sports. |
| `photos` | object\[\] |  | Listing photos, each with name, width\_px, height\_px, and google\_maps\_url. Used by the photo coverage check. |

> **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. Set `google_place_id` to `null` to return to unresolved, or send `google_listing_absent: true` to mark no listing. A submitted `google_listing_profile` replaces the stored profile completely; fields you omit are removed, not kept from the previous profile. Omit `google_listing_profile` to leave it unchanged. Switching `listing_management` to `signal_managed` deletes the stored profile.

#### Response envelope

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. |
| `folder` | string \| null |  | Optional organization folder. |
| `tags` | string\[\] |  | Up to three organization tags. |
| `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. |
| `effective_action_language` | ISO 639-1 string |  | Final action language after applying inheritance. |
| `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. |
| `listing_management` | signal\_managed \| customer\_managed |  | Who supplies the Google listing profile. signal\_managed: Signal reads it from Google. customer\_managed: Signal uses the profile you submit. |
| `google_listing_profile_observed_at` | datetime \| null |  | When the submitted customer-managed listing profile was observed. Null for signal-managed locations. |
| `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. |
| `google_place_resolution` | object |  | Place resolution with state (resolved, ambiguous, unresolved, mismatch, or no\_listing), source (customer or automatic), customer confirmation time, and ambiguous candidates containing only place\_id, name, address, and google\_maps\_url. |
| `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",
    "folder": "Europe",
    "tags": [
      "Flagship",
      "Airport"
    ],
    "description": "Acme flagship store in central Amsterdam.",
    "website": "https://acme.example/amsterdam",
    "brand_aliases": [
      "acme amsterdam",
      "acme centrum"
    ],
    "language": "en",
    "action_language": "en",
    "effective_action_language": "en",
    "include_parent_brand": true,
    "listing_management": "signal_managed",
    "google_listing_profile_observed_at": null,
    "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",
    "google_place_resolution": {
      "state": "resolved",
      "source": "customer",
      "confirmed_at": "2026-07-31T08:10:00Z",
      "candidates": []
    },
    "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. |
