StoraSign in

API and MCP

REST endpoints, authentication and MCP tools. Connect your agent in the Agents hub.

Overview

All JSON. Times ISO-8601 UTC, days YYYY-MM-DD. Errors: { error: { code, message, details? } } with codes not_found, unauthorized, forbidden, validation, rate_limited, conflict, internal and the matching HTTP status. Paged lists accept limit/offset and return {items,total,limit,offset,has_more}; max 500 (keyword table 2000). Ads terms also include window.

Connect an agent
  • Plugins: connect https://stora.rocks/mcp through OAuth. Choose the workspace and read, tracking/write or admin access. Revoke in Settings → Connected apps. OAuth tokens are accepted by /mcp; REST uses sessions or API keys.
  • Browser: session cookie sr_session. State-changing requests must send X-Requested-With: StoreRadar.
  • Agents: Authorization: Bearer srk_… (API key, workspace-scoped). read keys may call GET and MCP read tools. Write keys with admin=true can administer the workspace; owner-only operations require a signed-in owner.
  • Workspace selection: X-Account: <account_id>; default is the key's workspace or the session's last-used one.

Authentication

Public and rate-limited: 5 codes per email and 20 per IP per 10 minutes.

POST
/auth/request-code

Send a 6-digit code and magic link (rate-limited).

Body
{ email, turnstile_token, next? }
Returns
{ ok: true }
POST
/auth/verify

Exchange the code for a session cookie.

Body
{ email, code }
Returns
{ user, accounts, account_id }
GET
/auth/magic?token=&next=

Magic link: sets the cookie and resumes a safe internal path, or the workspace/onboarding.

POST
/auth/logout

Clear the session cookie.

GET
/auth/session

Current session or 401.

Returns
{ user, accounts, account_id }
POST
/auth/accept-invite

Accept an invite after signing in.

Body
{ token }

Plugin OAuth

Public OAuth 2.1 clients use S256 PKCE and resource=https://stora.rocks/mcp. The token and revocation endpoints accept form-urlencoded data and return OAuth protocol errors. Access expires in one hour; refresh rotates and expires in 30 days.

GET
/.well-known/oauth-protected-resource/mcp

MCP resource discovery.

GET
/.well-known/oauth-authorization-server

Issuer, endpoints, scopes and public-client registration.

POST
/oauth/register

Register a client with HTTPS or native loopback redirects.

Body
{ client_name, redirect_uris, token_endpoint_auth_method: "none" }
GET
/oauth/authorize

Sign in, select a workspace and consent. Requires response_type=code, client_id, redirect_uri, resource, S256 challenge; scope/state optional.

POST
/oauth/token

Code + verifier exchange or refresh-token rotation. client_id and resource required.

POST
/oauth/revoke

Revoke a connection using token and matching client_id.

GET
/oauth/connections

Signed-in browser: inspect and revoke your connections.

Me and workspaces

GET
/api/v1/me

User, workspaces and the selected workspace.

POST
/api/v1/accounts

Create a workspace; the creator is owner.

Body
{ name, timezone? }
PATCH
/api/v1/accounts/:id

Rename, timezone, ready-by hour of the daily scan (admin+).

Body
{ name?, timezone?, scan_hour? }
DELETE
/api/v1/accounts/:id

Delete the workspace and all its data (owner, signed-in session). Cascades; audit-logged.

Body
{ confirm: <workspace slug> } → { ok, deleted, account_id, apps, members }
POST
/api/v1/accounts/:id/switch

Set the session's default workspace.

GET
/api/v1/accounts/:id/members

Members with roles.

PATCH
/api/v1/accounts/:id/members/:user_id

Change a role.

Body
{ role }
DELETE
/api/v1/accounts/:id/members/:user_id

Remove a member (owner only for owners).

POST
/api/v1/accounts/:id/invites

Invite by email.

Body
{ email, role }
GET
/api/v1/accounts/:id/invites

Pending and accepted invites.

DELETE
/api/v1/accounts/:id/invites/:invite_id

Revoke an invite.

GET
/api/v1/accounts/:id/api-keys

Key metadata (never the key).

POST
/api/v1/accounts/:id/api-keys

Create a key; admin requires write scope and a signed-in admin/owner. The full key is returned once.

Body
{ name, scope, admin?: boolean }
Returns
{ key: "srk_…", …meta }
DELETE
/api/v1/accounts/:id/api-keys/:key_id

Revoke a key.

GET
/api/v1/accounts/:id/ads-connection

Apple Ads connection status and last sync.

PUT
/api/v1/accounts/:id/ads-connection

Save credentials; the .p8 PEM is encrypted at rest.

Body
{ client_id, team_id, key_id, org_id, private_key_pem, enabled }
POST
/api/v1/accounts/:id/ads-connection/test

Request a token and list campaigns.

Returns
{ ok, message }
DELETE
/api/v1/accounts/:id/ads-connection

Delete credentials.

POST
/api/v1/accounts/:id/ads-sync

Start an Apple Ads sync.

Returns
{ queued: true }

Store helpers

GET
/api/v1/store/storefronts

Every supported storefront (public, cached 1 day). Store codes in app and keyword writes must come from this list.

Returns
[{ code, name, flag, language, region }]
GET
/api/v1/store/resolve?url=|apple_id=|google_package=&store=us

Resolve a store URL or ID to listing details and keyword suggestions.

GET
/api/v1/store/hints?term=&store=us

App Store autocomplete.

Returns
{ terms: [...] }

Apps

GET
/api/v1/apps

Apps with health, visibility and the top digest item.

POST
/api/v1/apps

Add an app with keywords; queues a manual scan.

Body
{ name, apple_id?, google_package?, primary_store, stores[], keywords[], result_limit? }
GET
/api/v1/apps/:id

Full app: settings, counts, health, Apple Ads state.

PATCH
/api/v1/apps/:id

Update identifiers, markets, depth, settings. Validates settings.profile with field-level errors; unknown store codes → 400 naming the code.

DELETE
/api/v1/apps/:id

Delete an app and its data (owner/admin).

POST
/api/v1/apps/:id/scan

Queue a manual scan. 409 if one ran within the hour.

Returns
{ run_id, queued_jobs, eta_seconds }
GET
/api/v1/apps/:id/runs?limit=30

Scan runs with error text.

GET
/api/v1/apps/:id/health

Health row plus lane queue stats.

Keywords

GET
/api/v1/apps/:id/keywords

Keyword configuration with excluded_pairs and effective_pairs.

POST
/api/v1/apps/:id/keywords

Ensure keyword scopes; existing markets are kept.

Body
{ items: [{ term, stores?, platforms?, is_brand?, note?, source? }] }
Returns
{ created, extended, unchanged, skipped, items }
POST
/api/v1/apps/:id/keywords/ensure

Ensure 1–500 keyword scopes, returning added/effective pairs per item.

Body
{ items: [{term, stores, platforms, is_brand?, note?, source?}] }
Returns
{ created, extended, unchanged, skipped, items }
PATCH
/api/v1/apps/:id/keywords/:kid

Active, brand, note; replacing markets/platforms requires replace: true.

DELETE
/api/v1/apps/:id/keywords/:kid

Delete a keyword.

POST
/api/v1/apps/:id/keywords/bulk

Bulk scope/flag changes, pair pause/resume, or delete. Preview with dry_run: true.

Body
{ ids, add_stores?, remove_stores?, add_platforms?, remove_platforms?, pause_pairs?, resume_pairs?, set?: {is_active?, is_brand?}, delete?, dry_run? }
GET
/api/v1/apps/:id/keywords/table?platform=&store=&status=&q=&sort=&order=

The keyword table: one row per keyword, platform and market.

GET
/api/v1/apps/:id/keywords/:kid/history?platform=&store=&days=90

Ranks, popularity, paid, latest SERP and coverage.

GET
/api/v1/apps/:id/keywords/export.csv

CSV export of the keyword table.

Insights

GET
/api/v1/apps/:id/digest?since=&day=&kinds=

Digest items, newest first.

GET
/api/v1/apps/:id/actions?state=open|waiting|done|dismissed|snoozed|resolved|all

Growth actions with evidence and gate status: open is ready (gate passed), waiting has not passed yet. The first page adds 30-day quality.

Returns
{ items, total, limit, offset, has_more, quality?: { window_days, queue, decisions, precision, premature_rate, repeats, missed } }
PATCH
/api/v1/apps/:id/actions/:aid

Mark done, dismissed, snoozed or open, with an optional typed reason. Reopening an action whose gate has not passed returns it to waiting.

Body
{ state, reason?, reason_code?: wrong_intent|wrong_market|insufficient_evidence|not_useful (dismissed) | already_done (done) }
GET
/api/v1/apps/:id/ideas?status=new|research|tracked|dismissed|all&intent=&dismiss_reason=

Keyword ideas with intent, source, key and dismissal reason. New is the review queue; research keeps weaker candidates with evidence.admission.reasons.

POST
/api/v1/apps/:id/ideas/:iid/track

Ensure the idea platform/store pair before marking tracked; returns the ensure result.

Body
{ stores?, platforms? }
POST
/api/v1/apps/:id/ideas/:iid/dismiss

Dismiss an idea; default reason manual. no_demand and not_useful come back when the evidence changes; wrong_intent also labels it excluded.

Body
{ reason?, reason_code?: wrong_intent|wrong_market|no_demand|duplicate|not_useful }
POST
/api/v1/apps/:id/intents

Label 1–500 keywords or ideas; sessions default to manual, API keys to agent.

Body
{ items: [{ keyword_id? | idea_id?, intent, key?, source? }] }
Returns
{ items: [{ keyword_id? | idea_id?, result, intent?, intent_source?, intent_key? }] }
POST
/api/v1/apps/:id/ideas/:iid/reopen

Reopen a dismissed idea with manual source; excluded becomes unknown.

PUT
/api/v1/apps/:id/competitors/:store_id/role

Set direct/adjacent/excluded role; null clears to discovered.

Body
{ platform, role, reason? }
GET
/api/v1/apps/:id/analytics?platform=&store=&days=28

Visibility series, markets, movers, data quality. Without store (or store=all) every market is combined; without platform every platform.

GET
/api/v1/apps/:id/competitors?platform=&days=28&role=

Competitors by share of search, with role and reason.

GET
/api/v1/apps/:id/competitors/:store_id

Competitor detail.

POST
/api/v1/apps/:id/competitors/:store_id/discover

Discover a competitor's keywords in the given markets: candidates from their localized listing, autocomplete, Apple Ads popularity and your tracked terms, verified with search-result scans. 409 within 24 h for the same competitor and market; cap × markets ≤ 1,000 jobs.

Body
{ platform, stores[], cap? }
Returns
{ run_id, run_ids, queued_jobs, eta_seconds, reused_snapshots }
GET
/api/v1/apps/:id/competitors/:store_id/keywords?platform=&store=&top=10|30&status=all|gap|tracked

Their verified keywords per market with their rank, your rank, tracked flag, popularity, sources and gap score, plus run progress.

GET
/api/v1/apps/:id/competitors/gaps?platform=&store=&role=

Terms where competitors reach the top 10 and you are unranked or not tracking the market, scored by popularity × 1/rank × role.

GET
/api/v1/apps/:id/listing?store=

Live listing, locales, draft, coverage and change log.

PUT
/api/v1/apps/:id/listing/draft

Save the workspace draft for a market.

POST
/api/v1/apps/:id/listing/changes

Log a manual ledger entry.

Body
{ field, old_value, new_value, observed_at, note }
GET
/api/v1/apps/:id/experiments

Experiments. POST to create, PATCH /:eid to end.

GET
/api/v1/apps/:id/ads/summary

Spend, installs, CPI, waste, winners, negatives, target CPA, locales.

GET
/api/v1/apps/:id/ads/terms?days=30&store=

Paid search terms, aggregated.

GET
/api/v1/apps/:id/ads/impression-share?store=

Impression share per term.

GET
/api/v1/apps/:id/ads/demand?store=

Apple's curated genre term list.

Portfolio and agents

GET
/api/v1/portfolio

Benchmark rows per app and platform.

GET
/api/v1/changes?since=

Workspace-wide digest across apps: what changed since a day.

GET
/api/v1/openapi.json

OpenAPI spec.

GET
/api/health

Service health and the deployed git SHA.

MCP tools

The same workspace data is available through the MCP server at /mcp. The Agents hub has client configuration and ready-to-run prompts.

  • get_connected_profile
  • get_context
  • create_app
  • update_app
  • delete_app
  • resolve_store
  • get_storefronts
  • get_runs
  • update_keyword
  • delete_keyword
  • save_listing_draft
  • mark_draft_published
  • log_listing_change
  • list_experiments
  • create_experiment
  • update_experiment
  • get_ads_impression_share
  • get_ads_demand
  • get_portfolio
  • trigger_ads_sync
  • get_ads_connection
  • get_keywords
  • get_analytics
  • get_ads_terms
  • get_ads_campaigns
  • get_ads_state
  • list_apps
  • get_app
  • get_keywords_table
  • get_keyword_history
  • add_keywords
  • ensure_keyword_scopes
  • get_keyword_config
  • bulk_keywords
  • dismiss_idea
  • get_digest
  • get_actions
  • update_action
  • get_ideas
  • track_idea
  • get_listing
  • get_health
  • trigger_scan
  • get_competitors
  • get_competitor
  • discover_competitor_keywords
  • get_competitor_keywords
  • get_competitor_gaps
  • label_intents
  • reopen_idea
  • set_competitor_role
  • get_ads_summary
  • get_changes_since

v1 Static copy of docs/API.md. Deterministic data only: no LLM runs inside Stora.