Projects
GET/POST /api/v1/projects — list what a key can see, or create a project with an all-projects key.
GET /api/v1/projects and
POST /api/v1/projects
The project surface: see what a key can act as, or — with the right key —
create a new one. Session-cookie dashboard users have a separate route for
this (/api/orgs/:id/projects); this page is the key-authed one.
List projects
Authorization: Bearer <key> and projects:read. Returns every project the
key's scope resolves to — one project for a key scoped to a specific list of
just one, more for a key scoped to several projects or to all of them.
{
"projects": [
{ "id": "project-uuid", "org_id": "org-uuid", "name": "Marketing", "slug": "marketing", "created_at": "2026-01-10T00:00:00.000Z" }
]
}A key with no projects in scope gets { "projects": [] }, not an error.
curl https://kontacts.dev/api/v1/projects \ -H "Authorization: Bearer gm_live_…"
Create a project
Needs projects:write AND an all-projects key
Two separate requirements, both enforced: the key needs projects ≥
write, and it must be scoped to all projects in the workspace
(all_projects: true). A key pinned to a specific project list gets 403
here even with projects:write and even though it can authenticate fine
against every other endpoint on this site — a key scoped to fixed projects
has no say over a project that doesn't exist yet.
Request body
| Field | Required |
|---|---|
name | yes |
slug | no |
slug is derived from name (slugified) when omitted. Both are read as
plain strings from the JSON body — there's nothing else to configure at
creation; sending identity is set up as a best-effort side step of the
create call and isn't guaranteed to exist yet by the time the response comes
back.
Response
201 Created:
{
"project": {
"id": "project-uuid",
"org_id": "org-uuid",
"name": "Marketing",
"slug": "marketing",
"created_at": "2026-09-05T12:00:00.000Z"
}
}Errors
| Status | Body error | Cause |
|---|---|---|
| 401 | missing or malformed API key | No Authorization header, or it isn't Bearer <key>. |
| 401 | invalid API key | The key doesn't match any stored hash. |
| 401 | API key has been revoked | revoked_at is set. |
| 403 | this key lacks projects:write | Key doesn't hold projects ≥ write. |
| 403 | this key cannot create projects; mint one scoped to all projects in the workspace | Key holds projects:write but is scoped to a specific project list, not all projects. |
| 400 | name is required | Empty or missing name. |
| 400 | name must contain at least one alphanumeric character | name/slug slugifies to an empty string. |
| 403 | plan-specific, e.g. "The free plan includes 2 projects. Upgrade to add another." | Organisation is at its max_projects ceiling for its plan. |
| 400 | that slug is reserved | The slug shadows a reserved path such as api, dashboard or login. |
| 409 | slug "<slug>" is already taken | Slug collision. Slugs are unique across all of kontacts.dev, not just your organisation. |
| 500 | (unhandled) | Lookup failure or an unexpected error rethrown as a generic 500. |
Error strings are exact (aside from the plan-limit message, which
substitutes the org's actual limit and plan name), read from
apps/web/app/api/v1/projects/route.ts at the time this page was written.
The per-plan project cap is Free 2 / Indie Hacker 10 / Enterprise
unlimited — apps/web/lib/plans.ts, max_projects.
curl -X POST https://kontacts.dev/api/v1/projects \
-H "Authorization: Bearer gm_live_…" \
-H "Content-Type: application/json" \
-d '{"name":"Marketing"}'
Keys
Both list and create use the same keys as every other endpoint on this
site, minted under Settings → API keys
(/dashboard/settings/api-keys). See
Authentication for project reach, the projects
permission, and what an all-projects key is.