Skip to main content
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:
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

1

Go to the API settings page

In the SnowSEO dashboard, navigate to Settings → Integrations → API.
2

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.
3

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.
If you lose the key, there’s no way to retrieve it. Delete it and create a new one.

Making Your First Request

Here’s a complete example showing how to call the API from different environments:
// 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);
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.

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.
PropertyValue
Prefixsk_
ScopeBrand level (one brand per key)
Organization contextIncluded in key
Brand contextIncluded in key
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.

Common Integration Patterns

Serverless Functions (Vercel, Netlify, etc.)

// 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:
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

// 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:
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.retryAfter || 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 Codes Reference

HTTP Statuserror CodeCauseResolution
400MISSING_PARAMETERRequired field missingCheck the message for which field
400INVALID_URLURL format is wrongMust be a valid HTTPS URL
401UNAUTHORIZEDMissing or invalid keyVerify your API key is correct
403FORBIDDENKey doesn’t have accessKey may not belong to this brand
404NOT_FOUNDResource doesn’t existCheck the ID is correct
429RATE_LIMIT_EXCEEDEDToo many requestsWait and retry (see retryAfter)
500INTERNAL_ERRORServer-side errorRetry with exponential backoff
503SERVICE_UNAVAILABLETemporary outageRetry after a delay

Security Best Practices

Never expose your API key in frontend code, public repositories, or logs. Anyone with your key can access your SnowSEO data.
Always store your key in an environment variable, never hardcoded:
# .env (add this to .gitignore)
SNOWSEO_API_KEY=sk_live_xxxxxxxxxxxxxxxxxxxx
// Good
const apiKey = process.env.SNOWSEO_API_KEY;

// Bad — key visible in code
const apiKey = 'sk_live_abc123...';
Add your .env file to .gitignore:
# Environment variables
.env
.env.local
.env.production
For GitHub, use Secrets (Settings → Secrets and variables → Actions) to store API keys for CI/CD pipelines.
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.
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.
If you’re receiving webhooks from SnowSEO, always validate the Authorization header against your stored secret:
function validateWebhook(req, secret) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  return token === secret;
}

Rate Limits & Retries

Limits vary by endpoint — most are 20–60 requests per minute, with heavier endpoints like website-ratings capped at 10 per day. If you exceed the limit, you’ll get a 429 response:
{
  "error": "RATE_LIMIT_EXCEEDED",
  "message": "Too many requests. Please wait a moment and try again.",
  "retryAfter": 60
}

Implementing Retry Logic

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('retryAfter') || '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:
curl https://api.snowseo.com/v3/website-ratings?url=example.com \
  -H "Authorization: Bearer YOUR_API_KEY"
Expected response (200 OK):
{
  "domain": "example.com",
  "opr-score": 8.5,
  "opr-rank": "1234"
}
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