---
title: "API Tokens"
description: "Create and manage read-only personal API tokens with scoped permissions, optional account restrictions, and automatic expiration"
canonical_url: "https://docs.sealmetrics.com/api/api-tokens"
lang: "en"
date_generated: "2026-09-17T13:33:23.512Z"
source_hash: "293c4d376d634363ec24e87423b8afc45e68c54e90ab54d205394312d2852562"
content_type: "api-reference"
owner: "engineering"
llm_priority: "critical"
source_file: "api/api-tokens.mdx"
publisher: "Sealmetrics"
---

# API Tokens

Canonical page: https://docs.sealmetrics.com/api/api-tokens

Create and manage personal API tokens for programmatic access to the Sealmetrics API.

## Overview

API tokens provide:
- Long-lived authentication for integrations
- Scoped permissions per token
- Optional account restrictions
- Automatic expiration

**Base path:** `/api-tokens`

---

## Scopes

API tokens can be granted the following permissions:

| Scope | Description |
|-------|-------------|
| `stats:read` | Read analytics data (traffic, conversions, pages) |
| `sites:read` | Read site configuration (domains, settings, channel rules) |
| `accounts:read` | Read account information |
| `channel_rules:write` | Channel rules: create and edit **drafts** only. Nothing on live rules |
| `channel_rules:publish` | Channel rules: create **live** rules and edit/delete live ones. Implies `channel_rules:write` |

**Note:** API tokens are read-only apart from channel rules, which are the single exception. Every other write operation requires JWT authentication via the dashboard. See [Channel Groups](/api/channel-groups) for what each channel-rule scope allows, and [Channel Grouping](/platform/settings/tracking/channel-grouping#the-api-key-the-write-tools-need) for how to create the key.

**Warning:**
A token's scopes cannot be edited later. To change them, revoke the token and create a new one. A `403` response naming `Required scope: ...` means the token lacks that scope.

### List Available Scopes

```http
GET /api-tokens/scopes
```

**Response:**

```json
{
  "data": {
    "scopes": [
      {
        "id": "stats:read",
        "name": "Stats Read",
        "description": "Read analytics data"
      },
      {
        "id": "sites:read",
        "name": "Sites Read",
        "description": "Read site configuration"
      },
      {
        "id": "accounts:read",
        "name": "Accounts Read",
        "description": "Read account info"
      },
      {
        "id": "channel_rules:write",
        "name": "Channel Rules: Drafts",
        "description": "Channel rules: create and edit drafts only"
      },
      {
        "id": "channel_rules:publish",
        "name": "Channel Rules: Publish",
        "description": "Channel rules: create live rules and edit/delete live ones"
      }
    ]
  }
}
```

---

## List Tokens

```http
GET /api-tokens
```

**Query Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `include_inactive` | boolean | `false` | Include revoked tokens |

**Response:**

```json
{
  "data": {
    "tokens": [
      {
        "id": 1,
        "name": "Production API",
        "token_prefix": "sm_abc",
        "scopes": ["stats:read", "sites:read", "accounts:read"],
        "account_ids": ["acme-corp"],
        "is_active": true,
        "expires_at": "2026-01-10T00:00:00Z",
        "last_used_at": "2025-01-10T14:30:00Z",
        "created_at": "2025-01-01T10:00:00Z"
      }
    ],
    "total": 1
  }
}
```

---

## Create Token

```http
POST /api-tokens
```

**Request Body:**

```json
{
  "name": "My Integration",
  "scopes": ["stats:read", "sites:read"],
  "account_ids": ["acme-corp", "other-site"],
  "expires_in_days": 365
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Token name (1-100 chars) |
| `scopes` | string[] | Yes | Permission scopes |
| `account_ids` | string[] | No | Restrict to specific accounts |
| `expires_in_days` | int | No | Days until expiration, 1-365 (default: none — never expires) |

**Response (201 Created):**

```json
{
  "data": {
    "id": 2,
    "name": "My Integration",
    "token": "sm_abc123def456ghi789...",
    "token_prefix": "sm_abc",
    "scopes": ["stats:read", "sites:read"],
    "account_ids": ["acme-corp", "other-site"],
    "is_active": true,
    "expires_at": "2026-01-10T00:00:00Z",
    "created_at": "2025-01-10T14:30:00Z"
  }
}
```

**Important:** The full `token` value is only returned once. Store it securely.

---

## Get Token

```http
GET /api-tokens/{token_id}
```

**Response:**

```json
{
  "data": {
    "id": 1,
    "name": "Production API",
    "token_prefix": "sm_abc",
    "scopes": ["stats:read", "sites:read", "accounts:read"],
    "account_ids": ["acme-corp"],
    "is_active": true,
    "expires_at": "2026-01-10T00:00:00Z",
    "last_used_at": "2025-01-10T14:30:00Z",
    "created_at": "2025-01-01T10:00:00Z"
  }
}
```

---

## Revoke Token

```http
DELETE /api-tokens/{token_id}
```

Revokes a token immediately. This action cannot be undone.

**Response:**

```json
{
  "data": {
    "id": 1,
    "name": "Production API",
    "is_active": false,
    "revoked_at": "2025-01-10T14:30:00Z"
  }
}
```

---

## Using API Tokens

Include the token in the `X-API-Key` header:

```bash
curl -X GET "https://my.sealmetrics.com/api/v1/stats/overview?site_id=acme" \
  -H "X-API-Key: sm_abc123def456ghi789..."
```

---

## Token Best Practices

### 1. Use Minimal Scopes

Only grant the scopes your integration needs:

```json
{
  "name": "Analytics Only",
  "scopes": ["stats:read"]
}
```

### 2. Restrict to Specific Sites

If your integration only needs access to certain sites:

```json
{
  "name": "Client Dashboard",
  "scopes": ["stats:read"],
  "account_ids": ["client-a", "client-b"]
}
```

### 3. Set Appropriate Expiration

For temporary integrations, use short expiration:

```json
{
  "name": "Audit Script",
  "scopes": ["stats:read"],
  "expires_in_days": 7
}
```

### 4. Rotate Tokens Regularly

Create a new token before the old one expires:

```python
# Create new token
new_token = create_token("Production API v2", scopes=["stats:read", "sites:read"])

# Update your application config
update_config(new_token)

# Revoke old token after confirming new one works
revoke_token(old_token_id)
```

### 5. Never Commit Tokens

Store tokens in environment variables:

```bash
# .env file (never commit!)
SEALMETRICS_API_KEY=sm_abc123...
```

```python
import os
API_KEY = os.environ["SEALMETRICS_API_KEY"]
```

---

## Token Errors

The `error.code` is derived from the HTTP status (`401` → `unauthorized`,
`403` → `forbidden`). The specific reason is carried in the human-readable
`error.message`.

| HTTP Code | Error Code | Example message |
|-----------|------------|-----------------|
| 401 | `unauthorized` | `Invalid API key` (token not found or revoked) |
| 401 | `unauthorized` | `API key has expired` |
| 401 | `unauthorized` | `Invalid API key format` (missing `sm_` prefix) |
| 403 | `forbidden` | `Required scope: <scope>` (token lacks required scope) |
| 403 | `forbidden` | `Access denied to account: <id>` (account not in token's allowed list) |

---

## Code Examples

### Python

```python
import requests

BASE_URL = "https://my.sealmetrics.com/api/v1"

def create_api_token(auth_token: str, name: str, scopes: list) -> dict:
    """Create a new API token."""
    response = requests.post(
        f"{BASE_URL}/api-tokens",
        headers={"Authorization": f"Bearer {auth_token}"},
        json={
            "name": name,
            "scopes": scopes,
            "expires_in_days": 365
        }
    )
    response.raise_for_status()
    return response.json()["data"]

def revoke_token(auth_token: str, token_id: int):
    """Revoke an API token."""
    response = requests.delete(
        f"{BASE_URL}/api-tokens/{token_id}",
        headers={"Authorization": f"Bearer {auth_token}"}
    )
    response.raise_for_status()
```

### JavaScript

```javascript
async function createApiToken(authToken, name, scopes) {
  const response = await fetch(`${BASE_URL}/api-tokens`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${authToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      name,
      scopes,
      expires_in_days: 365
    })
  });

  if (!response.ok) {
    throw new Error(`Failed to create token: ${response.status}`);
  }

  const { data } = await response.json();
  return data;
}
```
