shebang!

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_scope for an app key).
  • Response200 { "projects": [Project, ...] }.

POST /api/platform/v1/projects

Creates a project.

  • Auth — master only.
  • Request bodyname (string, required), tag (string, required), color (string, required — one of the curated palette values), slug (string, required), oauth_client_id (string or null, optional).
  • Response201 { "project": Project }.
  • Errors400 invalid_project (missing/invalid field, with message), 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 (id must equal the key's projectId).
  • Path paramsid (uuid).
  • Response200 { "project": Project }.
  • Errors404 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 paramsid (uuid).
  • Request body — any of name, tag, color, slug (strings), oauth_client_id (string or null), status ("active" or "archived") — all optional, only supplied fields change.
  • Response200 { "project": Project }.
  • Errors400 invalid_project, 400 invalid_slug, 409 slug_taken, 409 client_already_attached (the named oauth_client_id is 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 paramsid (uuid).
  • Request bodyconfirm (string, required, must equal id), move_to (string, optional — another of the account's project slugs).
  • Response204 (empty body).
  • Errors400 (confirm doesn't match id), 400 invalid_body (move_to doesn'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 paramsid (uuid).
  • Response200 { "resources": {...} } (per-resource-type arrays; each sherbase database entry includes its own createdAt).
  • Errors404 not_found.

POST /api/platform/v1/projects/{id}/assign

Assigns an existing resource to project {id}.

  • Auth — master only.
  • Path paramsid (uuid, the destination project).
  • Request bodyresource_type (one of page, object, link, database, key, required), resource_id (uuid, required).
  • Response204 (empty body).
  • Errors400 invalid_resource_type, 404 not_found (malformed id/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 paramsid (uuid — validated only, not the destination).
  • Request body — same shape as assign: resource_type, resource_id.
  • Response204 (empty body).
  • Errors400 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 paramsid (uuid, the project being granted).
  • Request bodykey_id (string, required), level ("read" or "full", required).
  • Response201 { "grant": { "keyId", "projectId", "level", "createdAt" } }. Calling again with a different level updates the existing grant (one grant per key/project pair).
  • Errors400 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 POST above).
  • Path paramsid (uuid).
  • Request bodykey_id (string, required). A level field, if present, is accepted and ignored.
  • Response204 (empty body).
  • Errors400 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.
  • Response200 { "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 bodyname (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_id is no longer accepted.
  • Response201 { "key", "keyId", "keyPrefix", "tier": "app", "apps", "name", "projectId", "project" }. key is the plaintext shb_... credential, returned exactly once — nothing server-side keeps it past this response.
  • Errors400 (invalid name or apps), 400 invalid_body (malformed or unsupported project/app_id), 403 insufficient_scope (project names 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 paramsid (uuid).
  • Response204 (empty body).
  • Errors400 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.
  • Response200 { "clients": [...] }.
  • Errors403 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 GET above.
  • Request bodyname (string, 1–60 chars after trimming, required), redirect_uris (array of strings, required), client_type ("confidential" or "public", required).
  • Response201 { "client": {...} }.
  • Errors400 (invalid name, redirect_uris, or client_type, or a redirect URI that fails validation), 403 insufficient_scope, 403 forbidden.

Email

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 email app scope to grant an app key).
  • Request bodyto (string, required), subject (string, required), text (string, required — never echoed back or logged).
  • Response200 { "sent": true, "resend_id", "remaining_today" }.
  • Errors400 invalid_email_input, 429 email_quota_exceeded (with retry_after_hours), 502 send_failed.

Next

sherpage — pages.