# Public and secret keys

> The single most important page for anyone shipping a site.

Source: https://docs.dashfordevs.com/v1/authentication/public-and-secret-keys · Topic: API keys and authentication · Updated: 2026-11-07

**A secret key (`sk_…`) never goes in browser code.** Not in a `NEXT_PUBLIC_` variable, not in a mobile app, not in a public repository. Anyone who has it can change your organization's data.

Every API key is a pair: a public half and a secret half.

|  | Public key `pk_…` | Secret key `sk_…` |
| --- | --- | --- |
| Where it may live | Browser bundles, apps, anywhere | Your server only |
| Can read storefront data | Yes | Yes |
| Can change data (admin API) | **No** | Yes |
| Origin check (live keys) | Yes: only from your allowed origins | No |
| Rate limit | 120 requests per minute | 300 requests per minute |
| Typical user | Your website's pages | Your server, sync jobs, the MCP server |

Send either in the `X-API-Key` header:

```bash
curl https://papi.dashfordevs.com/api/storefront/products -H "X-API-Key: pk_live_…"
```

## Test and live keys

-   **Test keys** skip the origin check, so they work from localhost, curl and CI. Use them while building.
-   **Live keys** enforce HTTPS and your allowed origins for the public half.

## How papi checks a request

In this order, each one stopping the request with a clear error:

1.  Is there a key, and does it look like one? (`401 api_key_required`, `401 invalid_api_key_format`)
2.  Does it exist? (`401 invalid_api_key`)
3.  Is it active? (`403 api_key_inactive`)
4.  Is the organization active? (`403 organization_shutdown`)
5.  For a live public key: is the origin allowed and the connection HTTPS? (`403 origin_not_allowed`)
6.  For admin routes: is it a secret key? (`403 secret_key_required`)
7.  Under the rate limit? (`429`)

Then papi forwards the request with a short-lived **signed identity**. The services behind it never see your key, and check the identity again themselves.

## Keys in the SDK

The SDK protects you from the most common mistake: it **throws** if it is created with an `sk_` key in a browser, or if any `NEXT_PUBLIC_*` variable holds a secret key (it names the variable, never the value). Use `assertServerOnlyKey` from `dash4devs/security` in your server-only module.
