Skip to content

Authentication

There are three kinds of credential and they are deliberately not interchangeable. Which one you hold decides what you can reach, and the wrong one does not get a hint that a better one exists.

Last updated

Three credentials, three jobs

CredentialLooks likeWho holds itCan
Project API keycsk_live_…Your website, your build stepRead published content in one project
Dashboard sessionA short-lived bearer tokenA person signed in to the dashboardEverything their role allows
Agent tokencka_live_…An MCP client, a script, a CI jobOnly what it was granted, capped by its owner’s role

A dashboard session never becomes an external API credential and an API key never reaches the dashboard. They are different products for different consumers that happen to share a request pipeline.

Project API keys

This is the credential you put in your website. It belongs to exactly one project, it can only read, and it can only see published content. There is no write scope for it to be granted by mistake, because the public API has no write route at all.

The formattext
csk_live_0G8H2K4M6P9R_xJ3kQ8vN2mR7pL4wT6yB1cF5hD9gS0aZ3eU7iO2nM4
└┬─┘ └┬─┘ └─────┬────┘ └───────────────┬────────────────────────┘
 │    │     key id                secret, 32 bytes from a CSPRNG
 │    └─ environment: live or test
 └─ product-neutral prefix
  • The secret is shown once, at creation. It is stored as a keyed hash and cannot be recovered — if it is lost, create another and revoke the old one.
  • Revoking takes effect within five seconds, everywhere, and the row stays in your list marked revoked rather than disappearing.
  • The key id is readable aloud: no ambiguous 0/O or 1/I/l, so it survives being quoted to support over the phone.
  • A key from one workspace against another workspace’s project is a 404, not a 403. The surface does not confirm what it will not serve.

Locking a key to your own sites

If a key will be used from a browser, list the origins allowed to use it in the project’s settings. An empty list means unrestricted, which is the right default for a key used server-side and the wrong one for a key in a page.

The check runs on the real request rather than on the preflight, because a preflight carries no Authorization header and therefore cannot know which project’s list to apply. A refused origin gets 403 ORIGIN_NOT_ALLOWED.

Dashboard sessions

People sign in with an email and a password and get a short-lived access token plus a refresh cookie. The cookie is HttpOnly, SameSite=Strict and scoped to /v1/auth, so no script on the page can read it and no other site can send it.

A session is not scoped to one workspace, so a request has to say which one it means with X-Tenant-Id. That header cannot widen what the session can reach; it only picks from inside it, and naming a workspace you do not belong to is refused at the membership lookup.

Refreshingbash
# The refresh token rotates on every use. Presenting an old one revokes the
# whole session family -- that is theft detection, not an inconvenience.
curl -X POST https://api.cmskite.com/v1/auth/refresh --cookie-jar jar --cookie jar
RoleCan
OwnerEverything, including changing the plan and deleting the workspace
AdminManage projects, keys, members and see billing — but not change the plan
EditorWrite and delete content
ViewerRead content, projects and keys

Agent tokens

A credential for something that is not a person and not a website: an MCP client, a deployment script, a CI job. It is scoped to one workspace and granted a list of specific abilities when it is minted.

It is gated twice and needs both. The grant list is what the person ticked when they created it, and the role is that person’s, resolved fresh on every request — so demote somebody to viewer and the token they left running loses write access on the next call. It does not carry a snapshot of what they could do the day they made it.

What the grant list cannot contain is the point: there is no entry for minting API keys, revoking them, changing the plan, deleting a project or deleting the workspace. No agent token can hold those, however it is configured and whatever its owner can do.

Sending a credential

Examplehttp
Authorization: Bearer csk_live_0G8H2K4M6P9R_xJ3kQ8vN2mR7pL4wT6yB1cF5hD9gS0aZ3eU7iO2nM4

Always the Authorization header — never a query parameter. A credential in a URL is a credential in an access log, in a referrer header and in somebody’s browser history.