---
title: API keys
description: Two kinds of key, what each one may do, and why the browser gets its own.
---

Every request carries a key, and the key selects the project:

```
Authorization: Bearer sk_…
```

There is no project id in any URL. The same path serves different content for
different keys, which means you can point a staging deploy at a different
project by changing one environment variable.

## The two kinds

| | `sk_…` secret | `pk_…` publishable |
|---|---|---|
| Where it runs | Server-side only | Safe in a browser |
| Origin restriction | None | Locked to the origins you list |
| Can list releases | Yes | No |
| Can write | Yes | No, except a knowledge base vote |
| Recoverable after creation | **No** | Yes |

### Secret keys

A secret key is shown **once**, when you create it. We store a hash, not the
key, so there is no "reveal" button and support cannot recover it. Losing one
means creating another and revoking the old.

Treat it like a database password. It reads everything the project serves,
including unpublished releases, and it can write.

### Publishable keys

A publishable key is designed to be readable. It ships in page source, in a
mobile bundle, in a `view-source`. That is the point, and it is why the key
alone is not trusted as a claim about who is calling.

Two things bound it:

1. **An origin allow-list.** Add your site's origins to the key and a browser
   request from anywhere else is refused. `www.` is ignored when matching, so
   listing `https://example.com` covers `https://www.example.com`.
2. **A tighter rate budget.** Publishable keys are counted per key **and** per
   address, so one visitor hammering your help centre is limited without
   limiting every other visitor holding the same key.

> **WARNING: The origin allow-list is a browser control, not a wall**
>
> It works because browsers send `Origin` and cannot be made to lie about it.
> `curl` can send anything, so an allow-list does not make a publishable key
> secret. It stops your key being used to power somebody else's site, which is
> the realistic abuse. Never put content behind a publishable key that you would
> not put on the page it renders.

## Check a key

```http
GET /v1/ping
```

```bash
curl https://api.zevcontent.net/v1/ping \
  -H "Authorization: Bearer $ZEVCONTENT_KEY"
```

Confirms the key resolves and names the project it belongs to. This is the
first call to make when something is not behaving. It separates "wrong key"
from "wrong request", which are the two problems that look identical from a
404.

## Rotating and revoking

Revoking is immediate: the next request with that key is refused. There is no
grace period, so create the replacement, deploy it, and revoke afterwards.

Because a key belongs to one project, rotating one never affects another.

## Which key for which job

- **A static site build.** Secret. It runs on a build machine, not a browser,
  and it can ask for `media_urls=permanent` to refuse expiring image URLs.
- **A React or Vue frontend fetching at runtime.** Publishable, with your
  origins listed.
- **A help centre with helpful/not-helpful voting.** Publishable. The
  [vote endpoint](/modules/knowledge-base#voting) is the one write a browser
  should ever make, and it explicitly expects a publishable key.
- **A migration script or an AI agent creating content.** Secret. Writes
  require one.