# POST /v1/projects (/docs/api/reference/projects/create-project)



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

Create a project to hold one end-customer's brand context, layers, influencers, social accounts, and generated content. Set `customerExternalId` to your partner-side customer handle so later lookups don't require Layers IDs.

The call is synchronous and idempotent via the `Idempotency-Key` header. It provisions the project shell; downstream ingestion (GitHub, website, App Store) is a separate, async step.

<Callout type="info">
  Providing `appDescription` here also kicks off two background workflows:

  * **Keyword research.** Layers' research agent curates the project's
    TikTok hashtag bank over 4–5 minutes. Observe via
    [`GET /v1/projects/:id/keywords`](/docs/api/reference/keywords/list-keywords)
    (`refreshedAt` flips from `null` when the bank lands). Force a re-run
    with [`POST /v1/projects/:id/keywords/refresh`](/docs/api/reference/keywords/refresh-keywords).
  * **First influencer.** Layers generates a default persona (name,
    gender, visual identity) anchored on the project's brand context.
    Observe via
    [`GET /v1/projects/:id/influencers`](/docs/api/reference/influencers/list-influencers)
    — the row appears with `status: "pending"` immediately and flips
    through `training` to `status: "ready"` in \~1 minute. If your customer needs a
    different persona, create additional influencers with
    [`POST /v1/projects/:id/influencers`](/docs/api/reference/influencers/create-influencer).

  Both auto-triggers fire-and-forget — failures don't fail the create.
</Callout>

<Parameters
  title="Headers"
  rows="[
  { name: 'Idempotency-Key', type: 'string (UUID)', description: 'Optional but strongly recommended on every POST. Same key + same body replays the cached response (`409 IDEMPOTENCY_CONFLICT` on body mismatch). Without it, retries after a connection error create duplicate projects. See [Idempotency](/docs/api/operational/idempotency).' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'name', type: 'string', required: true, description: 'Internal display name. 3–30 chars.' },
  { name: 'customerExternalId', type: 'string', description: 'Your internal customer handle. Unique per organization. Looked up via ?customerExternalId=.' },
  { name: 'ownerEmail', type: 'string', description: 'Notification email. Defaults to the API key\'s registered owner.' },
  { name: 'timezone', type: 'string', required: true, description: 'IANA timezone. Controls cron schedule resolution. Use `&#x22;UTC&#x22;` if you have no preference.' },
  { name: 'primaryLanguage', type: 'string', description: 'BCP-47 language tag (e.g. `en`, `pt-BR`).', default: 'en' },
  { 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 — and the strongest single lever on content quality.' },
  { name: 'appDescription', type: 'string', description: 'Product pitch the planner uses for hooks and captions. 100–1000 chars. Same precondition for `/content/hooks`.' },
  { name: 'tagline', type: 'string', description: 'Short one-liner (≤ 80 chars) used in end-cards and overlays.' },
  { name: 'brandVoice', type: 'string', description: 'Caption tone preset. Defaults to `authentic` when omitted.', enum: ['authentic', 'witty', 'professional', 'warm', 'casual', 'educational'] },
  { name: 'targetGender', type: 'string', description: 'Audience gender. Drives influencer persona defaults at create time.', enum: ['all', 'female', 'male'] },
  { name: 'metadata', type: 'object', description: 'Opaque JSON, ≤ 8KB. Round-tripped unchanged.' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl https://api.layers.com/v1/projects \
      -H "Authorization: Bearer lp_..." \
      -H "Idempotency-Key: 4c1a2e92-7b18-4c4b-9b2a-d7a3f8b1c210" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Acme Coffee",
        "customerExternalId": "acme-coffee",
        "timezone": "America/Los_Angeles",
        "primaryLanguage": "en",
        "ownerEmail": "growth@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"
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const project = await layers.projects.create(
      {
        name: "Acme Coffee",
        customerExternalId: "acme-coffee",
        timezone: "America/Los_Angeles",
        primaryLanguage: "en",
        ownerEmail: "growth@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",
      },
      { idempotencyKey: crypto.randomUUID() }
    );
    ```
  </Tab>

  <Tab value="Python">
    ```python
    project = layers.projects.create(
        name="Acme Coffee",
        customer_external_id="acme-coffee",
        timezone="America/Los_Angeles",
        primary_language="en",
        owner_email="growth@gicgrowth.com",
        app_name="Acme Coffee",
        app_description=(
            "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.",
        brand_voice="warm",
        target_gender="all",
        idempotency_key=str(uuid.uuid4()),
    )
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="201" description="Created">
  ```json
  {
    "id": "prj_9cb958b5-11b5-4e30-8675-5d075d52da7c",
    "organizationId": "org_2481fa5c-a404-44ed-a561-565392499abc",
    "name": "Acme Coffee iOS",
    "status": "active",
    "customerExternalId": "acme-coffee",
    "timezone": "America/Los_Angeles",
    "primaryLanguage": "en",
    "ownerEmail": "growth@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",
    "platformIosBundleId": null,
    "platformAndroidBundleId": null,
    "platformWebDomain": null,
    "ingestState": {
      "github": null,
      "website": null,
      "appstore": null
    },
    "metadata": null,
    "createdAt": "2026-04-18T19:02:11.959888+00:00",
    "updatedAt": "2026-04-18T19:02:11.959888+00:00"
  }
  ```
</Response>

## Errors [#errors]

| Status | Code                   | When                                                                                                                                          |
| ------ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 422    | `VALIDATION`           | `name` empty, `timezone` invalid, `metadata` > 8KB.                                                                                           |
| 401    | `UNAUTHENTICATED`      | Missing or invalid key.                                                                                                                       |
| 403    | `FORBIDDEN_SCOPE`      | Key lacks `projects:write`.                                                                                                                   |
| 409    | `IDEMPOTENCY_CONFLICT` | Same `Idempotency-Key` replayed with a different body.                                                                                        |
| 409    | `CONFLICT`             | `customerExternalId` already in use on another project.                                                                                       |
| 422    | `VALIDATION_FAILED`    | Body includes `id` (resource ids are server-minted — see [Idempotency](/docs/api/operational/idempotency#resource-ids-are-server-generated)). |
| 429    | `RATE_LIMITED`         | Write budget exhausted.                                                                                                                       |

## See also [#see-also]

* [`PATCH /v1/projects/:id`](/docs/api/reference/projects/patch-project) - update fields after creation
* [Getting started](/docs/api/getting-started) - the canonical project → influencer → content flow
* [Idempotency](/docs/api/concepts/jobs#idempotency) - replay semantics
