# Authentication and API keys

The public API takes a secret key in the Authorization header. Each key belongs to one site and carries the events scope, the payments scope or both.

ProductLense's public API has two endpoints, both on `https://api.productlense.app`:

| Endpoint | Scope | Purpose |
|---|---|---|
| `POST /api/v1/events` | `events` | [Send events from your server](/docs/api/events) |
| `POST /api/v1/payments` | `payments` | [Send payments from any billing system](/docs/api/payments) |

The tracking script does not need a key: it posts to a public endpoint identified by the site id. Keys are only for calls from your own servers.

## Create a key

1. In the ProductLense dashboard, open the site's Settings → API keys.
2. Give the key a name (1 to 100 characters) and choose its scopes: `events`, `payments` or both.
3. Copy the key. It is shown once.

API keys are part of the Scale plan, and only the account's owner and admins can create or delete them.

> [!IMPORTANT]
> Keep keys on the server. A key sends events and payments for its site, so it must never appear in browser code, a mobile app or a public repository. ProductLense stores only a SHA-256 hash of each key, so a lost key cannot be shown again: create a new one and delete the old one.

## Send the key

Pass the key as a Bearer token:

```http
POST /api/v1/payments HTTP/1.1
Host: api.productlense.app
Authorization: Bearer mk_live_3f9a...
Content-Type: application/json
```

Keys start with `mk_live_` followed by 48 hexadecimal characters.

```bash tab="cURL"
curl -X POST https://api.productlense.app/api/v1/events \
  -H "Authorization: Bearer $PL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"s":"SITE_ID","e":"Trial started","u":"https://example.com/app","uid":"user_123"}'
```

```js tab="Node"
const res = await fetch('https://api.productlense.app/api/v1/events', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.PL_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ s: 'SITE_ID', e: 'Trial started', u: 'https://example.com/app', uid: 'user_123' }),
});
if (res.status !== 204) throw new Error(`ProductLense answered ${res.status}`);
```

```python tab="Python"
import os
import requests

res = requests.post(
    "https://api.productlense.app/api/v1/events",
    json={"s": "SITE_ID", "e": "Trial started", "u": "https://example.com/app", "uid": "user_123"},
    headers={"Authorization": f"Bearer {os.environ['PL_API_KEY']}"},
    timeout=10,
)
res.raise_for_status()
```

```go tab="Go"
body := strings.NewReader(`{"s":"SITE_ID","e":"Trial started","u":"https://example.com/app","uid":"user_123"}`)
req, _ := http.NewRequest(http.MethodPost, "https://api.productlense.app/api/v1/events", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("PL_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
	log.Fatalf("ProductLense answered %d", res.StatusCode)
}
```

## Authentication errors

| Status | Code | Message | Cause |
|---|---|---|---|
| 401 | `unauthorized` | `missing or malformed API key` | No `Authorization: Bearer` header, or the key does not start with `mk_live_` |
| 401 | `unauthorized` | `unknown or revoked API key` | The key was deleted or never existed |
| 403 | `forbidden` | `API key lacks the events scope` | The key does not have the scope the endpoint needs (the message names the scope) |
| 403 | `forbidden` | `api key does not belong to this site` | An events request names a site id other than the key's site |

## Rotate a key

Create the new key, deploy it, then delete the old key in Settings → API keys. Deleting a key takes effect on the next request.
