DataForSEO Integration
Complete guide to DataForSEO API integration, rate limits, cost management, and usage patterns.
API Overview
RankFabric integrates with multiple DataForSEO API products to provide comprehensive SEO and competitive intelligence data:
| API Product | Purpose | Base Cost |
|---|---|---|
| Backlinks API | Referring domains, backlink analysis, spam scores | $0.02-0.10/request |
| DataForSEO Labs | Keyword research, ranked keywords, domain metrics | $0.004-0.05/request |
| Keywords Data API | Keyword suggestions from Google Ads | $0.05/request |
| On-Page API | Instant page content fetching | $0.000125/URL |
| App Data API | Mobile app store data (Google Play, Apple) | $0.01-0.05/request |
| Business Data API | Business listings search | Variable |
File Structure
packages/api/src/lib/dataforseo/
├── dataforseo-api.js # App Data API (Google Play, Apple App Store)
├── dataforseo.js # Keywords API (Labs + Google Ads)
├── dataforseo-backlinks.js # Backlinks API (referring domains, summaries)
├── dataforseo-spam-score.js # Bulk spam scores and domain ranks
├── dataforseo-categories.js # Category taxonomy mapping
├── dataforseo-listings.js # App listings search
├── dataforseo-business.js # Business listings search
└── instant-pages-batch.js # On-Page instant pages (batched)
Authentication
Credential Storage
DataForSEO uses HTTP Basic Authentication. Credentials are stored as Cloudflare Worker secrets:
# Set credentials via Wrangler CLI
echo "your-login" | wrangler secret put DATAFORSEO_LOGIN
echo "your-password" | wrangler secret put DATAFORSEO_PASSWORD
Authentication Implementation
All DataForSEO modules use centralized authentication from dataforseo-api.js:
// Centralized auth function - import this instead of duplicating
export function getDataForSEOAuth(env) {
if (!env.DATAFORSEO_LOGIN || !env.DATAFORSEO_PASSWORD) {
throw new Error("DATAFORSEO_LOGIN or DATAFORSEO_PASSWORD missing");
}
return `Basic ${btoa(`${env.DATAFORSEO_LOGIN}:${env.DATAFORSEO_PASSWORD}`)}`;
}
// Usage in API calls
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: getDataForSEOAuth(env),
},
body: JSON.stringify(payload),
});
Environment Variables
Configure in wrangler.toml:
[vars]
DATAFORSEO_LABS_LIMIT = "100"
DATAFORSEO_LABS_MAX_REQUESTS = "1"
DATAFORSEO_DAILY_BUDGET = "50"
DATAFORSEO_LOCATION_CODE = "2840"
DATAFORSEO_LANGUAGE_CODE = "en"
Backlinks API
The Backlinks API provides link intelligence data including referring domains, individual backlinks, domain summaries, and spam scores.
Referring Domains
Get domains linking to a target with full metrics.
Endpoint: POST https://api.dataforseo.com/v3/backlinks/referring_domains/live
Cost: $0.02 base + $0.0001 per result
Request:
const params = {
target: "spotify.com",
limit: 1000,
offset: 0,
order_by: ["rank,desc"],
include_subdomains: true,
exclude_internal_backlinks: true,
include_indirect_links: true,
backlinks_status_type: "live",
internal_list_limit: 10,
rank_scale: "one_hundred",
};
const response = await backlinksRequest("/referring_domains/live", [params], env);
Response Structure:
{
"tasks": [{
"status_code": 20000,
"cost": 0.12,
"result": [{
"target": "spotify.com",
"total_count": 5847293,
"items_count": 1000,
"items": [{
"domain": "example.com",
"rank": 85,
"backlinks": 47,
"backlinks_spam_score": 12,
"referring_domains": 1250,
"broken_backlinks": 2,
"first_seen": "2019-02-16 14:22:37 +00:00",
"lost_date": null,
"referring_links_tld": { "com": 35, "org": 8, "net": 4 },
"referring_links_types": { "anchor": 40, "image": 5, "redirect": 2 },
"referring_links_platform_types": { "blogs": 15, "news": 10 },
"referring_links_semantic_locations": { "main": 30, "footer": 10 },
"referring_links_countries": { "US": 25, "GB": 12, "DE": 5 }
}]
}]
}]
}
Usage:
import { getReferringDomains, fetchAndStoreReferringDomains } from "./dataforseo-backlinks.js";
// Fetch only
const result = await getReferringDomains("spotify.com", { limit: 1000 }, env);
// Fetch and store in database
const stored = await fetchAndStoreReferringDomains("spotify.com", {
limit: 1000,
dataTier: "global_sample",
}, env);
// Returns: { total_count, fetched, inserted, updated, cost }
Domain Summary
Aggregate backlink statistics without individual domain details.
Endpoint: POST https://api.dataforseo.com/v3/backlinks/summary/live
Cost: $0.02 per request (cheap aggregate endpoint)
Request:
const params = {
target: "spotify.com",
include_subdomains: true,
exclude_internal_backlinks: true,
backlinks_status_type: "live",
};
Response:
{
"tasks": [{
"cost": 0.02,
"result": [{
"target": "spotify.com",
"rank": 92,
"backlinks": 847293847,
"referring_domains": 5847293,
"referring_domains_nofollow": 1234567,
"referring_main_domains": 4521000,
"referring_ips": 892345,
"referring_subnets": 234567,
"backlinks_spam_score": 8,
"broken_backlinks": 12847,
"broken_pages": 4521,
"referring_links_types": { "anchor": 70, "image": 20, "redirect": 10 },
"referring_links_platform_types": {
"news": 150000,
"blogs": 280000,
"ecommerce": 45000,
"message-boards": 32000,
"social": 89000
}
}]
}]
}
Usage:
import { getDomainSummary, fetchAndStoreDomainSummary } from "./dataforseo-backlinks.js";
// Fetch and store with weekly snapshots
const result = await fetchAndStoreDomainSummary("spotify.com", {}, env);
// Stores in domain_summaries table with year_week for historical tracking
Individual Backlinks
Get actual URLs linking to the target.
Endpoint: POST https://api.dataforseo.com/v3/backlinks/backlinks/live
Cost: $0.02 base + $0.0001 per result
Request:
const params = {
target: "spotify.com",
limit: 1000,
offset: 0,
order_by: ["rank,desc"],
include_subdomains: true,
exclude_internal_backlinks: true,
backlinks_status_type: "live",
mode: "as_is", // as_is, one_per_domain, one_per_anchor
};
Response includes:
url_from- Source URL of the backlinkurl_to- Target URL being linked toanchor- Anchor textdomain_from_rank- Source domain authoritypage_from_rank- Source page authoritybacklink_spam_score- Spam score (0-100)semantic_location- Where link appears (main, footer, sidebar)link_attributes- nofollow, ugc, sponsoredis_broken,is_lost- Link status
Domain Pages Summary
Get backlink stats for pages on YOUR domain (not linking domains).
Endpoint: POST https://api.dataforseo.com/v3/backlinks/domain_pages_summary/live
Cost: $0.02 base + $0.0001 per result
Usage:
import { getDomainPagesSummary, fetchAndStoreDomainPagesSummary } from "./dataforseo-backlinks.js";
// Get backlink stats for top pages on your domain
const result = await getDomainPagesSummary("notion.so", { limit: 500 }, env);
// Returns pages sorted by backlinks count with spam scores and link distributions
Bulk Spam Scores
Fetch spam scores for up to 1000 targets in one request.
Endpoint: POST https://api.dataforseo.com/v3/backlinks/bulk_spam_score/live
Cost: ~$0.02 per 1000 targets
Request:
const targets = ["forbes.com", "spamsite.xyz", "example.com"];
const response = await fetch("https://api.dataforseo.com/v3/backlinks/bulk_spam_score/live", {
method: "POST",
headers: {
Authorization: `Basic ${authB64}`,
"Content-Type": "application/json",
},
body: JSON.stringify([{ targets }]),
});
Response:
{
"tasks": [{
"cost": 0.02,
"result": [{
"items": [
{ "target": "forbes.com", "spam_score": 2 },
{ "target": "spamsite.xyz", "spam_score": 87 },
{ "target": "example.com", "spam_score": 5 }
]
}]
}]
}
Spam Score Tiers:
| Score | Tier | Description |
|---|---|---|
| 0-19 | clean | Low risk, high quality |
| 20-39 | low_risk | Acceptable |
| 40-59 | moderate | Monitor |
| 60-79 | high_risk | Caution advised |
| 80-100 | toxic | Disavow candidate |
Bulk Domain Ranks
Fetch domain authority ranks for up to 1000 domains.
Endpoint: POST https://api.dataforseo.com/v3/backlinks/bulk_ranks/live
Cost: ~$0.02 per 1000 targets
import { fetchBulkRanks, enrichDomainWithRank } from "./dataforseo-spam-score.js";
// Batch fetch
const { results, cost } = await fetchBulkRanks(["google.com", "example.com"], env);
// results = { "google.com": 100, "example.com": 45 }
// Fetch and update database with quality_tier
await enrichDomainWithRank(domainId, "example.com", env);
Keywords API
Ranked Keywords
Get keywords a domain actually ranks for with positions and URLs.
Endpoint: POST https://api.dataforseo.com/v3/dataforseo_labs/google/ranked_keywords/live
Cost: $0.05 per request
Request:
const payload = [{
target: "notion.so",
location_code: 2840,
language_code: "en",
limit: 100,
offset: 0,
order_by: ["keyword_data.keyword_info.search_volume,desc"],
load_rank_absolute: true,
ignore_synonyms: false,
include_clickstream_data: false,
}];
Response:
{
"tasks": [{
"cost": 0.05,
"result": [{
"total_count": 847293,
"items": [{
"keyword_data": {
"keyword": "notion templates",
"keyword_info": {
"search_volume": 27100,
"cpc": 2.45,
"competition": 0.65,
"monthly_searches": [
{ "month": 11, "year": 2025, "search_volume": 27100 }
]
},
"search_intent_info": {
"main_intent": "commercial"
},
"keyword_properties": {
"keyword_difficulty": 72
}
},
"ranked_serp_element": {
"url": "https://notion.so/templates",
"serp_item": {
"rank_absolute": 3,
"rank_group": 2,
"is_new": false,
"is_up": true
}
}
}]
}]
}]
}
Usage:
import { fetchRankedKeywords, fetchAllRankedKeywords } from "./dataforseo.js";
// Single page
const result = await fetchRankedKeywords("notion.so", env, { limit: 100 });
// Paginated fetch (up to max_items)
const all = await fetchAllRankedKeywords("notion.so", env, {
max_items: 1000,
batch_size: 100,
});
// Returns: { items, total_count, fetched, cost }
Keywords for Site
Get keyword opportunities for a domain.
Endpoint: POST https://api.dataforseo.com/v3/dataforseo_labs/google/keywords_for_site/live
Cost: ~$0.004 per 100 keywords
import { fetchKeywordsForSite } from "./dataforseo.js";
const keywords = await fetchKeywordsForSite("notion.so", env, {
limit: 100,
location_code: 2840,
language_code: "en",
});
Keyword Suggestions (Labs)
Get related keywords with full metrics.
Endpoint: POST https://api.dataforseo.com/v3/dataforseo_labs/google/keyword_suggestions/live
Cost: ~$0.004 per 100 keywords
import { fetchKeywordSuggestionsLabs } from "./dataforseo.js";
const result = await fetchKeywordSuggestionsLabs({
keyword: "project management",
location_code: 2840,
language_code: "en",
limit: 100,
include_seed_keyword: true,
include_serp_info: false,
order_by: ["keyword_info.search_volume,desc"],
}, env);
// Returns: { suggestions, meta, raw }
Keyword Suggestions (Google Ads)
Get related keywords from Google Ads API.
Endpoint: POST https://api.dataforseo.com/v3/keywords_data/google_ads/keywords_for_keywords/live
Cost: ~$0.05 per request
import { fetchKeywordSuggestions } from "./dataforseo.js";
const suggestions = await fetchKeywordSuggestions("seo tools", env, {
limit: 100,
location_code: 2840,
include_seed_keyword: true,
});
Categories for Domain
Get DataForSEO category IDs for a domain based on its keyword rankings.
Endpoint: POST https://api.dataforseo.com/v3/dataforseo_labs/google/categories_for_domain/live
import { fetchCategoriesForDomain } from "./dataforseo.js";
const categories = await fetchCategoriesForDomain("notion.so", env, {
limit: 100,
include_subcategories: false,
});
// Returns categories with organic/paid ETV and keyword counts
Domain Metrics by Category
Get top domains for a specific category with traffic metrics.
Endpoint: POST https://api.dataforseo.com/v3/dataforseo_labs/google/domain_metrics_by_categories/live
import { fetchDomainMetricsByCategory } from "./dataforseo.js";
const domains = await fetchDomainMetricsByCategory(12045, env, {
limit: 100,
first_date: "2025-09-01",
second_date: "2025-11-01",
});
// Returns domains with organic ETV, keyword counts, and period-over-period changes
On-Page API (Instant Pages)
Fetch rendered HTML content from URLs with metadata extraction.
Rate Limits
- 2,000 requests/minute
- 20 tasks (URLs) per request
- Max 5 same-domain URLs per batch
- 30 concurrent requests
Theoretical throughput: 40,000 URLs/minute
Costs
- Cost per URL: $0.000125
Batched Requests
import { fetchInstantPagesBatch, fetchInstantPagesSingle } from "./instant-pages-batch.js";
// Batch fetch (recommended)
const results = await fetchInstantPagesBatch(
["https://example.com/page1", "https://example.com/page2"],
env,
{
enable_javascript: true,
enable_browser_rendering: false,
load_resources: false,
store_raw_html: false,
}
);
// Returns: Map<url, result>
// Single URL convenience wrapper
const result = await fetchInstantPagesSingle("https://example.com", env);
Request Payload:
[{
"url": "https://example.com/page",
"check_spell": false,
"disable_cookie_popup": true,
"return_despite_timeout": true,
"load_resources": false,
"enable_javascript": true,
"enable_browser_rendering": false,
"store_raw_html": false
}]
Response Structure:
{
success: true,
url: "https://example.com/page",
status_code: 200,
content: {
title: "Page Title",
description: "Meta description",
h1: ["Main Heading"],
h2: ["Subheading 1", "Subheading 2"],
h3: [],
plain_text: "Full text content...",
word_count: 1500,
internal_links_count: 25,
external_links_count: 8,
},
raw: {
canonical: "https://example.com/page",
og_tags: { "og:title": "...", "og:image": "..." },
scripts: ["https://cdn.example.com/app.js"],
links: { internal: [...], external: [...] },
content_type: "text/html",
charset: "utf-8",
raw_html: null, // Only if store_raw_html: true
},
cost: 0.000125,
}
App Data API
Google Play App List
Get apps from Google Play charts by category.
Endpoint: POST https://api.dataforseo.com/v3/app_data/google/app_list/task_post
import { getGooglePlayCategoryApps } from "./dataforseo-api.js";
const task = await getGooglePlayCategoryApps("PRODUCTIVITY", {
appCollection: "topselling_free", // topselling_free, topselling_paid, topgrossing
locationCode: 2840,
depth: 100,
priority: 1, // 1 = normal (45 min), 2 = high (1 min)
}, env);
// Returns: { task_id, cost, status_code }
Apple App Store
import { getAppleCategoryApps, getAppleAppInfo } from "./dataforseo-api.js";
// Category list
const task = await getAppleCategoryApps("6007", { // Productivity category
appCollection: "top_free_ios",
depth: 100,
}, env);
// Individual app info
const appTask = await getAppleAppInfo("284882215", env); // Facebook app ID
App Listings Search (Live)
Instant results for app searches by category.
import { searchGooglePlayListings, searchAppleListings } from "./dataforseo-listings.js";
// Google Play
const result = await searchGooglePlayListings("Productivity", {
limit: 100,
appCollection: "topselling_free",
}, env);
// Apple
const result = await searchAppleListings(["Productivity"], {
limit: 100,
locationCode: 2840,
}, env);
Rate Limiting
Account Limits
| Limit Type | Value |
|---|---|
| Requests per minute | 2,000 |
| Concurrent requests | 30 |
| Tasks per request | Varies by endpoint (1-1000) |
| Daily requests | Unlimited (cost-based) |
Handling Rate Limits
// Rate limit error response
{
"status_code": 40901,
"status_message": "Rate limit exceeded"
}
// Retry with exponential backoff
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (err) {
if (err.message.includes("Rate limit") && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
await new Promise(res => setTimeout(res, delay));
continue;
}
throw err;
}
}
}
Cost Management
Per-Call Costs
| Endpoint | Base Cost | Per-Result Cost |
|---|---|---|
| Backlinks - Referring Domains | $0.02 | $0.0001/domain |
| Backlinks - Summary | $0.02 | - |
| Backlinks - Backlinks | $0.02 | $0.0001/link |
| Backlinks - Bulk Spam Score | $0.02 | Per 1000 targets |
| Backlinks - Bulk Ranks | $0.02 | Per 1000 targets |
| Labs - Ranked Keywords | $0.05 | - |
| Labs - Keywords for Site | ~$0.004 | Per 100 keywords |
| Labs - Keyword Suggestions | ~$0.004 | Per 100 keywords |
| Keywords Data - Keywords for Keywords | $0.05 | - |
| On-Page - Instant Pages | $0.000125 | Per URL |
| App Data - App List | $0.01-0.05 | Varies |
Cost Tracking
All API calls track costs via centralized cost tracker:
import { trackCost, COST_SERVICES } from "../utils/cost-tracker.js";
// Track after successful API call
if (task.cost > 0) {
await trackCost(env, {
service: COST_SERVICES.DATAFORSEO_BACKLINKS,
cost_usd: task.cost,
endpoint: "/referring_domains/live",
items_returned: result.items_count || 0,
success: true,
});
}
Budget Enforcement
// Check daily budget before expensive operations
const spent = await env.DFS_BUDGETS.get('budget:daily');
const limit = parseFloat(env.DATAFORSEO_DAILY_BUDGET || 50);
if (parseFloat(spent || 0) >= limit) {
throw new Error('Daily DataForSEO budget exceeded');
}
Cost Optimization Best Practices
-
Use batch endpoints - 100 keywords in 1 request = $0.004 vs 100 separate requests = $0.40
-
Use bulk endpoints - Bulk spam scores/ranks: 1000 targets for $0.02 vs individual lookups
-
Use summary endpoints - Domain summary ($0.02) vs full referring domains ($0.02 + $0.0001/result)
-
Cache aggressively - Category data rarely changes, keyword data changes monthly
-
Set daily budgets - Prevent runaway costs from bugs or loops
Error Handling
Status Codes
| Code | Meaning | Action |
|---|---|---|
| 20000 | Success | Process results |
| 20100 | Task in queue | Poll for results |
| 40001 | Invalid parameters | Fix request payload |
| 40101 | Authentication failed | Check credentials |
| 40201 | Rate limit exceeded | Implement backoff |
| 40204 | Insufficient balance | Add credits |
| 50000 | Internal server error | Retry with backoff |
Error Response Example
{
"status_code": 40101,
"status_message": "Incorrect login or password",
"tasks": null
}
Error Handling Pattern
async function makeDataForSEORequest(endpoint, payload, env) {
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: getDataForSEOAuth(env),
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
const err = new Error(
`DataForSEO API error ${response.status}: ${errorText.slice(0, 200)}`
);
// Set appropriate error status for upstream handling
if (response.status === 429) {
err.status = 429; // Rate limit - retry
} else if (response.status >= 400 && response.status < 500) {
err.status = 400; // Bad request - don't retry
} else {
err.status = 502; // Server error - retry
}
throw err;
}
const data = await response.json();
if (data.status_code !== 20000) {
throw new Error(`DataForSEO error: ${data.status_message} (code: ${data.status_code})`);
}
const task = data.tasks?.[0];
if (!task || task.status_code !== 20000) {
throw new Error(`DataForSEO task error: ${task?.status_message || "No task returned"}`);
}
return task;
}
Retry Strategy
| Error Type | Retry? | Strategy |
|---|---|---|
| Network errors | Yes | Exponential backoff |
| 5xx server errors | Yes | Exponential backoff |
| Rate limit (429/40201) | Yes | Longer backoff (5-30s) |
| 401 authentication | No | Check credentials |
| 400 bad request | No | Fix request |
| 402 insufficient funds | No | Add credits |
Caching
Cache Strategy by Data Type
| Data Type | TTL | Cache Key Pattern |
|---|---|---|
| Category taxonomy | 30 days | dfs:category:{id} |
| Domain summary | 7 days | dfs:summary:{domain}:{yearWeek} |
| Keyword suggestions | 7 days | dfs:suggestions:{keyword}:{location} |
| Ranked keywords | 1 day | dfs:ranked:{domain}:{location} |
| Spam scores | 7 days | dfs:spam:{domain} |
| Instant pages | 1 day | dfs:page:{urlHash} |
KV Cache Implementation
// Check cache before API call
const cacheKey = `dfs:suggestions:${keyword}:${locationCode}`;
const cached = await env.DFS_CACHE.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch from DataForSEO
const result = await fetchFromDataForSEO(...);
// Cache result with TTL
await env.DFS_CACHE.put(cacheKey, JSON.stringify(result), {
expirationTtl: 86400 * 7, // 7 days
});
return result;
Category Taxonomy Caching
DataForSEO categories are stored in KV with local JSON fallback:
import { getCategoryPath, getCategoryPaths } from "./dataforseo-categories.js";
// Single category lookup (uses in-memory cache -> KV -> local JSON)
const path = await getCategoryPath("12045", env);
// Returns: "Internet & Telecom > Web Services > SEO & SEM"
// Batch lookup
const paths = await getCategoryPaths(["12045", "12046"], env);
// Returns: { "12045": "...", "12046": "..." }
Location and Language Codes
Common Location Codes
| Code | Location |
|---|---|
| 2840 | United States |
| 2826 | United Kingdom |
| 2124 | Canada |
| 2036 | Australia |
| 2276 | Germany |
| 2250 | France |
| 2392 | Japan |
| 2076 | Brazil |
Common Language Codes
| Code | Language |
|---|---|
| en | English |
| es | Spanish |
| fr | French |
| de | German |
| pt | Portuguese |
| ja | Japanese |
| zh | Chinese |
Full lists: Location Codes | Language Codes
Database Integration
Upsert Patterns
RankFabric uses batched upserts to efficiently store DataForSEO data:
// Batched upsert for referring domains (50 per batch)
export async function upsertReferringDomains(items, targetDomain, env, options = {}) {
const statements = items.map((item) => {
const mapped = mapReferringDomain(item, targetDomain, options);
return env.DB.prepare(`
INSERT INTO referring_domains (...) VALUES (...)
ON CONFLICT(target_domain, referring_domain) DO UPDATE SET
rank = excluded.rank,
backlinks_count = excluded.backlinks_count,
...
`).bind(...);
});
// Execute in batches of 50 (D1 limit)
const BATCH_SIZE = 50;
for (let i = 0; i < statements.length; i += BATCH_SIZE) {
await env.DB.batch(statements.slice(i, i + BATCH_SIZE));
}
}
Weekly Snapshots
Domain summaries are stored with year_week for historical tracking:
export function getCurrentYearWeek() {
const now = new Date();
const year = now.getFullYear();
const oneJan = new Date(year, 0, 1);
const week = Math.ceil(((now - oneJan) / 86400000 + oneJan.getDay() + 1) / 7);
return year * 100 + week; // e.g., 202547
}