logoPandorLabs
Lead Generation

Product Hunt Catalog

Every Product Hunt product — website, socials, company facts, launches, reviews, alternatives, awards and team.

product-hunt-productsproduct-huntlead-generationstartupsmakersreviews

20,000 rows in the dataset

Download the dataset

Get every row this scraper has collected as one CSV or JSONL file. If the data is older than maxAgeHours, the scraper is re-run with its configured settings first. The key needs the datasets:export scope.

DOWNLOAD_ID=$(curl -s -X POST "https://api.pandorlabs.com/v1/datasets/product-hunt-products/downloads" \  -H "Authorization: Bearer $PANDORLABS_KEY" \  -H "Content-Type: application/json" \  -d '{"maxAgeHours":24,"format":"csv"}' | jq -r '.id')while true; do  STATUS=$(curl -s "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID" \    -H "Authorization: Bearer $PANDORLABS_KEY" | jq -r '.status')  [ "$STATUS" != "refreshing" ] && break  sleep 5donecurl -s "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID/file" \  -H "Authorization: Bearer $PANDORLABS_KEY" \  -o dataset.csv

At its current 20,000 rows, this download costs 200 credits when the data is fresh enough, or 400 credits when it has to be refreshed first.

The rate is 1 credit per 100 rows cached and 2 credits live, charged on the rows in the dataset when the download becomes ready. A failed refresh is not charged. The file stays downloadable for 24 hours. See pricing.

Start a run

To choose the targets yourself, send the parameters below as input. The call returns 202 with the run; the key needs the scrapers:run scope and the account a positive credit balance.

curl -X POST "https://api.pandorlabs.com/v1/scrapers/product-hunt-products/runs" \  -H "Authorization: Bearer $PANDORLABS_KEY" \  -H "Content-Type: application/json" \  -d '{    "input": {      "mode": "backfill",      "syncAfterDays": 30,      "maxDiscoverPagesPerCategory": 50,      "followLinks": true    }  }'

A run is billed at the live rate once it finishes: 2 credits per 100 rows it collected. A run that collects nothing costs nothing.

Parameters

Every parameter is optional. Anything you leave out uses the default shown here; an unknown name or an invalid value is rejected with a 400 that lists each problem.

Mode

  • mode

    enum

    Mode

    backfill — walk every category listing into the product queue, then scrape the queue. discover — walk each category newest-first until the pages hold only known products, then scrape what is new. sync — rescrape products older than syncAfterDays, oldest first. drain — only scrape what is already queued. Every mode scrapes new products before stale ones.

    Default
    backfill
    Allowed values
    backfill
    discover
    sync
    drain
  • syncAfterDays

    number

    Resync after (days)

    In sync mode, products last scraped longer ago than this are rescraped.

    Default
    30
    Range
    1 to 3650
  • maxDiscoverPagesPerCategory

    number

    Max newest-first pages per category

    Discover mode stops a category after two pages with no unknown product, or after this many pages.

    Default
    50
    Range
    1 to 10000
  • followLinks

    boolean

    Queue linked products

    Queue products seen in alternatives, customers and makers' product lists. Catches products filed under no category, which no listing ever shows.

    Default
    true

Advanced

  • categoriesInParallel

    number

    Category listings in parallel

    Default
    8
    Range
    1 to 64
  • concurrency

    number

    Products in parallel

    Products scraped at once. Each one fetches its sections in parallel and its team four profiles at a time, all under maxInFlight.

    Default
    32
    Range
    1 to 128
  • maxInFlight

    number

    Max requests in flight

    Global cap on simultaneous requests to Product Hunt, whatever the concurrency. A 429 pauses every worker together.

    Default
    96
    Range
    1 to 256
  • requestDelayMs

    number

    Delay between requests (ms)

    Default
    0
    Range
    0 to 60000
  • maxConsecutiveBlocks

    number

    Abort after consecutive blocks

    A challenged request is retried after a pause; once Cloudflare refuses this many requests in a row the run stops and keeps what it collected. High enough to ride out an isolated challenge under heavy concurrency, low enough that a challenged IP stops within seconds.

    Default
    10
    Range
    1 to 100
  • maxOfflineMinutes

    number

    Abort after offline (minutes)

    Connection failures pause the crawl and retry with backoff instead of skipping work. If Product Hunt stays unreachable this long, the run stops cleanly so it can be resumed.

    Default
    30
    Range
    1 to 1440

Access

  • cookie

    string

    Cloudflare cookie

    Usually unnecessary. Cloudflare only challenges some IPs (typically VPN or datacenter ranges); from those, solve the challenge in a browser on the same IP and paste its cf_clearance (the bare value or a full Cookie header) with that browser's userAgent.

    Default
    none
    Example
    cf_clearance=…
  • userAgent

    string

    User-Agent

    Only matters with a cookie: use the User-Agent of the browser that produced it.

    Default
    Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:156.0) Gecko/20100101 Firefox/156.0
  • impersonation

    enum

    TLS profile

    Match the browser family of the User-Agent.

    Default
    firefox
    Allowed values
    firefox
    chrome
  • useProxy

    boolean

    Use the proxy pool

    Off by default: a rotating pool mixes IPs Cloudflare lets through with ones it challenges.

    Default
    false

Scope

  • categories

    string[]

    Category slugs

    Empty discovers every category from /categories, recursing into subcategories.

    Default
    none
    Example
    engineering-development
  • products

    string[]

    Product slugs

    Scrape exactly these products and skip category discovery.

    Default
    none
    Example
    vercel
  • includeSubcategories

    boolean

    Include subcategories

    Default
    true
  • featuredOnly

    boolean

    Featured products only

    The site defaults to featured products; off collects the full category listing.

    Default
    false
  • maxProducts

    number

    Max products per run

    Default
    1000000
    Range
    1 to 1000000
  • maxCategories

    number

    Max categories per run

    Default
    5000
    Range
    1 to 5000
  • maxPagesPerCategory

    number

    Max listing pages per category

    Twenty products per page; Product Hunt caps the page size.

    Default
    10000
    Range
    1 to 10000
  • refreshAfterHours

    number

    Refresh after (hours)

    How long a category listing checkpoint stays valid for backfill resumes, and, with rescrapeKnownMakers, how old a maker must be to be refetched.

    Default
    24
    Range
    0 to 8760
  • resumeCategories

    boolean

    Resume category listings

    Checkpoints each category listing in lead_gen_product_hunt_crawl_state. Within refreshAfterHours a rerun skips categories already walked to the end and restarts partial ones one page before where they stopped, instead of re-reading every listing from page 1.

    Default
    true

Depth

  • productSections

    string[]

    Product sections

    Empty fetches all of: layout, about, reviewSummary, reviews, launches, alternatives, customers, team, awards, embed.

    Default
    none
    Example
    layout, about, reviewSummary, reviews, launches, alternatives, customers, team, awards, embed
  • scrapeTeamProfiles

    boolean

    Scrape team member profiles

    Writes each maker to lead_gen_product_hunt_makers.

    Default
    true
  • rescrapeKnownMakers

    boolean

    Rescrape known makers

    Off: a maker already in lead_gen_product_hunt_makers is never fetched again, only linked to the new product. On: makers older than refreshAfterHours are fetched and refreshed in place.

    Default
    false
  • profileSections

    string[]

    Profile sections

    Empty fetches all of: layout, about, activity, upvotes, forums, collections, stacks, reviews.

    Default
    none
    Example
    layout, about, activity, upvotes, forums, collections, stacks, reviews
  • maxReviewsPerProduct

    number

    Max reviews per product

    Default
    1000
    Range
    0 to 20000
  • maxLaunchesPerProduct

    number

    Max launches per product

    Default
    500
    Range
    0 to 5000
  • maxAlternativesPerProduct

    number

    Max alternatives per product

    Default
    100
    Range
    0 to 2000
  • maxCustomersPerProduct

    number

    Max customers per product

    Default
    500
    Range
    0 to 5000
  • maxTeamPerProduct

    number

    Max team members per product

    Default
    500
    Range
    0 to 5000
  • maxAwardsPerProduct

    number

    Max awards per product

    Default
    500
    Range
    0 to 5000
Input as JSON Schema

The same schema the API validates against. Use it as a tool input schema or to generate a typed client.

inputSchema
{  "type": "object",  "additionalProperties": false,  "properties": {    "mode": {      "type": "string",      "enum": [        "backfill",        "discover",        "sync",        "drain"      ],      "title": "Mode",      "description": "backfill — walk every category listing into the product queue, then scrape the queue. discover — walk each category newest-first until the pages hold only known products, then scrape what is new. sync — rescrape products older than syncAfterDays, oldest first. drain — only scrape what is already queued. Every mode scrapes new products before stale ones.",      "default": "backfill"    },    "syncAfterDays": {      "type": "number",      "minimum": 1,      "maximum": 3650,      "title": "Resync after (days)",      "description": "In sync mode, products last scraped longer ago than this are rescraped.",      "default": 30    },    "maxDiscoverPagesPerCategory": {      "type": "number",      "minimum": 1,      "maximum": 10000,      "title": "Max newest-first pages per category",      "description": "Discover mode stops a category after two pages with no unknown product, or after this many pages.",      "default": 50    },    "followLinks": {      "type": "boolean",      "title": "Queue linked products",      "description": "Queue products seen in alternatives, customers and makers' product lists. Catches products filed under no category, which no listing ever shows.",      "default": true    },    "categoriesInParallel": {      "type": "number",      "minimum": 1,      "maximum": 64,      "title": "Category listings in parallel",      "default": 8    },    "cookie": {      "type": "string",      "maxLength": 2000,      "title": "Cloudflare cookie",      "description": "Usually unnecessary. Cloudflare only challenges some IPs (typically VPN or datacenter ranges); from those, solve the challenge in a browser on the same IP and paste its cf_clearance (the bare value or a full Cookie header) with that browser's userAgent.",      "examples": [        "cf_clearance=…"      ]    },    "userAgent": {      "type": "string",      "maxLength": 2000,      "title": "User-Agent",      "description": "Only matters with a cookie: use the User-Agent of the browser that produced it.",      "default": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:156.0) Gecko/20100101 Firefox/156.0"    },    "impersonation": {      "type": "string",      "enum": [        "firefox",        "chrome"      ],      "title": "TLS profile",      "description": "Match the browser family of the User-Agent.",      "default": "firefox"    },    "useProxy": {      "type": "boolean",      "title": "Use the proxy pool",      "description": "Off by default: a rotating pool mixes IPs Cloudflare lets through with ones it challenges.",      "default": false    },    "categories": {      "type": "array",      "items": {        "type": "string"      },      "maxItems": 1000,      "title": "Category slugs",      "description": "Empty discovers every category from /categories, recursing into subcategories.",      "examples": [        "engineering-development"      ]    },    "products": {      "type": "array",      "items": {        "type": "string"      },      "maxItems": 1000,      "title": "Product slugs",      "description": "Scrape exactly these products and skip category discovery.",      "examples": [        "vercel"      ]    },    "includeSubcategories": {      "type": "boolean",      "title": "Include subcategories",      "default": true    },    "featuredOnly": {      "type": "boolean",      "title": "Featured products only",      "description": "The site defaults to featured products; off collects the full category listing.",      "default": false    },    "maxProducts": {      "type": "number",      "minimum": 1,      "maximum": 1000000,      "title": "Max products per run",      "default": 1000000    },    "maxCategories": {      "type": "number",      "minimum": 1,      "maximum": 5000,      "title": "Max categories per run",      "default": 5000    },    "maxPagesPerCategory": {      "type": "number",      "minimum": 1,      "maximum": 10000,      "title": "Max listing pages per category",      "description": "Twenty products per page; Product Hunt caps the page size.",      "default": 10000    },    "refreshAfterHours": {      "type": "number",      "minimum": 0,      "maximum": 8760,      "title": "Refresh after (hours)",      "description": "How long a category listing checkpoint stays valid for backfill resumes, and, with rescrapeKnownMakers, how old a maker must be to be refetched.",      "default": 24    },    "resumeCategories": {      "type": "boolean",      "title": "Resume category listings",      "description": "Checkpoints each category listing in lead_gen_product_hunt_crawl_state. Within refreshAfterHours a rerun skips categories already walked to the end and restarts partial ones one page before where they stopped, instead of re-reading every listing from page 1.",      "default": true    },    "productSections": {      "type": "array",      "items": {        "type": "string"      },      "maxItems": 1000,      "title": "Product sections",      "description": "Empty fetches all of: layout, about, reviewSummary, reviews, launches, alternatives, customers, team, awards, embed.",      "examples": [        "layout, about, reviewSummary, reviews, launches, alternatives, customers, team, awards, embed"      ]    },    "scrapeTeamProfiles": {      "type": "boolean",      "title": "Scrape team member profiles",      "description": "Writes each maker to lead_gen_product_hunt_makers.",      "default": true    },    "rescrapeKnownMakers": {      "type": "boolean",      "title": "Rescrape known makers",      "description": "Off: a maker already in lead_gen_product_hunt_makers is never fetched again, only linked to the new product. On: makers older than refreshAfterHours are fetched and refreshed in place.",      "default": false    },    "profileSections": {      "type": "array",      "items": {        "type": "string"      },      "maxItems": 1000,      "title": "Profile sections",      "description": "Empty fetches all of: layout, about, activity, upvotes, forums, collections, stacks, reviews.",      "examples": [        "layout, about, activity, upvotes, forums, collections, stacks, reviews"      ]    },    "maxReviewsPerProduct": {      "type": "number",      "minimum": 0,      "maximum": 20000,      "title": "Max reviews per product",      "default": 1000    },    "maxLaunchesPerProduct": {      "type": "number",      "minimum": 0,      "maximum": 5000,      "title": "Max launches per product",      "default": 500    },    "maxAlternativesPerProduct": {      "type": "number",      "minimum": 0,      "maximum": 2000,      "title": "Max alternatives per product",      "default": 100    },    "maxCustomersPerProduct": {      "type": "number",      "minimum": 0,      "maximum": 5000,      "title": "Max customers per product",      "default": 500    },    "maxTeamPerProduct": {      "type": "number",      "minimum": 0,      "maximum": 5000,      "title": "Max team members per product",      "default": 500    },    "maxAwardsPerProduct": {      "type": "number",      "minimum": 0,      "maximum": 5000,      "title": "Max awards per product",      "default": 500    },    "concurrency": {      "type": "number",      "minimum": 1,      "maximum": 128,      "title": "Products in parallel",      "description": "Products scraped at once. Each one fetches its sections in parallel and its team four profiles at a time, all under maxInFlight.",      "default": 32    },    "maxInFlight": {      "type": "number",      "minimum": 1,      "maximum": 256,      "title": "Max requests in flight",      "description": "Global cap on simultaneous requests to Product Hunt, whatever the concurrency. A 429 pauses every worker together.",      "default": 96    },    "requestDelayMs": {      "type": "number",      "minimum": 0,      "maximum": 60000,      "title": "Delay between requests (ms)",      "default": 0    },    "maxConsecutiveBlocks": {      "type": "number",      "minimum": 1,      "maximum": 100,      "title": "Abort after consecutive blocks",      "description": "A challenged request is retried after a pause; once Cloudflare refuses this many requests in a row the run stops and keeps what it collected. High enough to ride out an isolated challenge under heavy concurrency, low enough that a challenged IP stops within seconds.",      "default": 10    },    "maxOfflineMinutes": {      "type": "number",      "minimum": 1,      "maximum": 1440,      "title": "Abort after offline (minutes)",      "description": "Connection failures pause the crawl and retry with backoff instead of skipping work. If Product Hunt stays unreachable this long, the run stops cleanly so it can be resumed.",      "default": 30    }  }}

Run, poll, read rows

The run is asynchronous. Poll it until it reaches a terminal status and its billing is paid, then page through its rows. Until then the rows endpoint answers 409 (still queued or running) or 402 (payment required).

RUN_ID=$(curl -s -X POST "https://api.pandorlabs.com/v1/scrapers/product-hunt-products/runs" \  -H "Authorization: Bearer $PANDORLABS_KEY" \  -H "Content-Type: application/json" \  -d '{    "input": {      "mode": "backfill",      "syncAfterDays": 30,      "maxDiscoverPagesPerCategory": 50,      "followLinks": true    }  }' | jq -r '.id')while true; do  RUN=$(curl -s "https://api.pandorlabs.com/v1/runs/$RUN_ID" \    -H "Authorization: Bearer $PANDORLABS_KEY")  STATUS=$(echo "$RUN" | jq -r '.status')  BILLING=$(echo "$RUN" | jq -r '.billing.status')  case "$STATUS" in queued|running) ;; *) [ "$BILLING" != "pending" ] && break ;; esac  sleep 5doneecho "$RUN" | jq '{status, rowCount, billing}'curl -s "https://api.pandorlabs.com/v1/runs/$RUN_ID/rows?page=1&limit=100" \  -H "Authorization: Bearer $PANDORLABS_KEY"

See the API reference for pagination, cancelling and listing runs.

Output fields

Rows have no fixed schema; these are the fields observed in the rows collected so far. Coverage is the share of rows where the field is present.

FieldTypeCoverage
aboutobject100%
alternativesarray100%
alternatives_overviewobject100%
awardsarray100%
categoriesarray100%
category_slugsarray100%
customersarray100%
descriptionstring100%
detailed_reviews_countnumber100%
discovered_in_categorystring83%
embedobject100%
followers_countnumber100%
is_no_longer_onlineboolean100%
is_top_productboolean100%
launchesarray100%
layoutobject100%
logo_urlstring100%
namestring100%
posts_countnumber100%
pro_con_tagsarray100%
product_idstring100%
review_summaryobject100%
reviewsarray100%
reviews_countnumber100%
reviews_ratingnumber100%
section_errorsarray100%
section_totalsobject100%
slugstring100%
social_linksobject67%
taglinestring100%
tagsarray100%
teamarray100%
team_usernamesarray100%
was_in_y_combinatorboolean100%
websitestring100%
website_domainstring100%
created_atdate100%
first_launched_atdate100%
first_seen_atdate100%
last_scraped_atdate100%
latest_launch_atdate100%
updated_atdate100%
_idstring100%

© 2026 PandorLabs LLC. All rights reserved.