# PATCH /v1/projects/:id (/docs/api/reference/projects/patch-project)



<Endpoint method="PATCH" path="/v1/projects/:id" auth="Bearer" scope="projects:write" phase="1" />

Patch one or more user-editable fields. Omitted fields stay unchanged. System-managed fields - `id`, `organizationId`, `createdAt`, `ingestState`, `brandContext.*` populated by ingestion - are read-only; passing them returns `400 VALIDATION`.

To modify approval behavior, use [`PATCH /v1/projects/:id/content-review-policy`](/docs/api/reference/approval/policy) - approval lives on a separate resource so it has its own scope and audit trail.

<Callout type="info">
  Changing `appDescription` to a new non-empty value re-kicks background
  keyword research — Layers' research agent re-curates the TikTok hashtag
  bank over the next 4–5 minutes. PATCHes that omit `appDescription` (or
  set it to the same value) do not trigger a refresh. To force a manual
  re-run regardless of the field, call
  [`POST /v1/projects/:id/keywords/refresh`](/docs/api/reference/keywords/refresh-keywords).
</Callout>

<Parameters
  title="Path"
  rows="[
  { name: 'id', type: 'string', required: true, description: 'Project ID.' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'name', type: 'string', description: 'Internal display name. 3–30 chars.' },
  { name: 'status', type: 'string', description: 'Archiving stops scheduled posts and new content generation; it does not delete data.', enum: ['active', 'archived'] },
  { name: 'ownerEmail', type: 'string', description: 'Notification email.' },
  { name: 'timezone', type: 'string', description: 'IANA timezone. Takes effect on the next scheduler tick.' },
  { name: 'primaryLanguage', type: 'string', description: 'BCP-47 tag.' },
  { name: 'customerExternalId', type: 'string', description: 'Must remain unique within the organization.' },
  { name: 'appName', type: 'string', description: 'Product name the generator anchors hooks and captions on. 3–30 chars. Required before `GET /v1/projects/:id/content/hooks` returns a bank. Pass `null` to clear.' },
  { name: 'appDescription', type: 'string', description: 'Product pitch the planner uses for hooks and captions. 100–1000 chars. Same precondition for `/content/hooks`. Pass `null` to clear.' },
  { name: 'tagline', type: 'string', description: 'Short one-liner (≤ 80 chars) used in end-cards and overlays.' },
  { name: 'brandVoice', type: 'string', description: 'Caption tone preset.', enum: ['authentic', 'witty', 'professional', 'warm', 'casual', 'educational'] },
  { name: 'targetGender', type: 'string', description: 'Audience gender. Used as the default when creating new influencers.', enum: ['all', 'female', 'male'] },
  { name: 'metadata', type: 'object', description: 'Replaces the existing object in full - not a deep merge.' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl -X PATCH https://api.layers.com/v1/projects/prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39 \
      -H "Authorization: Bearer lp_..." \
      -H "Content-Type: application/json" \
      -d '{
        "timezone": "America/New_York",
        "ownerEmail": "ops@gicgrowth.com"
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const project = await layers.projects.update(
      "prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
      { timezone: "America/New_York", ownerEmail: "ops@gicgrowth.com" }
    );
    ```
  </Tab>

  <Tab value="Python">
    ```python
    project = layers.projects.update(
        "prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
        timezone="America/New_York",
        owner_email="ops@gicgrowth.com",
    )
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="OK">
  ```json
  {
    "id": "9cb958b5-11b5-4e30-8675-5d075d52da7c",
    "organizationId": "org_2481fa5c-a404-44ed-a561-565392499abc",
    "name": "Acme Coffee iOS",
    "status": "active",
    "customerExternalId": "acme-coffee",
    "timezone": "America/New_York",
    "primaryLanguage": "en",
    "ownerEmail": "ops@gicgrowth.com",
    "appName": "Acme Coffee",
    "appDescription": "Daily ritual coffee subscriptions for runners and night-shift workers. Single-origin beans from named farms, roasted weekly, and shipped on a cadence that matches how you actually drink coffee — so the bag never goes stale and you never run out before a hard workout.",
    "tagline": "Coffee that shows up before you run out.",
    "brandVoice": "warm",
    "targetGender": "all",
    "metadata": null,
    "createdAt": "2026-04-18T19:02:11.959888+00:00",
    "updatedAt": "2026-04-18T19:08:44.317953+00:00"
  }
  ```

  The full project record is returned, identical in shape to [`GET /v1/projects/:id`](/docs/api/reference/projects/get-project) - not a slim diff.
</Response>

## Errors [#errors]

| Status | Code              | When                                                                              |
| ------ | ----------------- | --------------------------------------------------------------------------------- |
| 422    | `VALIDATION`      | `:id` is not a UUID, body has unknown/read-only fields, or `timezone` is invalid. |
| 401    | `UNAUTHENTICATED` | Missing or invalid key.                                                           |
| 403    | `FORBIDDEN_SCOPE` | Key lacks `projects:write`.                                                       |
| 404    | `NOT_FOUND`       | Project does not exist in the key's organization.                                 |
| 409    | `CONFLICT`        | `customerExternalId` collides with another project in the organization.           |
| 429    | `RATE_LIMITED`    | Write budget exhausted.                                                           |

## See also [#see-also]

* [`GET /v1/projects/:id`](/docs/api/reference/projects/get-project) - read current state
* [`DELETE /v1/projects/:id`](/docs/api/reference/projects/archive-project) - soft-archive
* [`PATCH /v1/projects/:id/content-review-policy`](/docs/api/reference/approval/policy)
