LevelChatLevelChatDocs

Documentation

Workspace API

Customer-facing REST endpoints. JWT-org-scoped, role-gated, used by Studio and apps/meet — and ready for your own integrations.

Authentication

Every endpoint accepts the lc_session cookie minted by POST /v1/auth/login. The cookie is HttpOnly, Secure, and SameSite=Lax. Browser clients send it automatically when they fetch with credentials: 'include'; server-to-server callers attach it via the Cookie: header or pass the JWT directly with Authorization: Bearer <jwt>.

For machine-to-machine integrations we recommend the workspace API keys path below — a project-scoped key avoids the cookie rotation complexity of the user session.

Errors

Errors follow RFC 7807 problem+json — every non-2xx response includes a JSON body of the form { "type", "title", "status", "detail", "code" }. The code field is a stable string suitable for programmatic dispatch (e.g. slug_taken, forbidden_workspace_role, plan_required).

Endpoints

Each block below has cURL / TypeScript / Python samples — click a tab and the copy button on the right yanks the snippet to your clipboard. Sections are anchored, so you can deep-link a specific endpoint into a support ticket or PR description.

GET/v1/workspace/me

Returns the caller's identity, primary org, and entitlements.

RolesAny signed-in member

First call most integrations make. Returns the authenticated user, their primary organisation, deployment-mode (cloud or self_host), and the workspace role used to gate every other endpoint on this page.

Try it

curl https://api.levelchat.io/v1/workspace/me \
  -b "lc_session=$LC_SESSION"

Responses

200OK
{
  "user_id":   "usr_01J7BZ9WK4XRT4...",
  "email":     "[email protected]",
  "display_name": "Alice",
  "org": {
    "id":   "org_01J7BZ9WK4XRT4...",
    "name": "Acme",
    "role": "Owner"
  },
  "plan_selected":    true,
  "deployment_mode":  "cloud"
}
401UnauthorizedMissing or invalid session
GET/v1/workspace/projects

List every project in the caller’s org.

RolesOwnerAdminDeveloperMemberBillingViewer

Try it

curl https://api.levelchat.io/v1/workspace/projects \
  -b "lc_session=$LC_SESSION"

Responses

200OK
{
  "items": [
    {
      "id":          "prj_01J7C0...",
      "name":        "Production",
      "slug":        "prod",
      "environment": "prod",
      "created_at":  "2026-04-12T09:30:11Z"
    }
  ],
  "next_cursor": null
}
POST/v1/workspace/projects

Create a project — a logical container for rooms, API keys, webhooks.

RolesOwnerAdminDeveloper

Body

FieldTypeDescription
name*string1–80 characters. Shown in Studio + meet dashboard.
slugstringLowercase URL slug — regex ^[a-z0-9-]{3,63}$. Auto-generated from name when omitted. 409 slug_taken on collision.
environment'prod' | 'staging' | 'dev'= prodFree-form label used for filtering in the dashboard.

* required

Try it

curl -X POST https://api.levelchat.io/v1/workspace/projects \
  -b "lc_session=$LC_SESSION" \
  -H "content-type: application/json" \
  -d '{
    "name": "Production",
    "slug": "prod",
    "environment": "prod"
  }'

Responses

201Created
{
  "id":          "prj_01J7C0...",
  "name":        "Production",
  "slug":        "prod",
  "environment": "prod",
  "created_at":  "2026-04-12T09:30:11Z"
}
403ForbiddenCaller role lacks projects:create
409Conflictslug_taken
422Validation failed
GET/v1/workspace/rooms

List rooms in the caller’s org. Filter by project / state.

RolesOwnerAdminDeveloperMemberBillingViewer

Query

FieldTypeDescription
project_idstringRestrict to one project.
state'created' | 'active' | 'ended'Lifecycle filter.
limitinteger= 501–200. Pages with `next_cursor`.

Try it

curl "https://api.levelchat.io/v1/workspace/rooms?state=active&limit=20" \
  -b "lc_session=$LC_SESSION"

Responses

200OK
{
  "items": [
    {
      "id":         "rm_01J7C1...",
      "name":       "Weekly standup",
      "type":       "meeting",
      "state":      "active",
      "project_id": "prj_01J7C0...",
      "short_code": "ABCDEF12",
      "created_at": "2026-05-07T08:15:00Z"
    }
  ],
  "next_cursor": null
}
POST/v1/workspace/rooms

Create a meeting room. Returns a `short_code` for the share link.

RolesOwnerAdminDeveloper

Rooms support four canonical topologies: meeting, 1to1, webinar, live (per services/shared-go/capabilities/capabilities.go::RoomType). The legacy aliases broadcast, one-to-one, and p2p normalise at ingress for backwards-compat. The returned short_code is the canonical share-URL fragment — https://meet.your-domain/r/{short_code} — without leaking the room id.

Body

FieldTypeDescription
project_id*stringMust belong to caller's org.
namestringDisplay name. ≤ 200 chars.
type'meeting' | 'live' | 'webinar' | '1to1'= meetingCanonical topology — drives the layout in apps/meet + SDK behaviour. Legacy aliases broadcast, one-to-one, and p2p normalise at ingress.

* required

Try it

curl -X POST https://api.levelchat.io/v1/workspace/rooms \
  -b "lc_session=$LC_SESSION" \
  -H "content-type: application/json" \
  -d '{
    "project_id": "prj_01J7C0...",
    "name":       "Weekly standup",
    "type":       "meeting"
  }'

Responses

201Created
{
  "id":         "rm_01J7C1...",
  "name":       "Weekly standup",
  "type":       "meeting",
  "state":      "created",
  "project_id": "prj_01J7C0...",
  "short_code": "ABCDEF12",
  "created_at": "2026-05-07T08:15:00Z"
}
403ForbiddenCaller role lacks rooms:create
404Not foundproject_id not in caller's org
422Validation failed
GET/v1/workspace/recordings

List recordings across the caller’s org.

RolesOwnerAdminDeveloperMemberBillingViewer

Try it

curl https://api.levelchat.io/v1/workspace/recordings \
  -b "lc_session=$LC_SESSION"

Responses

200OK
{
  "items": [
    {
      "id":            "rec_01J7C2...",
      "room_id":       "rm_01J7C1...",
      "state":         "completed",
      "started_at":    "2026-05-07T08:15:30Z",
      "duration_sec":  1842,
      "size_bytes":    245760000
    }
  ]
}
POST/v1/workspace/recordings/:id/download

Mint a 5-minute presigned S3 URL for the recording artifact.

RolesOwnerAdminDeveloperMemberBilling

Bytes flow direct from S3 to the browser — they never traverse admin-api. The returned URL has a 5-minute TTL and a response-content-disposition: attachment header so the browser saves it to disk instead of inlining it.

Path

FieldTypeDescription
id*stringRecording id from `list-recordings`.

* required

Try it

curl -X POST https://api.levelchat.io/v1/workspace/recordings/rec_01J7C2.../download \
  -b "lc_session=$LC_SESSION"

Responses

200OK
{
  "download_url": "https://s3.eu-central-1.amazonaws.com/levelchat-recordings/...",
  "filename":    "weekly-standup-2026-05-07.mp4",
  "expires_at":  "2026-05-07T08:25:00Z"
}
404Not foundRecording not in caller’s org
GET/v1/workspace/webhooks

List every webhook endpoint in the org.

RolesOwnerAdminDeveloperMemberBillingViewer

Try it

curl https://api.levelchat.io/v1/workspace/webhooks \
  -b "lc_session=$LC_SESSION"

Responses

200OK
{
  "items": [
    {
      "id":         "wh_01J7C3...",
      "url":        "https://api.acme.com/levelchat-events",
      "events":     ["room.ended", "recording.completed"],
      "active":     true,
      "created_at": "2026-05-01T12:00:00Z"
    }
  ]
}
POST/v1/workspace/webhooks

Register a new webhook endpoint. Returns the signing secret once.

RolesOwnerAdminDeveloper

Every delivery includes an X-LC-Webhook-Sig header — base64(HMAC-SHA-256(secret, X-LC-Webhook-Ts + "." + raw_body)) (see /guides/webhooks for the full header set). The secret is shown only on creation; if you lose it, rotate the endpoint instead of trying to re-read it.

Body

FieldTypeDescription
url*stringHTTPS callback URL. localhost / 127.0.0.1 rejected in cloud.
events*string[]One or more of room.created, room.ended, recording.started, recording.completed, recording.failed.
descriptionstringFree-form note shown in the dashboard.

* required

Try it

curl -X POST https://api.levelchat.io/v1/workspace/webhooks \
  -b "lc_session=$LC_SESSION" \
  -H "content-type: application/json" \
  -d '{
    "url":    "https://api.acme.com/levelchat",
    "events": ["recording.completed"]
  }'

Responses

201Created
{
  "id":             "wh_01J7C3...",
  "url":            "https://api.acme.com/levelchat",
  "events":         ["recording.completed"],
  "active":         true,
  "signing_secret": "whsec_...",
  "created_at":     "2026-05-07T08:30:00Z"
}
422Validation failedurl not HTTPS / events empty
Verify signatures. Reject any inbound delivery whose X-LC-Webhook-Sig header doesn't match base64(HMAC-SHA-256(secret, X-LC-Webhook-Ts + "." + raw_body)) (canonical-string format; the timestamp is bound into the signature to defeat replay). Retry schedule per services/webhooks/internal/delivery/retry.go is 0s, 10s, 1m, 5m, 15m, 1h, 4h (7 attempts, ±20% jitter, ~5h 19m total window); HTTP 410 Gone terminates the schedule immediately. See /guides/webhooks for Node / Python / Go verifier examples.
POST/v1/workspace/projects/:projectId/api-keys

Mint a project-scoped API key. Returns the secret once.

RolesOwnerAdminDeveloper

Use API keys for server-to-server integrations — they avoid the cookie rotation complexity of lc_session. Keys are scoped to a single project; revoke individual keys when a CI runner or service is decommissioned.

Path

FieldTypeDescription
projectId*stringProject that owns the key.

* required

Body

FieldTypeDescription
name*stringHuman-readable label, e.g. "ci-runner".
scope'rooms:read' | 'rooms:write' | 'recordings:read' | 'recordings:write' | 'webhooks:write' | 'all'= allPermission scope. Most integrations want `all`.

* required

Try it

curl -X POST https://api.levelchat.io/v1/workspace/projects/prj_01J7C0.../api-keys \
  -b "lc_session=$LC_SESSION" \
  -H "content-type: application/json" \
  -d '{ "name": "ci-runner", "scope": "all" }'

Responses

201Created
{
  "id":         "key_01J7C4...",
  "name":       "ci-runner",
  "scope":      "all",
  "secret":     "lc_live_...",
  "prefix":     "lc_live_5K4F",
  "created_at": "2026-05-07T08:45:00Z"
}
The full secret is shown once. Store it in a vault. To recover from a lost secret, rotate the key — the old secret stops working immediately, the new one is returned in the response.
POST/v1/workspace/projects/:projectId/api-keys/:id/rotate

Issue a new secret and invalidate the previous one.

RolesOwnerAdminDeveloper

Try it

curl -X POST https://api.levelchat.io/v1/workspace/projects/prj_01J7C0.../api-keys/key_01J7C4.../rotate \
  -b "lc_session=$LC_SESSION"

Responses

200OK
{
  "id":     "key_01J7C4...",
  "secret": "lc_live_NEW...",
  "prefix": "lc_live_9XR8"
}
DELETE/v1/workspace/projects/:projectId/api-keys/:id

Permanently revoke an API key. Cannot be undone.

RolesOwnerAdminDeveloper

Try it

curl -X DELETE https://api.levelchat.io/v1/workspace/projects/prj_01J7C0.../api-keys/key_01J7C4... \
  -b "lc_session=$LC_SESSION"

Responses

204No content
404Not found
POST/v1/workspace/meet/invitations

Email up to 50 recipients a branded invite for a room.

RolesOwnerAdminDeveloperMemberBilling

Used by the meet dashboard's “Invite people” modal. Fans out via SMTP async; the response returns immediately with the count of accepted vs. invalid addresses.

Body

FieldTypeDescription
room_id*stringRoom id (or short_code) to invite people to.
recipients*string[]1–50 RFC5322 addresses. Invalid ones are returned in `rejected`.
notestringOptional 0–2000 char personal note rendered in the email body.

* required

Try it

curl -X POST https://api.levelchat.io/v1/workspace/meet/invitations \
  -b "lc_session=$LC_SESSION" \
  -H "content-type: application/json" \
  -d '{
    "room_id":    "rm_01J7C1...",
    "recipients": ["[email protected]", "[email protected]"],
    "note":       "Standup at 10:00 EU time"
  }'

Responses

200OK
{
  "queued":   2,
  "rejected": []
}
422Validation failed0 valid recipients or > 50
Rate limited — soft cap of 200 invitation sends per workspace per hour. Above that, requests are queued and trickled out so a typo in a script can't spam an address book.

See also

Workspace API — REST reference — LevelChat Docs