# Authentication

> How to authenticate against the ProductOS REST API: the in-app session for browser code, API tokens for everything else.

Source: https://productos.ca/documentation/api/v1/authentication (last reviewed 2026-09-22)

Every endpoint under `/api/v1/*` is workspace-scoped and requires authentication. ProductOS supports three auth paths, all of which resolve to the same user, workspace and role:

1. **Session cookie**: set automatically by the in-app login flow (`/login`). Used by the browser, no extra config.
2. **Supabase JWT**: in-app code already holds a session; external scripts use an API token.
3. **API token**: long-lived `pos_live_*` token issued from **Settings → API Tokens**. Best for third-party tools, CI jobs, and external automations.

**Which workspace.** An API token belongs to the workspace it was issued in and always acts there; to script another workspace, issue a token in it. A browser session acts in the workspace of the page making the request (the app sends it in the `x-productos-workspace` header); a request that names a workspace you don't belong to, or none, acts in the one you used last.

## Picking a method

| Caller | Recommended auth |
| :--- | :--- |
| Browser, in-app code | Session cookie (automatic) |
| Internal script, even one run by a signed-in person | API token (a script has no browser session) |
| External tool, CI, customer-built integration | API token |
| Your product sending feedback or NPS scores from a server | Feedback API secret key (`sk_live_`), see [Feedback API](https://productos.ca/documentation/api/v1/feedback-api) |
| Your product sending feedback from the browser | Publishable key (`pk_live_`), same as the widget |
| Inbound provider webhooks | Provider HMAC (verified by ProductOS, you don't send a bearer) |

## Using an API token

1. Open **Settings → API Tokens**.
2. Click **New token** (issuing tokens needs the `admin` or `owner` role). Pick a name (e.g. "n8n production") and the **role** the token should act as (defaults to `commenter`; capped at your own role). An expiry can be set when issuing through the API (`expires_at`); the dialog issues non-expiring tokens.
3. Copy the `pos_live_xxxxxxxxxxxxx` string. **This is the only time it's shown**: store it in your secret manager immediately.

Send it as a bearer header:

```bash title="curl"
curl 'https://productos.ca/api/v1/feedback?limit=10' \
  -H "Authorization: Bearer pos_live_xxxxxxxxxxxxx"
```

There is only one API token prefix, `pos_live_`. ProductOS does not have a sandbox / test-key environment. (Feedback intake keys, `sk_live_` and `pk_live_`, are a separate, narrower kind: they can only submit feedback. See [Feedback API](https://productos.ca/documentation/api/v1/feedback-api).)

## Capability is role-based, not scope-based

Tokens carry two things:

- A coarse **scope set** (`read`, `write`, `admin`), defaults to `["read", "write"]`. Scopes are recorded on the token; what a request may do is decided by role.
- A **role** at issuance (`viewer | commenter | member | admin | owner`), defaults to `commenter`.

The effective role is the more restrictive of:

- The token's stored role
- The token owner's *current* workspace role

The REST API and the MCP endpoint both check this on every request, so a token never outranks its owner, and a token whose owner has left the workspace stops working. ProductOS also revokes any token that outranks its owner the moment a role is lowered, and every token when the owner leaves.

There are no per-entity scope strings like `feedback:read` or `features:write`. The model is the [workspace role matrix](https://productos.ca/documentation/guides/concepts/workspaces-and-roles).

> **Demote = auto-revoke**
>
> ProductOS auto-revokes any token whose stored role is higher than the owner's current workspace role. If you demote a user from `member` to `commenter`, all of their `member`-role tokens are revoked the moment the role changes. You don't need to remember to revoke tokens manually.

## Webhook authentication

- **Outbound webhooks** *from* ProductOS are limited to outbound webhook providers (Slack, Microsoft Teams, Discord, Zapier, Make, n8n) configured under **Settings → Integrations**. Each posts to the URL you provided. There is no general-purpose outbound-webhook signature scheme; the planned design is in [Webhooks](https://productos.ca/documentation/api/v1/webhooks).
- **Inbound provider webhooks** (GitHub, Linear, etc.) are signed by *their* respective providers and verified by ProductOS using each provider's documented scheme. You never authenticate inbound integrations with a bearer.

## Common errors

| Code | Meaning | Fix |
| :--- | :--- | :--- |
| `401 unauthorized` | Missing, expired, or malformed token | Re-fetch the token; verify the `Bearer ` prefix; confirm the token wasn't revoked or auto-revoked by a role change |
| `403 forbidden` | Token owner's role lacks the required capability | Re-issue from a higher-role user or have an admin promote the owner |
| `404 not_found` | Resource doesn't exist *or* token's workspace doesn't have access | Verify the ID; confirm the token is for the right workspace |
| `429 rate_limited` | Too many requests in the rate-limit window | Back off; respect `retryAfterSeconds` in the response body |

## Next

- [Feedback reference](https://productos.ca/documentation/api/v1/reference/feedback): fields, filters, examples.
- [Workspaces and roles](https://productos.ca/documentation/guides/concepts/workspaces-and-roles): the full capability matrix that tokens compose against.
- [MCP server](https://productos.ca/documentation/api/v1/mcp): for Claude Code session integration.
