---
title: Authentication
description: Bearer keys, what each kind may do, and the failures that look like authentication and are not.
---

```
Authorization: Bearer sk_…
```

The key selects the project. No project id appears in any URL, so pointing a
deploy at a different project is a change to one environment variable.

## Key kinds

**`sk_…`, secret.** Server-side only. Reads everything the project serves,
can list every release, and can write. Shown once at creation; we store a hash,
so it cannot be recovered.

**`pk_…`, publishable.** Safe in a browser. Restricted to the origins listed
on it, cannot list releases, and cannot write, with one deliberate exception:
the knowledge base [vote endpoint](/modules/knowledge-base#voting).

Full comparison in [API keys](/guide/keys).

## Verify 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 its project. Make this call first when
something is not behaving. It separates "wrong key" from "wrong request",
which look identical from a 404.

## Failures

| Status | `code` | What it means |
|---|---|---|
| 401 | `unauthenticated` | Missing, malformed, unknown or revoked key. Revocation is immediate. |
| 403 | `origin_not_allowed` | A publishable key used from an origin not on its list. |
| 403 | `secret_key_required` | A write, or a release listing, attempted with a publishable key. |

> **INFO: 401 and 403 are different questions**
>
> `401` says we do not know who you are. `403` says we do, and this is not
> allowed. If you are getting `403`, the key is fine. Check which kind it is and
> where it is being called from.

## The origin check

A publishable key with origins listed is refused from any other **browser**
origin. The check compares the `Origin` header, ignoring `www.`, so
`https://example.com` covers `https://www.example.com`. Scheme and port are
part of an origin: `http://localhost:3000` and `http://localhost:5173` are not
the same.

It is best-effort by nature. `curl` sends whatever it likes, so an allow-list
does not make a publishable key secret. It stops your key powering somebody
else's site, which is the realistic abuse.

## What a key can see

A key sees its project's **published** content, and nothing outside that
project. There is no endpoint that crosses a project boundary, and an id from
another project is not found rather than forbidden, because a "forbidden"
would confirm the id exists.

Drafts are never served on `/v1`, whichever key you hold. A draft article
answers `blog_article_not_published` or `kb_article_not_published`, which says
what is wrong rather than pretending the slug does not exist.