---
title: "POST /projects"
method: POST
path: "/projects"
tags: ["projects"]
---

# POST /projects

`POST /projects`

## Request

- Base URL: `https://api.focusbear.io` (the document declares no server; this is the origin it was published from)
- URL: `https://api.focusbear.io/projects`
- Auth: HTTP bearer

## Request body

- CreateProjectDto
  - `name` string, required
  - `description` string
  - `custom_statuses` ProjectStatusDto[]
    - `id` string, required
    - `label` string, required
    - `color` string, required
    - `order` number, required
    - `should_complete_task` boolean, required
  - `due_date` string
  - `importance` number
  - `outcome` number, nullable: Outcome significance, 1..10. Omitted/null means unrated; independent of importance (1..4).
  - `metadata` object, nullable: Open per-project bag; `metadata.tags` is the reserved, validated, queryable key. Stored **unencrypted** so the server can index and filter it — do not put anything sensitive here. Capped at 8KB serialized.

## Response `201`

- ProjectResponseDto
  - `id` string, required: For native projects: the project UUID. For synced (ClickUp) projects: the focus_mode_tags.id when a tag exists, otherwise the synced_projects.id. Use synced_project_id to reliably identify the synced project record regardless of tag presence. Not normalized to synced_project_id as mobile/web clients use this id for GET /projects/:id lookups and routing. A swap to synced_project_id requires a coordinated client release.
  - `name` string, required
  - `description` string
  - `owner_id` string, required
  - `custom_statuses` object[], required
  - `icon` string, nullable: One emoji character, or `null` when the project has no icon yet — which is what a clear returns, and what a project awaiting generation carries. This is the schema clients decode, so the nullability belongs here rather than only in the tests that assert it.
  - `members` ProjectMemberResponseDto[]
    - `id` string, required
    - `user_id` string
    - `email` string
    - `role` 'owner' | 'admin' | 'member', required
    - `invitation_status` 'pending' | 'accepted' | 'declined' | 'failed', required
    - `invitation_sent_at` string, date-time
    - `invitation_responded_at` string, date-time
    - `created_at` string, required
    - `updated_at` string, required
  - `task_count` number
  - `uncompleted_task_count` number
  - `synced_project_id` string
  - `synced_platform` string, nullable: Stored integration platform for synced projects; null when the legacy source is unknown. Read only.
  - `due_date` string, nullable
  - `importance` number
  - `outcome` number, nullable, required: Outcome significance on the 1..10 wire scale; null for unrated or synced projects.
  - `status` 'active' | 'completed' | 'archived', required: Lifecycle state: active (default), completed, or archived.
  - `top_priority_task` TopPriorityTaskDto
    - `id` string, required
    - `title` string, required
    - `top_score` number, nullable, required: Materialised TOP score. Null for todos not yet scored by the daily cron.
  - `metadata` object, nullable: Open per-project bag; `metadata.tags` holds the project's tags, lower-cased. Null when unset, and always null for synced projects, which have no row in `projects` to carry it.
  - `created_at` string, required
  - `updated_at` string, required

## Other responses

- `409`: The project's owner already has a project with this name, ignoring case and surrounding spaces; body is { code: "project_name_taken", message }

---

[API](https://skmtc.dev/focusbear/apis/focusbear-api.md) - [All operations](https://skmtc.dev/focusbear/apis/focusbear-api/llms.txt) - [OpenAPI document](https://skmtc.dev/focusbear/apis/focusbear-api/revisions/3a83a3b9a811?raw)
