platform
The v1 reference for projects, keys, OAuth clients, and outbound email.
platform
Base path /api/platform/v1. See API reference for
authentication and the api.shebang.pro rewrite.
Most routes here take an optional project query parameter or body field
— a project slug (not id) — naming which of the account's projects the
call targets. Omitted, it defaults to the calling key's home project. A
write (create/update/delete/provision) against a named project requires
either the key's home project or a full grant on it — a read grant
only covers list/fetch. Naming a project the key can't reach at all
(wrong account, or no grant) is 403 insufficient_scope, never 404 —
existence is never leaked to a caller who can't reach it.
The Project object every project route returns:
{
"id": "uuid",
"name": "string",
"tag": "string",
"color": "string",
"slug": "string",
"oauthClientId": "string | null",
"status": "active | archived",
"createdAt": "ISO 8601",
"updatedAt": "ISO 8601"
}
Projects
GET /api/platform/v1/projects
Lists every project on the caller's account.
- Auth — master key or bearer only (
403 insufficient_scopefor an app key). - Response —
200 { "projects": [Project, ...] }.
POST /api/platform/v1/projects
Creates a project.
- Auth — master only.
- Request body —
name(string, required),tag(string, required),color(string, required — one of the curated palette values),slug(string, required),oauth_client_id(string ornull, optional). - Response —
201 { "project": Project }. - Errors —
400 invalid_project(missing/invalid field, withmessage),400 invalid_slug,409 slug_taken.
GET /api/platform/v1/projects/{id}
Fetches one project.
- Auth — master (any of the account's projects), or an app key
reading its own home project (
idmust equal the key'sprojectId). - Path params —
id(uuid). - Response —
200 { "project": Project }. - Errors —
404 not_found(malformed id, unowned project, or an app key naming a project other than its own).
PATCH /api/platform/v1/projects/{id}
Updates a project.
- Auth — master only.
- Path params —
id(uuid). - Request body — any of
name,tag,color,slug(strings),oauth_client_id(string ornull),status("active"or"archived") — all optional, only supplied fields change. - Response —
200 { "project": Project }. - Errors —
400 invalid_project,400 invalid_slug,409 slug_taken,409 client_already_attached(the namedoauth_client_idis already attached to a different project),404 not_found.
DELETE /api/platform/v1/projects/{id}
Deletes a project row. Confirm-guarded — the project itself must be
empty (no keys, pages, objects, links, or sherbase databases assigned to
it) unless move_to names another of the account's projects to move its
contents into first.
- Auth — master only.
- Path params —
id(uuid). - Request body —
confirm(string, required, must equalid),move_to(string, optional — another of the account's project slugs). - Response —
204(empty body). - Errors —
400(confirmdoesn't matchid),400 invalid_body(move_todoesn't name an owned, different project),409 project_not_empty,409 last_project(the account's only project),404 not_found.
GET /api/platform/v1/projects/{id}/resources
Lists everything assigned to a project — pages, objects, links, databases, keys.
- Auth — same read access as
GET /api/platform/v1/projects/{id}. - Path params —
id(uuid). - Response —
200 { "resources": {...} }(per-resource-type arrays; each sherbase database entry includes its owncreatedAt). - Errors —
404 not_found.
POST /api/platform/v1/projects/{id}/assign
Assigns an existing resource to project {id}.
- Auth — master only.
- Path params —
id(uuid, the destination project). - Request body —
resource_type(one ofpage,object,link,database,key, required),resource_id(uuid, required). - Response —
204(empty body). - Errors —
400 invalid_resource_type,404 not_found(malformedid/resource_id, or either doesn't belong to the caller's account).
POST /api/platform/v1/projects/{id}/unassign
Moves a resource to the account's default project — {id} in the
path is validated as a well-formed id but not otherwise consulted; the
destination is always the default project, not {id}.
- Auth — master only.
- Path params —
id(uuid — validated only, not the destination). - Request body — same shape as
assign:resource_type,resource_id. - Response —
204(empty body). - Errors —
400 invalid_resource_type,404 not_found.
POST /api/platform/v1/projects/{id}/grants
Grants a key access to project {id} beyond its own home project.
- Auth — master only (an OAuth-client bearer principal gets
403 insufficient_scope— it can never mint a grant). - Path params —
id(uuid, the project being granted). - Request body —
key_id(string, required),level("read"or"full", required). - Response —
201 { "grant": { "keyId", "projectId", "level", "createdAt" } }. Calling again with a differentlevelupdates the existing grant (one grant per key/project pair). - Errors —
400 invalid_body,404 not_found(key or project not owned by the caller),403 insufficient_scope(bearer principal).
DELETE /api/platform/v1/projects/{id}/grants
Revokes a key's grant on project {id}.
- Auth — master only (same bearer-principal restriction as
POSTabove). - Path params —
id(uuid). - Request body —
key_id(string, required). Alevelfield, if present, is accepted and ignored. - Response —
204(empty body). - Errors —
400 invalid_body,404 not_found,403 insufficient_scope(bearer principal).
Keys
GET /api/platform/v1/keys
Lists every key on the caller's account.
- Auth — master only.
- Response —
200 { "keys": [{ ..., "project": "slug | null" }, ...] }— every stored key field plus the home project's slug.
POST /api/platform/v1/keys
Mints a new app key (never a master — the only paths to a master key are the device-login approve flow and the dashboard's own "Create key" button).
- Auth — master only (an OAuth-client bearer principal gets
403 insufficient_scope— it can never mint a key). - Request body —
name(string, 1–60 chars, required),apps(non-empty array of"page" | "serve" | "link" | "base", required),project(string, optional — a project slug; defaults to the account's default project if omitted).app_idis no longer accepted. - Response —
201 { "key", "keyId", "keyPrefix", "tier": "app", "apps", "name", "projectId", "project" }.keyis the plaintextshb_...credential, returned exactly once — nothing server-side keeps it past this response. - Errors —
400(invalidnameorapps),400 invalid_body(malformed or unsupportedproject/app_id),403 insufficient_scope(projectnames a slug the caller doesn't own, or a bearer principal).
DELETE /api/platform/v1/keys/{id}
Revokes a key.
- Auth — master only. A master may revoke any key on the account, including another master, except the key currently authenticating this very call.
- Path params —
id(uuid). - Response —
204(empty body). - Errors —
400 cannot_revoke_current_key,404 not_found.
OAuth clients
Gated to one operator account (PLATFORM_ADMIN_USER_ID) — every other
master key gets the same 403 a non-master key would.
GET /api/platform/v1/oauth-clients
Lists every registered OAuth client.
- Auth — master, and the key's account must be the platform admin account.
- Response —
200 { "clients": [...] }. - Errors —
403 insufficient_scope(non-master),403 forbidden(master, but not the admin account).
POST /api/platform/v1/oauth-clients
Registers a new OAuth client.
- Auth — same admin-only gate as
GETabove. - Request body —
name(string, 1–60 chars after trimming, required),redirect_uris(array of strings, required),client_type("confidential"or"public", required). - Response —
201 { "client": {...} }. - Errors —
400(invalidname,redirect_uris, orclient_type, or a redirect URI that fails validation),403 insufficient_scope,403 forbidden.
POST /api/platform/v1/email
Sends one plain-text email on the caller's behalf (fixed from-address, reply-to the caller, a 10-per-user rolling-24h quota).
- Auth — master only (there is no
emailapp scope to grant an app key). - Request body —
to(string, required),subject(string, required),text(string, required — never echoed back or logged). - Response —
200 { "sent": true, "resend_id", "remaining_today" }. - Errors —
400 invalid_email_input,429 email_quota_exceeded(withretry_after_hours),502 send_failed.
Next
sherpage — pages.