Projects
Generated HTTP reference for the 8 operations the published OpenAPI document describes under projects.
Projects
This page is generated from the published OpenAPI document. It is complete with respect to that document and says nothing about surfaces the document does not describe yet. See what is generated here for what that means.
Base URL: https://api.codespar.dev
Every operation below requires a Bearer token. See Authentication.
The Projects API manages the second level of CodeSpar's tenancy model: Account -> Project. See the Projects concept for the full model.
Base URL: https://api.codespar.dev
All endpoints require authentication via Bearer token. See Authentication.
Project object
| Field | Type | Description |
|---|---|---|
id | string | Project ID in the form prj_<16chars> |
org_id | string | Parent account ID |
name | string | Display name (free-form) |
slug | string | URL-safe identifier, unique per account |
is_default | boolean | true for the account's default project (exactly one per account) |
created_at | string | ISO 8601 timestamp |
Slug rules
- Lowercase alphanumeric characters plus
_and- - Max length: 64 characters
defaultis reserved (only the auto-created default project uses it)- Must be unique within the account
Violations return slug_invalid, slug_reserved, or slug_conflict (see Errors).
GET /v1/projects
Lists all projects in the authenticated account.
Auth required: Yes
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
is_default | boolean | -- | Filter to only the default project (true) or non-default projects (false) |
limit | number | 50 | Results per page (max 100) |
offset | number | 0 | Pagination offset |
curl example
curl https://api.codespar.dev/v1/projects \
-H "Authorization: Bearer csk_live_abc123..."Response -- 200 OK
{
"data": [
{
"id": "prj_a1b2c3d4e5f6g7h8",
"org_id": "org_xyz789",
"name": "Default",
"slug": "default",
"is_default": true,
"created_at": "2026-04-01T10:00:00Z"
},
{
"id": "prj_i9j0k1l2m3n4o5p6",
"org_id": "org_xyz789",
"name": "Staging",
"slug": "staging",
"is_default": false,
"created_at": "2026-04-15T09:12:00Z"
}
],
"total": 2,
"limit": 50,
"offset": 0
}GET /v1/projects/:id
Retrieves a single project by ID.
Auth required: Yes
curl example
curl https://api.codespar.dev/v1/projects/prj_a1b2c3d4e5f6g7h8 \
-H "Authorization: Bearer csk_live_abc123..."Response -- 200 OK
{
"id": "prj_a1b2c3d4e5f6g7h8",
"org_id": "org_xyz789",
"name": "Default",
"slug": "default",
"is_default": true,
"created_at": "2026-04-01T10:00:00Z"
}POST /v1/projects
Creates a new project in the authenticated account.
Auth required: Yes
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
slug | string | Yes | URL-safe identifier (see slug rules) |
New projects are always created with is_default: false. To promote a project to default, use PATCH /v1/projects/:id with {"is_default": true}.
curl example
curl -X POST https://api.codespar.dev/v1/projects \
-H "Authorization: Bearer csk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "Production",
"slug": "prod"
}'Response -- 201 Created
{
"id": "prj_q7r8s9t0u1v2w3x4",
"org_id": "org_xyz789",
"name": "Production",
"slug": "prod",
"is_default": false,
"created_at": "2026-04-20T14:22:00Z"
}PATCH /v1/projects/:id
Updates a project's name, slug, or default status.
Auth required: Yes
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | New display name |
slug | string | No | New slug (subject to slug rules) |
is_default | boolean | No | Set to true to promote this project to default. Atomic: the previous default is demoted in the same transaction. |
is_default can only be set to true. You cannot un-set it directly -- to change the default, promote a different project instead. Sending {"is_default": false} on the current default project is a no-op or error (exact behavior: TODO -- confirm).
curl example -- promote to default
curl -X PATCH https://api.codespar.dev/v1/projects/prj_q7r8s9t0u1v2w3x4 \
-H "Authorization: Bearer csk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{"is_default": true}'Response -- 200 OK
{
"id": "prj_q7r8s9t0u1v2w3x4",
"org_id": "org_xyz789",
"name": "Production",
"slug": "prod",
"is_default": true,
"created_at": "2026-04-20T14:22:00Z"
}DELETE /v1/projects/:id
Deletes a project and all of its scoped resources (API keys, triggers, session history, connection records).
Auth required: Yes
Deletion cascades. There is no undo. Export audit logs first if you need to retain them.
curl example
curl -X DELETE https://api.codespar.dev/v1/projects/prj_q7r8s9t0u1v2w3x4 \
-H "Authorization: Bearer csk_live_abc123..."Response -- 200 OK
{
"id": "prj_q7r8s9t0u1v2w3x4",
"deleted": true
}Deletion fails with cannot_delete_default or cannot_delete_last_project (see below).
Project members (RBAC overrides)
Every user with account membership inherits a base role (owner, admin, member) that applies across every project in that account. The four endpoints below let an account admin narrow or widen that role for a specific project — useful when a contractor should see exactly one project, or a member needs admin rights on staging without becoming a full account admin.
A user absent from project_members inherits their account-level role unchanged. Explicit rows are overrides, not the source of truth.
Not reachable with an API key — use the dashboard
The four member endpoints live in the API's service-auth subtree
(requireServiceAuth), alongside api-keys, usage, billing and team. A csk_
Bearer key is not read at all there, so a Bearer call answers
401 {"error":"unauthorized"} — not a 403 and not a 404.
There is no key we can issue that changes this: the credential for that subtree is CodeSpar's own platform secret, and possession of it is the trust boundary for every account, so it never leaves our infrastructure. The dashboard is the supported surface. The request and response shapes below describe what it sends and renders.
GET /v1/projects/:id/members
Lists every user with effective access to the project, including account-level inherited members. The source field tells you whether the role came from a project override or from the account membership.
GET /v1/projects/prj_a1b2c3d4e5f6g7h8/membersResponse — 200 OK
{
"members": [
{
"user_id": "user_owner123",
"role": "owner",
"source": "org",
"email": "founder@codespar.dev",
"display_name": "Founder",
"avatar_url": null,
"added_at": "2026-04-15T10:00:00Z"
},
{
"user_id": "user_contractor456",
"role": "admin",
"source": "project",
"email": "ana@contractor.com",
"display_name": "Ana",
"avatar_url": null,
"added_at": "2026-04-22T18:30:00Z"
}
]
}Sorted: owner first, then admin, then member — oldest first within each tier.
POST /v1/projects/:id/members
Adds or upserts a project-level override. Requires account-level admin or owner.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
user_id | string | Yes | Must already be a member of the account. |
role | admin | member | Yes | Cannot override an account owner — they always retain full access. |
POST /v1/projects/prj_.../members
Content-Type: application/json
{"user_id": "user_contractor456", "role": "admin"}PATCH /v1/projects/:id/members/:user_id
Updates an existing override's role. Same gate (admin+ at account level).
PATCH /v1/projects/prj_.../members/user_contractor456
Content-Type: application/json
{"role": "member"}DELETE /v1/projects/:id/members/:user_id
Removes the override. The user falls back to their account-level role for this project.
DELETE /v1/projects/prj_.../members/user_contractor456Member-related error codes
| Status | Error code | When |
|---|---|---|
401 | unauthorized | Called with a Bearer key, or with no/invalid service key. Bare-string shape |
403 | insufficient_role | POST/PATCH/DELETE by a non-admin account member. Flat shape: {"error":"insufficient_role","required":"admin"} |
404 | not_found | The project id does not exist in this account, or a PATCH named a user with no override row |
404 | user_not_in_org | user_id is not a member of this account |
409 | cannot_override_owner | The target user is the account owner; an owner's role cannot be narrowed per project |
DELETE is idempotent: removing an override that does not exist answers 204,
not 404 — the end state is the same, the user inherits their account role.
Errors
All endpoints follow the standard error format:
{
"error": "error_code",
"message": "Human-readable error description.",
"status": 400
}Project-specific error codes
| Status | Error code | When |
|---|---|---|
400 | slug_invalid | Slug contains disallowed characters, exceeds 64 chars, or is empty |
400 | slug_reserved | Slug is default (reserved for the auto-created default project) |
409 | slug_conflict | Another project in the account already uses this slug |
409 | cannot_delete_default | DELETE on the default project. Promote another project first. |
409 | cannot_delete_last_project | DELETE on the only remaining project. Every account must have at least one project. |
Standard error codes
| Status | Error code | When |
|---|---|---|
401 | unauthorized | Invalid or missing API key |
403 | forbidden | API key lacks permission (e.g. a project-scoped key trying to manage a different project) |
404 | not_found | Project ID does not exist or does not belong to the authenticated account |
429 | rate_limited | Too many requests |
500 | internal_error | Server error. Retry with exponential backoff. |
Next steps
Every operation, from the spec
Generated from the published OpenAPI document, so it never drifts from what the API actually serves. The section above is written by hand and carries what a schema cannot: the object model, field rules, and the order to call things in.
GET /v1/projects
List projects
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
projects | array of object | yes | — |
Example response
{
"projects": [
{
"id": "obj_0000000000000000",
"org_id": "org_0000000000000000",
"name": "Example",
"slug": "example",
"is_default": true,
"environment": "live",
"created_at": "2026-01-15T12:00:00.000Z"
}
]
}Example request
curl -X GET https://api.codespar.dev/v1/projects \
-H "Authorization: Bearer $CODESPAR_API_KEY"POST /v1/projects
Create a project
Request body
| Field | Type | Required | Description |
|---|---|---|---|
environment | "live" | "test" | no | — |
name | string | yes | — |
settings | object | no | — |
slug | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
201 | object | Created |
400 | object | invalid_body when the body or an initial setting fails validation (details.issues for the body, details.key for a setting). slug_conflict when the slug is already taken in this organization. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
created_at | string (date-time) | yes | — |
environment | "live" | "test" | yes | Fixed at creation. There is no field to change it afterwards. |
id | string | yes | prj_ followed by a 16 character id. |
is_default | boolean | yes | At most one project per organization carries true, held by a partial unique index on the table. At LEAST one is not guaranteed by any constraint, and nothing in this API creates one: a project created through this API is always false, and the organization's first project is seeded elsewhere. |
name | string | yes | — |
org_id | string | yes | — |
slug | string | yes | Unique within the organization. |
Example response
{
"id": "obj_0000000000000000",
"org_id": "org_0000000000000000",
"name": "Example",
"slug": "example",
"is_default": true,
"environment": "live",
"created_at": "2026-01-15T12:00:00.000Z"
}Example request
curl -X POST https://api.codespar.dev/v1/projects \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example",
"slug": "example",
"environment": "live",
"settings": {}
}'GET /v1/projects/{id}
Read one project
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | Project id. An id from another organization reads as absent. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | not_found. Also the answer for an id owned by another organization. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
created_at | string (date-time) | yes | — |
environment | "live" | "test" | yes | Fixed at creation. There is no field to change it afterwards. |
id | string | yes | prj_ followed by a 16 character id. |
is_default | boolean | yes | At most one project per organization carries true, held by a partial unique index on the table. At LEAST one is not guaranteed by any constraint, and nothing in this API creates one: a project created through this API is always false, and the organization's first project is seeded elsewhere. |
name | string | yes | — |
org_id | string | yes | — |
slug | string | yes | Unique within the organization. |
Example response
{
"id": "obj_0000000000000000",
"org_id": "org_0000000000000000",
"name": "Example",
"slug": "example",
"is_default": true,
"environment": "live",
"created_at": "2026-01-15T12:00:00.000Z"
}Example request
curl -X GET https://api.codespar.dev/v1/projects/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"PATCH /v1/projects/{id}
Rename a project, change its slug, or promote it to default
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | Project id. An id from another organization reads as absent. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
is_default | true | no | — |
name | string | no | — |
slug | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_body when the patch is empty or a field fails validation (details.issues). slug_conflict when the slug is taken, or when a concurrent promotion collided (details.slug null). |
404 | object | not_found. Also the answer for an id owned by another organization. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
created_at | string (date-time) | yes | — |
environment | "live" | "test" | yes | Fixed at creation. There is no field to change it afterwards. |
id | string | yes | prj_ followed by a 16 character id. |
is_default | boolean | yes | At most one project per organization carries true, held by a partial unique index on the table. At LEAST one is not guaranteed by any constraint, and nothing in this API creates one: a project created through this API is always false, and the organization's first project is seeded elsewhere. |
name | string | yes | — |
org_id | string | yes | — |
slug | string | yes | Unique within the organization. |
Example response
{
"id": "obj_0000000000000000",
"org_id": "org_0000000000000000",
"name": "Example",
"slug": "example",
"is_default": true,
"environment": "live",
"created_at": "2026-01-15T12:00:00.000Z"
}Example request
curl -X PATCH https://api.codespar.dev/v1/projects/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example",
"slug": "example",
"is_default": true
}'DELETE /v1/projects/{id}
Delete a project
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | Project id. An id from another organization reads as absent. |
Responses
| Status | Body | Description |
|---|---|---|
204 | — | No Content |
404 | object | not_found. Also the answer for an id owned by another organization. |
409 | object | The delete conflicts with an invariant. details.project_id on all three; details.blocked_by lists \{ table, rows \} on the consumer-records refusal. |
Example request
curl -X DELETE https://api.codespar.dev/v1/projects/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/projects/{id}/settings
Read a project's effective settings
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | Project id. An id from another organization reads as absent. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | not_found. Also the answer for an id owned by another organization. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
settings | array of object | yes | — |
Example response
{
"settings": [
{
"key": "string",
"type": "boolean",
"description": "string",
"explicit": true,
"freshness": "request",
"payee_affecting": true,
"enum_values": [
"string"
]
}
]
}Example request
curl -X GET https://api.codespar.dev/v1/projects/{id}/settings \
-H "Authorization: Bearer $CODESPAR_API_KEY"PATCH /v1/projects/{id}/settings
Set or reset a project's settings
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | Project id. An id from another organization reads as absent. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
settings | object | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_body: the body did not match the schema (details.issues), the patch was empty, or a key failed the registry check (details.key). |
404 | object | not_found. Also the answer for an id owned by another organization. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
settings | array of object | yes | — |
Example response
{
"settings": [
{
"key": "string",
"type": "boolean",
"description": "string",
"explicit": true,
"freshness": "request",
"payee_affecting": true,
"enum_values": [
"string"
]
}
]
}Example request
curl -X PATCH https://api.codespar.dev/v1/projects/{id}/settings \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"settings": {}
}'GET /v1/projects/{id}/settings/history
Read a project's settings audit log
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | Project id. An id from another organization reads as absent. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | not_found. Also the answer for an id owned by another organization. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
history | array of object | yes | — |
Example response
{
"history": [
{
"setting_key": "string",
"actor": "string",
"explicit_before": true,
"explicit_after": true,
"created_at": "string"
}
]
}Example request
curl -X GET https://api.codespar.dev/v1/projects/{id}/settings/history \
-H "Authorization: Bearer $CODESPAR_API_KEY"