You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a new Semrush Elements API wrapper endpoint that returns the prompts (and their count) for the URL Inspector / Brand Presence dashboards, filtered by topic tag, AI model, and one or more Semrush projects.
This is the next endpoint in the Semrush Elements wrapper family introduced by:
It wraps a new Semrush element that is not yet registered in src/support/elements/element-ids.js:
POST .../products/ai/elements/406ba6e0-0de2-475e-80d9-42fab8616032
Observed upstream contract
The UI/POC calls the Semrush element directly today. This is the request/response we must reproduce through the SpaceCat wrapper (SpaceCat mints the IMS token and injects the workspace).
{
"type": "table",
"blocks": {
"data": [
{ "primary_intent": "informational", "prompt": "can i make ai influencer for free", "prompt_topic": "AI Instagram Influencers", "volume": 2119 },
{ "primary_intent": "informational", "prompt": "What is the best AI free image generator?", "prompt_topic": "AI Image Generators", "volume": 997 }
]
},
"config": { "data": null }
}
Gateway note: the curl above hits the public apis/v4-raw/external-api/v1/... path and wraps the payload in render_data. The SpaceCat transport (src/support/elements/elements-transport.js#fetchElement) posts to the internal v3 gateway/enterprise/pages/api/v3/workspaces/{workspaceId}/products/ai/elements/{elementId}/data and sends the inner object directly (no render_data wrapper) — exactly like the existing buildMarketsPayload / buildTopicsPayload / buildWeeksPayload builders. The new payload builder must return the un-wrapped { comparison_data_formatting, filters: { advanced: {...} } } shape, not the render_data-wrapped shape.
Proposed endpoint
GET /v2/orgs/:spaceCatId/serenity/all/brand-presence/prompts
Sits alongside the existing org-scoped elements routes in src/routes/index.js (lines 241–242):
GET /v2/orgs/:spaceCatId/serenity/all/brand-presence/url-inspector/filter-dimensions → listUrlInspectorFilterDimensions
GET /v2/orgs/:spaceCatId/serenity/all/brand-presence/weeks → listWeeks
Query parameters
Name
Required
Maps to
Notes
spaceCatId (path)
✅
workspace resolution
Resolved to semrush_workspace_id via authorizeOrg (already implemented)
model / platform
❌
CBF_model (OR group)
Run through resolveElementModel() (UI platform code → Semrush model, default search-gpt). model wins if both sent — mirror buildWeeksPayload
topic
❌
tags contains topic:<value>
The topic: prefix is added server-side (the filter-dimensions topics array returns bare labels). Support one or more (repeated param or CSV) → multiple contains filters
projectId
❌
CBF_project (OR group)
Semrush project UUID(s). Support one or more (repeated param or CSV). The UI already has these as semrush_project_id from the filter-dimensions regions array
Open question (resolve with product / UI before build):
Should the endpoint also accept a siteId and reverse-map it to the site's brand's Semrush projects (CBF_project), the way listWeeks reverse-maps siteId → brand → CBF_ws_brand? The observed payload passes explicit CBF_project UUIDs, so explicit projectId is the primary contract; siteId resolution would be an additive convenience. Recommend shipping explicit projectId first.
Response shape / naming. Branch is feat/prompt_count_endpoint. Confirm whether consumers want the count only, the prompt list, or both. Recommendation below returns both so one endpoint serves both needs.
Recommended response
{
"count": 6,
"prompts": [
{ "prompt": "can i make ai influencer for free", "topic": "AI Instagram Influencers", "intent": "informational", "volume": 2119 },
{ "prompt": "What is the best AI free image generator?", "topic": "AI Image Generators", "intent": "informational", "volume": 997 }
]
}
count = prompts.length. Field rename prompt_topic → topic, primary_intent → intent keeps the wrapper contract UI-facing rather than leaking raw Semrush column names (consistent with how the other definitions reshape their raw rows). Confirm final field names with the UI.
Implementation checklist
Mirror the #2755 (weeks) diff — it touched the exact same set of files.
src/support/elements/element-ids.js — register the new element:
// Topic Prompts (URL Inspector prompt list / count)PROMPTS_TABLE: '406ba6e0-0de2-475e-80d9-42fab8616032',// confirm canonical constant name
src/support/elements/definitions/prompts.js (new) — buildPromptsPayload({ model, platform, topics, projectIds }) returning the un-wrapped payload (advanced and group: tags contains topic:* filters + CBF_model OR group via resolveElementModel + CBF_project OR group), and transformPromptsResponse(raw) mapping raw.blocks.data[] → { prompt, topic, intent, volume } and computing count. Guard against a missing/empty blocks.data (return { count: 0, prompts: [] }).
src/support/elements/definitions/index.js — re-export the two new functions.
src/support/elements/elements-service.js — add getPrompts(workspaceId, params) that calls transport.fetchElement(workspaceId, ELEMENT_IDS.PROMPTS_TABLE, buildPromptsPayload(params)) and returns transformPromptsResponse(raw).
src/controllers/elements.js — add a listPrompts(ctx) handler modeled on listWeeks: authorizeOrg → extractQuery → parse topic/projectId (repeated + CSV) → buildService(ctx).getPrompts(...) → ok(result) → mapError. Export it from the controller factory.
src/routes/index.js — register GET /v2/orgs/:spaceCatId/serenity/all/brand-presence/prompts → elementsController.listPrompts.
test/routes/index.test.js — add the new route to the expected-routes list (the route-map test fails the build otherwise).
Docs — extend docs/llmo-semrush-apis/filter-dimensions-apis.md (add a "List Prompts" section) and add the new element row to docs/elements/semrush-elements-api-reference.md.
FACS param classification
No new dynamic :param is introduced (only the existing :spaceCatId, already classified), so the routeFacsCapabilities disjointness test needs no new alias — but the route must still be added to both capability maps in steps 7–8 (the FACS capability test enumerates all routes).
Auth model — REQUIRED (per platform guidance)
Any Semrush-wrapping API must use x-promise-token as the request header for the upstream Semrush call, and authenticate to SpaceCat itself with the spacecat JWT session token on Authorization. This is live in prod now.
Concretely, for this endpoint:
AuthN to SpaceCat: caller sends a spacecat JWT session token on Authorization: Bearer <jwt> (a non-IMS credential — authInfo.getType() !== 'ims').
AuthN to Semrush: caller sends x-promise-token (minted by POST /auth/v2/promise). The controller exchanges it for an IMS token and forwards that upstream.
The shared elements layer already implements exactly this and needs no new work — just make sure the new listPrompts handler goes through it (as listWeeks does):
resolveElementsImsToken (src/controllers/elements.js) checks x-promise-tokenfirst via the shared resolveSemrushImsToken helper; when present, the IMS-type gate in requireImsBearer is never hit, so a JWT-authenticated caller passes. When the header is absent it falls back to requiring IMS-type auth and returns the PROMISE_TOKEN_REQUIRED_ERROR_CODE 401 pointing the caller at the promise-token flow. Introduced in feat: add support for promise token for semrush api authentication #2753.
buildService(ctx) → resolveElementsImsToken(ctx) → createElementsTransport({ env, imsToken }). Do not re-read Authorization directly in the handler.
Implication for the checklist: step 5's listPrompts must call buildService(ctx) (not build a transport itself), so the promise-token path is inherited. Add a controller test asserting the promise-token path reaches getPrompts and the no-token/non-IMS path returns the PROMISE_TOKEN_REQUIRED_ERROR_CODE 401.
Other infra (already handled by the shared layer — no new work)
Unit (test/controllers/elements.js, test/support/elements/...): payload-builder shape (topic prefixing, OR groups for model + multiple projects, model resolution/fallback), response transform (field rename, count, empty-blocks.data → { count: 0, prompts: [] }), and controller auth/error branches. Note feat: Semrush Elements weeks endpoint for URL Inspector | LLMO-6011 #2755 marked its handler c8 ignore as a POC; prefer real unit tests here unless this is explicitly a throwaway POC.
Integration (test/it/): if the serenity Semrush mock supports this element, add a wiring test; otherwise note the mock gap.
Run npm run docs:lint / npm run docs:build after editing OpenAPI/docs, and npm test.
Open questions (block or clarify before merge)
Canonical element-ids.js constant name for 406ba6e0-... (is this Semrush's "Topic Prompts" element? cross-check the Serenity wiki element table).
Final response field names + whether count alone, prompts alone, or both.
Whether siteId → brand projects resolution is in scope for v1 (recommend: no — explicit projectId).
Are topic and projectIdsingle-value or multi-value in the UI's actual call? The observed payload shows a single topic and two projects (OR). Confirm cardinality so the query-param parsing matches.
Jira key — link this to the LLMO epic (the weeks endpoint was LLMO-6011); confirm the tracking issue for prompt count.
Context gathered from the current main (spacecat-api-service @ 2ee0efb9) elements wrapper layer and PRs #2666 / #2755.
Summary
Add a new Semrush Elements API wrapper endpoint that returns the prompts (and their count) for the URL Inspector / Brand Presence dashboards, filtered by topic tag, AI model, and one or more Semrush projects.
This is the next endpoint in the Semrush Elements wrapper family introduced by:
feat: introduce semrush elements apis wrapper for llmo ui(thesrc/support/elements/*layer +ElementsController)feat: Semrush Elements weeks endpoint for URL Inspector | LLMO-6011(the closest reference implementation — copy its shape)It wraps a new Semrush element that is not yet registered in
src/support/elements/element-ids.js:Observed upstream contract
The UI/POC calls the Semrush element directly today. This is the request/response we must reproduce through the SpaceCat wrapper (SpaceCat mints the IMS token and injects the workspace).
Request (raw Semrush
v4-raw/external-api, filters block only):{ "render_data": { "filters": { "simple": {}, "advanced": { "op": "and", "filters": [ { "col": "tags", "op": "contains", "val": "topic:AI" }, { "op": "or", "filters": [ { "op": "eq", "val": "search-gpt", "col": "CBF_model" } ] }, { "op": "or", "filters": [ { "op": "eq", "val": "b558a5e8-d9cb-4ace-907d-825eb4f9c0db", "col": "CBF_project" }, { "op": "eq", "val": "5f0e8c91-dbd5-4b96-91e4-803fc920a589", "col": "CBF_project" } ] } ] } } } }Response (
type: "table"):{ "type": "table", "blocks": { "data": [ { "primary_intent": "informational", "prompt": "can i make ai influencer for free", "prompt_topic": "AI Instagram Influencers", "volume": 2119 }, { "primary_intent": "informational", "prompt": "What is the best AI free image generator?", "prompt_topic": "AI Image Generators", "volume": 997 } ] }, "config": { "data": null } }Proposed endpoint
Sits alongside the existing org-scoped elements routes in
src/routes/index.js(lines 241–242):Query parameters
spaceCatId(path)semrush_workspace_idviaauthorizeOrg(already implemented)model/platformCBF_model(OR group)resolveElementModel()(UI platform code → Semrush model, defaultsearch-gpt).modelwins if both sent — mirrorbuildWeeksPayloadtopictags contains topic:<value>topic:prefix is added server-side (the filter-dimensionstopicsarray returns bare labels). Support one or more (repeated param or CSV) → multiplecontainsfiltersprojectIdCBF_project(OR group)semrush_project_idfrom the filter-dimensionsregionsarrayOpen question (resolve with product / UI before build):
siteIdand reverse-map it to the site's brand's Semrush projects (CBF_project), the waylistWeeksreverse-mapssiteId → brand → CBF_ws_brand? The observed payload passes explicitCBF_projectUUIDs, so explicitprojectIdis the primary contract;siteIdresolution would be an additive convenience. Recommend shipping explicitprojectIdfirst.feat/prompt_count_endpoint. Confirm whether consumers want the count only, the prompt list, or both. Recommendation below returns both so one endpoint serves both needs.Recommended response
{ "count": 6, "prompts": [ { "prompt": "can i make ai influencer for free", "topic": "AI Instagram Influencers", "intent": "informational", "volume": 2119 }, { "prompt": "What is the best AI free image generator?", "topic": "AI Image Generators", "intent": "informational", "volume": 997 } ] }count=prompts.length. Field renameprompt_topic → topic,primary_intent → intentkeeps the wrapper contract UI-facing rather than leaking raw Semrush column names (consistent with how the other definitions reshape their raw rows). Confirm final field names with the UI.Implementation checklist
Mirror the #2755 (weeks) diff — it touched the exact same set of files.
src/support/elements/element-ids.js— register the new element:src/support/elements/definitions/prompts.js(new) —buildPromptsPayload({ model, platform, topics, projectIds })returning the un-wrapped payload (advancedandgroup:tags contains topic:*filters +CBF_modelOR group viaresolveElementModel+CBF_projectOR group), andtransformPromptsResponse(raw)mappingraw.blocks.data[]→{ prompt, topic, intent, volume }and computingcount. Guard against a missing/emptyblocks.data(return{ count: 0, prompts: [] }).src/support/elements/definitions/index.js— re-export the two new functions.src/support/elements/elements-service.js— addgetPrompts(workspaceId, params)that callstransport.fetchElement(workspaceId, ELEMENT_IDS.PROMPTS_TABLE, buildPromptsPayload(params))and returnstransformPromptsResponse(raw).src/controllers/elements.js— add alistPrompts(ctx)handler modeled onlistWeeks:authorizeOrg→extractQuery→ parsetopic/projectId(repeated + CSV) →buildService(ctx).getPrompts(...)→ok(result)→mapError. Export it from the controller factory.src/routes/index.js— registerGET /v2/orgs/:spaceCatId/serenity/all/brand-presence/prompts → elementsController.listPrompts.src/routes/facs-capabilities.js— add'GET .../brand-presence/prompts': 'llmo/can_view'.src/routes/required-capabilities.js— add'GET .../brand-presence/prompts': 'organization:read'.test/routes/index.test.js— add the new route to the expected-routes list (the route-map test fails the build otherwise).docs/llmo-semrush-apis/filter-dimensions-apis.md(add a "List Prompts" section) and add the new element row todocs/elements/semrush-elements-api-reference.md.FACS param classification
No new dynamic
:paramis introduced (only the existing:spaceCatId, already classified), so therouteFacsCapabilitiesdisjointness test needs no new alias — but the route must still be added to both capability maps in steps 7–8 (the FACS capability test enumerates all routes).Auth model — REQUIRED (per platform guidance)
Concretely, for this endpoint:
Authorization: Bearer <jwt>(a non-IMS credential —authInfo.getType() !== 'ims').x-promise-token(minted byPOST /auth/v2/promise). The controller exchanges it for an IMS token and forwards that upstream.The shared elements layer already implements exactly this and needs no new work — just make sure the new
listPromptshandler goes through it (aslistWeeksdoes):resolveElementsImsToken(src/controllers/elements.js) checksx-promise-tokenfirst via the sharedresolveSemrushImsTokenhelper; when present, the IMS-type gate inrequireImsBeareris never hit, so a JWT-authenticated caller passes. When the header is absent it falls back to requiring IMS-type auth and returns thePROMISE_TOKEN_REQUIRED_ERROR_CODE401 pointing the caller at the promise-token flow. Introduced in feat: add support for promise token for semrush api authentication #2753.buildService(ctx)→resolveElementsImsToken(ctx)→createElementsTransport({ env, imsToken }). Do not re-readAuthorizationdirectly in the handler.Implication for the checklist: step 5's
listPromptsmust callbuildService(ctx)(not build a transport itself), so the promise-token path is inherited. Add a controller test asserting the promise-token path reachesgetPromptsand the no-token/non-IMS path returns thePROMISE_TOKEN_REQUIRED_ERROR_CODE401.Other infra (already handled by the shared layer — no new work)
authorizeOrg(org lookup +AccessControlUtil.hasAccess+ workspace resolution) — reuse as-is.mapError(upstream 401/403 → passthrough, other upstream → 502,ElementsTransportError/ErrorWithStatusCodehandled).Testing
test/controllers/elements.js,test/support/elements/...): payload-builder shape (topic prefixing, OR groups for model + multiple projects, model resolution/fallback), response transform (field rename, count, empty-blocks.data→{ count: 0, prompts: [] }), and controller auth/error branches. Note feat: Semrush Elements weeks endpoint for URL Inspector | LLMO-6011 #2755 marked its handlerc8 ignoreas a POC; prefer real unit tests here unless this is explicitly a throwaway POC.test/it/): if the serenity Semrush mock supports this element, add a wiring test; otherwise note the mock gap.npm run docs:lint/npm run docs:buildafter editing OpenAPI/docs, andnpm test.Open questions (block or clarify before merge)
element-ids.jsconstant name for406ba6e0-...(is this Semrush's "Topic Prompts" element? cross-check the Serenity wiki element table).countalone,promptsalone, or both.siteId → brand projectsresolution is in scope for v1 (recommend: no — explicitprojectId).topicandprojectIdsingle-value or multi-value in the UI's actual call? The observed payload shows a single topic and two projects (OR). Confirm cardinality so the query-param parsing matches.Context gathered from the current
main(spacecat-api-service@2ee0efb9) elements wrapper layer and PRs #2666 / #2755.