Contact us via [email protected] and we will get back to you as soon as possible to clarify the details.
After that, we provide you with an API key.
The base URL is https://api.medialister.com.
There is no user account, session, or login involved. Every caller sees the same catalog. Send your key in an apikey header on every request. Without it, the call is rejected at the gateway and never reaches the catalog.
curl "https://api.medialister.com/api/offers?perPage=20" \
-H "apikey: {apikey here}"
If your client also sends an Authorization: Bearer … header, it is ignored rather than validated. An expired session on your side cannot break these calls.
Rate limit: 120 requests per minute per key, counted in fixed one-minute windows and shared across both endpoints below. Going over returns 429 with a Retry-After header. Respect that value instead of retrying immediately.
| Endpoint | Returns |
|---|---|
GET /api/offers |
A paginated collection of offers. Everything in this article applies here. |
GET /api/offers/{id} |
A single offer, in the same shape as a collection member. Returns 404 if the ID is malformed, unknown, or belongs to an offer that isn't publicly visible — the three cases are deliberately indistinguishable. |
Here is a Postman collection to get started:
{
"info": {"_postman_id": "033a0297-b4dc-4cdc-8dda-54dadbd9598b","name": "Medialister API","schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json","_exporter_id": "6960664","_collection_link": "https://dark-satellite-733361.postman.co/workspace/My-Workspace~a8a6af31-f68d-474d-80d8-e5431c8ecbd8/collection/6960664-033a0297-b4dc-4cdc-8dda-54dadbd9598b?action=share&source=collection_link&creator=6960664"
},
"item": [{
"name": "List offers",
"request": {
"auth": {
"type": "apikey",
"apikey": [
{
"key": "value",
"value": "{{apikey}}",
"type": "string"
},
{
"key": "key",
"value": "apikey",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{endpoint}}/api/offers",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"offers"
]
}
},
"response": []}
],
"event": [{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"packages": {},
"requests": {},
"exec": [
""
]
}},{
"listen": "test",
"script": {
"type": "text/javascript",
"packages": {},
"requests": {},
"exec": [
""
]
}}
],
"variable": [{
"key": "endpoint",
"value": ""},{
"key": "apikey",
"value": ""}
]}
One parameter, q, with two behaviors.
As free text, it matches publisher and outlet names. q=fintech returns offers from outlets whose names match, ranked by relevance instead of the usual newest-first order.
As a domain lookup, it pulls every offer for one or more sites at once. Pass a single domain, or a list separated by commas, semicolons, or newlines. The maximum is 200 domains per request; more than that returns a 400.
GET /api/offers?q=fintech
GET /api/offers?q=example.com
GET /api/offers?q=example.com,techblog.io,news.example.org
q on its own ranks by relevance. q together with sortBy ranks by your sort instead, and relevance only breaks ties. If you want the best text matches first, leave sortBy off.
Different parameters narrow each other. Values inside one parameter widen it.
So language=en&priceMax=500 means English and $500 or less, while mediaCategory=Technology,Finance means Technology or Finance.
Comma and pipe both work as separators, and you can pass up to 50 values per parameter.
These are matched against real catalog values. Case doesn't matter, but the name must be complete. A name that doesn't exist returns 400 and tells you which value it couldn't place, so a typo can never quietly widen your results.
| Parameter | Matches | Example |
|---|---|---|
formatType |
Type of placement | formatType=Article,Press Release |
mediaType |
Kind of outlet | mediaType=Blog|News |
mediaCategory |
Editorial category | mediaCategory=Technology |
mediaAudience |
Audience the outlet serves | mediaAudience=Business |
location |
Outlet's own location, city or country | location=United States,Miami |
audienceByCountry |
Country the readership comes from | audienceByCountry=Canada |
mediaBadge |
Badge held by the outlet | mediaBadge=Verified |
formatBadge |
Badge held by the offer | formatBadge=Popular |
Because an unknown name is rejected rather than ignored, you need the current list of valid values before you use these filters. Ask [email protected] and we will send the lists for your integration. The Marketplace Filters article explains what each filter means.
These have no fixed vocabulary to check against, so an unknown value isn't an error. It simply matches nothing and you get an empty page.
| Parameter | Accepts | Example |
|---|---|---|
language |
Two-letter language codes | language=en|es |
acceptedTopic |
Sensitive topics the publisher accepts | acceptedTopic=Crypto|Gambling |
hyperlinksType |
do-follow, no-follow, sponsored-hyperlinks |
hyperlinksType=do-follow |
All of these take whole numbers and are inclusive. Zero means "no bound", so priceMin=0 behaves the same as leaving it out. A negative number, a decimal, or anything non-numeric returns 400 and names the parameter.
| Measure | Parameters |
|---|---|
| Price, in USD | priceMin, priceMax |
| Monthly audience | audienceMin, audienceMax |
| Ahrefs Domain Rating | ahrefsDrMin, ahrefsDrMax |
| Semrush organic traffic | semrushTrafficMin, semrushTrafficMax |
| Medialister Attention Index | maiMin, maiMax |
| Rating | ratingMin, ratingMax |
| Estimated views | estimatedViewsMin, estimatedViewsMax |
| Turnaround time, in days | turnAroundTimeMax |
| Hyperlinks allowed | hyperlinksAmountMin |
Turnaround time has a maximum only, and hyperlinks allowed has a minimum only.
Six flags narrow the results to offers that have the condition. true, false, 1, 0, on, and off are all understood; anything else returns 400.
Only true does anything. Setting a flag to false is the same as leaving it out, so there is no way to ask for offers without a condition.
| Parameter | Keeps offers that… |
|---|---|
homePageAnnounce |
include a homepage announcement |
permanentPlacement |
stay published permanently |
pageIndex |
allow the page to be indexed |
uniqueText |
require unique text |
noChangesText |
publish the text without changes |
withVideo |
support video |
| Parameter | Default | Notes |
|---|---|---|
sortBy |
createdAt |
One of the eleven fields below. Anything else returns 400 and lists what is allowed. |
sortOrder |
— | Either asc or desc. Any other value returns 400. Set it explicitly whenever direction matters. |
sortBy |
Orders by |
|---|---|
createdAt |
When the offer was added |
updatedAt |
When the offer last changed |
price |
Offer price |
name |
Outlet name, alphabetically |
turnAroundTime |
Days to publication, so asc is fastest first |
audience |
Audience size |
ahrefsDr |
Ahrefs Domain Rating |
semrushOrganicTraffic |
Semrush organic traffic |
estimatedViews |
Estimated views per placement |
rating |
Outlet rating |
mai |
Medialister Attention Index |
With no sortBy and no q, offers come back newest first. Add q and relevance ranking takes over. Add sortBy and your sort applies in either case.
Always pass sortOrder alongside sortBy when the direction matters to you. Don't rely on the default direction.
| Parameter | Default | Notes |
|---|---|---|
page |
1 |
1-indexed. Zero, a negative number, or a non-number returns 400. A page past the end returns a normal 200 with an empty hydra:member and the true hydra:totalItems. |
perPage |
10 |
Maximum 100. Asking for more than 100 is not an error — you are quietly given 100. Zero or a non-number returns 400. |
Paging is limited to the first 100,000 matches. If your result set is larger than that, narrow the filters rather than paging further.
This endpoint validates your query string rather than ignoring what it doesn't understand. A mistake comes back as a 400 that names the parameter, so you never silently receive a wider slice of the catalog than you asked for.
| Status | Cause |
|---|---|
| 400 | A number that isn't whole, or is negative — ahrefsDrMax=abc, priceMin=-50, audienceMin=10.5 |
| 400 | An unrecognized boolean — permanentPlacement=maybe, withVideo=2 |
| 400 | A repeated or array-style parameter — priceMin[]=1 |
| 400 | A name filter naming something that doesn't exist — location=Atlantis, formatType=DoesNotExist |
| 400 | An unknown sortBy, or a sortOrder that isn't asc or desc |
| 400 | A page or perPage that is zero, negative, or non-numeric; or more than 200 domains in q |
| 404 | An offer ID that is malformed, unknown, or not publicly visible |
| 429 | More than 120 requests in a minute. Respect Retry-After. |
An empty value counts as absent. ?priceMin= and ?permanentPlacement= are both fine, so templated query strings with unfilled slots work as you would expect. And a value from one of the open vocabularies that matches nothing, like language=zz, returns an empty page rather than a 400.
Cheapest strong-authority offers first:
/api/offers?ahrefsDrMin=50&semrushTrafficMin=10000&sortBy=price&sortOrder=asc
English or Spanish tech and finance coverage, $200 to $2,000:
/api/offers?mediaCategory=Technology,Finance&language=en|es&priceMin=200&priceMax=2000
Fast, permanent placements in the US and UK:
/api/offers?location=United States,United Kingdom&turnAroundTimeMax=3&permanentPlacement=true&sortBy=turnAroundTime&sortOrder=asc
Do-follow articles allowing at least two links, biggest audience first:
/api/offers?formatType=Article&hyperlinksType=do-follow&hyperlinksAmountMin=2&sortBy=audience&sortOrder=desc
Crypto-friendly outlets reaching a US readership:
/api/offers?acceptedTopic=Crypto&audienceByCountry=United States&sortBy=mai&sortOrder=desc
Checking whether three specific sites have offers:
/api/offers?q=beleggen.nl,guruwatch.nl,techcrunch.com
Paging through a filtered result in JavaScript:
const params = new URLSearchParams({
mediaCategory: "Technology,Finance",
language: "en",
ahrefsDrMin: "40",
sortBy: "ahrefsDr",
sortOrder: "desc",
perPage: "100",
});
let page = 1, seen = 0, total = Infinity;
const offers = [];
while (seen < total) {
params.set("page", String(page));
const res = await fetch(`https://api.medialister.com/api/offers?${params}`, {
headers: { apikey: apiKey },
});
// A 400 means the query string is wrong, and the body names the parameter
if (!res.ok) throw new Error(`offers page ${page}: ${res.status}`);
const body = await res.json();
const batch = body["hydra:member"];
total = body["hydra:totalItems"];
offers.push(...batch);
seen += batch.length;
page++;
if (batch.length === 0) break;
}
Results arrive in a JSON-LD collection, so read hydra:member rather than member. Every seoMetric field is nullable, and an outlet with no metrics on file may have no seoMetric object at all.
{
"@context": "/api/contexts/Format",
"@id": "/api/offers",
"@type": "hydra:Collection",
"hydra:totalItems": 108125,
"hydra:member": [
{
"@id": "/api/formats/7a0088a6-2945-48be-9317-e381131c56fd",
"@type": "Format",
"id": "7a0088a6-2945-48be-9317-e381131c56fd",
"formatType": {
"@id": "/api/format_types/1b04f264-8bd8-48a0-9fac-ac97bf801ce3",
"@type": "FormatType",
"name": "Article"
},
"mediaProject": {
"@id": "/api/media_projects/26bb8dbe-e141-4bfd-82af-1a3fc36a37ab",
"@type": "MediaProject",
"website": "https://www.citizen.co.za",
"languages": ["en"],
"fullLanguages": { "en": "English" }
},
"price": "214.84",
"urlExamples": [
"www.citizen.co.za/south-coast-herald/news-headlines/advertorial/2024/01/12/mobile-gambling-experience-in-south-africa-a-game-changer-for-casino-enthusiasts/"
],
"seoMetric": {
"@id": "/api/seo_metrics/latest_media_metrics",
"@type": "SeoMetric",
"audience": 7932373,
"audienceSourceDirect": 275452,
"audienceSourceReferrals": 18052,
"audienceSourceSearch": 264048,
"audienceSourceSocial": 51026,
"sourceDirect": 44.99,
"sourceReferrals": 2.95,
"sourceSearch": 43.12,
"sourceSocial": 8.33,
"sourceDisplay": 0.54,
"sourceMail": 0.07,
"ahrefsDr": 76,
"organicTrafficByAhrefs": 445727,
"estimatedViews": 5870,
"leadingCountries": {
"South Africa": 7525046,
"India": 67723,
"Ethiopia": 50121,
"United States of America": 40975,
"United Kingdom": 27717
},
"bounceRate": 0.38635,
"timeOnSite": 56.9112,
"pagePerVisit": 1.84471,
"mai": 37,
"visits": []
}
}, {.....}, {...}
],
"hydra:view": { "@type": "hydra:PartialCollectionView" }
}
Note that urlExamples entries are not always fully qualified URLs — some are stored without a scheme, as above.
| Field | Description |
|---|---|
@context |
JSON-LD context path for the resource type |
@id |
Canonical URI of this collection |
@type |
Always hydra:Collection for paginated lists |
hydra:totalItems |
Total number of offers matching your query, across all pages |
hydra:member |
Array of individual offer objects |
hydra:view |
Pagination view for the current request |
| Field | Description |
|---|---|
@id |
Canonical URI of this offer |
@type |
Always Format |
id |
UUID of the offer. Pass it to /api/offers/{id}. |
price |
Marketplace offer price in USD, as a two-decimal string — "450.00", not 450 |
urlExamples |
Example URLs of previously published placements on this media. Often empty. |
formatType.name |
Publication format: Article, Press Release, Contributor Post, Guest Post, Interview, Paid News, or Mention. Compare these case-insensitively — capitalization varies. |
mediaProject.website |
Domain of the publisher's media website |
mediaProject.languages |
Language codes the outlet publishes in |
mediaProject.fullLanguages |
The same languages as a map of code to English name |
seoMetric |
The outlet's traffic and authority metrics. Sits beside mediaProject, not inside it, and can be null. |
Several things you can filter or sort on are not in the response body: the outlet's name, its categories, audiences, badges, and location, and the offer's turnaround time, conditions, and accepted topics. rating can be filtered and sorted on but is not returned either. Plan your integration around the fields in the table above.
seoMetric)Every field here is nullable. An outlet with no metrics on file returns nulls, or no seoMetric object at all.
| Field | Description |
|---|---|
audience |
Estimated total monthly visits (from SimilarWeb) |
semrushOrganicTraffic |
Estimated monthly organic search traffic according to Semrush |
organicTrafficByAhrefs |
Estimated monthly organic search visitors according to Ahrefs |
audienceSourceDirect |
Monthly visits from direct traffic (absolute) |
audienceSourceReferrals |
Monthly visits from referral sources (absolute) |
audienceSourceSearch |
Monthly visits from search engines (absolute) |
audienceSourceSocial |
Monthly visits from social media (absolute) |
audienceSourceDisplay |
Monthly visits from display ads (absolute) |
audienceSourceMail |
Monthly visits from email (absolute) |
visits |
Historical visit data array (may be empty if not yet populated) |
| Field | Description |
|---|---|
sourceDirect |
Share of direct traffic, % |
sourceReferrals |
Share of referral traffic, % |
sourceSearch |
Share of search traffic, % |
sourceSocial |
Share of social traffic, % |
sourceDisplay |
Share of display ad traffic, % |
sourceMail |
Share of email traffic, % |
| Field | Description |
|---|---|
ahrefsDr |
Ahrefs Domain Rating (0-100). Measures backlink authority. Higher is better. |
mozDomainAuthority |
Moz Domain Authority (0-100) |
semrushAuthorityScore |
Semrush Authority Score (0-100) |
| Field | Description |
|---|---|
bounceRate |
Share of sessions where users left without any interaction (0-1 scale, lower is better) |
timeOnSite |
Average session duration in seconds |
pagePerVisit |
Average number of pages viewed per session |
| Field | Description |
|---|---|
estimatedViews |
Estimated views a single placement is likely to receive, calculated from monthly traffic with format-specific coefficients |
mai |
Medialister Attention Index, a proprietary metric measuring reader attention quality on this media in days (higher is better). See the Medialister Attention Index (MAI) article. |
| Field | Description |
|---|---|
leadingCountries |
Top countries by audience volume. Keys are country names, values are estimated monthly visitor counts from each country. |
Your filters narrow a catalog that is already gated. An offer is only visible when all of the following hold. If one fails, the offer is simply absent, with nothing to indicate it exists.
| Requirement | What it means |
|---|---|
| Listed for sale | The publisher has actively put the offer on sale |
| Not archived | Archived offers never reappear in the catalog |
| Outlet approved | Outlets in draft, in review, declined, or banned are excluded |
| Publisher approved | The publisher account that owns the outlet has been approved |
| Outlet not restricted | Outlets restricted from public listings are excluded |
| Types still offered | Both the offer's format type and the outlet's media type are still available on the platform |
Take the format ID and put it into this URL:
https://app.medialister.com/media/{format_id}
A typo in a name filter is a 400, not fewer results. That is deliberate. The alternative would hand you the whole catalog and let it read as "all of these match". Read the message — it names both the parameter and the value it could not place.
Names must match exactly. Case doesn't matter, but partial names don't resolve. "United States" works; "USA" and "United St" do not. There is no fuzzy matching, on purpose, because a near-miss would otherwise return some other real place's offers.
false never filters. The six condition flags only narrow when set to true. You cannot ask for offers that lack a condition.
Prices are strings. "450.00", not 450. Parse before you compare or sum.
Sorting reads indexed values while the body comes from the live record. Just after a publisher edits an offer, the order and the range filters can lag the returned figures for a moment. It settles on its own.
I got a 400. Read the response body. It names the parameter and, for name filters, the value it couldn't place. Ask support for the current list of valid names for that filter.
My filter returns nothing at all. Check whether it's one of the open vocabularies — language, acceptedTopic, hyperlinksType — where an unknown value matches nothing instead of erroring.
Setting a flag to false didn't change anything. That's expected. Only true narrows.
I asked for 500 per page and got 100. 100 is the maximum, and requests above it are quietly clamped rather than rejected.
My text search results aren't ranked how I expect. If you pass sortBy alongside q, the sort wins and relevance only breaks ties. Drop sortBy to get the best matches first.
I'm getting 429s. The limit is 120 requests per minute per key, shared across both endpoints. Respect the Retry-After header rather than retrying straight away.
Anything else, write to [email protected].