Skip to content

Coverage is still narrow. Live prices have been read from 1 of 129 panels (2,182 services). The rest are listed because they are real, reachable operators — not because we have seen their rates. Panels with no synced catalogue show no prices rather than an estimate. How the data is gathered.

v1, no keyCC BY 4.0Data as of

SMM panel data API

Endpoints
7

GET only

Panels exposed
129
Price observations
2,182
Authentication
None

no key, no account

How do you make your first request?

Copy any of these. They are complete commands against the live API — nothing to substitute except the values you want to change.

The ten cheapest panels, as JSON

curl -s "https://panelcompare.com/api/v1/panels?sort=cheapest&limit=10"

Panels with an API that accept crypto, most trusted first

curl -s "https://panelcompare.com/api/v1/panels?feature=api&payment=crypto&sort=trust&limit=25"

Everyone selling Instagram followers, cheapest first, with the market index

curl -s "https://panelcompare.com/api/v1/services/instagram/followers?limit=20"

One panel with its whole catalogue

curl -s "https://panelcompare.com/api/v1/panels/panelfollows"

Market totals, and the whole price index into a file

curl -s "https://panelcompare.com/api/v1/stats"
curl -s "https://panelcompare.com/api/v1/price-index?limit=200" -o price-index.json

Which endpoints exist?

All GET, all JSON, all under the same base URL. Nothing here accepts a write.

PanelCompare read API endpoints, version 1
EndpointReturnsCaching
GET /api/v1Discovery document: every endpoint, its query parameters, the bulk files and the feeds.Static, revalidated every 7 days
GET /api/v1/panelsRanked, filterable list of tracked panels with scores, capabilities and payment rails.Dynamic per query string, 10 min at the edge
GET /api/v1/panels/{slug}One panel plus its full catalogue of offerings, each with the market median it is measured against.Prerendered per panel, revalidated every 6 hours
GET /api/v1/servicesThe canonical service taxonomy every panel’s raw service names are mapped onto.Dynamic, 10 min at the edge
GET /api/v1/services/{platform}/{service}Every panel selling one service, cheapest first, with the current index row and up to 90 days of history.Dynamic, 10 min at the edge
GET /api/v1/price-indexThe daily price index in exactly the shape the CSV and JSON downloads publish.Revalidated every 30 minutes
GET /api/v1/statsMarket-wide totals and per-platform rollups — the numbers the site itself cites.Revalidated every 30 minutes

What does every response look like?

One envelope for every endpoint: the payload under data, everything about the payload under _meta. Data lifted out of a response arrives somewhere else still carrying its licence and its source.

{
  "data": [ ... ],
  "_meta": {
    "generatedAt": "2026-09-07T00:43:06.000Z",
    "license": "CC-BY-4.0",
    "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
    "attribution": "PanelCompare — https://panelcompare.com",
    "docs": "https://panelcompare.com/api-docs",
    "methodology": "https://panelcompare.com/methodology",
    "pagination": { "limit": 50, "offset": 0, "total": 129, "next": "..." }
  }
}
Fields in the _meta block
FieldTypeWhat it holds
_meta.generatedAtstringISO 8601 timestamp of the moment the response was serialised.
_meta.licensestringAlways CC-BY-4.0.
_meta.licenseUrlstringCanonical licence deed.
_meta.attributionstringThe exact credit line to reproduce.
_meta.docsstringThis page.
_meta.methodologystringHow the scores and the index are computed.
_meta.paginationobject | absentOn list endpoints only: limit, offset, total and a ready-made next URL.

How does pagination work?

Offset-based, with the next page pre-built for you. Follow _meta.pagination.next until it is null rather than incrementing an offset yourself.

Pagination parameters
ParameterTypeNotes
limitintegerDefault 50, maximum 200. Values above the maximum are clamped rather than rejected.
offsetintegerDefault 0. Negative and non-numeric values fall back to 0.
next="https://panelcompare.com/api/v1/panels?limit=200"
while [ -n "$next" ] && [ "$next" != "null" ]; do
  page=$(curl -s "$next")
  echo "$page" | jq -c '.data[]'
  next=$(echo "$page" | jq -r '._meta.pagination.next')
done

One exception worth knowing: on /api/v1/services/{platform}/{service} the offer list is capped by limit but not paged — offset is parsed and has no effect, and no pagination block is returned. Raise limit to 200 to see the full offer list for a service.

GET /api/v1/panels

The ranked panel list, with every filter the site itself uses. Panels with status flagged are excluded from this endpoint entirely. An unknown sort or feature value returns 400 with the valid values in the message, rather than silently ignoring it.

Query parameters for /api/v1/panels
ParameterTypeNotes
sortstringbest (default), cheapest, trust, newest, services, uptime, rating. Anything else is a 400.
limit / offsetintegerStandard pagination. Default 50, maximum 200.
platformstringPlatform slug, e.g. instagram. Matches panels carrying at least one service for it.
paymentstringPayment method slug, e.g. crypto, upi, paypal.
countrystringISO country code of the panel’s registered jurisdiction. Case-insensitive.
featurestring, repeatableCapability slug. Repeat the parameter to require several: ?feature=api&feature=refill. Unknown values are a 400.
minTrustnumberLower bound on the 0–100 trust score.
maxDepositnumberUpper bound on minimum deposit. Accepted by the endpoint but omitted from the discovery document.
qstringSubstring match against panel name and domain.
Panel object fields
FieldTypeWhat it holds
slugstringStable identifier. This is the panel’s identity across every endpoint.
namestringDisplay name.
domainstringBare hostname, no scheme.
websitestringhttps:// plus the domain. Never the monetised outbound URL.
urlstringCanonical PanelCompare page for the panel.
tagline / descriptionstring | nullOne-sentence and long-form summaries.
statusstringactive, unverified, paused, offline or flagged.
launchedYearnumber | nullStated or evidenced launch year.
countryCodestring | nullRegistered jurisdiction where the panel publishes one. Never inferred.
currencystringThe currency the panel prices in.
minDepositnumber | nullSmallest accepted deposit, in that currency.
isAffiliate / isClaimed / isSponsoredbooleanMonetisation and verification state, exposed rather than hidden.
scoresobjectoverall, trust, price, feature, reliability, support.
ratingobjectaverage and count, from moderated reviews only.
reliabilityobjectuptimePct30d, avgResponseMs, lastCheckedAt.
catalogueobjectserviceCount, categoryCount, cheapestPer1000.
featuresobjectTwelve booleans plus refillDays. Keys are camelCase: api, childPanel, dripFeed, refill, cancelButton, geoTargeting, subscriptions, ticketSupport, liveChat, freeTrial, massOrder.
paymentMethodsstring[]Payment method slugs, matching the ?payment= filter.
platformsstring[]Platform slugs the panel sells for.
languagesstring[]Interface languages.
supplyobjectrole, upstreamDomain, markup, confidence, resellerCount. Inferred, so confidence travels with it.
listedSince / updatedAt / reviewedAtstring | nullISO 8601 timestamps.
curl -s "https://panelcompare.com/api/v1/panels?feature=refill&feature=api&minTrust=60&sort=trust" \
  | jq '.data[] | {slug, trust: .scores.trust, from: .catalogue.cheapestPer1000}'

GET /api/v1/panels/{slug}

One panel, with every field above plus an offerings array covering its whole active catalogue. Each offering carries marketMedianPer1000 and marketSampleSize beside the price, so a consumer can reproduce a “below market” verdict instead of taking one on trust. _meta.offeringCount gives the array length. An unknown slug returns 404 with code panel_not_found.

curl -s "https://panelcompare.com/api/v1/panels/panelfollows" \
  | jq '.data.offerings[] | select(.pricePer1000 < .marketMedianPer1000) | {service: .service.name, price: .pricePer1000, market: .marketMedianPer1000}'

GET /api/v1/services

The canonical taxonomy. Every panel’s raw service names are mapped onto these rows, which is what makes cross-panel comparison possible at all — so this is the endpoint to read before any other. Each row carries a reference price band and the typical order bounds for the service. An unknown platform returns 404 with code platform_not_found.

Query parameters for /api/v1/services
ParameterTypeNotes
platformstringPlatform slug. Omit for the full taxonomy across all platforms.
limit / offsetintegerStandard pagination, applied after the platform filter.
curl -s "https://panelcompare.com/api/v1/services?platform=tiktok&limit=200" \
  | jq -r '.data[] | [.slug, .unit, .referencePricePer1000.median] | @tsv'

GET /api/v1/services/{platform}/{service}

The price table for one service: the canonical row, the current index figures, up to 90 days of index history, and every panel selling it ordered cheapest first. This is the endpoint behind the comparison tables on the site. An unknown path returns 404 with code service_not_found.

Query parameters for a single service
ParameterTypeNotes
limitintegerCaps the offers array. Default 50, maximum 200.
minTrustnumberExcludes offers from panels below this trust score, which is how you get a shortlist rather than a price floor.
offsetintegerParsed but not applied on this endpoint; the offers array is capped rather than paged.
Offer object fields
FieldTypeWhat it holds
panelobjectslug, name, url, trustScore, overallScore.
pricePer1000numberRate per 1,000 units in the offer’s currency.
currencystringCurrency of this offer.
minOrder / maxOrdernumber | nullOrder quantity bounds.
qualitystringbot, mixed, real, hq-real, targeted or unknown.
refillDaysnumberRefill window in days. 0 is none, 9999 is lifetime.
dripFeed / cancellablebooleanPer-offer, not per-panel — panels support these unevenly across their catalogue.
startTimeMinutes / speedPerDaynumber | nullDelivery claims as the panel states them.
sourcestringapi, manual or import — how the row entered the index.
lastSeenAtstringISO 8601 timestamp of the last sync that saw this row.
curl -s "https://panelcompare.com/api/v1/services/instagram/followers?minTrust=60&limit=5" \
  | jq '{median: .data.priceIndex.medianPricePer1000,
         cheapest: .data.offers[0] | {panel: .panel.name, price: .pricePer1000, refillDays}}'

GET /api/v1/price-index

The daily aggregate, one row per service, in exactly the shape the bulk CSV and JSON downloads publish. One dataset, three transports — a reader who diffs them finds nothing. _meta.day carries the aggregate date and _meta.bulk the paths to the whole-file downloads, which are the right choice for anything larger than a page.

Price index row fields
FieldTypeWhat it holds
platform_slug / platformstringPlatform identifier and display name.
service_slug / servicestringCanonical service identifier and display name.
unitstringThe noun one unit counts: followers, views, hours.
daystringYYYY-MM-DD the aggregate was computed for.
currencystringAlways USD in this dataset.
min_price_per_1000numberLowest active offer that day.
p25_price_per_1000number25th percentile.
median_price_per_1000numberMedian — the figure to quote if you quote one.
p75_price_per_1000number75th percentile.
max_price_per_1000numberHighest active offer that day.
panels_samplednumberPanels in the sample. Below about 5 the median is noise.
urlstringThe page on this site showing the same row.
curl -s "https://panelcompare.com/api/v1/price-index?limit=200" \
  | jq -r '.data[] | select(.panels_sampled >= 5)
           | [.platform, .service, .median_price_per_1000] | @csv'

GET /api/v1/stats

Market-wide totals and a per-platform rollup: services, offerings, panels selling and the cheapest rate for each platform. These are the figures rendered on the market statistics page, read from the same queries. One naming note worth reading before you quote it: prices.meanOfServiceMediansPer1000 is the mean of every service’s median, not a basket price and not the market median.

curl -s "https://panelcompare.com/api/v1/stats" \
  | jq '{panels: .data.panels.total,
         offerings: .data.catalogue.offerings,
         deepest: (.data.platforms | max_by(.offerings) | .name)}'

What do errors look like?

Errors drop the envelope entirely and return an error object with a stable machine code. They are sent with Cache-Control: no-store, so a fixed request is not served a stale failure.

{
  "error": {
    "code": "invalid_feature",
    "message": "unknown feature turbo; valid values are api, child-panel, drip-feed, ..."
  }
}
Error codes returned by the API
CodeStatusWhen it happens
invalid_sort400The sort parameter is not one of the seven accepted values. The message lists them.
invalid_feature400A feature parameter is not a known capability slug. The message lists every valid slug.
panel_not_found404No panel with that slug.
platform_not_found404No platform with that slug on /api/v1/services.
service_not_found404No service at that platform/service path.

What are the rate limits and caching rules?

Stated plainly, including where the answer is “nothing yet”.

Rate limits, caching and response headers
PolicyCurrent value
AuthenticationNone. No key, no account, no signup.
Rate limitNone enforced. No quota headers are returned, because there is no quota to report. Prefer the bulk downloads for whole-dataset work, and if a limit ever becomes necessary it will be documented here before it is enforced.
MethodsGET and OPTIONS. There is no write surface.
CORSAccess-Control-Allow-Origin: * on every endpoint, so browser clients need no proxy.
Cachingpublic, s-maxage=600, stale-while-revalidate=3600. Shared caches hold a response for ten minutes and may serve it stale for an hour while refreshing.
Content typeapplication/json; charset=utf-8, pretty-printed with two-space indentation.
IndexingResponses carry X-Robots-Tag: noindex, and /api/ is disallowed in robots.txt. Neither affects a script or an agent fetching the endpoint directly.

What are you allowed to do with the data?

Published under Creative Commons Attribution 4.0 International. Commercial use, redistribution and derivative works are all permitted.

The one condition is attribution. Credit PanelCompare and link back to the source. Every response hands you the exact string to use in _meta.attribution, so nothing has to be composed by hand.

Attribution, in text

Data: PanelCompare (https://panelcompare.com), CC BY 4.0. Retrieved Sep 7, 2026.

Attribution, in HTML

<p>Data: <a href="https://panelcompare.com" rel="noopener">PanelCompare</a>,
  <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>.</p>

If you republish figures at scale, carry the observation date with them. A price in this market is a measurement of one day, and a rate quoted without its date is a claim rather than a fact — which is the failure mode this whole dataset exists to fix.

Is there a bulk download?

Yes, and it is the right choice for anything larger than a page of results. Same rows, same column names, same licence.

CSV column names are part of the same contract as the JSON field names: columns get added, never renamed or reordered, because renaming one silently breaks every notebook built on the file.

What is not in the API?

Stated so nobody builds against something that does not exist.

  • No write endpoints. Reviews, panel submissions and reports go through the site, not the API.
  • No reviews endpoint. Panel objects expose an aggregate rating, but individual review text is not served over the API.
  • No uptime history. Panels expose reliability.uptimePct30d as a rolling figure; the underlying probe log is not exposed.
  • No webhooks and no push. Poll on the cache window, or take the daily bulk file.
  • No historical panel scores. Price history is available per service through the index history; score history is not.

API: common questions

Do I need an API key?

No. There is no key, no registration and no account. Every endpoint is a public GET with CORS open to all origins, so it works from a browser, a notebook or a server with nothing configured. The only condition on using the data is attribution under CC BY 4.0.

What is the rate limit?

None is enforced today. Responses carry a ten-minute shared-cache directive, so a well-behaved client hitting a CDN-fronted deployment mostly reads cache rather than the database. If a client ever causes a problem we will add limits and document them on this page before enforcing them, rather than discovering them by being blocked.

Why does robots.txt disallow /api/?

Because a crawler spending its budget on JSON is a crawler not reading the pages that explain what the JSON means. The disallow is a crawl-efficiency measure aimed at bots, and it has no effect on you: a browser, a script or an agent fetching the endpoint directly is unaffected, and this documentation page is fully indexable so the API stays discoverable.

Can I use this data commercially?

Yes. CC BY 4.0 permits commercial use, redistribution and derivative works, including inside a paid product. The single requirement is attribution: credit PanelCompare and link to the source. If you republish figures at scale, please also carry the observation date — a price from this market is meaningless without one.

How current is the data?

Prices and catalogues refresh on a thirty-minute window, panel scores and capability flags on a six-hour window, and the price index is a daily aggregate. Every response carries _meta.generatedAt, and most objects carry their own updatedAt or lastSeenAt, so a consumer never has to guess how stale a figure is.

What happens if a field changes?

Field names are treated as a contract from the first consumer onward. Fields get added freely; renaming or removing one happens behind a /api/v2 rather than in place. The same rule governs the CSV column names in the bulk download.

Related

The same data, rendered for people rather than for machines.