Developers

API Reference

A free, public, read-only API over PainSignal’s corpus. Every endpoint below is a plain GET with no authentication. The machine-readable version of this page is at /openapi.json.

Conventions

Response envelope
Every JSON endpoint returns { data, meta, error }. On success error is null; on failure data is null and error is a human-readable string. Bulk export is the sole exception and returns raw CSV or JSONL.
Pagination
Offset-based via page and limit, with the total in meta.total. Both are clamped server-side, so an out-of-range value is corrected rather than rejected.
Taxonomy values
industry and problem_category match exactly and are case-sensitive. They are generated by an LLM and consolidated nightly, so treat them as data to be fetched from /api/v1/industries rather than as a fixed enum you can hard-code.
Rate limiting
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and RateLimit-Policy. A 429 adds Retry-After. Send a free API key as X-API-Key or Authorization: Bearer to move up a tier.
Field meanings
severity_score is 1–5, opportunity_score is 0–100, and willingness_to_pay is one of none, implied, explicit. Full definitions are in the field dictionary.

Data

GET/api/v1/signals

List problems

Individual reported problems, each with an AI-generated app brief describing what could be built.

This is the finest-grained record type. One row is one person describing one problem. `keyword` runs full-text search over the problem statement, industry, category, gap description and app-brief title; results are then ordered by relevance rather than by `sort`.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

ParameterTypeDescription
pageinteger1-indexed page number.Range 1–1000. Default 1.
limitintegerRows per page.Range 1–100. Default 20.
keywordstringFull-text search. Supports quoted phrases and -negation. Max 200 characters.
industrystringExact industry name as returned by /api/v1/industries. Case-sensitive.
problem_categorystringExact category name as returned by /api/v1/categories.
min_severityintegerOnly problems rated at least this severe.Range 1–5.
sortstringOrdering. Ignored when `keyword` is present.One of: `created_at`, `severity`, `vote_score`, `industry`Default created_at.

Example request

curl -s 'https://painsignal.net/api/v1/signals?keyword=scheduling%20double%20booking&industry=Plumbing' \
  | jq .

Example response

{
  "data": [
    {
      "id": "6f2e9c1a-0b44-4d8e-9a77-17e5f0aa2b31",
      "slug": "shiftsync-rn-17e5f0aa",
      "painStatement": "Scheduling staff across three locations means rebuilding the same rota in a spreadsheet every week, and shift swaps get lost in group texts.",
      "industry": "Health, Wellness, and Beauty",
      "jobRole": "Owner",
      "problemCategory": "Staffing & Scheduling",
      "severityScore": 4,
      "gapDescription": "No affordable tool handles multi-location rotas with a swap-request workflow.",
      "willingnessToPay": "implied",
      "existingSolutionsMentioned": [
        "Excel",
        "WhatsApp"
      ],
      "solutionTitle": "ShiftSync",
      "solutionDescription": "Multi-location staff scheduling with self-serve shift swaps.",
      "solutionFeatures": [
        "Drag-and-drop rota builder",
        "Swap requests with manager approval"
      ],
      "solutionTargetUsers": "Owners of 2-10 location service businesses",
      "solutionRevenueModel": "Per-location monthly subscription",
      "confidenceScore": 0.88,
      "voteScore": 3,
      "clusterId": "b1c0d3e4-5f6a-4b7c-8d9e-0a1b2c3d4e5f",
      "createdAt": "2026-06-14T09:12:44.031Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 23367
  },
  "error": null
}
GET/api/v1/signals/{id}

Get one problem

A single problem by UUID or slug, including its full app brief.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

ParameterTypeDescription
idrequiredstringpathThe problem's UUID or its URL slug.

Example request

curl -s 'https://painsignal.net/api/v1/signals/shiftsync-rn-17e5f0aa' \
  | jq .

Example response

{
  "data": {
    "id": "6f2e9c1a-0b44-4d8e-9a77-17e5f0aa2b31",
    "painStatement": "…"
  },
  "meta": null,
  "error": null
}
GET/api/v1/clusters

List opportunities

Problems grouped into buildable product themes, with build complexity, revenue potential and competition analysis.

An opportunity is a cluster of semantically similar problems. Clusters with two or more members and a generated app brief are the ones surfaced as app ideas; the rest are reachable but not promoted. `build_complexity` and `revenue_potential` are 1-5 LLM judgements, present only on analysed clusters.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

ParameterTypeDescription
pageinteger1-indexed page number.Range 1–1000. Default 1.
limitintegerRows per page.Range 1–100. Default 20.
industrystringExact industry name as returned by /api/v1/industries. Case-sensitive.
problem_categorystringExact category name.
sortstringOrdering.One of: `score`, `signal_count`, `severity`, `newest`, `votes`Default score.

Example request

curl -s 'https://painsignal.net/api/v1/clusters?industry=Plumbing' \
  | jq .

Example response

{
  "data": [
    {
      "id": "b1c0d3e4-5f6a-4b7c-8d9e-0a1b2c3d4e5f",
      "slug": "multi-location-staff-scheduling-b1c0d3e4",
      "title": "Multi-location staff scheduling",
      "description": "Owners of small multi-site service businesses rebuild rotas by hand every week.",
      "primaryIndustry": "Health, Wellness, and Beauty",
      "problemCategory": "Staffing & Scheduling",
      "signalCount": 14,
      "avgSeverity": 3.9,
      "opportunityScore": 71,
      "buildComplexity": 3,
      "revenuePotential": 4,
      "competitionLevel": "medium",
      "trendDirection": "rising",
      "appBriefTitle": "ShiftSync",
      "industriesAffected": {
        "Health, Wellness, and Beauty": 9,
        "Restaurants": 5
      }
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 11033
  },
  "error": null
}
GET/api/v1/clusters/{id}

Get one opportunity

A single opportunity by UUID or slug, including its member problems.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

ParameterTypeDescription
idrequiredstringpathThe opportunity's UUID or its URL slug.

Example request

curl -s 'https://painsignal.net/api/v1/clusters/ID' \
  | jq .

Example response

{
  "data": {
    "id": "b1c0d3e4-…",
    "signals": []
  },
  "meta": null,
  "error": null
}

Taxonomy

GET/api/v1/industries

List industries

The industry taxonomy with problem counts.

The taxonomy is not pre-built. An LLM assigns an industry per problem and a nightly job merges near-duplicates, so this list grows and consolidates over time. Treat the names as data, not as a fixed enum.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

No parameters.

Example request

curl -s 'https://painsignal.net/api/v1/industries' \
  | jq .

Example response

{
  "data": [
    {
      "id": "…",
      "name": "Plumbing",
      "displayName": "Plumbing",
      "signalCount": 968,
      "clusterCount": 421
    }
  ],
  "meta": null,
  "error": null
}
GET/api/v1/categories

List problem categories

The problem-category taxonomy with counts.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

No parameters.

Example request

curl -s 'https://painsignal.net/api/v1/categories' \
  | jq .

Example response

{
  "data": [
    {
      "id": "…",
      "name": "Scheduling & Booking",
      "signalCount": 263
    }
  ],
  "meta": null,
  "error": null
}

Analytics

GET/api/v1/trends

Trending opportunities

Opportunities whose report volume is accelerating.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

ParameterTypeDescription
limitintegerRows to return.Range 1–50. Default 20.

Example request

curl -s 'https://painsignal.net/api/v1/trends' \
  | jq .

Example response

{
  "data": [
    {
      "id": "…",
      "title": "…",
      "trendDirection": "rising",
      "trendVelocity": 2.4
    }
  ],
  "meta": null,
  "error": null
}
GET/api/v1/analytics

Market analytics

Corpus-wide aggregates: totals, severity, willingness-to-pay economics and industry/category league tables.

Everything the /analytics dashboard renders, as JSON. Cached for an hour.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

ParameterTypeDescription
includestringComma-separated sections. Omit for all of them.One of: `overview`, `industries`, `categories`, `wtp`, `matrix`, `shift`

Example request

curl -s 'https://painsignal.net/api/v1/analytics?include=overview%2Cindustries' \
  | jq .

Example response

{
  "data": {
    "overview": {
      "problems": 23367,
      "opportunities": 11033,
      "industries": 87,
      "categories": 415,
      "avgSeverity": 3.14,
      "wtpExplicit": 867,
      "withNamedIncumbent": 10339
    },
    "industries": [
      {
        "name": "Automotive Repair",
        "slug": "automotive-repair",
        "problems": 1757,
        "avgSeverity": 3.22,
        "wtpExplicitPct": 4
      }
    ]
  },
  "meta": null,
  "error": null
}
GET/api/v1/analytics/timeseries

Problem volume over time

Report volume bucketed by month, quarter or year, optionally filtered by industry or category.

The time axis is when the problem was REPORTED (the source post's date), not when it was classified — so this describes the market, not our ingestion schedule. The most recent bucket is always partial and should be dropped when drawing a trend line.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

ParameterTypeDescription
bucketstringBucket size.One of: `month`, `quarter`, `year`Default month.
industrystringExact industry name as returned by /api/v1/industries. Case-sensitive.
categorystringExact category name.
sinceintegerFirst year to include.Minimum 2000. Default 2015.

Example request

curl -s 'https://painsignal.net/api/v1/analytics/timeseries?industry=Plumbing' \
  | jq .

Example response

{
  "data": [
    {
      "period": "2026-01-01",
      "problems": 1842,
      "avgSeverity": 3.11
    },
    {
      "period": "2026-02-01",
      "problems": 2104,
      "avgSeverity": 3.18
    }
  ],
  "meta": null,
  "error": null
}
GET/api/v1/analytics/tools

Tool Gap Index

Incumbent tools ranked by how often people describe them failing, with the industries where each hurts most.

Built from the tool each worker named when describing what they currently use. Raw names are normalised — QuickBooks Online, QBO and QuickBooks Desktop fold into one entry. Pass `slug` for the full detail of one tool, including the aggregated software gaps and manual workarounds people described.

Rate limit: 60/min anonymous, 600/min with an API key

Parameters

ParameterTypeDescription
slugstringReturn full detail for one tool instead of the index.
limitintegerRows in the index.Range 1–250. Default 100.

Example request

curl -s 'https://painsignal.net/api/v1/analytics/tools?slug=quickbooks' \
  | jq .

Example response

{
  "data": [
    {
      "name": "QuickBooks",
      "slug": "quickbooks",
      "kind": "product",
      "mentions": 493,
      "avgSeverity": 3.22,
      "wtpExplicit": 31,
      "topIndustries": [
        {
          "name": "Accounting & Bookkeeping",
          "count": 214
        }
      ]
    }
  ],
  "meta": null,
  "error": null
}

Search

Bulk

GET/api/v1/export

Bulk export

Stream the corpus as CSV or JSONL for offline analysis, filtered the same way as the list endpoints.

Anonymous callers get up to 1,000 rows per request. A free API key removes that cap, so a full-corpus pull is one request. The response streams, so it starts immediately and never holds the whole result set in memory.

Rate limit: 5/hour anonymous, 60/hour with an API key

Parameters

ParameterTypeDescription
typestringWhich record type to export.One of: `problems`, `opportunities`Default problems.
formatstringOutput format.One of: `csv`, `jsonl`Default csv.
industrystringExact industry name as returned by /api/v1/industries. Case-sensitive.
categorystringExact category name.
min_severityintegerOnly rows at least this severe.Range 1–5.
limitintegerMax rows. Capped at 1,000 without an API key, 50,000 with one.Range 1–50000.

Example request

curl -s 'https://painsignal.net/api/v1/export?industry=Plumbing'

Example response

id,slug,pain_statement,industry,job_role,problem_category,severity_score,…

Tools

POST/api/v1/spec-builder

Build spec generator

Turn a problem or opportunity into a build-ready technical spec: user stories, data model, API surface, stack and a phased MVP.

Body takes either `{ id, type }` — where `id` is a problem or opportunity slug or UUID and `type` is `problem` or `opportunity` — or `{ idea }` for free text. Specs built from a stored record are grounded in the actual reported gaps and workarounds; free-text specs are grounded only in what you wrote, and the model is instructed to flag its own assumptions. This is a live LLM generation, hence the hourly limit.

Rate limit: 5/hour anonymous, 30/hour with an API key

Parameters

ParameterTypeDescription
idstringbodyProblem or opportunity slug/UUID. Mutually exclusive with `idea`.
typestringbodyWhich record `id` refers to.One of: `problem`, `opportunity`Default opportunity.
ideastringbodyFree-text description, 20–2000 characters. Used when `id` is absent.

Example request

curl -s -X POST 'https://painsignal.net/api/v1/spec-builder' \
  -H 'Content-Type: application/json' \
  -d '{"id":"multi-location-staff-scheduling-b1c0d3e4"}' \
  | jq .

Example response

{
  "data": {
    "spec": {
      "summary": "Multi-location rota management with self-serve shift swaps.",
      "mvp_scope": {
        "estimate": "3 weeks",
        "in": [
          "Drag-and-drop rota builder"
        ]
      },
      "data_model": [
        {
          "entity": "shift",
          "fields": [
            "id uuid",
            "location_id uuid",
            "starts_at timestamptz"
          ]
        }
      ]
    },
    "source": {
      "title": "ShiftSync",
      "url": "https://painsignal.net/opportunity/…"
    },
    "model": "deepseek-v4-pro"
  },
  "meta": null,
  "error": null
}
POST/api/v1/match

Match opportunities to your skills

Rank opportunities by semantic similarity to a free-text description of what you can build.

Body takes `{ skills, limit?, maxComplexity? }`. Matches against opportunity centroids rather than individual problems, and only returns clusters with two or more independent reports — so a result is a theme, not one person's bad day. Describe the problem domain rather than your stack: the corpus is indexed by what is broken in someone's job, and nobody complaining about their rota mentions React. One embedding per call, no LLM generation.

Rate limit: 10/min anonymous, 60/min with an API key

Parameters

ParameterTypeDescription
skillsrequiredstringbodyWhat you can build and what interests you. At least 15 characters.
limitintegerbodyResults to return.Range 1–30. Default 12.
maxComplexityintegerbodyOnly opportunities at or below this build complexity. Restricts results to analysed clusters, which are a subset of the corpus.Range 1–5.

Example request

curl -s -X POST 'https://painsignal.net/api/v1/match' \
  -H 'Content-Type: application/json' \
  -d '{"skills":"Next.js and Postgres, interested in scheduling and logistics"}' \
  | jq .

Example response

{
  "data": [
    {
      "id": "b1c0d3e4-…",
      "title": "ShiftSync",
      "industry": "Health, Wellness, and Beauty",
      "signalCount": 14,
      "avgSeverity": 3.9,
      "buildComplexity": 3,
      "competitionLevel": "medium",
      "similarity": 0.72,
      "url": "https://painsignal.net/opportunity/multi-location-staff-scheduling-b1c0d3e4"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 12,
    "total": 12
  },
  "error": null
}

Need higher limits?

Keys are free and self-issued. If your use case needs more than the keyed tier allows, just ask.

Create a key

Join the beta — full access for the first 1,000 builders

Join Beta