# Package Health: Crates, Go, Maven, NuGet, Packagist, RubyGems (`leadharbor/package-registry-health`) Actor

Check packages on crates.io, Go modules, Maven Central, NuGet, Packagist and RubyGems in one schema: exists, latest version, release age, licence, repository, yanked or deprecated, plus readable flags.

- **URL**: https://apify.com/leadharbor/package-registry-health.md
- **Developed by:** [Mohammed Samaan](https://apify.com/leadharbor) (community)
- **Categories:** Developer tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.80 / 1,000 package checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Package Health: crates.io, Go, Maven, NuGet, Packagist, RubyGems

Give this actor a list of packages and it tells you, for each one, whether it exists, what the latest version is, when it was released, whether that version is yanked, deprecated or relocated, what licence and repository the registry lists, and how many downloads the registry reports (where it reports them). All six registries come back in the same JSON shape, so a CI job, a dependency review or an AI coding agent reads one format instead of six.

Typical uses:

- Check a dependency list before a release or an audit: which packages have not shipped in two years, which are abandoned, which have no licence listed.
- Let an AI agent confirm that a package name it is about to add really exists in the ecosystem (a missing package returns `exists: false`).
- Build a spreadsheet of licences and repositories for a Rust, Go, Java, .NET, PHP or Ruby project.

npm and PyPI are deliberately not covered.

### Sample input

```json
{
  "packages": [
    "crates:fastrand",
    "go:github.com/google/uuid",
    "maven:com.google.guava:guava",
    "nuget:Serilog",
    "packagist:swiftmailer/swiftmailer",
    "rubygems:rake"
  ],
  "contactEmail": ""
}
```

### Input

| Field | Type | Meaning |
|---|---|---|
| `packages` | array, 1 to 1000 | Each entry is `"ecosystem:name"` or an object `{"ecosystem": "...", "name": "..."}`. Maven also accepts `{"ecosystem": "maven", "groupId": "...", "artifactId": "..."}`. Ecosystems: `crates`, `go`, `maven`, `nuget`, `packagist`, `rubygems` (aliases such as `cargo`, `golang`, `composer`, `gem` work). |
| `contactEmail` | string, optional | Sent to the registries inside the User-Agent header (Packagist and crates.io ask API users to identify themselves). Leave empty to send only the actor name. |

Name forms: crates `serde`; Go module path `github.com/google/uuid`; Maven `groupId:artifactId` (no version, the latest is always checked); NuGet `Newtonsoft.Json`; Packagist `vendor/package` (lower-cased for you); RubyGems `rake`. Duplicates in one run are dropped and counted in the run summary, so you are not charged twice; names are compared the way each registry compares them (crates.io treats case, `-` and `_` as the same, so `serde_json` and `serde-json` are one crate; NuGet and Packagist ignore case; Go, Maven and RubyGems names are case sensitive). A bad entry (unknown ecosystem, malformed name) gets its own `invalid_input` row with the reason and does not stop the run. More than 1000 packages, or an empty list, fails the run with a message naming the field.

### Output

One dataset row per package. Every row has the same keys.

| Field | Meaning |
|---|---|
| `status` | `ok` (found), `not_found` (the registry says it does not exist), `error` (registry or network failure, nothing known), `invalid_input` |
| `exists` | `true`, `false`, or `null` when the answer is unknown (error rows) |
| `latestVersion`, `latestReleaseDate`, `daysSinceRelease` | See "What counts as latest" below. `daysSinceRelease` is counted from the run time. |
| `deprecatedOrYanked` | `true` / `false`, or `null` when the registry data needed to decide could not be read |
| `deprecationNote` | The registry's own message (yank message, deprecation text, replacement, relocation target) |
| `license` | The licence exactly as the registry gives it. `licenseSpdx` is filled only when that text is a valid SPDX identifier or expression from the list in `src/spdx-ids.js` (OSI-approved and FSF-libre identifiers), otherwise `null`. |
| `repositoryUrl`, `homepageUrl` | As listed by the registry |
| `downloads` | Total downloads when the registry reports them (crates.io, NuGet, Packagist, RubyGems). `null` for Go and Maven, which publish none. |
| `flags` | Readable rules, see below |
| `warnings` | Things you should know about this row (a licence taken from a parent POM, a pre-release marker, a missing optional lookup) |
| `source` | The registry URLs that were read |
| `error`, `errorKind` | Specific message for `error` and `invalid_input` rows, and a short kind: `timeout`, `network`, `server_error`, `rate_limited`, `http_error`, `bad_response`, `blocked`, `too_large`, `registry_unavailable` (the registry was skipped after repeated failures), `invalid_input`, `unexpected` |
| `position`, `input`, `checkedAt`, `schemaVersion` | 1-based place in your list, your entry as sent, UTC time, schema version (currently 1) |

Rows are written as each package finishes, so the order can differ from your list; sort by `position`.

#### Sample output (real run, 21 Oct 2026)

```json
{
  "schemaVersion": 1,
  "position": 15,
  "input": "packagist:swiftmailer/swiftmailer",
  "ecosystem": "packagist",
  "name": "swiftmailer/swiftmailer",
  "status": "ok",
  "exists": true,
  "latestVersion": "v6.3.0",
  "latestReleaseDate": "2021-10-18T15:26:12.000Z",
  "daysSinceRelease": 1828,
  "deprecatedOrYanked": true,
  "deprecationNote": "Abandoned. Suggested replacement: symfony/mailer",
  "license": "MIT",
  "licenseSpdx": "MIT",
  "repositoryUrl": "https://github.com/swiftmailer/swiftmailer",
  "homepageUrl": "https://swiftmailer.symfony.com",
  "downloads": 452698131,
  "flags": ["no-release-in-24-months", "latest-version-yanked-or-deprecated"],
  "warnings": [],
  "source": ["https://repo.packagist.org/p2/swiftmailer/swiftmailer.json", "https://packagist.org/packages/swiftmailer/swiftmailer.json"],
  "checkedAt": "2026-10-21T10:37:43.875Z",
  "error": null
}
```

### Flag rules

There is no health score. A flag is one plain rule applied to the row:

| Flag | Raised when |
|---|---|
| `no-release-in-24-months` | `daysSinceRelease` is greater than 730 |
| `latest-version-yanked-or-deprecated` | `deprecatedOrYanked` is `true`: crates.io yanked, Go `// Deprecated:` in go.mod, NuGet deprecation, Maven `<relocation>`, Packagist `abandoned`. RubyGems has no such marker, so `deprecatedOrYanked` is `null` there and it never raises this. |
| `license-missing` | The registry supports licence data and none is listed. Never raised for Go (the proxy serves no licence data). |
| `repository-missing` | The registry supports a repository link and none is listed. Never raised for Go, whose proxy omits the origin for older versions; for github.com, gitlab.com and bitbucket.org module paths the repository is inferred from the path (with a warning). |
| `no-listed-version` | The package exists but every version is unlisted (NuGet) or none is readable |

A quiet package can still be fine and a flagged one can still be fit for purpose; the flags only tell you where to look.

### What counts as latest

| Registry | `latestVersion` | `latestReleaseDate` | Notes |
|---|---|---|---|
| crates.io | `max_stable_version` (highest non-yanked stable), else `max_version` | `created_at` of that version | If the newest published version is yanked, a warning says so |
| Go | `@latest` from the module proxy | The proxy's `Time` | Licence and downloads are not available from the proxy. Retractions are not checked. |
| Maven Central | `<release>` in `maven-metadata.xml`; if that is a pre-release, the newest version without a pre-release qualifier | `Last-Modified` header of that version's POM on Central (close to upload time, not guaranteed identical), falling back to the metadata `lastUpdated` | Licence and repository come from the POM, or from up to three parent POMs when the artifact declares none (a warning says so). Several licences are joined with `; `. |
| NuGet | Highest listed non-pre-release version, else highest listed pre-release | `published` | Licence is `licenseExpression`; if the package only has a licence file or a licence URL, that URL is returned with a warning and `licenseSpdx` stays `null`. Repository comes from the nuspec. |
| Packagist | Highest stable version in the static `p2` file | `time` | `repositoryUrl` is the `source.url` listed there (often ending in `.git`). Downloads come from the small `/stats.json` endpoint. Several licences are joined with `OR` (Composer treats a list as alternatives) |
| RubyGems | `version` from the gem document (latest non-pre-release) | `version_created_at` | `repositoryUrl` is `source_code_uri` only; a homepage is not guessed to be a repository |

### Registry usage rules followed

| Registry | Policy read | Rule followed |
|---|---|---|
| crates.io | https://crates.io/data-access | The policy lists a sparse index (no rate limit) and a daily database dump first, and describes API use within 1 request per second and with a User-Agent that identifies the application and a way to contact the operator. The sparse index has no release dates, licences, repository links or download counts, which this actor reports, and the dump is a bulk download of everything that this actor does not use, so it reads the API: requests are 1.1 s apart, one at a time, with the User-Agent `package-registry-health/0.1 (Apify actor; +https://apify.com/leadharbor/package-registry-health)` plus your optional contact email. |
| Go modules | https://proxy.golang.org/ and https://go.dev/ref/mod#goproxy-protocol | No numeric limit is published. The actor keeps to about 5 requests per second, 4 at a time, and backs off on 429 and 5xx. |
| Maven Central | https://central.sonatype.org/faq/429-error/ and https://central.sonatype.org/consume/ | Consumption limits apply to very high-volume traffic and a 429 is a temporary block. The actor reads `repo1.maven.org/maven2` (the URL Central documents) with at most about 4 requests per second, 2 at a time, never retries harder after a 429, and stops using Maven Central for the rest of the run after 3 rate-limit answers in a row. |
| NuGet | https://learn.microsoft.com/en-us/nuget/api/overview | No numeric limit is published on the pages read. Service URLs are taken from `api.nuget.org/v3/index.json`; about 5 requests per second, 3 at a time. |
| Packagist | https://packagist.org/apidoc | Send a User-Agent with a contact (add your email in `contactEmail`), at most 10 concurrent requests, prefer the static `repo.packagist.org` files. Version data uses the static file; downloads use the small `stats.json` endpoint. 3 at a time, about 5 per second. |
| RubyGems | https://guides.rubygems.org/rubygems-org-rate-limits/ | API limit 10 requests per second per IP. The actor stays at about 5 per second, 3 at a time, and honours `Retry-After`. |

This actor reads public registry metadata only. It does not ask any provider for permission beyond their published API rules, and you are responsible for how you use the data you collect and for complying with each registry's terms.

Every request has a 15 second timeout. A 429, a 5xx, a timeout or a network error is retried up to 3 times with back-off (1 s, 2 s, 4 s, capped at 30 s). If a registry asks for a wait longer than 30 s, the item fails fast with a `rate_limited` error row instead of waiting. Redirects are followed by hand, at most 3 hops, and only to the same registry hosts: a redirect to any other host or address is refused before it is requested.

**Outages and run time.** If a registry fails 5 requests in a row (timeouts, 5xx, network errors) or answers 429 three times in a row, it is skipped for the rest of the run: its remaining packages become `error` rows with `errorKind: "registry_unavailable"`, are not charged, and the other registries keep going. The run also stops starting new packages after a 50 minute budget and says so in the status message and the `SUMMARY` record.

Because of the crates.io rule, a run with many crates is slow by design: N crates need at least 1.1 s times N (1000 crates is about 18 minutes). Other registries run in parallel with it.

### Pricing

Pay per event: **`package-checked`, $0.0008 per package** (price to be confirmed on the Store page when published).

| Charged | Not charged |
|---|---|
| `ok` rows and `not_found` rows (a definite answer, including "does not exist") | `error` rows (registry down, timeout, rate limited), `invalid_input` rows, duplicates |

Worked example: 250 packages, of which 240 exist, 6 do not exist and 4 hit a registry timeout: 246 charged x $0.0008 = $0.1968. Each row is charged first and delivered only if the charge was accepted, so the dataset never holds more definite answers than you paid for. If you set a maximum charge for the run and it is reached, the actor stops starting new packages and says so in the run status and the `SUMMARY` record (`charged` counts accepted charges only).

### Use with AI agents / MCP

Send `{"packages": ["crates:serde", "maven:com.google.guava:guava"]}`. Read `status` first. If it is `ok`, use `exists`, `latestVersion`, `deprecatedOrYanked` and `flags`. `exists: false` with `status: not_found` means the registry has no such package. Treat `status: error` as "unknown, retry later", never as "does not exist".

Memory: measured peak RSS was 123 MB (88 MB at start) for 30 live packages across all six registries on Node 24 on Windows; the actor is set to 512 MB (Node 20 on the Apify image was not measured separately).

### FAQ

**Is there a health score?** No. Only the flags above, each a rule you can read.

**Why is `license` sometimes a URL or a sentence?** It is shown as the registry gives it. `licenseSpdx` is filled only when the text is a valid SPDX identifier or expression from the bundled list.

**Why does `serde_json` and `serde-json` cost once?** They are the same crate on crates.io.

**Why are Go licences empty?** The Go proxy serves none. This actor does not scrape pkg.go.dev or GitHub.

**Does it read private packages?** No. Public registry endpoints only; no logins, cookies, proxies or browsers.

**How current is the data?** Read live from each registry at run time, except Packagist downloads (cached up to 12 hours by Packagist) and NuGet download counts (from the NuGet search index, which can lag).

### Limits

- Release dates for Maven are the POM file's `Last-Modified` header, an approximation of the upload time.
- Maven's latest is chosen by the metadata marker and a pre-release qualifier pattern (alpha, beta, rc, milestone, snapshot and similar); an unusual version scheme could be misread.
- Go retractions and pseudo-version edge cases are not analysed beyond the deprecation comment.
- Yanked versions other than the latest are not reported. RubyGems yanks are invisible because a yanked version disappears from the listing, which is why RubyGems rows have `deprecatedOrYanked: null`.
- Each ecosystem's own definition of "latest", "deprecated" and "licence" is used; the actor does not compare registries with each other.

### Related actors

More actors by the same developer: https://apify.com/leadharbor

### Open-source credits

| Component | Licence | Verified from |
|---|---|---|
| `apify` 3.7.2 (Apify SDK for JavaScript), the only direct dependency | Apache-2.0 | `node_modules/apify/LICENSE.md` (Apache License 2.0 text) |
| Transitive packages of the SDK (187 installed) | MIT 135, Apache-2.0 18, BSD-2-Clause 14, ISC 8, BSD-3-Clause 5, other permissive 5 (MIT OR CC0-1.0, BlueOak-1.0.0, 0BSD, MIT/Apache-2.0 dual), plus `caniuse-lite` (CC-BY-4.0 data, attribution only) and `map-stream` (no `license` field in package.json; its `LICENCE` file is the MIT text) | `license` field of each installed package.json, `node_modules/map-stream/LICENCE`, `node_modules/caniuse-lite/LICENSE` |
| SPDX identifier list in `src/spdx-ids.js` | Identifiers only, generated from SPDX License List v3.29.0 (https://spdx.org/licenses/) | `licenses.json` and `exceptions.json` of the SPDX license-list-data repository |

Everything else (HTTP client, rate limiter, adapters, POM and go.mod readers) is written for this actor with Node built-ins.

# Actor input Schema

## `packages` (type: `array`):

One entry per package, written as ecosystem:name. Ecosystems: crates, go, maven, nuget, packagist, rubygems. Go uses the module path (github.com/google/uuid), Maven uses groupId:artifactId (com.google.guava:guava), Packagist uses vendor/package. Through the API you can also send objects such as {"ecosystem": "maven", "groupId": "junit", "artifactId": "junit"}. Up to 1000 per run. npm and PyPI are not covered.

## `contactEmail` (type: `string`):

Registries such as Packagist and crates.io ask API users to identify themselves. If you fill this in, it is sent to the registries inside the User-Agent header so they can reach you about your traffic. Leave empty to send only the actor name.

## Actor input object example

```json
{
  "packages": [
    "crates:serde",
    "crates:tokio",
    "crates:fastrand",
    "go:github.com/google/uuid",
    "go:github.com/gin-gonic/gin",
    "go:github.com/golang/protobuf",
    "maven:com.google.guava:guava",
    "maven:com.google.code.gson:gson",
    "maven:junit:junit-dep",
    "nuget:Newtonsoft.Json",
    "nuget:Serilog",
    "nuget:WindowsAzure.Storage",
    "packagist:monolog/monolog",
    "packagist:symfony/console",
    "packagist:swiftmailer/swiftmailer",
    "rubygems:rake",
    "rubygems:rails",
    "rubygems:nokogiri"
  ]
}
```

# Actor output Schema

## `results` (type: `string`):

Table of the checked packages.

## `summary` (type: `string`):

Counts of found, not found, errors and charged events.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "packages": [
        "crates:serde",
        "crates:tokio",
        "crates:fastrand",
        "go:github.com/google/uuid",
        "go:github.com/gin-gonic/gin",
        "go:github.com/golang/protobuf",
        "maven:com.google.guava:guava",
        "maven:com.google.code.gson:gson",
        "maven:junit:junit-dep",
        "nuget:Newtonsoft.Json",
        "nuget:Serilog",
        "nuget:WindowsAzure.Storage",
        "packagist:monolog/monolog",
        "packagist:symfony/console",
        "packagist:swiftmailer/swiftmailer",
        "rubygems:rake",
        "rubygems:rails",
        "rubygems:nokogiri"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("leadharbor/package-registry-health").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "packages": [
        "crates:serde",
        "crates:tokio",
        "crates:fastrand",
        "go:github.com/google/uuid",
        "go:github.com/gin-gonic/gin",
        "go:github.com/golang/protobuf",
        "maven:com.google.guava:guava",
        "maven:com.google.code.gson:gson",
        "maven:junit:junit-dep",
        "nuget:Newtonsoft.Json",
        "nuget:Serilog",
        "nuget:WindowsAzure.Storage",
        "packagist:monolog/monolog",
        "packagist:symfony/console",
        "packagist:swiftmailer/swiftmailer",
        "rubygems:rake",
        "rubygems:rails",
        "rubygems:nokogiri",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("leadharbor/package-registry-health").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "packages": [
    "crates:serde",
    "crates:tokio",
    "crates:fastrand",
    "go:github.com/google/uuid",
    "go:github.com/gin-gonic/gin",
    "go:github.com/golang/protobuf",
    "maven:com.google.guava:guava",
    "maven:com.google.code.gson:gson",
    "maven:junit:junit-dep",
    "nuget:Newtonsoft.Json",
    "nuget:Serilog",
    "nuget:WindowsAzure.Storage",
    "packagist:monolog/monolog",
    "packagist:symfony/console",
    "packagist:swiftmailer/swiftmailer",
    "rubygems:rake",
    "rubygems:rails",
    "rubygems:nokogiri"
  ]
}' |
apify call leadharbor/package-registry-health --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,leadharbor/package-registry-health"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/AUOy6QyIIVbdxbtqk/builds/Eg2xrxShsrmPL3bXS/openapi.json
