Projects
GET/POST /api/v1/projects — list what a key can see, or create a project with an org-wide 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> — any key format works. A project key
returns an array with just its own project. An org/scoped key returns
every project its scope resolves to.
{
"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
Org-wide keys only
Only a key with scope: "*" and an org_id can create a project. A
project key, or a scoped key limited to specific projects/workspaces,
gets 403 here even though it can authenticate fine against every other
endpoint on this site.
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 cannot create projects; mint an org-wide (*) key | Key is a project key, or a scoped (non-*) key. |
| 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. |
| 409 | a project with slug "<slug>" already exists in this org | Slug collision within the 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 jwt_…" \
-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 the difference between a project
key and an org-wide key.