catchbuys API documentation
The catchbuys API gives you programmatic access to 14,200+ digital businesses for sale, aggregated daily from 22 marketplaces (Flippa, Empire Flippers, Quiet Light, TrustMRR, and more).
All endpoints live under this base URL:
Responses are JSON. Authentication uses Sanctum personal access tokens, passed as Authorization: Bearer YOUR_API_KEY.
Authentication
Every authenticated endpoint requires a Bearer token. Tokens are issued from your account.
To get a token:
- Create a free account.
- Visit /api/access.
- Click "Create Token", give it a name, copy the displayed token. It is shown only once.
Send the token on every request:
Example header
Rate limiting
All authenticated endpoints are throttled to 60 requests per minute per token. Free during the POC.
Each response includes the standard rate-limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests per minute (60). |
X-RateLimit-Remaining |
Requests remaining in the current window. |
Exceeding the limit returns HTTP 429 Too Many Requests. Wait until the window resets and retry.
Tip: use per_page=100 to maximise data per request — pagination is cheaper than re-querying.
Endpoints
/api/v1/listings
List listings with optional filters.
Returns a paginated array under data plus a meta object with pagination info.
/api/v1/listings/{id}
Get a single listing by ID.
Returns the full listing object including description, total_revenue, growth_30d, customers, first_listed_at.
/api/v1/stats
Platform statistics.
Total active listings, count by type and by source.
/api/v1/sources
Marketplace catalogue.
Returns slug, label and current count for each source.
/api/v1/user
Authenticated user info.
Returns id, name, email.
/api/v1/ping
Public health check (no auth required).
Returns { "ok": true, "service": "catchbuys", "time": "..." }.
Query parameters
All parameters apply to GET /api/v1/listings.
| Parameter | Type | Default | Description |
|---|---|---|---|
q |
string | — | Keyword in listing name (LIKE match). |
source |
string | — | Single source slug.flippa, empireflippers, quietlight, trustmrr, dealslide, businessesforsale, websiteclosers, daltons, indiemaker, microns, acquirebase, investorsclub, latonas, dotmarket, bpifrance, nicheinvestor, moneynomad, fih, ecommercebrokers, appbusinessbrokers, sellerforce, acquisitionsdirect |
sources[] |
string[] | — | Multiple source slugs (repeat the param). |
types[] |
string[] | — | Business model.saas, ecom, site, agency, other |
industries[] |
string[] | — | Exact industry match. |
mrr_min |
integer | — | Minimum monthly revenue. |
price_min |
integer | — | Minimum asking price. |
price_max |
integer | — | Maximum asking price. |
age_min |
integer | — | Minimum age in years. |
margin_min |
integer | — | Minimum profit margin (%). |
sort |
string | mrr | Sort key.mrr, price, multiple, margin, age, added |
dir |
string | desc | Sort direction (asc or desc). |
per_page |
integer | 25 | Results per page (1-100). |
page |
integer | 1 | Page number (1-indexed). |
Response fields
Fields returned for each listing on /api/v1/listings. Some are only included on the single-listing endpoint.
| Field | Type | Nullable | Description |
|---|---|---|---|
| Identity | |||
id |
integer | No | Internal listing ID. |
source |
string | No | Source slug (flippa, empireflippers, …). |
source_id |
string | Yes | Source-side identifier when available. |
name |
string | No | Listing name as shown on the source. |
url |
string | Yes | URL of the listing on the source marketplace. |
website |
string | Yes | Real website URL when disclosed (mostly for TrustMRR / Flippa). |
type |
string | Yes | saas | ecom | site | agency | other |
industry |
string | Yes | Industry / niche (source-defined). |
language |
string | Yes | Listing language (mostly en, fr). |
| Pricing & financials | |||
asking_price |
integer | Yes | Asking price in the listing currency. |
currency |
string | Yes | USD | EUR | GBP |
monthly_revenue |
integer | Yes | MRR (where reported). |
annual_revenue |
integer | Yes | TTM revenue. |
annual_profit |
integer | Yes | Annual profit / SDE (premium brokers). |
profit_margin |
float | Yes | Profit margin in percent. |
multiple |
float | Yes | Asking price ÷ annual_profit when present, else ÷ annual_revenue. |
| Performance | |||
age_years |
integer | Yes | Years since the business was founded (source-reported). |
monthly_traffic |
integer | Yes | Monthly visitors (where reported). |
thumbnail_url |
string | Yes | Listing thumbnail URL. |
| Detail-only fields | |||
description |
string | Yes | Long description (single-listing endpoint only). |
total_revenue |
integer | Yes | All-time revenue (TrustMRR only). |
growth_30d |
float | Yes | 30-day growth rate (TrustMRR only). |
customers |
integer | Yes | Customer count (TrustMRR only). |
first_listed_at |
string (ISO 8601) | Yes | When the source first listed the business (TrustMRR only). |
| Timestamps | |||
created_at |
string (ISO 8601) | No | When catchbuys first imported the listing. |
updated_at |
string (ISO 8601) | No | Last refresh. |
multiple — computed against annual_profit when present (premium brokers like Quiet Light, Empire Flippers, Latonas, AcquisitionsDirect price against SDE / profit), otherwise against annual_revenue. This matches what the source displays and avoids the apples-to-oranges comparison you would get from a naive price ÷ revenue calc.
Pagination
Listing endpoints return a flat data array plus a meta object describing the page.
meta fields
total | Total number of matching rows. |
page | Current page (1-indexed). |
per_page | Items per page. |
last_page | Final page number. |
has_more | true if another page exists after this one. |
{
"data": [ ... ],
"meta": {
"total": 9384,
"page": 1,
"per_page": 25,
"last_page": 376,
"has_more": true
}
}
Code examples
Copy-paste starters in three languages. Replace YOUR_API_KEY with the token you generated at /api/access.
# List SaaS listings with $5k+ MRR, sorted by lowest multiple
curl "https://catchbuys.com/api/v1/listings?types[]=saas&mrr_min=5000&sort=multiple&dir=asc&per_page=10" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
# Get a single listing by ID
curl "https://catchbuys.com/api/v1/listings/12345" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
# Filter by industry and price range
curl "https://catchbuys.com/api/v1/listings?industries[]=Health&price_max=500000&age_min=3" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
# Multiple sources at once
curl "https://catchbuys.com/api/v1/listings?sources[]=flippa&sources[]=empireflippers&mrr_min=10000" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
const API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://catchbuys.com/api/v1';
// List listings with filters
const params = new URLSearchParams();
params.append('types[]', 'saas');
params.append('mrr_min', '5000');
params.append('per_page', '10');
const response = await fetch(`${BASE_URL}/listings?${params}`, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Accept': 'application/json'
}
});
const { data, meta } = await response.json();
console.log(`Found ${meta.total} listings`);
// Get a single listing
const listing = await fetch(`${BASE_URL}/listings/12345`, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Accept': 'application/json'
}
}).then(r => r.json());
import requests
API_KEY = 'YOUR_API_KEY'
BASE_URL = 'https://catchbuys.com/api/v1'
headers = {
'Authorization': f'Bearer {API_KEY}',
'Accept': 'application/json'
}
# List listings with filters
response = requests.get(f'{BASE_URL}/listings', headers=headers, params={
'types[]': 'saas',
'mrr_min': 5000,
'sort': 'multiple',
'dir': 'asc',
'per_page': 25
})
data = response.json()
print(f"Found {data['meta']['total']} listings")
for listing in data['data']:
print(f"{listing['name']} — MRR: {listing['monthly_revenue']} — Asking: {listing['asking_price']} ({listing['multiple']}x)")
# Get a single listing
listing = requests.get(f'{BASE_URL}/listings/12345', headers=headers).json()
Errors
Standard HTTP status codes. Errors return a JSON body with a message field.
| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Request succeeded. |
| 401 | Unauthenticated | Missing or invalid Bearer token. |
| 403 | Forbidden | Token does not have access to the resource. |
| 404 | Not found | Listing or endpoint does not exist. |
| 422 | Validation error | A query parameter is malformed. |
| 429 | Too many requests | Rate limit exceeded — wait for the window to reset. |
Example error bodies
// 401 Unauthenticated
{
"message": "Unauthenticated."
}
// 429 Too Many Requests
{
"message": "Too Many Attempts."
}
MCP server
catchbuys also exposes an MCP server so AI agents (Claude Desktop, Claude Code, Cursor, Windsurf, custom) can query the inventory in plain English. Same Bearer token as the REST API.
Once configured, an agent can answer prompts like:
Setup
Add this to your MCP client config.
Claude Code / Cursor (~/.claude.json or .cursor/mcp.json)
{
"mcpServers": {
"catchbuys": {
"type": "http",
"url": "https://catchbuys.com/mcp/catchbuys",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"catchbuys": {
"command": "npx",
"args": [
"mcp-remote",
"https://catchbuys.com/mcp/catchbuys",
"--header",
"Authorization: Bearer YOUR_API_KEY"
]
}
}
}
Claude Desktop requires Node.js for npx mcp-remote.
Troubleshooting
401 Unauthenticated
Your token is missing or invalid.
- Make sure the header format is exactly:
Authorization: Bearer YOUR_TOKEN(with "Bearer " prefix and a space). - Copy the full token including the number prefix and pipe character (e.g.
42|abc...). - Tokens are shown only once at creation. If lost, revoke and create a new one at /api/access.
429 Too Many Requests
You exceeded 60 requests/minute.
- Wait until the rate-limit window resets (max 60s) and retry.
- Use
per_page=100to pull 4× more rows per call. - Cache results client-side when scanning the entire catalogue.
MCP server disconnected
If your MCP client shows "Server disconnected" for catchbuys:
- Check that your token is still valid (create a new one if needed).
- Restart the MCP client completely (Cmd+Q on Mac for Claude Desktop, not just close the window).
- Verify your config file syntax — JSON parsers are unforgiving about trailing commas.
Still stuck? Drop us a line — include the failing request and the response body so we can reproduce.