# How Plausible Analytics Generates and Validates API Tokens for External Integrations

> Discover how Plausible Analytics generates and validates API tokens for external integrations using SHA-256 hashing and HTTP Basic authentication for secure access. Learn more now.

- Repository: [Plausible Analytics/analytics](https://github.com/plausible/analytics)
- Tags: how-to-guide
- Published: 2026-05-19

---

**Plausible Analytics uses SHA-256 hashed tokens with human-readable prefixes, transmitted via HTTP Basic authentication, where raw tokens are revealed only once during creation and validated through constant-time hash comparison.**

The [plausible/analytics](https://github.com/plausible/analytics) repository implements a lightweight, token-based authentication scheme specifically designed for third-party integrations such as WordPress plugins and Zapier. This system generates cryptographically secure tokens in [`lib/plausible/plugins/api/token.ex`](https://github.com/plausible/analytics/blob/main/lib/plausible/plugins/api/token.ex), stores only their hashes in the database, and validates incoming requests through a dedicated Phoenix plug that minimizes write operations to the `plugins_api_tokens` table.

## Token Generation and Secure Hash Storage

Token creation begins in [`lib/plausible/plugins/api/token.ex`](https://github.com/plausible/analytics/blob/main/lib/plausible/plugins/api/token.ex), where the `generate/1` function produces a cryptographically random byte string and prefixes it with `plausible-plugin` (or `plausible-plugin-<env>` in non-production environments) to aid secret-scanning tools. This raw token is immediately hashed using SHA-256 via Erlang's `:crypto.hash/2`, ensuring that compromised database access cannot reveal functional tokens.

```elixir

# lib/plausible/plugins/api/token.ex

def generate(random_bytes \\ random_bytes()) do
  raw  = prefixed(random_bytes)               # → "plausible-plugin-<rand>"

  hash = :crypto.hash(:sha256, raw)           # stored hash

  %{raw: raw, hash: hash}
end

```

The high-level `create/3` function in [`lib/plausible/plugins/api/tokens.ex`](https://github.com/plausible/analytics/blob/main/lib/plausible/plugins/api/tokens.ex) orchestrates persistence. It accepts a `%Site{}` struct, description, and the generated token map, returning a tuple containing the database record and the raw token string. This raw value is displayed exactly once in the LiveView UI ([`lib/plausible_web/live/plugins/api/token_form.ex`](https://github.com/plausible/analytics/blob/main/lib/plausible_web/live/plugins/api/token_form.ex)) and never persisted or transmitted again.

```elixir

# lib/plausible/plugins/api/tokens.ex

def create(%Site{} = site, description, generated_token \\ Token.generate()) do
  changeset = Token.insert_changeset(site, generated_token, %{description: description})
  case Repo.insert(changeset) do
    {:ok, saved_token} -> {:ok, saved_token, generated_token.raw}
  end
end

```

## Token Extraction from HTTP Requests

External integrations authenticate via the `Authorization: Basic` header. The `AuthorizePluginsAPI` plug in [`lib/plausible_web/plugs/authorize_plugins_api.ex`](https://github.com/plausible/analytics/blob/main/lib/plausible_web/plugs/authorize_plugins_api.ex) extracts credentials using Base64 decoding, accepting either `user:token` or standalone token formats.

```elixir

# lib/plausible_web/plugs/authorize_plugins_api.ex

defp extract_token(conn) do
  with ["Basic " <> encoded] <- get_req_header(conn, "authorization"),
       {:ok, decoded}       <- Base.decode64(encoded) do
    case :binary.split(decoded, ":") do
      [_user, token] -> {:ok, token}
      [token]        -> {:ok, token}
    end
  else
    _ -> {:unauthorized, conn}
  end
end

```

Missing or malformed headers immediately return **401 Unauthorized** responses, rejecting the request before it reaches the controller.

## Token Validation and Last-Seen Tracking

Validation occurs through constant-time hash comparison in `Tokens.find/1`, which queries the `plugins_api_tokens` table using the SHA-256 hash of the supplied raw token and preloads the associated `Site`. Upon successful lookup, the system updates the `last_used_at` timestamp only if the previous value exceeds 5 minutes old, significantly reducing database write churn for high-traffic integrations.

```elixir

# lib/plausible_web/plugs/authorize_plugins_api.ex

defp authorize(conn, token_value) do
  case Tokens.find(token_value) do
    {:ok, token} ->
      {:ok, token} = Tokens.update_last_seen(token)
      {:ok, Plug.Conn.assign(conn, :authorized_site, token.site)}
    {:error, :not_found} -> {:unauthorized, conn}
  end
end

```

The `update_last_seen/2` logic in [`lib/plausible/plugins/api/tokens.ex`](https://github.com/plausible/analytics/blob/main/lib/plausible/plugins/api/tokens.ex) implements this 5-minute throttle, ensuring administrators retain usage visibility without overwhelming the database with timestamp updates on every API call.

## Practical Implementation Examples

### Creating Tokens Programmatically

Generate tokens from Mix tasks or administrative scripts using the internal API:

```elixir
site   = Plausible.Repo.get_by!(Plausible.Site, domain: "example.com")
{:ok, token, raw} = Plausible.Plugins.API.Tokens.create(site, "Zapier integration")
IO.puts("Store this token safely – it will never be shown again:")
IO.puts(raw)

```

### Authenticating External Requests

Send requests with the raw token as the Basic auth password (username is ignored):

```bash
RAW_TOKEN="plausible-plugin-abc123def456..."
curl -u ":${RAW_TOKEN}" \
     -H "Accept: application/json" \
     https://plausible.io/api/v1/sites/example.com/stats

```

### Revoking Tokens

Delete tokens through the UI or directly via `Tokens.delete/2`, which removes entries matching both the site ID and token ID:

```elixir

# lib/plausible/plugins/api/tokens.ex

def delete(site, token_id) do
  Repo.delete_all(from(t in Token, where: t.site_id == ^site.id and t.id == ^token_id))
  :ok
end

```

## Summary

- **SHA-256 hashing**: Raw tokens are hashed before storage in the `plugins_api_tokens` table; only hashes persist in the database.
- **One-time display**: The unhashed token appears only once during creation in `Plausible.Plugins.API.Tokens.create/3`, after which it cannot be retrieved.
- **HTTP Basic auth**: The `AuthorizePluginsAPI` plug extracts tokens from `Authorization: Basic` headers, supporting both `user:token` and standalone token formats.
- **Throttled metadata updates**: The `last_used_at` timestamp updates only every 5 minutes to prevent database overload while tracking activity.
- **Site scoping**: Validated tokens automatically preload their associated `Site` into `conn.assigns.authorized_site` for downstream use.

## Frequently Asked Questions

### What hashing algorithm does Plausible use for API token storage?

Plausible uses **SHA-256** cryptographic hashing implemented via Erlang's `:crypto.hash/2` function. The raw token is hashed immediately in `Token.generate/1` and only the resulting hash is stored in the database, ensuring that database breaches do not compromise active tokens.

### How does Plausible minimize database load when tracking API token usage?

The system implements a **5-minute throttle** in `Tokens.update_last_seen/2`. When a token is used for validation, the function compares the current time against the existing `last_used_at` timestamp and only performs a database write if more than 5 minutes have elapsed since the last update. This prevents write amplification from high-frequency API integrations.

### Can I retrieve a raw API token after creating it?

No. Plausible follows a "show once" security model where `Tokens.create/3` returns the raw token only during the initial creation phase. This value is displayed in the LiveView modal ([`token_form.ex`](https://github.com/plausible/analytics/blob/main/token_form.ex)) and then discarded from memory; subsequent database queries only return the SHA-256 hash, making it impossible to leak raw tokens through API responses or database dumps.

### Why does the token have a readable prefix?

The `plausible-plugin` prefix (added via `Token.prefixed/1`) serves dual purposes: it aids secret-scanning tools like GitHub Secret Scanner in identifying accidentally exposed tokens, and it allows environment-specific variants (e.g., `plausible-plugin-dev`) for staging environments, configured in `Token.prefix/0`.