Skip to main content

Projects

A project is the ownership and billing boundary for collaborative work. One user owns it and pays for everything done inside it; contributors join by email invite and spend the owner's credits, with every charge attributed to whoever triggered it.

Every account has an auto-created default project that can't be deleted. Work with no projectId lands there and is self-billed, which is why most API callers never touch this resource.

Reach for it when you want pooled billing — one payer, many actors, per-actor spend reporting.

The project object

FieldTypeDescription
idstringProject id (UUID).
namestringDisplay name.
descriptionstring | nullFree text, up to 2,000 characters.
avatarUrlstring | nullSigned read URL for the avatar image. Expires.
roleenumYour role on this project: owner or contributor.
ownerobject{ email, name } of the paying account.
createdAt · updatedAtstringISO 8601.

Roles

ActionOwnerContributor
Read the project, its media, workflows, and runs
Run generations and workflows (billed to the owner)
Link media into the project
Invite and remove members
Rename, update, or delete the project
Cancel another member's job
See all members' ledger entriesown only
Leave the project❌ (delete instead)

Projects are capped at 20 members and pending invites combined.


POST /v1/projects

Create a project. You become its owner.

The id is client-supplied, not server-generated — pass a UUID you mint. Reusing an existing id returns conflict (409).

Body

NameTypeRequiredDescription
idstringyesA UUID you generate.
namestringyes1–200 characters.

Returns { project, members, invites? }invites is present only for the owner, which on creation is always you.

Request
curl -s -X POST https://api.corte.so/v1/projects \
-H "Authorization: Bearer corte_sk_…" \
-H "Content-Type: application/json" \
-d '{ "id": "1b6d4f90-77c2-4a5e-b3d8-2e9a0c15f7b4", "name": "Autumn Campaign" }'
Response
{
"project": {
"id": "1b6d4f90-77c2-4a5e-b3d8-2e9a0c15f7b4",
"name": "Autumn Campaign",
"description": null,
"avatarUrl": null,
"role": "owner",
"owner": { "email": "you@example.com", "name": "You" },
"createdAt": "2026-07-28T19:00:01.220Z",
"updatedAt": "2026-07-28T19:00:01.220Z"
},
"members": [
{ "userId": "3f8c1a92-…", "email": "you@example.com", "name": "You", "role": "owner", "joinedAt": "2026-07-28T19:00:01.244Z" }
],
"invites": []
}

GET /v1/projects

List every project you belong to, most recently updated first, plus invites awaiting your response.

Returns

FieldTypeDescription
projectsobject[]Projects you own or contribute to.
pendingInvitesobject[]Invites addressed to your email, redeemable now.
defaultProjectIdstringYour undeletable default project.
Request
curl -s https://api.corte.so/v1/projects \
-H "Authorization: Bearer corte_sk_…"
Response
{
"projects": [
{
"id": "1b6d4f90-77c2-4a5e-b3d8-2e9a0c15f7b4",
"name": "Autumn Campaign",
"description": "Everything for the September drop",
"avatarUrl": null,
"role": "owner",
"owner": { "email": "you@example.com", "name": "You" },
"createdAt": "2026-07-28T19:00:01.220Z",
"updatedAt": "2026-07-28T19:14:52.881Z"
}
],
"pendingInvites": [
{
"id": "0a5e2c71-…",
"projectId": "88b3d049-…",
"projectName": "Client — Northwind",
"email": "you@example.com",
"invitedBy": { "email": "lead@agency.com", "name": "Sam" },
"createdAt": "2026-07-26T10:22:14.003Z"
}
],
"defaultProjectId": "9d47f2b8-1e60-4c3a-a5f9-70b8e14c2a6d"
}

GET /v1/projects/:id

Fetch one project with its member list. Requires membership.

Returns { project, members, invites? }. invites appears only when you're the owner.

Request
curl -s https://api.corte.so/v1/projects/1b6d4f90-… \
-H "Authorization: Bearer corte_sk_…"
Response
{
"project": { "id": "1b6d4f90-…", "name": "Autumn Campaign", "role": "owner", "…": "…" },
"members": [
{ "userId": "3f8c1a92-…", "email": "you@example.com", "name": "You", "role": "owner", "joinedAt": "2026-07-28T19:00:01.244Z" },
{ "userId": "b2e91d07-…", "email": "editor@agency.com", "name": "Jo", "role": "contributor", "joinedAt": "2026-07-28T19:31:07.660Z" }
],
"invites": [
{ "id": "4c7a0e93-…", "projectId": "1b6d4f90-…", "projectName": "Autumn Campaign", "email": "new@agency.com", "invitedBy": { "email": "you@example.com", "name": "You" }, "createdAt": "2026-07-28T19:40:11.512Z" }
]
}

Members are sorted owner-first, then by join time.


PATCH /v1/projects/:id

Update a project. Owner only. Omitted fields are unchanged; null clears a nullable field.

Body

NameTypeDescription
namestring1–200 characters.
descriptionstring | nullUp to 2,000 characters. null clears it.
avatarStorageIdstring | nullA committed image object you can read. null clears the avatar.

Returns { project, members, invites? }.

Request
curl -s -X PATCH https://api.corte.so/v1/projects/1b6d4f90-… \
-H "Authorization: Bearer corte_sk_…" \
-H "Content-Type: application/json" \
-d '{ "description": "Everything for the September drop" }'

DELETE /v1/projects/:id

Delete a project. Owner only. Your default project can't be deleted (forbidden).

This is destructive and cascading. It:

  • cancels every in-flight generation billed to the project,
  • deletes its cuts and their content,
  • unlinks its shared media (the underlying objects survive, owned by whoever uploaded them),
  • deletes members and pending invites,
  • moves each workflow to its creator's default project rather than deleting it — workflows survive, runs follow them.

Returns { "ok": true }.


Invites & members

POST /v1/projects/:id/invites

Invite by email. Owner only. Sends an invite email and returns the pending invite.

Body{ "email": "…" }. Lowercased and validated.

Returns the invite object.

Request
curl -s -X POST https://api.corte.so/v1/projects/1b6d4f90-…/invites \
-H "Authorization: Bearer corte_sk_…" \
-H "Content-Type: application/json" \
-d '{ "email": "editor@agency.com" }'
Response
{
"id": "4c7a0e93-2b18-4f56-90ad-6e1c85f0d372",
"projectId": "1b6d4f90-77c2-4a5e-b3d8-2e9a0c15f7b4",
"projectName": "Autumn Campaign",
"email": "editor@agency.com",
"invitedBy": { "email": "you@example.com", "name": "You" },
"createdAt": "2026-07-28T19:40:11.512Z"
}

invalid_params covers inviting yourself, an existing member, a duplicate invite, and exceeding the 20-member cap.

DELETE /v1/projects/:id/invites/:inviteId

Revoke a pending invite. Owner only. Returns { "ok": true }.

POST /v1/project-invites/:inviteId/accept

Accept an invite addressed to your email. You join as a contributor, and the invite is consumed.

Returns the project object.

Response
{
"id": "88b3d049-…",
"name": "Client — Northwind",
"description": null,
"avatarUrl": null,
"role": "contributor",
"owner": { "email": "lead@agency.com", "name": "Sam" },
"createdAt": "2026-07-20T08:14:00.117Z",
"updatedAt": "2026-07-26T10:22:14.003Z"
}

POST /v1/project-invites/:inviteId/decline

Decline an invite addressed to your email. Returns { "ok": true }.

DELETE /v1/projects/:id/members/:userId

Remove a member, or remove yourself to leave. The owner may remove anyone; a contributor may only remove themselves. The owner can't leave — delete the project instead (forbidden).

Returns { "ok": true }.


Shared media

Uploads are private to the uploader. Linking an object into a project makes it readable by every member — that's what lets a contributor pass your storageId to a generation.

POST /v1/projects/:id/storage

Link committed objects into the project. Any member may call it. Idempotent: already-linked ids are skipped.

Body

NameTypeDescription
storageIdsstring[]1–200 committed object ids. Each must be yours, or already linked to this project.

Returns { "ok": true }.

Request
curl -s -X POST https://api.corte.so/v1/projects/1b6d4f90-…/storage \
-H "Authorization: Bearer corte_sk_…" \
-H "Content-Type: application/json" \
-d '{ "storageIds": ["b0e7c9a1-4d52-4f0e-8c31-77a2e5d9f6b8"] }'

GET /v1/projects/:id/storage

List the project's shared media, newest link first.

Query parameters

NameTypeDefaultDescription
limitnumberunsetPage size, 1500. Omit for the full list (media pickers hydrate everything).
offsetnumber0Only meaningful with limit.
sortstringby newest linkPass size to sort largest-first.
kindstringunsetKeep only one media kind: image, video or audio. Matches the content type's major part.

Returns { "objects": [...], "total": number } — the storage object shape plus addedByUserId. total counts the objects after kind filtering, so paging through a filtered list is safe.

Response
{
"objects": [
{
"storageId": "b0e7c9a1-4d52-4f0e-8c31-77a2e5d9f6b8",
"filename": "trail-shoe.jpg",
"contentType": "image/jpeg",
"sizeBytes": 482113,
"origin": "upload",
"url": "https://media.corte.so/…?X-Amz-Signature=…",
"createdAt": "2026-07-28T15:02:44.881Z",
"addedByUserId": "3f8c1a92-b5d7-4e60-9c14-8a72d0e5b391"
}
],
"total": 18
}

GET /v1/projects/:id/usage

The project's credit ledger, plus per-actor spend totals. Paginated.

Owners see every member's entries. Contributors see only their own — the same filter applies to totalsByActor.

Query parameters

NameTypeDefaultDescription
limitnumber50Max 200.
offsetnumber0

Returns

FieldTypeDescription
entriesobject[]Ledger rows, newest first. Same shape as GET /v1/usage, always with actorUserId and actorEmail.
totalsByActorobject[]{ actorUserId, actorEmail, spentCredits } over the project's whole history, not just this page.
totalnumberLedger rows visible to you.
Request
curl -s "https://api.corte.so/v1/projects/1b6d4f90-…/usage?limit=2" \
-H "Authorization: Bearer corte_sk_…"
Response
{
"entries": [
{
"id": "aa41e0c7-…",
"kind": "generation",
"deltaCredits": -85,
"ref": "5f2b9c14-…",
"note": "kling-3",
"createdAt": "2026-07-28T14:04:47.019Z",
"projectId": "1b6d4f90-77c2-4a5e-b3d8-2e9a0c15f7b4",
"actorUserId": "b2e91d07-…",
"actorEmail": "editor@agency.com",
"previewUrl": "https://media.corte.so/…?X-Amz-Signature=…",
"previewKind": "video"
},
{
"id": "7d02b91f-…",
"kind": "generation",
"deltaCredits": -12,
"ref": "a91c2d7e-…",
"note": "nano-banana-2",
"createdAt": "2026-07-28T13:51:02.664Z",
"projectId": "1b6d4f90-77c2-4a5e-b3d8-2e9a0c15f7b4",
"actorUserId": "3f8c1a92-…",
"actorEmail": "you@example.com",
"previewUrl": null,
"previewKind": null
}
],
"totalsByActor": [
{ "actorUserId": "b2e91d07-…", "actorEmail": "editor@agency.com", "spentCredits": 1840 },
{ "actorUserId": "3f8c1a92-…", "actorEmail": "you@example.com", "spentCredits": 612 }
],
"total": 47
}

Billing to a project

projectId is accepted by POST /v1/generations, POST /v1/applications/:id/run, POST /v1/transcriptions, and POST /v1/workflows/:id/execute.

Passing it means the owner pays and you're recorded as the actor. If the owner's balance is short, the error is owner_insufficient_credits (402) — and only the owner can resolve it.