> For the documentation index, fetch https://sync.so/docs/llms.txt. Append .md to a page URL for Markdown. Documentation-search MCP: https://sync.so/docs/_mcp/server. # Projects > Group your API-created generations and assets into projects so they show up together in the sync. labs Studio — and pick them up again from the API. By default, a generation created from the API isn't filed under anything in particular. **Projects** let you group related generations and assets together so they appear under one project in the [Studio](https://sync.so/login) — and so you can list them back out from the API. It's a two-way road: create a project from the API, attach work to it, and it's visible in the Studio UI; create a project in Studio, and the API can attach to it too. A project is lightweight — only a `name` is required. ## Create a project `POST /v2/projects` returns the new project's `id`. `visibility` defaults to `USER` (visible only to the key owner) and `mode` to `CREATOR`; both are optional. **`cURL`** ```bash title="cURL" curl -X POST https://api.sync.so/v2/projects \ -H "x-api-key: $SYNC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "My API Project" }' ``` **`Python`** ```python title="Python" project = client.projects.create(name="My API Project") print(project.id) ``` **`TypeScript`** ```typescript title="TypeScript" const project = await client.projects.create({ name: "My API Project" }); console.log(project.id); ``` To make project creation safe to retry, send your own UUID in `id`. Reusing it returns the existing project and ignores the other fields. An ID belonging to another user or organization, or to a deleted project, returns `409`. You can set `defaultWorkflow` when creating or updating a project. It selects the Studio workflow, such as `lipsync`, `translate_and_dub`, `image_to_video`, or `edit_dialogue`; it does not change generation inputs. ## Attach generations and assets Pass the project's `id` as `projectId` when you create a generation or register an asset. The field is **optional** on both endpoints — omit it and the work simply isn't grouped under a project. **`Attach a generation`** ```bash title="Attach a generation" curl -X POST https://api.sync.so/v2/generate \ -H "x-api-key: $SYNC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "lipsync-2", "projectId": "612bb64a-e7f4-40c1-977b-3e9797845637", "input": [ { "type": "video", "url": "https://assets.sync.so/docs/example-video.mp4" }, { "type": "audio", "url": "https://assets.sync.so/docs/example-audio.wav" } ] }' ``` **`Attach an asset`** ```bash title="Attach an asset" curl -X POST https://api.sync.so/v2/assets \ -H "x-api-key: $SYNC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://assets.sync.so/org-123/my-video.mp4", "type": "VIDEO", "projectId": "612bb64a-e7f4-40c1-977b-3e9797845637" }' ``` The [generation](/api-reference/api/generate-api/create) and asset responses echo back the `projectId` they were filed under (or `null` when none was given). > **Note** > > `projectId` must reference a project in **your organization**. If it doesn't (wrong id, or a project from another org), the create request is rejected with **422 Unprocessable Entity** — the generation or asset is not created. A missing or `null` `projectId` is always fine. ## Attach an existing asset If an asset already exists — say, one you uploaded earlier or picked from your library — you can attach it to a project after the fact with `POST /v2/projects/{id}/assets`, passing the asset's id as `assetId` in the body. The asset must belong to your organization. This is the counterpart to setting `projectId` at creation time: use it to file work you didn't group up front. **`cURL`** ```bash title="cURL" curl -X POST https://api.sync.so/v2/projects/612bb64a-e7f4-40c1-977b-3e9797845637/assets \ -H "x-api-key: $SYNC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "assetId": "8f14e45f-ceea-467d-9a1b-3e4d5c6f7a8b" }' ``` The call is idempotent — attaching an asset that is already linked to the project succeeds without creating a duplicate — and returns **204 No Content** on success. A project or asset that isn't in your organization returns **404 Not Found**. ## List a project's generations To pick up a project's work again from the API, pass its `id` as `projectId` to `GET /v2/generations`. Add `limit` to get results newest first in pages of up to 100, then pass the last generation's `id` as `cursor` to fetch the next page. **`First page`** ```bash title="First page" curl "https://api.sync.so/v2/generations?projectId=612bb64a-e7f4-40c1-977b-3e9797845637&limit=20" \ -H "x-api-key: $SYNC_API_KEY" ``` **`Next page`** ```bash title="Next page" curl "https://api.sync.so/v2/generations?projectId=612bb64a-e7f4-40c1-977b-3e9797845637&limit=20&cursor=" \ -H "x-api-key: $SYNC_API_KEY" ``` The filter returns every generation in the project, including ones created with any API key in your organization. Which projects you can read depends on their `visibility`: | Project visibility | Readable by | | ------------------ | ------------------------------------------ | | `ORGANIZATION` | Any API key in the organization | | `USER` | Only API keys owned by the project's owner | If the project is private to someone else, deleted, or in another organization, the request returns an empty list instead of an error. A `projectId` that isn't a UUID returns **400 Bad Request**. ## See it in Studio Once attached, generations and assets show up under the project in the [Studio](https://sync.so/login). This works in both directions — a project you create in Studio can be targeted from the API by its `id`, and a project you create from the API appears in Studio. Deleting a project soft-deletes the project and its generations and removes its asset links. Assets with no other active project are also soft-deleted. Assets linked to another active project are retained. Soft-deleted records are no longer available through normal API reads. > **Note** > > You can't delete a project while any of its generations are still queued or processing — the request is rejected with **409 Conflict** and nothing is deleted. Wait for those generations to reach a terminal state (`COMPLETED`, `FAILED`, or `REJECTED`), then retry the delete. Once all of a project's generations are terminal, deletion works normally. ## Detach an asset from draft content `DELETE /v2/projects/{id}/assets/{assetId}` returns `204` with no body. It removes the asset from the project's draft content, including its thumbnail, while preserving the asset in the project asset library and retaining its access grants. Reattaching the asset restores the link. The operation is idempotent: detaching an absent or already detached link also succeeds. An inaccessible project returns `404`. An API key without an owner returns `422`. This is different from deleting a project or deleting an asset. ```bash curl -X DELETE "https://api.sync.so/v2/projects/$PROJECT_ID/assets/$ASSET_ID" \ -H "x-api-key: $SYNC_API_KEY" ``` ## Manage projects | Action | Endpoint | | -------------------- | ------------------------------------------- | | Create | `POST /v2/projects` | | List | `GET /v2/projects` | | Get one | `GET /v2/projects/{id}` | | Attach an asset | `POST /v2/projects/{id}/assets` | | Detach draft content | `DELETE /v2/projects/{id}/assets/{assetId}` | | List its generations | `GET /v2/generations?projectId={id}` | | Update | `PATCH /v2/projects/{id}` | | Delete | `DELETE /v2/projects/{id}` | `GET /v2/projects` is cursor-paginated (`limit`, `cursor`) and supports `searchQuery` (by name) and `sortBy` (`updatedAt` or `name`). `PATCH` only changes the fields you send. **`List projects`** ```bash title="List projects" curl https://api.sync.so/v2/projects?limit=10 \ -H "x-api-key: $SYNC_API_KEY" ``` **`Rename a project`** ```bash title="Rename a project" curl -X PATCH https://api.sync.so/v2/projects/612bb64a-e7f4-40c1-977b-3e9797845637 \ -H "x-api-key: $SYNC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Renamed Project" }' ``` > Group your API-created generations and assets into projects so they show up together in the sync. labs Studio — and pick them up again from the API.