The endpoint
One address. Paste it into any MCP client that speaks Streamable HTTP.
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
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:
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):
{
"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:
{
"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.
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.
| Document | Address |
|---|---|
| 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.
- Authorization code with
S256PKCE, mandatory. There is no client-credentials grant: every connection is consented by a person, at a consent screen that shows the scopes requested and the hostname you will be returned to. - Scopes:
influencers:readandinfluencers:refreshfor the lookup tools, plusoffline_accessif your client wants a refresh token.offline_accessis advertised by the authorization server and is not an API capability. - An access token lasts about 1 hour(s). Refresh tokens last 30 days and rotate: the response that issues a replacement invalidates its predecessor, and redeeming a spent one fails with
invalid_grant. - Tokens are audience-bound to this endpoint. A token minted for another resource is refused, and revoking a connection takes effect on the next request rather than at the next expiry.
- Clients are identified by a metadata document they host, or by credentials registered ahead of time. A loopback redirect is matched ignoring the port, so a native client binding an ephemeral port works.
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.
| Argument | Type | Required | Description |
|---|---|---|---|
platform | string (instagram, tiktok, youtube, snapchat, twitter) | Yes | Platform the creator publishes on. |
username | string | Yes | Creator handle. A leading @ and letter case are ignored, so @Nike, nike and NIKE name the same creator. |
fields | array | No | Optional. 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.
{
"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.
| Argument | Type | Required | Description |
|---|---|---|---|
items | array | Yes | Between 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. |
fields | array | No | Optional. 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.
{
"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.
{}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:
- the result, if the analysis finished inside the window;
status: "in_progress"withcall_again: true, meaning the analysis is still running and you should make the identical call again.
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:
| Field | What it carries |
|---|---|
platform | Platform the profile was looked up on. |
username | Handle on that platform. |
influencer_name | Display name. |
profile_url | Canonical profile URL. |
user_id | Platform-native account id, for de-duplicating a handle that changed. |
is_verified | Whether the platform verifies the account. |
is_private | Whether the account is private, which bounds what any analysis can see. |
account_type | Account class the platform assigns (creator, professional, business). |
categories | Content niches, at most 5. |
biography | Profile bio, truncated to 200 characters. |
creator_location | Creator's own stated location, as a single string. |
creator_languages | Languages the creator publishes in. |
contact | Contactability: email, phone_number, website, external_links (at most 5) and direct_message_available. Members the platform does not expose are omitted. |
followers | Follower / subscriber count. |
engagement_rate | Engagement rate in percent, as the platform's own analysis computes it. |
average_likes | Average likes per post. |
average_comments | Average comments per post (replies on twitter). |
average_views | Average views per post, where the platform reports views. |
average_interactions | Average total interactions per post. |
total_posts | Lifetime post count. |
following_count | Accounts the creator follows — the ratio to followers is an authenticity signal. |
audience_quality | Authenticity 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_genders | Audience gender split as percentages. |
audience_age_ranges | Top 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_countries | Top 3 audience countries as percentages. |
audience_languages | Top 3 audience languages as percentages. |
posting_activity | Cadence and recency: posts_per_week, average_post_interval_days, posts_last_30d, last_post_date, has_active_story. |
brand_collaborations | Sponsored-content history: has_brand_deals, sponsored_posts_count and up to 5 brand names. |
data_freshness | When 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:
| Field | What it carries |
|---|---|
audience_cities | Full audience city distribution. |
audience_interests | Audience interest categories. |
audience_ethnicity | Audience ethnicity distribution. |
audience_types | Follower-quality breakdown (real people, mass followers, suspicious). |
audience_reachability | How many accounts the audience follows, bucketed. |
brand_affinities | Brands the audience already engages with. |
follower_growth | Follower count time series. |
like_growth | Average-likes time series. |
top_hashtags | Hashtags the creator uses most. |
mentions | Accounts the creator mentions most. |
posts_recent | Recent posts, in full. |
posts_popular | Best-performing posts, in full. |
lookalikes | Similar creators the provider suggests. |
paid_partnerships | Sponsored posts, in full. |
paid_post_performance | How sponsored content performs against organic. |
content_type_breakdown | Performance split by content type (video, short, image, text). |
brand_safety | Risk 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:
- an
in_progressturn — however many times you repeat the call; - a call you cancelled by closing the stream;
- a technical failure (
upstream_error,internal_error); - a refusal decided before any work was ordered — a bad argument, a missing scope, no subscription, or an out-of-credits refusal.
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:
| Code | Meaning |
|---|---|
-32602 | An 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. |
-31001 | A budget is spent. The error names how long to wait, in data.retryAfterSeconds. |
-31002 | Out of credits, or no active Creator Data API subscription. No analysis was ordered. |
-31003 | The service is shedding load. Retry after the wait. |
-32601 | A protocol method this revision does not define. |
-32020 | A mirrored request header contradicts the body, or a required one is missing. Only the stateless revision mirrors anything. |
-32022 | The 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.
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.
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.