Skip to Content
ProvidersSEMrush

SEMrush

Category: SEO & Research
Integration type: Tenant-configured API key (stored in integrations table)
External API: SEMrush REST API v3


Purpose

SEMrush is the primary keyword research and competitive analysis tool. The Keyword Researcher agent and Site Auditor agent call SEMrush APIs to:

  • Find keyword clusters for a given domain/industry
  • Get keyword difficulty, search volume, and CPC data
  • Pull competitor domain keyword gaps
  • Retrieve domain-level organic traffic estimates

Tenants provide their own SEMrush API key (tied to their SEMrush subscription). The platform does not maintain a shared SEMrush account — every API call bills to the tenant’s SEMrush account.


Config Structure

Tenant integration (stored in integrations table)

// integrations row { tenant_id: '<uuid>', provider: 'semrush', api_key: '<encrypted SEMrush API key>', // AES-256-GCM metadata: { database: 'us', // SEMrush regional database: 'us' | 'uk' | 'in' | 'au' | ... }, connected_at: TIMESTAMPTZ, }
interface SemrushConfig { apiKey: string; // SEMrush API key from semrush.com/api/ database: string; // Regional database code (affects keyword data source) }

Supported regional databases

CodeRegion
usUnited States
ukUnited Kingdom
inIndia
auAustralia
caCanada
deGermany

Integration Pattern

Tool layer (packages/tools/src/semrush.ts)

The SEMrush tool is called by the Keyword Researcher agent via the tool integration layer.

class SemrushTool { constructor( private apiKey: string, private database: string, ) {} async getKeywordData(keywords: string[]): Promise<KeywordDataRow[]> { const response = await axios.get('https://api.semrush.com/', { params: { type: 'phrase_these', key: this.apiKey, phrase: keywords.join(';'), database: this.database, export_columns: 'Ph,Nq,Cp,Co,Nr', // Ph = keyword phrase, Nq = search volume, Cp = CPC, Co = competition, Nr = results }, }); return this.parseCsvResponse(response.data); } async getDomainKeywords(domain: string, limit = 50): Promise<DomainKeywordRow[]> { const response = await axios.get('https://api.semrush.com/', { params: { type: 'domain_organic', key: this.apiKey, domain: domain, database: this.database, display_limit: limit, display_sort: 'tr_desc', // Sort by traffic descending export_columns: 'Ph,Po,Nq,Cp,Ur', }, }); return this.parseCsvResponse(response.data); } async getKeywordGap( domain: string, competitors: string[], ): Promise<KeywordGapRow[]> { const response = await axios.get('https://api.semrush.com/', { params: { type: 'phrase_kdi', key: this.apiKey, domains: [domain, ...competitors].join(';'), database: this.database, display_filter: '+|Po|Lt|4', // Only keywords where domain ranks page 1 }, }); return this.parseCsvResponse(response.data); } private parseCsvResponse(raw: string): Record<string, string>[] { const [header, ...rows] = raw.trim().split('\r\n'); const cols = header.split(';'); return rows.map(row => { const vals = row.split(';'); return Object.fromEntries(cols.map((c, i) => [c, vals[i]])); }); } }

API response format

SEMrush returns semicolon-delimited CSV (not JSON). Each call type has different column codes. The tool layer normalises all responses into typed objects before returning them to the agent.


API Units

SEMrush billing is based on API units (not API calls). Different report types cost different numbers of units:

Report typeUnits per row returned
phrase_these (keyword data)10 units per keyword
domain_organic (domain keywords)10 units per row
phrase_kdi (keyword gap)10 units per domain

Tenants need a SEMrush plan with API access (Business plan or higher). The platform does not enforce unit limits — if a tenant’s account runs out of units, the SEMrush API returns an error that surfaces as an activity failure.


Test Cases

Unit tests (packages/tools/src/semrush.test.ts)

TestApproach
getKeywordData() builds correct API paramsMock axios.get; assert type, key, phrase, database
getKeywordData() parses CSV response correctlyFeed mock CSV string; assert parsed rows
getDomainKeywords() returns sorted domain keywordsMock CSV; assert display_sort=tr_desc in params
getKeywordGap() includes all domains in paramsAssert domains param contains main + competitors
Throws on non-200 responseMock 403; assert error propagated
Throws on SEMrush error response ERROR 50 :: ...Feed error string as CSV body; assert detected and thrown

Integration tests

TestApproach
Real keyword data for known keywordUse test API key; fetch seo tools; assert Nq > 0
Domain keywords for known domainFetch for semrush.com; assert results returned

Error Handling

SEMrush API errors are returned as HTTP 200 with body like ERROR 50 :: NOTHING_FOUND. The tool layer must check for this pattern:

if (response.data.startsWith('ERROR')) { const [, code, message] = response.data.match(/ERROR (\d+) :: (.+)/) ?? []; throw new SemrushApiError(Number(code), message, { domain, database }); }

Common error codes:

CodeMeaningAction
50Nothing foundReturn empty array — not a hard failure
120No API units remainingFail activity with CREDITS_EXHAUSTED
130Invalid API keyFail and prompt tenant to reconnect

© 2026 Leadmetrics — Internal use only