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
Go to the API settings page
In the SnowSEO dashboard, navigate to Settings → Integrations → API .
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.
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:
Node.js
Python
Next.js API Route
// 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.
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" )
Set a timeout on your HTTP client. The SnowSEO API can take several seconds for heavy operations like multi-page site audits.
// 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 );
}
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
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 Status error CodeCause Resolution 400MISSING_PARAMETERRequired field missing Check the message for which field 400INVALID_URLURL format is wrong Must be a valid HTTPS URL 401UNAUTHORIZEDMissing or invalid key Verify your API key is correct 403FORBIDDENKey doesn’t have access Key may not belong to this brand 404NOT_FOUNDResource doesn’t exist Check the ID is correct 429RATE_LIMIT_EXCEEDEDToo many requests Wait and retry (see retryAfter) 500INTERNAL_ERRORServer-side error Retry with exponential backoff 503SERVICE_UNAVAILABLETemporary outage Retry 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.
Use environment variables
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...' ;
Never commit secrets to version control
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.
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.
Rotate your API keys every 90 days or immediately if you suspect compromise:
Create a new key in the dashboard
Update your environment variable
Deploy and verify it works
Delete the old key
This gives you zero downtime while staying secure.
Validate incoming webhooks
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:
The key is correctly set in your environment variable
You’re using Bearer (with the space) before the key
The key hasn’t been deleted from the dashboard