Skip to main content
GuidesOctober 27, 2026 · 10 min read

How to Search GitHub Issues and Pull Requests With an API

Search GitHub issues and pull requests with Spidra Search, then export a clean CSV or JSON dataset with original links and metadata checks.

How to Search GitHub Issues and Pull Requests With an API

You can learn a lot about a developer tool from its public GitHub issues. Bug reports reveal where users struggle. Feature requests show what they wish the product could do. Pull requests offer clues about the changes contributors are proposing.

The problem is getting those discussions into a form you can actually analyze. Opening issues one by one works for a quick check. It falls apart when you're comparing several repositories, looking for repeated complaints, or building a developer intelligence product.

In this tutorial, you'll use an API to search GitHub issues and pull requests, export a clean dataset to CSV and JSON, and keep the original GitHub links. We'll use Spidra Search for discovery and compare it with GitHub's own API so you can choose the right approach.

Search GitHub issues with Spidra in one API call

Spidra Search supports a developer source for GitHub issues and pull requests. It accepts GitHub-style search qualifiers, rather than requiring you to run a generic web search filtered to github.com.

Install the Spidra Node.js SDK:

npm install spidra

Then set your Spidra API key as an environment variable and run this JavaScript in a .mjs file:

import { SpidraClient } from 'spidra';

const client = new SpidraClient({ apiKey: process.env.SPIDRA_API_KEY });
const result = await client.search({
  query: '"memory leak" is:issue repo:vercel/next.js',
  sources: ['developer'],
  limit: 10,
});

for (const item of result.data?.developer ?? []) {
  console.log(item.title, item.url);
}

That query looks for issues mentioning “memory leak” in the Next.js repository. sources: ['developer'] tells Spidra to search the GitHub-focused source. limit: 10 requests up to ten results from that source.

Spidra returns structured search hits, including fields such as titles, URLs, and descriptions. The presence of additional metadata depends on the result. A search hit is not necessarily a complete GitHub issue record, so we'll keep missing fields explicit in our export.

Filter GitHub issues and pull requests with qualifiers

GitHub's issue search syntax is particularly useful when your question is more specific than “find bugs.” These qualifiers work within the query string:

QualifierExampleUse
repo:repo:microsoft/playwrightRestrict results to one repository
is:issuetimeout is:issueReturn issues
is:prtimeout is:prReturn pull requests
is:openis:issue is:openFind open items
is:mergedis:pr is:mergedFind merged PRs
label:label:bugRestrict by GitHub label
in:titletimeout in:titleSearch titles
created:created:>=2026-01-01Find records created since a date
updated:updated:>=2026-01-01Find records updated since a date

For example:

timeout is:issue is:open repo:microsoft/playwright updated:>=2026-01-01

One subtle point: updated: does not mean the issue was opened recently. An old issue with a new comment can qualify. Use created: for new reports and updated: for recent activity. The GitHub search guide covers more qualifiers.

Choose two repositories and publish the exact queries

Let's investigate two common reliability topics: memory leaks in Next.js and timeouts in Playwright. We will search issues and PRs separately so the types stay visible in the dataset.

These are the exact four queries our exporter uses:

"memory leak" is:issue repo:vercel/next.js updated:>=2026-01-01
"memory leak" is:pr repo:vercel/next.js updated:>=2026-01-01
timeout is:issue repo:microsoft/playwright updated:>=2026-01-01
timeout is:pr repo:microsoft/playwright updated:>=2026-01-01

Each pair uses the same topic, repository, and time window. The only change is is:issue versus is:pr.

This is a bounded search experiment, not a count of all defects in either project. Matching a phrase doesn't prove a record is a confirmed bug or that a PR resolves it. Search rankings and the number of pages retrieved also affect the sample.

Build a Node.js exporter for GitHub issues and PRs

The final dataset should answer four questions: which repository does a record belong to, is it an issue or PR, what is it about, and where can someone verify it?

We'll also keep available author and date fields without assuming that a generic search date is GitHub's created_at timestamp.

Create a file named github-issues-prs-basic.mjs and paste in the script below. It needs Node.js 18+ and the spidra package. The example fetches at most two pages of 20 hits for each query, retaining raw responses and reporting skipped or unrecognized URLs.

// Node.js 18+  |  npm install spidra
// Run: SPIDRA_API_KEY=your_key node github-issues-prs-basic.mjs
import { SpidraClient } from 'spidra';
import { mkdir, writeFile } from 'node:fs/promises';

if (!process.env.SPIDRA_API_KEY) throw new Error('Set SPIDRA_API_KEY first.');
const spidra = new SpidraClient({ apiKey: process.env.SPIDRA_API_KEY });
const targets = [
  { repo: 'vercel/next.js', term: '"memory leak"' },
  { repo: 'microsoft/playwright', term: 'timeout' },
];
const found = new Map();
const raw = [];
const runs = [];
const skipped = [];

function githubIdentity(link) {
  try {
    const url = new URL(link);
    if (url.hostname.toLowerCase() !== 'github.com') return null;
    const m = url.pathname.match(/^\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)\/?$/);
    if (!m) return null;
    return {
      repository: `${m[1]}/${m[2]}`,
      number: Number(m[4]),
      kind: m[3] === 'pull' ? 'pr' : 'issue',
    };
  } catch { return null; }
}

function csvCell(value) {
  let text = value == null ? '' : Array.isArray(value) ? value.join('; ') : String(value);
  if (/^\s*[=+@-]/.test(text)) text = `'${text}`; // Prevent spreadsheet formula execution.
  return `"${text.replaceAll('"', '""')}"`;
}

for (const { repo, term } of targets) {
  for (const type of ['issue', 'pr']) {
    const query = `${term} is:${type} repo:${repo} updated:>=2026-01-01`;
    let nextToken;
    let returned = 0;
    let pages = 0;

    for (let page = 1; page <= 2; page++) {
      const response = await spidra.search({
        query, sources: ['developer'], limit: 20,
        ...(nextToken ? { pageTokens: { developer: nextToken } } : {}),
      });
      raw.push({ query, page, collected_at: new Date().toISOString(), response });
      const hits = response.data?.developer ?? [];
      pages++;
      returned += hits.length;

      for (const hit of hits) {
        const id = githubIdentity(hit.url);
        if (!id || id.repository.toLowerCase() !== repo.toLowerCase()) {
          skipped.push({ query, url: hit.url ?? null });
          continue;
        }
        const key = `${id.repository.toLowerCase()}/${id.kind}/${id.number}`;
        const previous = found.get(key);
        if (previous) {
          if (!previous.matched_queries.includes(query)) previous.matched_queries.push(query);
          continue;
        }
        found.set(key, {
          ...id,
          title: hit.title ?? null,
          original_url: hit.url,
          description: hit.description ?? null,
          author: Array.isArray(hit.authors) ? hit.authors.join('; ') : hit.authors ?? null,
          source_reported_date: hit.date ?? null,
          state: hit.state ?? null,
          labels: hit.labels ?? null,
          created_at: hit.created_at ?? null,
          updated_at: hit.updated_at ?? null,
          matched_queries: [query],
        });
      }
      nextToken = response.data?.nextPageTokens?.developer;
      if (!nextToken || hits.length === 0) break;
    }
    runs.push({ query, returned_hits: returned, pages_fetched: pages });
  }
}

const records = [...found.values()];
const columns = [
  'repository', 'number', 'kind', 'title', 'original_url', 'description',
  'author', 'source_reported_date', 'state', 'labels', 'created_at',
  'updated_at', 'matched_queries',
];
const report = {
  collected_at: new Date().toISOString(),
  unique_records: records.length,
  issues: records.filter(r => r.kind === 'issue').length,
  pull_requests: records.filter(r => r.kind === 'pr').length,
  missing_fields: Object.fromEntries(
    ['author', 'source_reported_date', 'state', 'labels', 'created_at', 'updated_at']
      .map(field => [field, records.filter(r => r[field] == null).length])
  ),
  skipped_results: skipped,
  queries: runs,
};
const csv = [columns.join(','), ...records.map(r =>
  columns.map(c => csvCell(r[c])).join(',')
)].join('\n') + '\n';
await mkdir('github-search-output', { recursive: true });
await Promise.all([
  writeFile('github-search-output/raw-spidra.json', JSON.stringify(raw, null, 2)),
  writeFile('github-search-output/github-activity.json', JSON.stringify(records, null, 2)),
  writeFile('github-search-output/github-activity.csv', csv),
  writeFile('github-search-output/report.json', JSON.stringify(report, null, 2)),
]);
console.log(report);

Run the script

On macOS or Linux:

export SPIDRA_API_KEY="your_spidra_api_key"
node github-issues-prs-basic.mjs

On Windows PowerShell:

$env:SPIDRA_API_KEY="your_spidra_api_key"
node github-issues-prs-basic.mjs

Never commit your API key to version control. Create an API key in the Spidra dashboard.

The script creates four files in github-search-output/:

FileContents
github-activity.csvClean rows for Sheets, Excel, or further analysis
github-activity.jsonStructured records with repository, type, source link, and available metadata
raw-spidra.jsonThe original responses, saved alongside their exact queries and collection times
report.jsonRetrieved counts, issue/PR mix, missing-field counts, and skipped results

Three implementation details are worth noting. The script classifies records using the actual URL path (/issues/123 or /pull/123), not just the requested qualifier. It deduplicates records by repository, type, and number. And it escapes CSV content so contributor-controlled titles cannot execute as spreadsheet formulas when the file is opened.

Spidra returns a per-source nextPageTokens value when a continuation is available. The script passes that opaque token back; it never tries to invent one. For larger collections, increase the page cap and add retries and monitoring appropriate to your application. See the Spidra pagination documentation.

What did we find? Separate verified records from API measurements

Here is a small set of public GitHub pages verified independently, useful for understanding why the original links belong in the dataset:

TypeGitHub recordWhat it illustrates
IssueNext.js #91484A Pages Router memory-leak report with reproduction details
PRNext.js #91581A proposed LRU cache change that references issue #91484
PRNext.js #95355Another proposed bounded-cache change referring to the same report
IssuePlaywright #42693A timeout report with a concrete MCP reproduction scenario

This hand-selected illustration includes two issues and two PRs. It is not the issue-to-PR ratio returned by Spidra, and the listed pages should not be mistaken for the raw Spidra response. That ratio must be calculated from report.json after running the queries with an API key.

When you run the exporter, inspect these three things before drawing conclusions:

  1. Issue/PR mix. Read issues, pull_requests, and the per-query returned_hits in report.json. Those are counts of retrieved matches, not necessarily all GitHub matches.
  2. Missing metadata. Check missing_fields for author, source_reported_date, state, labels, created_at, and updated_at. The script retains null when the source doesn't provide a field. Missing does not mean false.
  3. Raw results and original links. Open raw-spidra.json to see the complete response objects and follow a few original_url values to the source discussions. If an unknown URL was skipped, investigate it before discarding that result permanently.

This distinction matters. An article should not announce that “Spidra found 50 bugs” without a recorded query, timestamp, page limit, and evidence that those matches really were bug reports. A search result might be a feature request, a duplicate report, or a PR referencing the issue rather than a separate defect.

When is GitHub's official Search API better?

If you only need GitHub issues and PRs, GitHub's own API may be simpler. Its REST Search API provides the /search/issues endpoint, with more GitHub-specific fields.

Here is the equivalent GitHub query. Use the same search terms and qualifiers to make any comparison meaningful:

curl --get 'https://api.github.com/search/issues' \
  -H 'Accept: application/vnd.github+json' \
  -H 'X-GitHub-Api-Version: 2026-03-10' \
  --data-urlencode 'q="memory leak" is:issue repo:vercel/next.js updated:>=2026-01-01' \
  --data-urlencode 'per_page=20'

GitHub returns an items array along with total_count and incomplete_results. Search items can expose state, labels, comment counts, and creation/update timestamps. Pull requests are included in this endpoint and can be distinguished using the presence of the pull_request property.

RequirementSpidra developer searchGitHub API
Find public issues and PRsYesYes
Return source links and descriptionsYesYes
Fetch full GitHub-specific lifecycle fieldsNot guaranteed in search hitsUse the relevant GitHub endpoints
Search other source categories with one platformYes; web, news, research, and moreNo; focused on GitHub
Work with private repositories and GitHub write operationsNot documented for this search flowSupported with appropriate permissions

Don't compare unlike numbers. GitHub's total_count describes matches reported for its query. Our report.json measures the records actually retrieved within the configured page cap. Also, GitHub search responses can be incomplete, and the REST API exposes no more than 1,000 search results per query. GitHub applies separate search rate limits, which should shape any high-volume collection plan.

GitHub expanded issue search in April 2026 with semantic and hybrid modes through search_type=semantic and search_type=hybrid. Those are features of GitHub's official API, not documented options of Spidra's developer source. For exact or reproducible phrase matching, stick to a clearly documented query style and record any search mode used.

If your product needs PR review details, lifecycle events, authoritative issue state, or private-repository access, start with GitHub's API. If it needs to discover relevant GitHub discussions alongside web pages, research papers, and news, Spidra can make the discovery step more consistent. A practical combination is to find links with Spidra, then enrich selected records using GitHub's API.

Use the dataset for developer intelligence, not a bug leaderboard

A clean export can support several useful workflows. A developer-tool founder can review feature requests in competing products. A research team can identify recurring complaints and investigate whether work has been proposed to address them. An engineering team can collect links for triage, clustering, or human review.

Keep the interpretation modest: an issue isn't automatically a confirmed defect, a PR isn't necessarily merged, and neither proves a fix has shipped. Larger projects also generate more activity. Use the dataset to locate questions worth investigating, not to rank engineering quality by raw counts.

Conclusion

The useful part of a GitHub issues API isn't just finding matching URLs. It's turning those matches into a traceable dataset: exact search queries, original links, consistent issue/PR classification, raw responses, and visible missing fields.

Spidra Search provides a GitHub-focused developer source and a path to broader multi-source discovery. GitHub's own API remains the straightforward choice when your requirements are entirely GitHub-specific or demand richer repository metadata.

Start with the exporter, inspect its report and source links, and build your analysis on the records you can verify. To explore the developer search source, see the Spidra Search documentation.

Share this article

You build the product.
Spidra handles the data work.

Start with 300 free credits, about 300 pages. No credit card required.