---
title: "Create a site"
method: POST
path: "/v1/projects"
tags: ["Sites"]
---

# Create a site

`POST /v1/projects`

## Request

- Base URL: `https://hootlens-api-368877148687.us-east1.run.app`
- URL: `https://hootlens-api-368877148687.us-east1.run.app/v1/projects`
- Auth: HTTP bearer

Adds a website to the authenticated account, within its current site allowance. A verified signed-in account owner may create directly. An OAuth agent must have explicitly approved sites:create and all-project access; site-bound personal access tokens and demo readers receive 403. Third-party OAuth tokens call through the hosted MCP endpoint, while first-party local MCP/CLI sign-in can call REST directly. Agents start with balanced privacy (strict is also allowed) and required consent. Agents cannot request full privacy or implied consent. Origins reject credentials, query parameters and fragments. Account IDs and owner IDs are not accepted in the body. Idempotency-Key is required for OAuth agents and optional for people. For 24 hours the same account, key and normalized settings return the existing site without consuming another slot; different settings return 409. Expired keys may create a new site. Revocation and current email verification are checked before a replay. Idempotency-Replayed is true on replay and false on new creation. Site creation produces configuration and does not prove a tracking tag has been installed.

Required token scopes are listed in x-hootlens-scopes. A signed-in person needs the matching permission on the site instead (the viewer and editor roles are presets of the permissions).

## Headers

- `Idempotency-Key` string

## Request body

- ProjectCreate
  - `name` string, required
  - `origins` string[], required
  - `privacy` object
    - `preset` 'full' | 'balanced' | 'strict', required
  - `consent` 'implied' | 'required'

## Response `200`

The existing site for this account and request key.

- object
  - `project` ProjectWithRole, required
    - `id` string, required: An identifier: 1 to 80 letters, digits, underscores or hyphens.
    - `name` string, required
    - `origins` string[], required
    - `ownerId` string
    - `createdAt` integer, required: Epoch milliseconds.
    - `retentionDays` integer, required
    - `privacy` object: The privacy policy. Absent on sites created before configurable privacy, which are strict.
      - `preset` 'full' | 'balanced' | 'strict', required
      - `keepCampaignDigits` boolean: Keep digits in utm_ values. Off when absent.
    - `internalTraffic` object: How the site's own team is told apart from visitors. Absent when nobody is.
      - `browser` boolean, required: Visits from browsers that opened the internal link are internal.
      - `ips` string[], required: Listed IP addresses or ranges. Only the owner sees them.
    - `consent` 'implied' | 'required' | 'regional': Absent on sites created before configurable consent, which require consent.
    - `previewOrigins` string[]: Host patterns for pull request previews, such as https://deploy-preview-*--mysite.netlify.app. They may send test visits only. Absent means none.
    - `consentRegions` string[]
    - `sampleRate` number: 0 to 1.
    - `ai` object
      - `explanations` boolean, required
    - `errorCapture` boolean: Whether the site records JavaScript errors. On for sites created since the setting was added; absent (off) on earlier sites until the owner turns it on.
    - `targeting` object: Page paths to record. Absent records every path.
      - `include` string[], required
      - `exclude` string[], required
    - `pathPresets` PathPreset[]: Per-path privacy presets. They only ever make capture stricter than the site preset. Absent means none.
      - `match` string, required: A page path pattern in the page targeting syntax.
      - `preset` 'safe-form' | 'balanced' | 'strict', required
    - `pageGroups` object[]
      - `id` string
      - `name` string
      - `match` string[]
    - `grouping` object: Locale prefix, experiment parameter and page version mode overrides.
    - `goals` Goal[]
      - union
        - object
          - `kind` 'click', required
          - `name` string, required
          - `by` 'selector', required
          - `match` string, required
          - `id` string, required
          - `since` integer, required
        - object
          - `kind` 'click', required
          - `name` string, required
          - `by` 'hootId', required
          - `match` string, required
          - `id` string, required
          - `since` integer, required
        - object
          - `kind` 'click', required
          - `name` string, required
          - `by` 'label', required
          - `match` string, required
          - `id` string, required
          - `since` integer, required
        - object
          - `kind` 'page', required
          - `name` string, required
          - `match` string, required
          - `id` string, required
          - `since` integer, required
    - `funnels` Funnel[]
      - `name` string, required
      - `steps` object[], required
        - `goalId` string, required
      - `id` string, required
      - `createdAt` integer, required
    - `savedSegments` object[]
      - `id` string
      - `name` string
    - `elementNames` ElementDisplayName[]
      - `key` string, required: The element's selector, as findings and heatmaps list it.
      - `name` string, required: What the team calls the element.
      - `updatedAt` integer, required: Epoch milliseconds.
      - `updatedBy` string: The user who last set it.
    - `updatedAt` integer: Epoch milliseconds.
    - `role` 'owner' | 'editor' | 'viewer' | 'custom', required
    - `permissions` string[], required

## Other responses

- `201`: The new site.
- `400`: The request fields are invalid.
- `401`: The credential is missing, expired or revoked.
- `402`: The site (project) or account plan does not allow this.
- `403`: The credential or the person's permissions on the site do not permit this action.
- `404`: The site, or the record inside it, does not exist or the caller is not a member (a non-member cannot tell the two apart).
- `409`: The request conflicts with the current state of the record.
- `429`: A rate limit or a per-site limit was reached.

---

[API](https://skmtc.dev/hootlens/apis/hoot-lens-api.md) · [All operations](https://skmtc.dev/hootlens/apis/hoot-lens-api/llms.txt) · [OpenAPI document](https://skmtc.dev/hootlens/apis/hoot-lens-api/revisions/f64f8dc7b84e?raw)
