> ## Documentation Index
> Fetch the complete documentation index at: https://snowseo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> How to authenticate with the SnowSEO API, manage your keys, and handle common integration scenarios.

Every request to the SnowSEO API must be authenticated with an API key. There's no OAuth flow — just include your key and you're good to go.

***

## Quick Start

If you already have an API key, here's the fastest way to make a request:

```bash theme={null}
curl https://api.snowseo.com/v3/website-audit \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "example.com"}'
```

Replace `YOUR_API_KEY` with your actual key (starts with `sk_`). That's it — no other headers needed.

***

## Creating an API Key

<Steps>
  <Step title="Go to the API settings page">
    In the SnowSEO dashboard, navigate to **Settings → Integrations → API**.
  </Step>

  <Step title="Create a new key">
    Click **Create new key**. Give it a descriptive name so you remember what it's for — e.g., `Production Server`, `CI/CD Pipeline`, or `Analytics Dashboard`.
  </Step>

  <Step title="Copy and store the key immediately">
    The full key is shown **only once** after creation. Copy it right away and store it in a secure location.

    <Warning>
      If you lose the key, there's no way to retrieve it. Delete it and create a new one.
    </Warning>
  </Step>
</Steps>

***

## Making Your First Request

Here's a complete example showing how to call the API from different environments:

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    // Using the built-in fetch (Node 18+)
    const response = await fetch('https://api.snowseo.com/v3/website-audit', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.SNOWSEO_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ url: 'example.com' }),
    });

    if (!response.ok) {
      const error = await response.json();
      console.error('API Error:', error);
      throw new Error(`HTTP ${response.status}: ${error.message}`);
    }

    const data = await response.json();
    console.log('SEO Score:', data.seoAudit?.overallScore);
    ```

    <Tip>
      Always check `response.ok` or `response.status` before trying to parse the JSON. The API returns detailed error objects that are easy to miss if you assume every response is valid.
    </Tip>
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import httpx
    import os
    import json

    client = httpx.Client(
        base_url="https://api.snowseo.com/v3",
        headers={"Authorization": f"Bearer {os.environ['SNOWSEO_API_KEY']}"},
        timeout=30.0,
    )

    try:
        response = client.post("/website-audit", json={"url": "example.com"})
        response.raise_for_status()  # Raises for 4xx/5xx errors
        data = response.json()
        print(f"SEO Score: {data['seoAudit']['overallScore']}")
    except httpx.HTTPStatusError as e:
        print(f"API Error: {e.response.json()}")
    except httpx.TimeoutException:
        print("Request timed out")
    ```

    <Tip>
      Set a timeout on your HTTP client. The SnowSEO API can take several seconds for heavy operations like multi-page site audits.
    </Tip>
  </Tab>

  <Tab title="Next.js API Route">
    ```typescript theme={null}
    // pages/api/seo-check.ts or app/api/seo-check/route.ts
    import { NextResponse } from 'next/server';

    export async function GET(request: Request) {
      const { searchParams } = new URL(request.url);
      const url = searchParams.get('url');

      if (!url) {
        return NextResponse.json(
          { error: 'URL parameter is required' },
          { status: 400 }
        );
      }

      const apiKey = process.env.SNOWSEO_API_KEY;
      if (!apiKey) {
        return NextResponse.json(
          { error: 'API key not configured' },
          { status: 500 }
        );
      }

      const response = await fetch('https://api.snowseo.com/v3/website-audit', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ url }),
      });

      const data = await response.json();

      if (!response.ok) {
        return NextResponse.json(data, { status: response.status });
      }

      return NextResponse.json(data);
    }
    ```
  </Tab>
</Tabs>

***

## Key Scopes & Permissions

Each API key is scoped to a **single brand** (team) within your organization. The key automatically carries your organization and brand context — you don't need to pass `teamId` separately.

| Property                 | Value                           |
| ------------------------ | ------------------------------- |
| **Prefix**               | `sk_`                           |
| **Scope**                | Brand level (one brand per key) |
| **Organization context** | Included in key                 |
| **Brand context**        | Included in key                 |

<Note>
  A single organization can have multiple brands (teams). If you need to access data from multiple brands, create a separate API key for each one.
</Note>

***

## Common Integration Patterns

### Serverless Functions (Vercel, Netlify, etc.)

```javascript theme={null}
// Never call the SnowSEO API directly from the browser.
// Always proxy through your serverless function.
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { url } = req.body;

  const response = await fetch('https://api.snowseo.com/v3/website-audit', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SNOWSEO_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ url }),
  });

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

### Webhook Handler (Receiving Data)

If you're building an integration that receives webhooks from SnowSEO (e.g., article published events), validate the `Authorization` header:

```javascript theme={null}
export default function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).send('Method Not Allowed');
  }

  const authHeader = req.headers.authorization;
  const token = authHeader?.replace('Bearer ', '');

  // Validate against your stored secret
  if (token !== process.env.MY_WEBHOOK_SECRET) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const { event, article } = req.body;

  if (event === 'article.published') {
    console.log(`Article published: ${article.title}`);
    console.log(`URL: ${article.slug}`);
  }

  res.status(200).json({ received: true });
}
```

### Automated Reporting Script

```javascript theme={null}
// Run this on a schedule (e.g., cron job)
import cron from 'node-cron';

async function fetchKeywordReport() {
  const response = await fetch('https://api.snowseo.com/v3/rank-tracking/keywords', {
    headers: { 'Authorization': `Bearer ${process.env.SNOWSEO_API_KEY}` },
  });

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

  const { items } = await response.json();
  return items.filter(k => k.position <= 10);
}

// Schedule daily at 9 AM
cron.schedule('0 9 * * *', async () => {
  console.log('Running daily keyword report...');
  const topKeywords = await fetchKeywordReport();
  console.log(`Found ${topKeywords.length} keywords in top 10`);
});
```

***

## Error Handling

The API returns structured error responses. Here's how to handle them properly:

```javascript theme={null}
async function safeApiCall(endpoint, options = {}) {
  const url = endpoint.startsWith('http')
    ? endpoint
    : `https://api.snowseo.com/v3${endpoint}`;

  const response = await fetch(url, {
    ...options,
    headers: {
      'Authorization': `Bearer ${process.env.SNOWSEO_API_KEY}`,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });

  // Parse response body regardless of status
  const data = await response.json().catch(() => ({}));

  if (!response.ok) {
    // Handle specific error codes
    switch (response.status) {
      case 401:
        throw new Error('Invalid or expired API key. Check SNOWSEO_API_KEY.');
      case 403:
        throw new Error(`Access denied: ${data.message || 'Forbidden'}`);
      case 429:
        const retryAfter = data.retryAfterSeconds || 60;
        throw new Error(`Rate limited. Retry after ${retryAfter} seconds.`);
      case 500:
        throw new Error('SnowSEO API error. Try again later.');
      default:
        throw new Error(`API Error: ${data.message || response.statusText}`);
    }
  }

  return data;
}

// Usage
try {
  const audit = await safeApiCall('/website-audit', {
    method: 'POST',
    body: JSON.stringify({ url: 'example.com' }),
  });
  console.log(`Score: ${audit.seoAudit.overallScore}`);
} catch (error) {
  console.error(error.message);
  // Decide: retry, alert, or graceful degradation
}
```

### Error Reference

The API does not use machine-readable error codes. The `error` field always holds a human-readable message (e.g. `"teamId is required"`, `"Team not found"`, `"Rate limit exceeded."`). Branch on the **HTTP status code**, and surface `error` / `message` to the user.

| HTTP Status | Cause                                                  | Resolution                                                           |
| ----------- | ------------------------------------------------------ | -------------------------------------------------------------------- |
| `400`       | Bad request — required field missing or malformed      | Check the `error` message for which field                            |
| `401`       | Missing or invalid API key                             | Verify your API key is correct                                       |
| `403`       | Valid key but no access, or a plan/usage limit was hit | Key may not belong to this brand, or you've exceeded your plan limit |
| `404`       | Resource doesn't exist                                 | Check the ID is correct                                              |
| `429`       | Too many requests                                      | Wait and retry (see `retryAfterSeconds`)                             |
| `500`       | Server-side error                                      | Retry with exponential backoff                                       |
| `503`       | Temporary outage                                       | Retry after a delay                                                  |

***

## Security Best Practices

<Warning>
  **Never expose your API key in frontend code, public repositories, or logs.** Anyone with your key can access your SnowSEO data.
</Warning>

<AccordionGroup>
  <Accordion title="Use environment variables">
    Always store your key in an environment variable, never hardcoded:

    ```bash theme={null}
    # .env (add this to .gitignore)
    SNOWSEO_API_KEY=sk_xxxxxxxxxxxxxxxxxxxx
    ```

    ```javascript theme={null}
    // Good
    const apiKey = process.env.SNOWSEO_API_KEY;

    // Bad — key visible in code
    const apiKey = 'sk_abc123...';
    ```
  </Accordion>

  <Accordion title="Never commit secrets to version control">
    Add your `.env` file to `.gitignore`:

    ```gitignore theme={null}
    # Environment variables
    .env
    .env.local
    .env.production
    ```

    For GitHub, use **Secrets** (Settings → Secrets and variables → Actions) to store API keys for CI/CD pipelines.
  </Accordion>

  <Accordion title="Use separate keys per environment">
    Create different API keys for development, staging, and production:

    * `MyApp-Dev`
    * `MyApp-Staging`
    * `MyApp-Production`

    This way you can revoke a compromised key without affecting other environments.
  </Accordion>

  <Accordion title="Rotate keys periodically">
    Rotate your API keys every 90 days or immediately if you suspect compromise:

    1. Create a new key in the dashboard
    2. Update your environment variable
    3. Deploy and verify it works
    4. Delete the old key

    This gives you zero downtime while staying secure.
  </Accordion>

  <Accordion title="Validate incoming webhooks">
    If you're receiving webhooks from SnowSEO, always validate the `Authorization` header against your stored secret:

    ```javascript theme={null}
    function validateWebhook(req, secret) {
      const token = req.headers.authorization?.replace('Bearer ', '');
      return token === secret;
    }
    ```
  </Accordion>
</AccordionGroup>

***

## Rate Limits & Retries

Limits vary by endpoint — up to 60 requests per minute per endpoint, while heavier endpoints may have stricter caps. If you exceed the limit, you'll get a `429` response:

```json theme={null}
{
  "success": false,
  "statusCode": 429,
  "code": "Too Many Requests",
  "error": "Rate limit exceeded.",
  "message": "Rate limit exceeded.",
  "retryAfterSeconds": 60
}
```

### Implementing Retry Logic

```javascript theme={null}
async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (response.status === 429) {
        const retryAfter = parseInt(
          response.headers.get('retry-after') || '5',
          10
        );
        const backoff = retryAfter * 1000 * Math.pow(2, attempt);

        console.log(`Rate limited. Waiting ${backoff}ms before retry...`);
        await new Promise(resolve => setTimeout(resolve, backoff));
        continue;
      }

      return response;
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      await new Promise(resolve =>
        setTimeout(resolve, 1000 * Math.pow(2, attempt))
      );
    }
  }
  throw new Error('Max retries exceeded');
}

// Usage
const response = await fetchWithRetry(
  'https://api.snowseo.com/v3/website-audit',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SNOWSEO_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ url: 'example.com' }),
  }
);
```

***

## Quick Test

Verify your key works with a simple request:

```bash theme={null}
curl https://api.snowseo.com/v3/rank-tracking/keywords \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Expected response (200 OK):

```json theme={null}
{
  "keywords": []
}
```

If you get `401 Unauthorized`, double-check:

1. The key is correctly set in your environment variable
2. You're using `Bearer ` (with the space) before the key
3. The key hasn't been deleted from the dashboard
