Skip to content

Commit 934dca7

Browse files
committed
feat(ahrefs): validate integration, fix cents/column bugs, add 13 v3 endpoints
Audited the existing Ahrefs integration against live API v3 docs and fixed real bugs: broken_backlinks selected the wrong column (http_code_target instead of http_code), and keyword_overview/metrics/metrics_history returned CPC/cost fields in USD cents without converting to USD. Also fixed the top-pages mode dropdown missing the "exact" option. Added 13 new tools covering previously-unsupported Ahrefs v3 endpoints: Rank Tracker (overview, SERP overview, competitors overview, competitors stats), Batch Analysis, Site Audit page explorer, four history/trend endpoints (domain rating, metrics, referring domains, keywords), Related Terms, Anchors, and Paid Pages. Note: the CPC/cost unit fix silently shifts existing organicCost/paidCost/cpc values by 100x for any workflow already consuming them (the old values were wrong, in cents instead of dollars).
1 parent 018283b commit 934dca7

22 files changed

Lines changed: 3468 additions & 14 deletions

apps/docs/content/docs/en/integrations/ahrefs.mdx

Lines changed: 372 additions & 2 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/ahrefs.ts

Lines changed: 840 additions & 5 deletions
Large diffs are not rendered by default.

apps/sim/lib/integrations/integrations.json

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"updatedAt": "2026-07-02",
2+
"updatedAt": "2026-07-06",
33
"integrations": [
44
{
55
"type": "onepassword",
@@ -387,12 +387,64 @@
387387
"name": "Top Pages",
388388
"description": "Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value."
389389
},
390+
{
391+
"name": "Paid Pages",
392+
"description": "Get a target domain's pages that receive paid search traffic, sorted by estimated paid traffic. Returns page URLs with their paid traffic, keyword counts, and estimated spend."
393+
},
394+
{
395+
"name": "Anchors",
396+
"description": "Get the anchor text distribution for a target domain or URL's backlinks, showing how many links and referring domains use each anchor text."
397+
},
390398
{
391399
"name": "Keyword Overview",
392400
"description": "Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential."
401+
},
402+
{
403+
"name": "Related Terms",
404+
"description": "Get keyword ideas related to a seed keyword: terms the same top-ranking pages also rank for (\"also rank for\") or also discuss (\"also talk about\"), with volume, difficulty, and CPC."
405+
},
406+
{
407+
"name": "Domain Rating History",
408+
"description": "Get the historical Domain Rating (DR) trend for a target domain or URL over a date range, grouped daily, weekly, or monthly."
409+
},
410+
{
411+
"name": "Metrics History",
412+
"description": "Get the historical organic and paid traffic trend for a target domain or URL over a date range: organic traffic/cost and paid traffic/cost at each point in time."
413+
},
414+
{
415+
"name": "Referring Domains History",
416+
"description": "Get the historical referring domains trend for a target domain or URL over a date range, grouped daily, weekly, or monthly."
417+
},
418+
{
419+
"name": "Keywords History",
420+
"description": "Get the historical organic keyword ranking distribution for a target domain or URL over a date range: how many keywords rank in each position bucket at each point in time."
421+
},
422+
{
423+
"name": "Batch Analysis",
424+
"description": "Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once."
425+
},
426+
{
427+
"name": "Site Audit Page Explorer",
428+
"description": "Get crawled pages from an Ahrefs Site Audit project with health and SEO metrics: HTTP status, title, link counts, backlinks, indexability, and traffic. Optionally filter to pages affected by a specific issue."
429+
},
430+
{
431+
"name": "Rank Tracker Overview",
432+
"description": "Get ranking overview metrics for the keywords tracked in an Ahrefs Rank Tracker project: position, search volume, keyword difficulty, and estimated traffic. This endpoint is free and does not consume API units."
433+
},
434+
{
435+
"name": "Rank Tracker SERP Overview",
436+
"description": "Get the full SERP (search engine results page) for a keyword tracked in an Ahrefs Rank Tracker project, including every ranking URL with its position, title, and authority metrics. This endpoint is free and does not consume API units."
437+
},
438+
{
439+
"name": "Rank Tracker Competitors Overview",
440+
"description": "Get competitor rankings for the keywords tracked in an Ahrefs Rank Tracker project: each tracked keyword's volume and difficulty alongside every competitor's position, traffic, and traffic value. This endpoint is free and does not consume API units."
441+
},
442+
{
443+
"name": "Rank Tracker Competitors Stats",
444+
"description": "Get aggregate competitor stats for an Ahrefs Rank Tracker project: each competitor's traffic, traffic value, average position, and share of voice across all tracked keywords. This endpoint is free and does not consume API units."
393445
}
394446
],
395-
"operationCount": 10,
447+
"operationCount": 23,
396448
"triggers": [],
397449
"triggerCount": 0,
398450
"authType": "api-key",

apps/sim/tools/ahrefs/anchors.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { AhrefsAnchorsParams, AhrefsAnchorsResponse } from '@/tools/ahrefs/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
const SELECT_FIELDS = 'anchor,links_to_target,dofollow_links,refdomains,first_seen,last_seen'
5+
6+
export const anchorsTool: ToolConfig<AhrefsAnchorsParams, AhrefsAnchorsResponse> = {
7+
id: 'ahrefs_anchors',
8+
name: 'Ahrefs Anchors',
9+
description:
10+
"Get the anchor text distribution for a target domain or URL's backlinks, showing how many links and referring domains use each anchor text.",
11+
version: '1.0.0',
12+
13+
params: {
14+
target: {
15+
type: 'string',
16+
required: true,
17+
visibility: 'user-or-llm',
18+
description:
19+
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
20+
},
21+
mode: {
22+
type: 'string',
23+
required: false,
24+
visibility: 'user-or-llm',
25+
description:
26+
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
27+
},
28+
history: {
29+
type: 'string',
30+
required: false,
31+
visibility: 'user-or-llm',
32+
description:
33+
'Historical scope: "live" (currently live), "all_time" (default, includes lost backlinks), or "since:YYYY-MM-DD" (backlinks found since a date)',
34+
},
35+
limit: {
36+
type: 'number',
37+
required: false,
38+
visibility: 'user-or-llm',
39+
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
40+
},
41+
apiKey: {
42+
type: 'string',
43+
required: true,
44+
visibility: 'user-only',
45+
description: 'Ahrefs API Key',
46+
},
47+
},
48+
49+
request: {
50+
url: (params) => {
51+
const url = new URL('https://api.ahrefs.com/v3/site-explorer/anchors')
52+
url.searchParams.set('target', params.target)
53+
url.searchParams.set('select', SELECT_FIELDS)
54+
if (params.mode) url.searchParams.set('mode', params.mode)
55+
url.searchParams.set('history', params.history || 'all_time')
56+
if (params.limit) url.searchParams.set('limit', String(params.limit))
57+
return url.toString()
58+
},
59+
method: 'GET',
60+
headers: (params) => ({
61+
Accept: 'application/json',
62+
Authorization: `Bearer ${params.apiKey}`,
63+
}),
64+
},
65+
66+
transformResponse: async (response: Response) => {
67+
const data = await response.json()
68+
69+
if (!response.ok) {
70+
throw new Error(data.error?.message || data.error || 'Failed to get anchors')
71+
}
72+
73+
const anchors = (data.anchors || []).map((item: any) => ({
74+
anchor: item.anchor || '',
75+
backlinks: item.links_to_target ?? 0,
76+
dofollowBacklinks: item.dofollow_links ?? 0,
77+
referringDomains: item.refdomains ?? 0,
78+
firstSeen: item.first_seen || '',
79+
lastSeen: item.last_seen ?? null,
80+
}))
81+
82+
return {
83+
success: true,
84+
output: {
85+
anchors,
86+
},
87+
}
88+
},
89+
90+
outputs: {
91+
anchors: {
92+
type: 'array',
93+
description: 'Anchor text distribution for the backlink profile',
94+
items: {
95+
type: 'object',
96+
properties: {
97+
anchor: { type: 'string', description: 'The anchor text' },
98+
backlinks: { type: 'number', description: 'Total backlinks using this anchor text' },
99+
dofollowBacklinks: {
100+
type: 'number',
101+
description: 'Number of dofollow backlinks using this anchor text',
102+
},
103+
referringDomains: {
104+
type: 'number',
105+
description: 'Number of unique referring domains using this anchor text',
106+
},
107+
firstSeen: {
108+
type: 'string',
109+
description: 'When a link with this anchor was first found',
110+
},
111+
lastSeen: {
112+
type: 'string',
113+
description: 'When a backlink with this anchor was last seen (null if still live)',
114+
optional: true,
115+
},
116+
},
117+
},
118+
},
119+
},
120+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type { AhrefsBatchAnalysisParams, AhrefsBatchAnalysisResponse } from '@/tools/ahrefs/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
const SELECT_FIELDS =
5+
'url,domain_rating,ahrefs_rank,backlinks,refdomains,org_traffic,org_keywords,paid_traffic'
6+
7+
export const batchAnalysisTool: ToolConfig<AhrefsBatchAnalysisParams, AhrefsBatchAnalysisResponse> =
8+
{
9+
id: 'ahrefs_batch_analysis',
10+
name: 'Ahrefs Batch Analysis',
11+
description:
12+
'Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once.',
13+
version: '1.0.0',
14+
15+
params: {
16+
targets: {
17+
type: 'string',
18+
required: true,
19+
visibility: 'user-or-llm',
20+
description:
21+
'Comma-separated list of domains or URLs to analyze. Example: "example.com,competitor.com"',
22+
},
23+
mode: {
24+
type: 'string',
25+
required: false,
26+
visibility: 'user-or-llm',
27+
description:
28+
'Analysis mode applied to every target: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
29+
},
30+
protocol: {
31+
type: 'string',
32+
required: false,
33+
visibility: 'user-or-llm',
34+
description: 'Protocol applied to every target: "both" (default), "http", or "https"',
35+
},
36+
country: {
37+
type: 'string',
38+
required: false,
39+
visibility: 'user-or-llm',
40+
description: 'Country code for traffic data. Example: "us", "gb", "de"',
41+
},
42+
volumeMode: {
43+
type: 'string',
44+
required: false,
45+
visibility: 'user-or-llm',
46+
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
47+
},
48+
apiKey: {
49+
type: 'string',
50+
required: true,
51+
visibility: 'user-only',
52+
description: 'Ahrefs API Key',
53+
},
54+
},
55+
56+
request: {
57+
url: () => 'https://api.ahrefs.com/v3/batch-analysis/batch-analysis',
58+
method: 'POST',
59+
headers: (params) => ({
60+
Accept: 'application/json',
61+
'Content-Type': 'application/json',
62+
Authorization: `Bearer ${params.apiKey}`,
63+
}),
64+
body: (params) => {
65+
const targets = params.targets
66+
.split(',')
67+
.map((target) => target.trim())
68+
.filter((target) => target.length > 0)
69+
.map((url) => ({
70+
url,
71+
mode: params.mode || 'subdomains',
72+
protocol: params.protocol || 'both',
73+
}))
74+
75+
return {
76+
select: SELECT_FIELDS.split(','),
77+
targets,
78+
...(params.country ? { country: params.country } : {}),
79+
volume_mode: params.volumeMode || 'monthly',
80+
}
81+
},
82+
},
83+
84+
transformResponse: async (response: Response) => {
85+
const data = await response.json()
86+
87+
if (!response.ok) {
88+
throw new Error(data.error?.message || data.error || 'Failed to run batch analysis')
89+
}
90+
91+
const results = (data.targets || data.results || []).map((item: any) => ({
92+
url: item.url || '',
93+
index: item.index ?? 0,
94+
domainRating: item.domain_rating ?? null,
95+
ahrefsRank: item.ahrefs_rank ?? null,
96+
backlinks: item.backlinks ?? null,
97+
referringDomains: item.refdomains ?? null,
98+
organicTraffic: item.org_traffic ?? null,
99+
organicKeywords: item.org_keywords ?? null,
100+
paidTraffic: item.paid_traffic ?? null,
101+
error: item.error ?? null,
102+
}))
103+
104+
return {
105+
success: true,
106+
output: {
107+
results,
108+
},
109+
}
110+
},
111+
112+
outputs: {
113+
results: {
114+
type: 'array',
115+
description: 'Bulk metrics for each analyzed target, in submission order',
116+
items: {
117+
type: 'object',
118+
properties: {
119+
url: { type: 'string', description: 'The analyzed target URL or domain' },
120+
index: { type: 'number', description: 'Index of the target in the submitted list' },
121+
domainRating: {
122+
type: 'number',
123+
description: 'Domain Rating score (0-100)',
124+
optional: true,
125+
},
126+
ahrefsRank: {
127+
type: 'number',
128+
description: 'Ahrefs Rank (global ranking)',
129+
optional: true,
130+
},
131+
backlinks: {
132+
type: 'number',
133+
description: 'Total backlinks to the target',
134+
optional: true,
135+
},
136+
referringDomains: {
137+
type: 'number',
138+
description: 'Unique domains linking to the target',
139+
optional: true,
140+
},
141+
organicTraffic: {
142+
type: 'number',
143+
description: 'Estimated monthly organic traffic',
144+
optional: true,
145+
},
146+
organicKeywords: {
147+
type: 'number',
148+
description: 'Number of organic keywords ranked (top 100)',
149+
optional: true,
150+
},
151+
paidTraffic: {
152+
type: 'number',
153+
description: 'Estimated monthly paid search traffic',
154+
optional: true,
155+
},
156+
error: {
157+
type: 'string',
158+
description: 'Error message if this target could not be analyzed',
159+
optional: true,
160+
},
161+
},
162+
},
163+
},
164+
},
165+
}

apps/sim/tools/ahrefs/broken_backlinks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
} from '@/tools/ahrefs/types'
55
import type { ToolConfig } from '@/tools/types'
66

7-
const SELECT_FIELDS = 'url_from,url_to,http_code_target,anchor,domain_rating_source'
7+
const SELECT_FIELDS = 'url_from,url_to,http_code,anchor,domain_rating_source'
88

99
export const brokenBacklinksTool: ToolConfig<
1010
AhrefsBrokenBacklinksParams,
@@ -71,7 +71,7 @@ export const brokenBacklinksTool: ToolConfig<
7171
const brokenBacklinks = (data.backlinks || []).map((link: any) => ({
7272
urlFrom: link.url_from || '',
7373
urlTo: link.url_to || '',
74-
httpCode: link.http_code_target ?? null,
74+
httpCode: link.http_code ?? null,
7575
anchor: link.anchor || '',
7676
domainRatingSource: link.domain_rating_source ?? 0,
7777
}))

0 commit comments

Comments
 (0)