Developer reference

REST API and MCP server

Everything on the dashboard, as JSON for your own reporting or as tools for a coding assistant. Read-only, scoped to your account, same numbers as the dashboard.

Last updated 20 September 2026

Who has it

The API and the MCP server are part of the Agency and Pro plans. A key made on one of those plans stops working the day the subscription lapses: every request checks the subscription, not a flag set when the key was created. On any other plan the endpoints answer 403 with { "code": "api_locked" }.

Both surfaces are read-only by design. Adding brands, questions or topics stays on the dashboard, so nothing holding a key can spend money on your behalf.

Keys

Create keys on the dashboard under API access (/geo/dashboard). A key looks like mdk_live_ followed by 32 characters. The full key is shown exactly once, at creation; we store only a hash of it, so a key we cannot show you again is also a key a leaked database could not reveal. The list shows each key's first 12 characters, when it was made and when it was last used.

  • A key reaches every brand on the account that made it, current and future. That is the same scope as the dashboard login that created it, and no wider.
  • An account holds up to 10 live keys. Revoke one to make room; revoked keys stay listed so their last-used date is still readable.
  • Revocation is immediate. There is no grace period and no way to un-revoke.

Authentication

Send the key as a bearer token on every request, REST or MCP:

Authorization: Bearer mdk_live_…
StatusMeaning
401No Authorization header, a malformed key, or a key that is invalid or revoked.
403 api_lockedThe key is valid but the account's subscription is not on Agency or Pro, or is not active.
404The brand in the path does not exist or is not owned by the key's account. The two cases are not distinguished.
400The brand id in the path is not a UUID.

Every response carries Cache-Control: no-store. There is no enforced rate limit today; the data changes once a week, when the scan runs, so polling faster than that returns the same answer.

REST endpoints

Base URL https://www.mediadesk.asia/api/v1. All endpoints are GET.

PathReturns
/brandsEvery brand on the account, with market and tracked-question count. Call this first to get brand ids.
/brands/{brandId}/summaryThe latest finished scan's rollup: visibility score overall and per engine with week-over-week deltas, citation rates, tone, per-topic and per-language tallies, competitors, the full per-question matrix and the losing questions. Identical to the dashboard.
/brands/{brandId}/resultsRaw rows, one per question × engine × scan, straight from the record. The CSV export as JSON. ?scope=latest (default) or all; ?limit= up to 5000.
/brands/{brandId}/recommendationsOne recommendation per losing question in the latest scan, with kind, evidence, any MediaDesk publisher placement that would answer it, and status.
/brands/{brandId}/topicsThe brand's question topics with prompt counts. Per-topic rates and scores are in the summary.

Example

curl https://www.mediadesk.asia/api/v1/brands \
  -H "Authorization: Bearer mdk_live_…"

{
  "brands": [
    {
      "id": "baaea6ea-…",
      "name": "OasisEye",
      "domain": "oasiseye.my",
      "market": "MY",
      "prompts": 27,
      "createdAt": "2026-08-12T09:14:02.000Z"
    }
  ]
}
curl "https://www.mediadesk.asia/api/v1/brands/baaea6ea-…/results?scope=latest&limit=2" \
  -H "Authorization: Bearer mdk_live_…"

{
  "brand": { "id": "baaea6ea-…", "name": "OasisEye", "domain": "oasiseye.my", "market": "MY" },
  "scans": [
    { "id": "…", "label": "Weekly scan", "startedAt": "…", "completedAt": "…", "status": "completed" }
  ],
  "results": [
    {
      "scanId": "…",
      "promptId": "…",
      "question": "best eye clinic for LASIK in Kuala Lumpur",
      "questionType": "recommendation",
      "topic": "LASIK",
      "language": "en",
      "engine": "gemini",
      "verdict": "cited",
      "votesFor": 3,
      "votesTotal": 3,
      "position": "2nd of 8 sources",
      "citedUrl": "https://oasiseye.my/lasik",
      "sentiment": "positive",
      "competitors": ["…"],
      "sources": [{ "url": "…", "title": "…" }],
      "scannedAt": "…"
    }
  ]
}

Reading the fields

The API returns the same objects the dashboard renders, so the rules the dashboard follows apply to what you pull. Three of them matter when you build a report on top of it.

A verdict has four values, and two of them are not zero

VerdictMeaning
citedThe engine's answer named the brand or cited its site, by majority across repeat runs.
not_citedThe engine searched and answered, and the brand was not in it.
no_dataNo run produced a grounded answer (the engine replied without searching, or every run errored). There is no verdict either way. The raw results endpoint never backfills this; treat it as missing, not as not_cited.
not_scannedThe engine is not part of the plan's scan for this brand. Appears only in the summary's question matrix and in each engine's status.

Rates and scores are null when nothing was measured

rate is cited over decided answers, and is null when decided is zero. visibility is the published 0–100 score (formula) or null. A nullis "not measured", never "scored zero"; a chart that plots it as 0 will misreport the brand. Every delta is null when either week is missing.

Votes tell you how sure the verdict is

votesFor / votesTotal are the repeat runs that saw the brand over the runs that searched at all. A 3/3 and a 2/3 are the same verdict and different evidence; week-over-week comparisons should carry the counts, not just the boolean. Both are null on a no_data row.

Recommendation kinds

  • claim_mention — the engines name the brand in the answer but link elsewhere for the facts; the gap is a first-party page to cite.
  • localise_question — an English, non-branded question for a Malaysian brand where every winning source sits outside the market.
  • earn_placement — a third-party publisher wins the question; placements lists any MediaDesk publisher among the winners.
  • publish_page — none of the above: the question is won by pages the brand has no equivalent of.

status is open, done or dismissed, set on the dashboard and carried week to week. evidence.author is llm when the copy was written from the scan's evidence and template when the writer was unavailable and a templated line was stored instead.

MCP server

The same read surface as tools, at https://www.mediadesk.asia/api/mcp, so a coding assistant can answer "how did OasisEye do on Gemini this week" from the record instead of from memory. Streamable HTTP, stateless, authenticated with the same key as a bearer header. An assistant quoting a number gets the dashboard's number.

ToolReturns
list_brandsBrands on the account with ids. Assistants are told to call this first.
get_summaryThe summary above, minus the per-question matrix.
get_questionsThe per-question matrix: topic, language, per-engine verdict, position and tone.
get_resultsRaw rows; scope latest or all, limit up to 2000 (default 500).
get_recommendationsRecommendations, as above.
get_topicsTopics with counts.

The server's own instructions to the assistant say what this page says: a null score or a not_scanned engine is no measurement, never zero.

Claude Code

claude mcp add --transport http mediadesk https://www.mediadesk.asia/api/mcp \
  --header "Authorization: Bearer mdk_live_…"

Cursor

Add to .cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project:

{
  "mcpServers": {
    "mediadesk": {
      "url": "https://www.mediadesk.asia/api/mcp",
      "headers": { "Authorization": "Bearer mdk_live_…" }
    }
  }
}

Codex

In ~/.codex/config.toml, with the key in an environment variable so it does not sit in the config file:

[mcp_servers.mediadesk]
url = "https://www.mediadesk.asia/api/mcp"
bearer_token_env_var = "MEDIADESK_API_KEY"

Then export MEDIADESK_API_KEY=mdk_live_… before starting Codex.

Anything else

Any MCP client that speaks streamable HTTP and can send a static header works with the Cursor-style config above. The server answers an unauthenticated request with a 401 that also carries an OAuth resource-metadata challenge; clients that send the bearer header can ignore it.

Keys are secrets. Do not commit them, paste them into a shared MCP config, or put them in a URL. If one leaks, revoke it on the dashboard and make another; the old one stops working at once.