# Authentication

Authenticate with API keys, limit them to some profiles, make them read-only or bar whole areas of the API, and rotate them safely.

Every request to `https://api.pozy.ai/v1` carries a bearer credential in the `Authorization` header. For your own servers that credential is an API key. The MCP server signs in with OAuth, and the dashboard uses your browser session. All of them hit the same API with the same rules.

```bash Terminal
curl https://api.pozy.ai/v1/accounts \
  -H "Authorization: Bearer $POZY_API_KEY"
```

## API keys

A key looks like `pz_live_` followed by 64 hex characters. It belongs to the workspace, not to the person who created it, so it keeps working when that member leaves.

Pozy stores only a SHA-256 hash of each key. The full value is returned once, when you create it. Lists show a preview such as `pz_live_3f9a...c41e` so you can tell keys apart.

> **Keep keys on the server:**
> 
> A key can do anything its settings allow, including publishing and spending money on ads, numbers and messages. Never ship one in a mobile app, a browser bundle or a public repository. If one leaks, delete it at once and create a new one.

### Create a key in the dashboard

Open [API keys](https://pozy.ai/dashboard/api-keys), click **Create key**, pick a name and the settings below, and copy the value. Workspace owners and admins can create keys.

### Create a key with the API

`POST /v1/api-keys` creates a key from code, for example when you onboard a customer and want a key that only reaches their profile. The call needs a full-access, read-write key (or an owner or admin session).

```bash cURL
curl -X POST https://api.pozy.ai/v1/api-keys \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Reporting job",
    "permission": "read",
    "disabledResourceGroups": ["billing", "webhooks"],
    "expiresIn": 90
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/api-keys", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Reporting job",
    permission: "read",
    disabledResourceGroups: ["billing", "webhooks"],
    expiresIn: 90,
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.apiKey.key); // shown once
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/api-keys",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "name": "Reporting job",
        "permission": "read",
        "disabledResourceGroups": ["billing", "webhooks"],
        "expiresIn": 90,
    },
)
print(resp.json()["apiKey"]["key"])  # shown once
```

```json
{
  "message": "API key created",
  "apiKey": {
    "id": "key_Vb3nQ8rT1xLm6Zc0Wp9s",
    "name": "Reporting job",
    "key": "pz_live_3f9a0c7e...",
    "keyPreview": "pz_live_3f9a...c41e",
    "scope": "full",
    "profileIds": [],
    "permission": "read",
    "disabledResourceGroups": ["billing", "webhooks"],
    "expiresAt": "2027-01-28T10:12:44.000Z",
    "createdAt": "2026-10-30T10:12:44.000Z"
  }
}
```

### Key settings

| Field | Values | Default | Effect |
| --- | --- | --- | --- |
| `name` | text | required | Label shown in lists and logs. |
| `scope` | `full`, `profiles` | `full` | `profiles` limits the key to the profiles in `profileIds`. |
| `profileIds` | profile ids | none | Required when `scope` is `profiles`. Every id must belong to the workspace. |
| `permission` | `read-write`, `read` | `read-write` | `read` allows only `GET` requests. |
| `disabledResourceGroups` | group names | none | Areas of the API the key may not call. See the table below. |
| `expiresIn` | days | never expires | The key stops working after that many days. |

## Profile-scoped keys

A key with `scope: "profiles"` is limited to the profiles you list. Any request that names a profile outside that list, in the path, the query string or the body, fails with `403` and `code: "profile_out_of_scope"`. A request that names an account (`accountId`, `accountIds`, `targetAccountId` or `sourceAccountId`) belonging to another profile fails with `403` and `code: "account_out_of_scope"`. `GET /v1/profiles` returns only the profiles in scope.

Use one per customer when you [build a platform](https://pozy.ai/docs/build/tenant-keys), so a bug or a leaked key in one tenant cannot touch another. With a scoped key, always pass `profileId` on list calls (accounts, posts, conversations) so results stay inside the tenant.

Scoped keys cannot manage API keys, and they cannot read workspace-wide billing. Calls that need the whole workspace return `403` with `code: "profile_scoped_key"`.

## Read-only keys

With `permission: "read"`, any request other than `GET` fails before it runs:

```json
{
  "error": "This credential is read-only.",
  "type": "permission_error",
  "code": "read_only_key"
}
```

Give read-only keys to dashboards, reporting jobs, BI tools and AI assistants that only need to look.

## Disabled resource groups

Every endpoint belongs to one resource group. A key with a group in `disabledResourceGroups` gets `403` with `code: "resource_group_disabled"` on any endpoint in that group, read or write.

| Group | Covers |
| --- | --- |
| `publishing` | Posts, queues, validation tools, media uploads, blog articles |
| `engagement` | Comments, reviews, mentions, likes and reposts, Discord server actions, Google Business reviews |
| `messages` | DMs and conversations, broadcasts, sequences, comment-to-DM, WhatsApp and RCS messages, email, voice agents |
| `contacts` | Contacts, custom field values, ad lead forms and leads, WhatsApp blocked users |
| `analytics` | Post, account and inbox analytics |
| `ads` | Ad accounts, campaigns, ad sets, creatives, audiences, conversions, pixels, catalogs, rules, insights |
| `telephony` | Phone numbers, SMS, voice calls, branded calling, WhatsApp numbers and calling, verification codes |
| `accounts` | Connect flows, connected accounts, profiles, account groups, account settings, workflows, WhatsApp templates and flows, Google Business locations |
| `billing` | Usage and billing |
| `webhooks` | Webhook endpoints, delivery logs, test deliveries |
| `commerce` | Stores, products, orders and other commerce data |
| `creator_campaigns` | Creator campaigns, participants and submissions |
| `money` | Balance, top-ups, payees and transfers |
| `calendar` | Calendar events |
| `files` | Files |

The [API reference](https://pozy.ai/docs/api) shows each endpoint's group under **Authentication**.

> **Least privilege:**
> 
> Start from the narrowest key that works. A scheduling backend needs `publishing` and `accounts`, so it can disable `ads`, `telephony`, `money`, `billing` and `webhooks`. A support inbox needs `messages`, `engagement` and `contacts`.

## Rotate and revoke keys

Deleting a key revokes it immediately. Requests that use it start failing with `401` on the next call.

To rotate without downtime:

1. Create the new key with the same settings.
2. Deploy it everywhere the old key is used.
3. Check the old key's **Last used** time in the dashboard. Once it stops moving, delete it.

```bash cURL
curl -X DELETE https://api.pozy.ai/v1/api-keys/key_Vb3nQ8rT1xLm6Zc0Wp9s \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/api-keys/key_Vb3nQ8rT1xLm6Zc0Wp9s", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
```
```python Python
requests.delete(
    "https://api.pozy.ai/v1/api-keys/key_Vb3nQ8rT1xLm6Zc0Wp9s",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
)
```

## Check a credential

`GET /v1/auth/verify` returns `valid: true` with the credential type (`api_key`, `oauth` or `session`), its scope and the workspace it belongs to. Use it in health checks and when you debug a `401` or `403`.

## OAuth for the MCP server

The [MCP server](https://pozy.ai/docs/mcp) at `https://mcp.pozy.ai/mcp` signs you in through the browser with your Pozy account. It acts as you, so your workspace role applies: a `viewer` gets read-only access, and a `member` cannot manage keys, webhooks or billing. You can revoke these sessions from **Settings → Connected apps** in the dashboard. Clients that cannot do OAuth can send an API key as the bearer token instead.

## Dashboard sessions

The dashboard calls the same public API with your session cookie. Write requests from another site are refused with `code: "csrf_rejected"`, so a session cannot be used from a page you do not trust. Dashboard traffic has its own rate limit budget, 120 requests a minute per workspace, and does not eat into your keys' limit.

## Errors you may see

| Status | `code` | Meaning | Fix |
| --- | --- | --- | --- |
| 401 | `invalid_api_key` | Missing, mistyped, revoked or expired credential. | Check the `Authorization: Bearer` header and the key's expiry. |
| 403 | `read_only_key` | A write with a read-only key. | Use a read-write key. |
| 403 | `resource_group_disabled` | The endpoint's group is disabled on this key. | Use a key that allows the group. |
| 403 | `insufficient_permissions` | The key type cannot do this, for example a scoped or read-only key managing keys. | Use a full-access, read-write key. |
| 403 | `role_forbidden` | Your workspace role cannot do this (sessions and OAuth tokens). | Ask an owner or admin for a different role. |
| 403 | `profile_out_of_scope` | A profile-scoped key named a profile it does not cover. | Use a key scoped to that profile. |
| 403 | `account_out_of_scope` | A profile-scoped key named an account that belongs to another profile. | Check the account id, or use a key scoped to that account's profile. |
| 403 | `profile_scoped_key` | A profile-scoped key called a workspace-wide endpoint. | Pass a `profileId` where the endpoint accepts one, or use a full-access key. |
| 403 | `feature_not_available` | The feature is not enabled for the workspace. | Contact support@pozy.ai. |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/auth/verify`](https://pozy.ai/docs/api/verifyCredential) | Verify credential |
| [`GET /v1/api-keys`](https://pozy.ai/docs/api/listApiKeys) | List keys |
| [`POST /v1/api-keys`](https://pozy.ai/docs/api/createApiKey) | Create key |
| [`DELETE /v1/api-keys/{keyId}`](https://pozy.ai/docs/api/deleteApiKey) | Delete key |
