# API tokens (/docs/guides/api-tokens)

Your site, your build and your scripts reach Content with a token. A token belongs to one space and one environment, so a production site uses a production token, and a develop token sees nothing of production.

## Kinds [#kinds]

You create three kinds under **Settings → API tokens**:

| Kind       | Starts with | May                                                                                                            | Belongs to                |
| ---------- | ----------- | -------------------------------------------------------------------------------------------------------------- | ------------------------- |
| Delivery   | `cms_d_`    | Read published content from the [delivery API](/docs/api/delivery)                                             | The environment           |
| Preview    | `cms_p_`    | Read drafts too                                                                                                | The environment           |
| Management | `cms_m_`    | With the **model** scope, sync the [content model](/docs/guides/model-in-git); with **content**, write content | The person who created it |

- **Delivery tokens** are made for being seen: CORS is open, so a page can read published content with one from the browser.
- **Preview tokens** see what isn't published yet. Keep them on your server: from a browser they only work on the environment's [preview URL](/docs/guides/spaces#settings).
- **Management tokens** act as the person who created them, with that person's [role](/docs/guides/roles) at the time of each request: they stop working when the person leaves the organization, and can't do more than the person may. Pick what one may do under **What it may do**: the **content model** (the [CLI](/docs/sdk/cli) and the [management API](/docs/api/management)) and **content** (the agent API's [content routes](/docs/api/management#content-with-a-management-token), for migrations, imports and backups). The delivery API refuses them.

Content makes more tokens itself, which you never create or copy:

| Token  | Starts with | Made when                                                                       | Lasts             |
| ------ | ----------- | ------------------------------------------------------------------------------- | ----------------- |
| Canvas | `cms_v_`    | The [canvas](/docs/guides/canvas) opens your site, for the person looking at it | 8 hours           |
| Site   | `cms_s_`    | An editor [signs in on your site](/docs/guides/site-editing)                    | 8 hours           |
| Form   | `cms_f_`    | The prompt bar's form opens on your site                                        | As its site token |

Canvas and site tokens read drafts as their person, like a preview token with that person's role, and are refused from a browser on any origin but the preview URL.

## Creating and revoking [#creating-and-revoking]

Admins and developers create tokens under **Settings → API tokens**: a name (what it is for, like "Vercel production"), a kind and, for a management token, its scopes. The token is shown **once**, right after it is created: copy it into your host's secrets or an untracked `.env` file. Only a hash of it is stored, so a lost token can't be shown again; create a new one.

Every token expires: in 30 days, 90 days, 1 year or never, picked under **Expires** when you create it. 1 year is the default, and creating a token without picking asks you to confirm that date first. After it, the token is refused with `401` and `token_expired`: create a new one, put it where the old one was, and revoke the old one. The list marks a token that expires within 14 days, and one unused for 90 days, which is likely forgotten and safe to revoke. The organization's admins and the person who created the token get an email 14 and 3 days before it expires.

Each token lists when it was last used. **Revoke** stops it on the next request, or within 60 seconds for a delivery token answered from the [edge cache](/docs/api/delivery#caching). Revoking is recorded in [Activity](/docs/guides/activity), never the token itself.

Agents list, create and revoke tokens with `listApiTokens`, `createApiToken` and `revokeApiToken`, in develop and staging. Production tokens are made by a person in the dashboard.

## Without a stored token [#without-a-stored-token]

A deployment on Vercel, or a GitHub Actions workflow, can use the OIDC token its host gives it instead of an API token: nothing to store, rotate or let expire. Connect it under **Connected deployments** on the space's page on [www.obelism.studio](https://www.obelism.studio): its Vercel team and project ids, or its GitHub repository id, and a rule per environment of the deployment. Each rule maps that environment (a Vercel environment like `production` or `preview`, a GitHub environment or ref, or `any` for the rest) to one environment of the space, with what Content allows there:

| Content              | Reads as                                                             |
| -------------------- | -------------------------------------------------------------------- |
| Off                  | refused                                                              |
| Published            | a delivery token                                                     |
| Published and drafts | a delivery token, or a preview token on requests that ask for drafts |
| Management           | the above, and a management token with its scopes, acting as you     |

Oxygen issues no OIDC token, so a storefront on [Shopify Oxygen](/docs/getting-started/oxygen) uses a stored delivery or preview token instead.

Then pass the [SDK](/docs/sdk/javascript) a token source:

```ts
import { ContentClient } from '@obelism/content'
import { vercelOidc } from '@obelism/content/oidc' // needs @vercel/oidc installed

const content = new ContentClient({
	token: vercelOidc(), // or githubOidc() in a workflow with `permissions: id-token: write`
	space: process.env.CONTENT_SPACE_ID,
	environment: process.env.CONTENT_ENVIRONMENT,
})

const draft = await content.getPage(path, { drafts: true }) // a preview route
```

Without the SDK, request the host's token for `https://content.obelism.studio` and send it as the bearer token.

The token's environment picks the rule, so a site's preview deployments read the environment their rule maps them to. A site that names its space and environment (`Obelism-Space` and `Obelism-Environment`, the SDK's `space` and `environment`) gets `401` with `token_place_mismatch` from a deployment whose rule maps it elsewhere. Drafts are only read when a request asks for them with `Obelism-Drafts: true` (the SDK's `drafts: true`), so a site's pages and its preview can share one connection without a page showing a draft.

A token no connection lets in gets `401` with `no_connection`; one whose environment has no rule, `no_rule`; one whose rule has Content off, `product_off`. Asking for drafts with a rule that reads published content only gets `403` with `drafts_not_allowed`. A project connected to more than one space names the one it means with the `Obelism-Space` header, or gets `401` with `ambiguous_space`.

Delivery requests with a host token skip the edge cache, since the token changes every hour or two; they are still answered from the content cache.

### Local development [#local-development]

`vercelOidc()` works on your own machine too. Link the repository to the Vercel project with `vercel link` and sign in with `vercel login`: the SDK takes the token `vercel env pull` wrote to your `.env`, or asks Vercel for a new one with your sign-in when there is none or it expired. That token's environment is `development`, so the connection needs a rule for `development` (its template maps it to develop), and `CONTENT_ENVIRONMENT` names the same environment. Only members of the Vercel team get a token: anyone else, and CI outside Vercel and GitHub Actions, uses a delivery token.

## Sending one [#sending-one]

```sh
curl -H "Obelism-Token: cms_d_…" \
  "https://content.obelism.studio/api/delivery/v1/pages?path=home"
```

The delivery API takes a token in `Obelism-Token` or as `Authorization: Bearer …`; only `Obelism-Token` lets a delivery token be answered from the edge cache. The management, agent and site APIs take `Authorization: Bearer …`. The [SDK](/docs/sdk/javascript) sends the right one.

## Environment variables [#environment-variables]

**Settings → API tokens → Copy as .env** copies what a site needs to reach the space in that environment, for every product it uses, grouped per product:

```bash
# Improve
IMPROVE_ORGANIZATION_ID=org_…
IMPROVE_SPACE_ID=space_…
IMPROVE_ENVIRONMENT=production
IMPROVE_API_TOKEN="<from Improve › API tokens>"
# Content
CONTENT_SPACE_ID=space_…
CONTENT_ENVIRONMENT=production
CONTENT_DELIVERY_TOKEN="<from Content › API tokens>"
# Numbers
NUMBERS_ORGANIZATION_ID=org_…
NUMBERS_SPACE_ID=space_…
NUMBERS_ENVIRONMENT=production
NUMBERS_API_TOKEN="<from Numbers › API tokens on Setup>"
```

A token line names the page to make one on, since a token is only shown when it is created.

When a [connected deployment](#without-a-stored-token) reaches the environment, **Copy for connected deployments** next to it copies the same lines without the token lines of the products it reaches, since the deployment sends its own.

With a [Shopify store](/docs/guides/shopify#on-the-site) connected, it adds `SHOPIFY_STORE_DOMAIN` and `SHOPIFY_STOREFRONT_TOKEN` under Content. The SDK and `npx @obelism/content init` use the same names, plus `CONTENT_PREVIEW_TOKEN` for drafts and `CONTENT_MANAGEMENT_TOKEN` for the CLI.
