---
url: /authentication.md
---

# Authentication

Every `/api/v1` call is authenticated. There are two credentials (an API key for
software, a session for the browser) and a required `{brandId}` path segment to
target brand-scoped resources.

## API keys

API keys are managed in the dashboard. Create a key on the [Developer page](https://dash.socialized.dev/developer).
The raw secret (`sk_…`) is returned once, at creation, and never again. Store it
somewhere safe.

Send it as a bearer token on every request:

```sh
curl "$API/brands/$BRAND/publications" -H "Authorization: Bearer sk_…"
```

> \[!TIP]
> The [socialized.dev CLI](/cli) stores the key for you (`socialized.dev login`)
> and attaches it to every request, so you don't manage the header by hand. In CI,
> set `SOCIALIZED_DEV_API_KEY` instead of logging in.

Each key has an explicit scope:

| Access | Grants |
| --- | --- |
| Read | Read brands, assets, publications, and webhooks |
| Write | Read + upload assets, edit webhooks, and publish videos |
| Admin | Write + manage brands and API keys |

Reach is either **all brands** in the account or a selected brand list. A key that
names a brand outside its reach gets `403 brand_out_of_scope`; a key missing the
requested action gets `403 insufficient_scope`.

Admin keys can manage keys over the API:

```sh
curl "$API/keys" -H "Authorization: Bearer $SK"
curl "$API/keys" \
  -H "Authorization: Bearer $SK" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-publisher","reach":"account","access":"write"}'
```

## Brands

A brand is a publishing identity inside your account: it owns the connections
and the publications. Connections and publications are **brand-scoped**, while
assets are **account-scoped** (reusable across any brand in the account).

List your brands to retrieve their IDs:

```sh
# List your brands and pick one (the id looks like brand_…)
curl -s "$API/brands" -H "Authorization: Bearer $SK"
export BRAND="brand_…"
```

Each row carries its `connectedPlatforms` so you can see what's
wired up without an extra call, and an `archivedAt` timestamp (`null` for a
live brand). `DELETE /brands/{brandId}` **archives** a brand: its platform
connections are disconnected, writes under it return `409 brand_archived`, and
its publication history stays readable — posts already live on platforms are
not touched. The last live brand in an account can't be archived
(`409 last_brand`). Once you have a brand ID, target brand-scoped resources
under `/brands/{brandId}`:

```sh
curl "$API/brands/$BRAND/publications" -H "Authorization: Bearer $SK"
```

## Sessions (the dashboard)

The web app authenticates with a session cookie (email magic-link or Google),
not an API key, but it talks to the *same* `/api/v1`. Anything you read here
about the API is exactly what the dashboard does. You never handle the session
yourself; it's mentioned only so the two credentials aren't a mystery.

Connection management and profile avatar uploads are session-only dashboard
actions. Publishing, asset uploads, webhooks, brands, and key management accept
API keys when the key grants that resource/action.

## Errors

A missing or invalid credential is `401 unauthorized`. An authenticated caller
whose account has no workspace yet is `403 no_workspace`. Naming a `{brandId}`
that isn't one of your brands is `404 brand_not_found`. Naming a brand outside a
key's reach is `403 brand_out_of_scope`; missing a grant is `403 insufficient_scope`. See
[Errors & idempotency](/errors) for the full vocabulary.
