Lolly Creator Data API — MCP Server

Creator profiles, audience demographics and engagement for instagram, tiktok, youtube, snapchat and twitter — as tool calls, over the Model Context Protocol.

The endpoint

One address. Paste it into any MCP client that speaks Streamable HTTP.

https://api-app.lolly.com/api/public/v1/mcp

Three read-only tools are published there: lookup_influencer, lookup_influencers_bulk and get_usage_and_limits. None of them writes anything — every tool is annotated read-only and non-destructive, so a client can call them without asking a user to confirm each one.

Protocol revisions served: 2026-07-28 (stateless — no handshake, no session), plus 2025-11-25 and 2025-06-18 (session-based). Your client negotiates; you do not configure this.

The MCP endpoint is part of the Creator Data API and needs the same active subscription the REST lookup endpoints need. It is metered from the same credit balance and the same rate-limit budgets, so connecting an agent costs what the equivalent REST integration costs — no more, and no less.

Add it to your client

Two ways to authorize, covered in full under Authentication: paste an API key you already have, or let the client run the OAuth flow. The examples below show the key, because it is the one that needs configuration — for OAuth you add the address alone and your client does the rest.

Claude Code

shell
claude mcp add --transport http lolly https://api-app.lolly.com/api/public/v1/mcp \
  --header "Authorization: Bearer lly_live_<your_key>"

For OAuth instead, add it without the header and run /mcp inside Claude Code to authorize in the browser:

shell
claude mcp add --transport http lolly https://api-app.lolly.com/api/public/v1/mcp

Claude.ai (custom connector)

Settings → Connectors → Add custom connector, paste the address above, and connect. Claude.ai runs the OAuth flow and asks you to sign in to Lolly and approve the connection; there is nowhere to paste an API key in that dialog, so this path is OAuth.

Cursor

Add the server to ~/.cursor/mcp.json (global) or .cursor/mcp.json (one project):

json
{
  "mcpServers": {
    "lolly": {
      "url": "https://api-app.lolly.com/api/public/v1/mcp",
      "headers": { "Authorization": "Bearer lly_live_<your_key>" }
    }
  }
}

VS Code

Add it to .vscode/mcp.json in your workspace:

json
{
  "servers": {
    "lolly": {
      "type": "http",
      "url": "https://api-app.lolly.com/api/public/v1/mcp",
      "headers": { "Authorization": "Bearer lly_live_<your_key>" }
    }
  }
}

Wherever you put it, the file holds a live credential. Keep a project-local MCP config out of version control, or use the OAuth path, which stores no secret in your repository.

Check the connection

Ask your agent for get_usage_and_limits. It orders no analysis, spends no budget and charges nothing, so it is the safe first call — if it answers with two budgets, you are connected and authorized.

Authentication

The endpoint accepts either credential on the same Authorization: Bearer header, and both resolve to the same identity: the same scopes, the same budgets, the same credit balance, the same usage records.

An API key you already have

A live lly_live_… key works immediately, with no authorization flow to complete. Generate one in your Lolly account under API access; status, expiry, revocation and scopes are enforced exactly as on the REST endpoints.

http
Authorization: Bearer lly_live_<your_key>

The two lookup tools require influencers:read and influencers:refresh together — the same pair the REST lookup routes require, and keys generated in the dashboard carry both. A key holding only one of the two is not offered those tools in the catalogue at all, and calling one by name is refused for insufficient scope with both scopes named in the challenge. get_usage_and_limits needs no scope: any key that authenticates may read its own budget.

OAuth 2.1, for a client that authorizes itself

Clients that run their own authorization — Claude.ai's custom connectors, Claude Code's /mcp, anything that follows the MCP authorization spec — need no key. Point them at the endpoint address and they will discover everything else: an unauthenticated call is refused 401 with a WWW-Authenticate challenge pointing at the protected resource metadata, which names the authorization server.

DocumentAddress
Protected resource metadata (RFC 9728)https://api-app.lolly.com/.well-known/oauth-protected-resource/api/public/v1/mcp
Authorization server metadata (RFC 8414)https://api-app.lolly.com/.well-known/oauth-authorization-server

Those two addresses are the ones the 401 challenge points at and the ones a token's audience is bound to, so they are stated as the service publishes them rather than relative to whichever host you are reading this on.

Browsers

A request carrying no Origin header — every non-browser client — is served normally. A request carrying an origin that is not on the permitted list is refused 403 before anything else happens, so a page cannot drive the endpoint on a signed-in user's behalf.

The tools

Three, all read-only. Their input schemas below are the same ones tools/list publishes to your client.

lookup_influencer Look Up Creator Profile

Holds the request open and streams progress — see Hold and repeat. Required scopes: influencers:read and influencers:refresh.

ArgumentTypeRequiredDescription
platformstring (instagram, tiktok, youtube, snapchat, twitter)YesPlatform the creator publishes on.
usernamestringYesCreator handle. A leading @ and letter case are ignored, so @Nike, nike and NIKE name the same creator.
fieldsarrayNoOptional. Which profile fields to return. Omit it for the documented compact projection (version 1, 29 fields) — that is the right choice almost always, and the only workable choice for a large bulk call. Pass an explicit list for exactly those fields and no other profile data. Pass ["*"] for the complete upstream payload, which is roughly 8 KB per creator and will not fit many of them in one context. Selectable fields: platform, username, influencer_name, profile_url, user_id, is_verified, is_private, account_type, categories, biography, creator_location, creator_languages, contact, followers, engagement_rate, average_likes, average_comments, average_views, average_interactions, total_posts, following_count, audience_quality, audience_genders, audience_age_ranges, audience_countries, audience_languages, posting_activity, brand_collaborations, data_freshness, audience_cities, audience_interests, audience_ethnicity, audience_types, audience_reachability, brand_affinities, follower_growth, like_growth, top_hashtags, mentions, posts_recent, posts_popular, lookalikes, paid_partnerships, paid_post_performance, content_type_breakdown, brand_safety.

Returns status, platform, username, call_again, profile, projection, projection_version and message.

example arguments
{
  "platform": "instagram",
  "username": "nike"
}

lookup_influencers_bulk Look Up Many Creator Profiles

Holds the request open and streams progress — see Hold and repeat. Required scopes: influencers:read and influencers:refresh.

ArgumentTypeRequiredDescription
itemsarrayYesBetween 1 and 100 creators, platforms mixed freely. Handles equal after normalisation collapse into one item, analysed and charged once — so the result can be shorter than this list. Match results by their (platform, username) pair, never by position.
fieldsarrayNoOptional. Which profile fields to return. Omit it for the documented compact projection (version 1, 29 fields) — that is the right choice almost always, and the only workable choice for a large bulk call. Pass an explicit list for exactly those fields and no other profile data. Pass ["*"] for the complete upstream payload, which is roughly 8 KB per creator and will not fit many of them in one context. Selectable fields: platform, username, influencer_name, profile_url, user_id, is_verified, is_private, account_type, categories, biography, creator_location, creator_languages, contact, followers, engagement_rate, average_likes, average_comments, average_views, average_interactions, total_posts, following_count, audience_quality, audience_genders, audience_age_ranges, audience_countries, audience_languages, posting_activity, brand_collaborations, data_freshness, audience_cities, audience_interests, audience_ethnicity, audience_types, audience_reachability, brand_affinities, follower_growth, like_growth, top_hashtags, mentions, posts_recent, posts_popular, lookalikes, paid_partnerships, paid_post_performance, content_type_breakdown, brand_safety.

Returns status, call_again, submitted_count, unique_count, settled_count, pending_count, projection, projection_version, message and items.

example arguments
{
  "items": [
    {"platform": "instagram", "username": "nike"},
    {"platform": "tiktok", "username": "khaby.lame"}
  ],
  "fields": ["username", "followers", "engagement_rate", "audience_countries"]
}

get_usage_and_limits Get Remaining API Budget

Answers immediately; nothing to wait for. Required scopes: none beyond a valid credential.

No arguments.

Returns requests and work_items.

example arguments
{}

Hold and repeat

This is the one behaviour worth reading twice, because it looks like a failure and is not.

A full creator analysis takes about 70 seconds and is guaranteed to reach a terminal outcome within 12 minutes. No MCP client will hold a request that long, so the lookup tools hold it for about 30 seconds — streaming progress while they wait — and then return one of two things:

Repeating is free. A repeat attaches to the analysis already running rather than ordering a second one. It spends no request budget, no work budget and no credits, and it is never rate limited — exactly like polling a REST lookup that answered 202. Keep repeating until the status changes; the analysis keeps running between your calls.

The bulk tool behaves the same way and carries the outcomes settled so far on every turn, so a long batch is collected across a few identical calls rather than one long one. A repeat resolves to the same batch, not a new one, because the tool derives an idempotency key from the set of handles you sent — which also means changing fields between repeats re-reads the same batch through a different projection instead of paying for a second one.

Some plans resolve a lookup in a few seconds. Those return on the first call with no progress at all, and the same result shape — nothing branches on which happened.

Abandoning the call is how you cancel: close the stream and the endpoint stops the work, sends nothing further, and charges nothing. Progress notifications are sent only when your client asks for them, by including a progressToken in the request's _meta — every MCP client that displays progress does this for you.

What a profile result contains

A raw creator profile is roughly 8 KB of JSON. One is affordable in an agent's context; a hundred of them are not. So every tool that returns profile data returns a compact projection by default, identical on all five platforms (instagram, tiktok, youtube, snapchat and twitter) and identical whichever tier served the lookup. A full bulk result at the 100-creator ceiling stays under 250 KiB.

Two overrides, both on the fields argument: name the fields you want for exactly those and nothing else, or pass ["*"] for the complete payload — byte for byte what the REST lookup endpoint returns for that creator. Every result states which of the three it carries, in projection and projection_version (currently 1).

Compact profile projection, version 1. Every profile-returning tool returns this shape by default, on all five platforms and from both the quick and the report tier, so a result never has to be branched on. Every field below is always present; it is null when the platform does not provide it.

Included by default:

FieldWhat it carries
platformPlatform the profile was looked up on.
usernameHandle on that platform.
influencer_nameDisplay name.
profile_urlCanonical profile URL.
user_idPlatform-native account id, for de-duplicating a handle that changed.
is_verifiedWhether the platform verifies the account.
is_privateWhether the account is private, which bounds what any analysis can see.
account_typeAccount class the platform assigns (creator, professional, business).
categoriesContent niches, at most 5.
biographyProfile bio, truncated to 200 characters.
creator_locationCreator's own stated location, as a single string.
creator_languagesLanguages the creator publishes in.
contactContactability: email, phone_number, website, external_links (at most 5) and direct_message_available. Members the platform does not expose are omitted.
followersFollower / subscriber count.
engagement_rateEngagement rate in percent, as the platform's own analysis computes it.
average_likesAverage likes per post.
average_commentsAverage comments per post (replies on twitter).
average_viewsAverage views per post, where the platform reports views.
average_interactionsAverage total interactions per post.
total_postsLifetime post count.
following_countAccounts the creator follows — the ratio to followers is an authenticity signal.
audience_qualityAuthenticity signals: fake_followers, fake_likers, creator_authenticity, engagement_quality, real_audience_share, verified_followers_share, brand_safety_risk_level. Members the platform does not compute are omitted.
audience_gendersAudience gender split as percentages.
audience_age_rangesTop 3 audience age ranges as percentages. On instagram, whose payload splits ages by gender, the two are merged weighted by the audience gender split.
audience_countriesTop 3 audience countries as percentages.
audience_languagesTop 3 audience languages as percentages.
posting_activityCadence and recency: posts_per_week, average_post_interval_days, posts_last_30d, last_post_date, has_active_story.
brand_collaborationsSponsored-content history: has_brand_deals, sponsored_posts_count and up to 5 brand names.
data_freshnessWhen the data was collected: fetched_at, public_data_updated_at, profile_updated_at. Stated by snapchat and twitter only.

Omitted by default, available through the field selector:

FieldWhat it carries
audience_citiesFull audience city distribution.
audience_interestsAudience interest categories.
audience_ethnicityAudience ethnicity distribution.
audience_typesFollower-quality breakdown (real people, mass followers, suspicious).
audience_reachabilityHow many accounts the audience follows, bucketed.
brand_affinitiesBrands the audience already engages with.
follower_growthFollower count time series.
like_growthAverage-likes time series.
top_hashtagsHashtags the creator uses most.
mentionsAccounts the creator mentions most.
posts_recentRecent posts, in full.
posts_popularBest-performing posts, in full.
lookalikesSimilar creators the provider suggests.
paid_partnershipsSponsored posts, in full.
paid_post_performanceHow sponsored content performs against organic.
content_type_breakdownPerformance split by content type (video, short, image, text).
brand_safetyRisk level, risk topics and sentiment, where the platform assesses them.

Also omitted, and reachable only through the complete payload (fields=["*"]): every remaining section of the upstream profile — the full post, reel and story objects with their media and captions, the mentioned-by and collaborator graphs, household income and education distributions, active-hour and active-day maps, per-post sponsorship detail, and any platform-specific block not listed above.

Overrides: pass fields=[...] for exactly the fields named and nothing else, or fields=["*"] for the complete payload, byte for byte what the REST lookup endpoint returns.

Free turns and metered turns

An MCP tool call is charged exactly like the equivalent REST request. Nothing about this endpoint is priced separately.

Credits

A credit is charged only on a definitive result: a returned profile, or a confirmed no-result (not_found / not_influencer — an account that exists but sits below the influencer threshold). A Report analysis costs 1 credit; a Quick analysis costs 0.05 credit. In a bulk call each creator is charged once, on its own definitive outcome.

Never charged:

Budgets

Two budgets are metered per credential, and one call can spend both. Requests — currently 4 per second. Work items — currently 240 per 60 seconds on a sliding window, where one work item is one creator analysis ordered: a single lookup spends one, a bulk call spends one per creator.

A turn that follows work already running spends neither budget and can never be throttled. A single lookup spends its work item before the billing outcome is known, so an out-of-credits refusal has still spent one; a bulk call checks affordability first and a refused batch spends nothing at all. Because an agent never sees a response header, the budgets are read through get_usage_and_limits instead of X-RateLimit-* — and reading them is itself free, unthrottled and orders nothing, so it is safe to call before fanning out a large job.

Exempt from your budgets is not exempt from all limiting: a coarse service-wide ceiling protects the endpoint from aggregate load and applies to every call, free ones included. If you meet it the call is refused with a wait to honour, while your own budgets still read as unspent. Nothing has been charged.

Errors

Two kinds, and the difference matters: a tool that ran and found nothing reports that in its result, while a request that was refused comes back as a JSON-RPC error.

A definitive no-result, an upstream failure and a still-running analysis are all outcomes, carried in the result's status. In a bulk call each creator carries its own — one creator resolving to not_found never fails the call.

Refusals, with the code your client will see:

CodeMeaning
-32602An argument the model can fix: an unknown tool name, an unknown field in fields, a bad platform, too many creators. Nothing was ordered and nothing was charged.
-31001A budget is spent. The error names how long to wait, in data.retryAfterSeconds.
-31002Out of credits, or no active Creator Data API subscription. No analysis was ordered.
-31003The service is shedding load. Retry after the wait.
-32601A protocol method this revision does not define.
-32020A mirrored request header contradicts the body, or a required one is missing. Only the stateless revision mirrors anything.
-32022The declared protocol revision is not served. The error lists the ones that are.

Authorization stays HTTP-shaped, because that is what an OAuth client reacts to: 401 with a WWW-Authenticate challenge pointing at the protected resource metadata when the credential is missing, invalid or expired, and 403 naming every missing scope in one challenge when it is merely insufficient.

Talking to the endpoint directly

You do not need this to use the server — a client does it for you. It is here for anyone writing their own.

The catalogue, session-based revision

The simplest possible request: declare a session-based revision and no session is needed to list the tools.

shell
curl -sS -X POST "https://api-app.lolly.com/api/public/v1/mcp" \
  -H "Authorization: Bearer lly_live_<your_key>" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

A lookup, stateless revision

The stateless revision carries per-request metadata in params._meta and mirrors the method and tool name into headers, which the server checks against the body. A tool that may hold the request is answered as an event stream, so read it unbuffered (curl -N) — the last event carries the result.

shell
curl -sSN -X POST "https://api-app.lolly.com/api/public/v1/mcp" \
  -H "Authorization: Bearer lly_live_<your_key>" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/call" \
  -H "Mcp-Name: lookup_influencer" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"lookup_influencer","arguments":{"platform":"instagram","username":"nike"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"progressToken":1}}}'

Both revisions accept the same tool arguments and return the same result shape. The differences are transport only: the session-based revisions negotiate once through an initialize handshake and carry an Mcp-Session-Id afterwards, while the stateless revision has no handshake, no session and no stream resumption.