How to Build Screener API Requests - Starter Examples
Most API docs hand you a request shape and leave you to figure out the rest. This article works the other way: it starts with five real investing screens, the same ones built into the DFin stock screener, and shows how each one becomes an API call you can run, modify, and actually use.
Each example includes the request body, an explanation of why the filters are set the way they are, and a suggestion for where to take it next. By the end, you will have a working starting point for five different investing workflows, plus enough understanding of the request structure to adapt them to your own needs.
Setup
All five examples use the advanced screener endpoint, which supports the full screener contract. The request structure is simple: filters defines what you are looking for, page controls how many rows come back, fields picks your columns, and result_format shapes the response.
| Item | Value |
|---|---|
| Endpoint | POST /api/v1/screener/advanced/ |
| API key variable | DFIN_API_KEY |
| Default output | result_format: rows |
Set your API key as an environment variable, then send the JSON body from whichever example fits your workflow.
For endpoint-level details, parameter rules, and the generated Postman collection, see the API v1 screener reference.
1.0 Large profitable companies
The goal
Build a quality-first starting list of established U.S. businesses.
This starts with large-cap U.S. companies above $10B, then filters for a strong 3-year average return on invested capital. It is not a narrow niche screen. It is a wide net with a quality filter draped over it.
Why ROIC?
ROIC is one of the more honest measures of how well a business uses the money entrusted to it. A company that consistently earns high returns on reinvested capital tends to compound value over time, which is usually the whole point.
Where to take it next
This is a natural foundation for a watchlist. Once you have your results, add a sector filter to compare within industries, pull in valuation fields like P/E or EV/EBITDA, or sort by market cap descending so the biggest names float to the top.
Filters
| Filter | API field | Payload setting | What it does |
|---|---|---|---|
| Country | country | operator: "include", values: ["United States"] | Keeps only companies whose country value is United States. |
| Market Cap (USD) | marketCap_usd | min: 10,000,000,000 | Filters companies by U.S. dollar market capitalization. The request sets a minimum market cap, so companies below $10B are excluded. |
| ROIC | returnOnInvestedCapital | 3-year average, min: 0.15 | Requires the company's 3-year average ROIC to be at least 15%. |
Request body
Use this body with /api/v1/screener/advanced/. The example includes the starter-screen filters, a manageable first page of results, and a small set of useful columns.
{
"filters": {
"country": {
"operator": "include",
"values": [
"United States"
]
},
"marketCap_usd": {
"min": 10000000000
},
"returnOnInvestedCapital": {
"missing_policy": "strict",
"rules": [
{
"kind": "range",
"operand": {
"kind": "average",
"anchor": {
"scope": "fy",
"offset": 0
},
"years": 3
},
"min": 0.15
}
]
}
},
"sort": {},
"page": {
"limit": 100,
"offset": 0
},
"fields": [
"full_ticker",
"companyName",
"marketCap_usd"
],
"result_format": "rows"
}Code
/api/v1/screener/advanced/
curl -X POST "https://www.dfin.pro/api/v1/screener/advanced/" \
-H "Authorization: Bearer ${DFIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"filters":{"country":{"operator":"include","values":["United States"]},"marketCap_usd":{"min":10000000000},"returnOnInvestedCapital":{"missing_policy":"strict","rules":[{"kind":"range","operand":{"kind":"average","anchor":{"scope":"fy","offset":0},"years":3},"min":0.15}]}},"sort":{},"page":{"limit":100,"offset":0},"fields":["full_ticker","companyName","marketCap_usd"],"result_format":"rows"}'
import os, requests
url = "https://www.dfin.pro/api/v1/screener/advanced/"
headers = {"Authorization": f"Bearer {os.environ['DFIN_API_KEY']}"}
payload = {'filters': {'country': {'operator': 'include', 'values': ['United States']},
'marketCap_usd': {'min': 10000000000},
'returnOnInvestedCapital': {'missing_policy': 'strict',
'rules': [{'kind': 'range',
'operand': {'kind': 'average',
'anchor': {'scope': 'fy',
'offset': 0},
'years': 3},
'min': 0.15}]}},
'sort': {},
'page': {'limit': 100, 'offset': 0},
'fields': ['full_ticker', 'companyName', 'marketCap_usd'],
'result_format': 'rows'}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
2.0 Improving margins
The goal
Find companies where the economics are quietly getting better.
This screen does not care whether margins are high in absolute terms. It cares whether they are improving. Specifically, it compares the recent 2-year average EBITDA margin against the 5-year average and keeps only the companies trending in the right direction.
Why this matters
Improving margins can mean a lot of things: pricing power gaining traction, fixed costs spreading over more revenue, or a product mix shifting toward higher-value work. Whatever the cause, it often shows up in margins before it shows up anywhere else. It is a useful early signal.
Where to take it next
Pair this with a size or sector filter to get more meaningful comparisons. A 2-point margin improvement means something different in software than it does in retail.
Filters
| Filter | API field | Payload setting | What it does |
|---|---|---|---|
| EBITDA Margin | ebitdaMargin | 2-year average > 5-year average | Compares recent EBITDA margin with the longer-term average and keeps companies where the recent period is better. |
Request body
Use this body with /api/v1/screener/advanced/. The example includes the starter-screen filters, a manageable first page of results, and a small set of useful columns.
{
"filters": {
"ebitdaMargin": {
"missing_policy": "strict",
"rules": [
{
"kind": "compare",
"left": {
"kind": "average",
"anchor": {
"scope": "fy",
"offset": 0
},
"years": 2
},
"operator": "gt",
"right": {
"kind": "average",
"anchor": {
"scope": "fy",
"offset": 0
},
"years": 5
}
}
]
}
},
"sort": {},
"page": {
"limit": 100,
"offset": 0
},
"fields": [
"full_ticker",
"companyName",
"marketCap_usd"
],
"result_format": "rows"
}Code
/api/v1/screener/advanced/
curl -X POST "https://www.dfin.pro/api/v1/screener/advanced/" \
-H "Authorization: Bearer ${DFIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"filters":{"ebitdaMargin":{"missing_policy":"strict","rules":[{"kind":"compare","left":{"kind":"average","anchor":{"scope":"fy","offset":0},"years":2},"operator":"gt","right":{"kind":"average","anchor":{"scope":"fy","offset":0},"years":5}}]}},"sort":{},"page":{"limit":100,"offset":0},"fields":["full_ticker","companyName","marketCap_usd"],"result_format":"rows"}'
import os, requests
url = "https://www.dfin.pro/api/v1/screener/advanced/"
headers = {"Authorization": f"Bearer {os.environ['DFIN_API_KEY']}"}
payload = {'filters': {'ebitdaMargin': {'missing_policy': 'strict',
'rules': [{'kind': 'compare',
'left': {'kind': 'average',
'anchor': {'scope': 'fy', 'offset': 0},
'years': 2},
'operator': 'gt',
'right': {'kind': 'average',
'anchor': {'scope': 'fy', 'offset': 0},
'years': 5}}]}},
'sort': {},
'page': {'limit': 100, 'offset': 0},
'fields': ['full_ticker', 'companyName', 'marketCap_usd'],
'result_format': 'rows'}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
3.0 Lower leverage
The goal
Screen out companies where the balance sheet is doing the heavy lifting.
Two filters: net debt below 2.5x EBITDA, and total debt below 50% of assets. Not zero debt. Just a level that leaves room for things to go wrong without the equity story unraveling.
Why it matters
Leverage is quiet until it is not. Companies that carry too much debt can look fine during good years and then face real problems when demand softens, rates rise, or a refinancing window closes at the wrong time. This screen is a simple way to avoid that exposure before you start thinking about growth or valuation.
Where to take it next
Use this as a filter before you evaluate anything else. If a company passes here, the rest of your analysis can focus on upside, not on whether the balance sheet is a hidden risk.
Filters
| Filter | API field | Payload setting | What it does |
|---|---|---|---|
| Net Debt/EBITDA | netDebtToEBITDA | TTM, max: 2.5 | Filters companies by trailing-twelve-month net debt relative to EBITDA. The request excludes companies above 2.5x. |
| Debt/Assets | debtToAssetsRatio | TTM, max: 0.5 | Filters companies by trailing-twelve-month debt as a share of assets. The request excludes companies above 50%. |
Request body
Use this body with /api/v1/screener/advanced/. The example includes the starter-screen filters, a manageable first page of results, and a small set of useful columns.
{
"filters": {
"netDebtToEBITDA": {
"missing_policy": "strict",
"rules": [
{
"kind": "range",
"operand": {
"kind": "single",
"scope": "ttm"
},
"max": 2.5
}
]
},
"debtToAssetsRatio": {
"missing_policy": "strict",
"rules": [
{
"kind": "range",
"operand": {
"kind": "single",
"scope": "ttm"
},
"max": 0.5
}
]
}
},
"sort": {},
"page": {
"limit": 100,
"offset": 0
},
"fields": [
"full_ticker",
"companyName",
"marketCap_usd"
],
"result_format": "rows"
}Code
/api/v1/screener/advanced/
curl -X POST "https://www.dfin.pro/api/v1/screener/advanced/" \
-H "Authorization: Bearer ${DFIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"filters":{"netDebtToEBITDA":{"missing_policy":"strict","rules":[{"kind":"range","operand":{"kind":"single","scope":"ttm"},"max":2.5}]},"debtToAssetsRatio":{"missing_policy":"strict","rules":[{"kind":"range","operand":{"kind":"single","scope":"ttm"},"max":0.5}]}},"sort":{},"page":{"limit":100,"offset":0},"fields":["full_ticker","companyName","marketCap_usd"],"result_format":"rows"}'
import os, requests
url = "https://www.dfin.pro/api/v1/screener/advanced/"
headers = {"Authorization": f"Bearer {os.environ['DFIN_API_KEY']}"}
payload = {'filters': {'netDebtToEBITDA': {'missing_policy': 'strict',
'rules': [{'kind': 'range',
'operand': {'kind': 'single', 'scope': 'ttm'},
'max': 2.5}]},
'debtToAssetsRatio': {'missing_policy': 'strict',
'rules': [{'kind': 'range',
'operand': {'kind': 'single', 'scope': 'ttm'},
'max': 0.5}]}},
'sort': {},
'page': {'limit': 100, 'offset': 0},
'fields': ['full_ticker', 'companyName', 'marketCap_usd'],
'result_format': 'rows'}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
4.0 Growing businesses
The goal
A simple first pass for companies with real top-line momentum.
This screen asks for at least 10% revenue CAGR over five years. That is it. The five-year window matters because it filters out companies that had one great year and makes you focus on businesses that have been consistently expanding.
Why revenue?
It is usually the clearest signal that something real is happening: demand is growing, market share is being won, or a business is genuinely scaling. It does not tell you whether the growth is profitable or sustainable, which is exactly why this screen works best as a starting filter, not a final one.
Where to take it next
Layer in profitability or balance-sheet filters to separate healthy growers from companies that are growing by spending heavily or taking on debt. Growth is great context. It is not a thesis on its own.
Filters
| Filter | API field | Payload setting | What it does |
|---|---|---|---|
| Income Statement Growth | incomeStatementGrowth | revenue, 5-year CAGR, min: 0.1 | Filters for companies whose reported revenue has compounded at 10% or better over five years. |
Request body
Use this body with /api/v1/screener/advanced/. The example includes the starter-screen filters, a manageable first page of results, and a small set of useful columns.
{
"filters": {
"incomeStatementGrowth": {
"rules": [
{
"metric": "revenue",
"horizon_years": 5,
"value_type": "cagr",
"currency_basis": "reported",
"min": 0.1
}
]
}
},
"sort": {},
"page": {
"limit": 100,
"offset": 0
},
"fields": [
"full_ticker",
"companyName",
"marketCap_usd"
],
"result_format": "rows"
}Code
/api/v1/screener/advanced/
curl -X POST "https://www.dfin.pro/api/v1/screener/advanced/" \
-H "Authorization: Bearer ${DFIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"filters":{"incomeStatementGrowth":{"rules":[{"metric":"revenue","horizon_years":5,"value_type":"cagr","currency_basis":"reported","min":0.1}]}},"sort":{},"page":{"limit":100,"offset":0},"fields":["full_ticker","companyName","marketCap_usd"],"result_format":"rows"}'
import os, requests
url = "https://www.dfin.pro/api/v1/screener/advanced/"
headers = {"Authorization": f"Bearer {os.environ['DFIN_API_KEY']}"}
payload = {'filters': {'incomeStatementGrowth': {'rules': [{'metric': 'revenue',
'horizon_years': 5,
'value_type': 'cagr',
'currency_basis': 'reported',
'min': 0.1}]}},
'sort': {},
'page': {'limit': 100, 'offset': 0},
'fields': ['full_ticker', 'companyName', 'marketCap_usd'],
'result_format': 'rows'}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
6.0 Modify these examples
The fastest way to learn the screener API is to take one of these examples, run it exactly as written, and then change a single thing. See what happens. Then change something else.
Return only tickers
Set result_format to tickers when you only need a clean list of matching symbols. This is useful when the API result is feeding another workflow, such as a watchlist, a scheduled job, or a second data request.
Request more fields
When you use result_format: rows, the fields array controls the columns returned with each company. Add fields like gics_sector, return_1y, or netDebtToEBITDATTM when you want the response to include enough context for ranking, sorting, or quick review.
Adjust the result count
If a screen returns too many companies, tighten one filter threshold at a time. If it returns too few, loosen one threshold. Changing one value at a time makes it much easier to understand which filter is doing the real work.
Check available filters and fields
Call /api/v1/screener/options/ before building a new screen from scratch. It lists the current filters and output fields, so you can extend these examples without guessing at field names.
Ready to build more complex growth rules? See the advanced growth filter examples.