Workspaces
The vendor's account: created once, then read and updated.
packages/api/src/resources/workspaces.controller.ts by packages/docs/tools/generate-reference.mjs. Every example is a real exchange recorded against the control plane by packages/api/src/testing/reference-examples.spec.ts, with ids from that run. Credentials in the formats this product issues (kr_, krsk_, krses_, JWTs) and values under credential-named fields (password, *_secret, *_token, *_key, lookup_hash) are elided at record time; the recorded file and every page of this site are swept for credential shapes (kr_, krsk_, krses_, JWTs, URLs carrying a password, password fields) before either is accepted. Do not edit by hand: src/reference.spec.ts regenerates it and fails on a difference.POST /v1/workspaces
The one endpoint a secret key cannot authenticate, because it is what mints
the first one. The workspace identifier is generated first so the insert
can run under the workspace's own scope: workspace's policy is on id,
so a row can only be created by a session already scoped to it -- which
means even this handler never needs an owner connection.
Two credentials reach it (WorkspaceCreationGuard), and they produce
deliberately different workspaces. A person becomes its owner, which is
the self-serve path a dashboard needs. The bootstrap token creates one
with no members at all, which is what a scripted deployment wants and what
week 2 shipped -- a seeded workspace whose first human arrives by invitation.
A person whose session was in no workspace lands in the one they just made,
exactly as POST /v1/auth/invites/accept does. One already working
somewhere is not moved out from under themselves.
Answers 201 on success.
Authentication.
- A signed-in person's
krses_session (who becomes the owner), orKEYRING_BOOTSTRAP_TOKEN(a workspace with no members).
Body. Validated by this schema, from the control plane's own source:
const CreateWorkspace = z.object({
name: Name,
slug: Slug,
billing_email: z.email().optional(),
env: Env.default('live'),
});Example.
curl https://keyring-api.belghalem.fr/v1/workspaces \
-X POST \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Payments",
"slug": "acme-payments",
"billing_email": "billing@acme.example"
}'HTTP/1.1 201 Created
Content-Type: application/json
{
"workspace": {
"object": "workspace",
"id": "01a0ade7-b8df-722f-854f-ebac1cc717a7",
"name": "Acme Payments",
"slug": "acme-payments",
"billing_email": "billing@acme.example",
"embed_origins": [],
"created_at": "2026-09-17T05:47:19.647Z",
"updated_at": "2026-09-17T05:47:19.647Z"
},
"secret_key": {
"object": "secret_key",
"id": "01a0ade7-b8e3-7531-bbd6-1fa72692a681",
"env": "live",
"display_prefix": "krsk_live_HSnKWG",
"name": "Bootstrap secret key",
"meta": {},
"expires_at": null,
"revoked_at": null,
"revoked_reason": null,
"quarantined_at": null,
"quarantine_reason": null,
"rotated_from": null,
"created_at": "2026-09-17T05:47:19.647Z",
"last_used_at": null,
"key": "krsk_live_HSnKWG…",
"key_shown_once": true
},
"membership": {
"user_id": "01a0ade7-b878-7739-871a-8faaa66097ed",
"role": "owner"
}
}GET /v1/workspace
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Example.
curl https://keyring-api.belghalem.fr/v1/workspace \
-H "Authorization: Bearer $KEYRING_SECRET_KEY"HTTP/1.1 200 OK
Content-Type: application/json
{
"object": "workspace",
"id": "01a0ade7-b8df-722f-854f-ebac1cc717a7",
"name": "Acme Payments",
"slug": "acme-payments",
"billing_email": "billing@acme.example",
"embed_origins": [],
"created_at": "2026-09-17T05:47:19.647Z",
"updated_at": "2026-09-17T05:47:19.647Z"
}Example: With a revoked secret key.
curl https://keyring-api.belghalem.fr/v1/workspace \
-H "Authorization: Bearer $KEYRING_SECRET_KEY"HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": {
"code": "invalid_key",
"message": "Invalid API key."
}
}PATCH /v1/workspace
Answers 200 on success.
Authentication.
- A
krsk_secret key or akrses_dashboard session, asAuthorization: Bearer.
Body. Validated by this schema, from the control plane's own source:
const UpdateWorkspace = z.object({
name: Name.optional(),
billing_email: z.email().nullable().optional(),
embed_origins: EmbedOrigins.optional(),
});Report section 7.3: the origins an embed token may be presented from. The
database checks the same shape (keyring.embed_origins_valid, migration
0010) and keeps checking it whatever writes the column; this is here so a bad
entry is a 400 naming the field rather than a 500 wrapping a constraint
violation.
A serialised origin and nothing else: no path, no trailing slash, no *.
Anything else compares unequal to what a browser puts in Origin, so
registering one would buy a control that silently never matches.
const EmbedOrigins = z
.array(
z
.string()
.max(255)
.regex(
/^https?:\/\/[A-Za-z0-9.-]{1,253}(:[0-9]{1,5})?$/,
'must be a scheme, host and optional port with no path or trailing slash',
),
)
.max(20)
.refine((origins) => new Set(origins).size === origins.length, {
message: 'embed origins must be unique',
});Example.
curl https://keyring-api.belghalem.fr/v1/workspace \
-X PATCH \
-H "Authorization: Bearer $KEYRING_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"embed_origins": [
"https://app.acme.example"
]
}'HTTP/1.1 200 OK
Content-Type: application/json
{
"object": "workspace",
"id": "01a0ade7-b8df-722f-854f-ebac1cc717a7",
"name": "Acme Payments",
"slug": "acme-payments",
"billing_email": "billing@acme.example",
"embed_origins": [
"https://app.acme.example"
],
"created_at": "2026-09-17T05:47:19.647Z",
"updated_at": "2026-09-17T05:47:19.668Z"
}Shared validators
Defined once in packages/api/src/validation.ts and used by the schemas above.
const Env = z.enum(['live', 'test']);const Name = z.string().min(1).max(200);const Slug = z.string().regex(
/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/,
// The length floor is in the pattern and invisible from outside it, so the
// message says it: "p1" is rejected and "must be a lowercase slug" does not
// explain why.
'must be 3 to 63 characters of lowercase letters, digits and hyphens, starting and ending with a letter or digit',
);