REST API
One JSON endpoint for pipelines, scripts and tools. Send a package.json, get back a diff, the complete new file, and an explanation for every package.
Overview
- Base URL:
https://api.ripedeps.com. The paths are fixed:POST /plan,GET /stats,GET /healthz. - Every response, errors included, is
application/jsonwith documented fields. - The API has no version in its URLs. Validate responses against the published JSON Schema and ignore fields you do not know.
- Calls wait for the finished plan. A cold cache can take tens of seconds; the server answers
504after 60 seconds, and a retry is much faster. - No authentication, no API keys. Requests are rate limited.
Privacy. Uploaded package.json files and the resulting recommendations are stored for analysis and for improving the algorithm. Do not send files that contain secrets. Details.

Quick start
One line, nothing to prepare: plan a tiny package.json that contains only zod, holding new releases back for 7 days, and print the diff:
curl -s https://api.ripedeps.com/plan -H 'content-type: application/json' -d '{"packageJson":"{\"dependencies\":{\"zod\":\"^3.22.0\"}}","minAgeDays":7,"skipAdoptConfig":true}' | jq -r .diffNow with your own file: plan the package.json in the current directory and print the diff:
jq -n --rawfile pj package.json '{ packageJson: $pj, minAgeDays: 7 }' \
| curl -s -X POST https://api.ripedeps.com/plan \
-H 'content-type: application/json' --data-binary @- \
| jq -r .diffApply it with patch -p1:
jq -n --rawfile pj package.json '{ packageJson: $pj, minAgeDays: 7 }' \
| curl -s -X POST https://api.ripedeps.com/plan \
-H 'content-type: application/json' --data-binary @- \
| jq -r .diff \
| patch -p1Request
POST https://api.ripedeps.com/plan with Content-Type: application/json. Send exactly one of packageJson andpackageJsonUrl. Unknown keys are rejected.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
packageJson | string | one of the two | The full text of package.json. Formatting is preserved in the diff. Up to 256 KB. |
packageJsonUrl | string (https URL) | one of the two | The raw URL of a package.json. The service downloads it. See URL input below. |
focus | string[] | empty (everything) | Exact names or * globs. A bare name like trpc also matches @trpc/*. Up to 50 entries. |
minAgeDays | integer 0–365 | 0, or the file’s ripedeps entry | Minimum age of a version since its release. 7 is a good start. |
maxBump | patch | minor | major | minor, or the file’s ripedeps entry | The highest allowed step, measured from the version in your file. |
skipAdoptConfig | boolean | false | true: do not write the effective settings into the output as a ripedeps entry. |
Where the settings come from
For minAgeDays and maxBump the order is: your request parameter, then the ripedeps entry of the file, then the default. Only parameters you actually send count as explicit, so a file that already carries its settings keeps working without any parameters. The response tells you the source of each value in meta.settings.
"ripedeps": { "minAgeDays": 7, "maxBump": "minor" }focus is never read from, or written to, the file. By default the output carries a ripedeps entry with the effective settings; send "skipAdoptConfig": true to leave your file's entry untouched.
Request examples
Inline file
{
"packageJson": "{\n \"dependencies\": { \"zod\": \"^3.22.0\" }\n}\n",
"minAgeDays": 7,
"maxBump": "minor"
}From a URL
The service downloads the file. Use the raw file URL (HTML pages are rejected). Only https on port 443 is accepted, no credentials in the URL, at most 3 redirects, 10 seconds, and the same size limit as inline files. Addresses in private or reserved ranges are blocked, including after DNS resolution and redirects.
{
"packageJsonUrl": "https://raw.githubusercontent.com/owner/repo/main/package.json",
"minAgeDays": 7
}Upgrade only a vulnerable package (focus)
You do not have to upgrade everything. Name the package, or a family, and only it moves, plus the packages its newest versions point to. Everything else comes back as outside-focus and stays untouched. Ripedeps does not know which packages are vulnerable, so you name them. If you need the fix today, sendminAgeDays: 0. skipAdoptConfig: true keeps the rest of your file, including theripedeps entry, exactly as it is.
{ "packageJson": "…", "focus": ["axios"], "minAgeDays": 0, "skipAdoptConfig": true }jq -n --rawfile pj package.json '{ packageJson: $pj, focus: ["axios"], minAgeDays: 0, skipAdoptConfig: true }' \
| curl -s -X POST https://api.ripedeps.com/plan -H 'content-type: application/json' --data-binary @- \
| jq -r .diffSeveral packages and families work the same way:"focus": ["@trpc/*", "react"].
Allow bigger steps, keep your file untouched
{ "packageJson": "…", "maxBump": "major", "minAgeDays": 14, "skipAdoptConfig": true }The sample file used in the responses below
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"axios": "^1.6.0",
"express": "^4.18.2",
"lodash": "^4.17.20",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"eslint": "^9.10.0",
"prettier": "^3.1.0",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=20"
}
}Response
200 with the plan. The body is always the full plan; there is no other format. Responses carry Cache-Control: no-store.
| Field | Meaning |
|---|---|
diff | Unified diff with a/package.json and b/package.json headers. patch -p1 applies it. Empty when nothing changes. |
updatedPackageJson | The complete new file, formatted like your input. |
packages | One entry per root: ranges, versions, step, status, release time, reasons and an explanation sentence. |
warnings | Things you should know: duplicates, unavoidable constraint violations with a hint, unknown keys in the ripedeps entry. |
notes | Changes to the output that are not upgrades, for example that the ripedeps entry was added or updated. |
meta | The settings used and where each came from (request, file, default), the input source and size, whether the solution is exact, the number of independent groups, and stale when expired package data was served. |
A real response (shortened) for a file with next and react, 7-day gate, minor steps:
{
"diff": "--- a/package.json\n+++ b/package.json\n…",
"meta": {
"components": 3,
"exact": true,
"input": {
"bytes": 119,
"source": "inline"
},
"settings": {
"maxBump": {
"source": "request",
"value": "minor"
},
"minAgeDays": {
"source": "request",
"value": 7
}
},
"stale": false
},
"notes": [],
"packages": [
{
"bestVersion": "15.5.28",
"explanation": "`next` 15.0.0 -> 15.5.28 (minor step, 9 days old, age gate 7); newest version that qualifies; not taken: `next@16.4.3` is a major step from 15.0.0, above the allowed minor level",
"name": "next",
"newRange": "^15.5.28",
"newVersion": "15.5.28",
"oldRange": "^15.0.0",
"oldVersion": "15.0.0",
"publishedAt": "2026-10-14T17:36:08.917Z",
"reasons": [
{
"kind": "level-limit",
"message": "`next@16.4.3` is a major step from 15.0.0, above the allowed minor level",
"version": "16.4.3"
}
],
"section": "dependencies",
"status": "upgrade",
"step": "minor"
},
{
"bestVersion": "18.3.1",
"explanation": "`react` held at 18.3.1; not taken: `react@19.3.1` is a major step from 18.3.1, above the allowed minor level",
"name": "react",
"newRange": "^18.3.1",
"newVersion": "18.3.1",
"oldRange": "^18.3.1",
"oldVersion": "18.3.1",
"publishedAt": "2024-04-26T16:42:26.496Z",
"reasons": [
{
"kind": "level-limit",
"message": "`react@19.3.1` is a major step from 18.3.1, above the allowed minor level",
"version": "19.3.1"
}
],
"section": "dependencies",
"status": "held",
"step": "none"
}
],
"updatedPackageJson": "{\n \"name\": \"web\",\n \"dependencies\": {\n \"next\": \"^15.5.28\",\n \"react\": \"^18.3.1\",\n \"react-dom\": \"^18.3.1\"\n }\n}\n",
"warnings": []
}The notes of a request that adopts the settings:
[
"The `ripedeps` entry was added (minAgeDays 7, maxBump minor) so later runs use the same limits"
]The packages list
Each entry is the decision for one package (status, the chosen version and bestVersion) together with its explanation (explanation and reasons[]). The ranking that led to the decision is not a response field. See how it works. Real output:
{
"bestVersion": "15.5.28",
"explanation": "`next` 15.0.0 -> 15.5.28 (minor step, 9 days old, age gate 7); newest version that qualifies; not taken: `next@16.4.3` is a major step from 15.0.0, above the allowed minor level",
"name": "next",
"newRange": "^15.5.28",
"newVersion": "15.5.28",
"oldRange": "^15.0.0",
"oldVersion": "15.0.0",
"publishedAt": "2026-10-14T17:36:08.917Z",
"reasons": [
{
"kind": "level-limit",
"message": "`next@16.4.3` is a major step from 15.0.0, above the allowed minor level",
"version": "16.4.3"
}
],
"section": "dependencies",
"status": "upgrade",
"step": "minor"
}status | Meaning |
|---|---|
| upgrade | The range moves up. |
| held | A newer version exists but is not proposed. |
| current | Already the newest version that qualifies. |
| outside-focus | Not part of the focus, unchanged. |
| skipped | Not processable; skipReason says why. |
step is none, patch, minor or major. reasons[].kind is one of age-gate, engines,level-limit, peer, dependency or pulled-in. skipReason is one of non-semver-specifier, open-ended,complex-range, unsupported-range, prerelease, invalid-value, unknown-package or lookup-failed.
For a focused request the others are reported like this:
{
"bestVersion": "1.20.0",
"explanation": "`axios` is outside the focus, left at 1.6.0",
"name": "axios",
"newRange": "^1.6.0",
"newVersion": "1.6.0",
"oldRange": "^1.6.0",
"oldVersion": "1.6.0",
"publishedAt": "2023-10-26T21:15:55.685Z",
"reasons": [],
"section": "dependencies",
"status": "outside-focus",
"step": "none"
}Errors
Every error has the same shape. The code is stable; the message is for people.
{
"error": {
"code": "no_processable_dependency",
"message": "package.json has no dependency that can be processed"
}
}| Code | HTTP | Meaning |
|---|---|---|
invalid_json | 400 | The request body is not valid JSON. |
invalid_request | 400 | Unknown or invalid parameters, or the wrong content type. |
invalid_package_json | 400 | The package.json text is not valid JSON. |
invalid_ripedeps_config | 400 | The ripedeps entry in the file has an invalid value. The message names the key. |
invalid_input_url | 400 | Bad packageJsonUrl, or both or neither of packageJson and packageJsonUrl were sent. |
too_many_focus_entries | 400 | More than 50 focus entries. |
payload_too_large | 413 | The file is larger than 256 KB. |
too_many_dependencies | 413 | More than 300 dependencies. |
no_processable_dependency | 422 | Nothing in the file can be planned (every entry is skipped). |
input_url_blocked | 422 | The URL, or a redirect target, points to an address that is not allowed. |
input_fetch_failed | 422 | The URL could not be downloaded: DNS, timeout, connection or a non-2xx answer. |
input_not_package_json | 422 | The downloaded file is HTML, or not a JSON object. Use the raw file URL. |
not_found | 404 | Unknown path. |
method_not_allowed | 405 | Wrong method for a known path. The Allow header lists the right one. |
rate_limited | 429 | Too many requests. Slow down and retry. |
registry_unreachable | 502 | The npm registry could not be reached and there is no cached data. |
plan_timeout | 504 | The plan did not finish in time. Retry: what was loaded stays cached. |
unhealthy | 503 | Returned by /healthz when the database is not reachable. |
internal_error | 500 | Unexpected error. |
Other endpoints
GET /stats
Public counters, cached for a minute (Cache-Control: public, max-age=60, Access-Control-Allow-Origin: *). The numbers below are an example of the shape.
{
"packageJsonProcessed": 1204,
"packageJsonImproved": 981,
"packagesStored": 3512,
"versionsStored": 561230,
"asOf": "2026-10-24T12:17:02.388Z"
}GET /healthz
Returns {"status":"ok"}, or 503 unhealthy when the database cannot be reached.
Behaviour and limits
- Limits: file up to 256 KB, up to 300 dependencies, up to 50 focus entries,
minAgeDays0–365. - Rate limit: about 30 requests per minute with a burst of 10. Beyond that you get
429 rate_limited. - Timeouts: a plan has 60 seconds. On
504 plan_timeoutretry the same request; the package data loaded so far is cached, so it is much faster. Set your client timeout above 60 seconds. - Package data: read from the public npm registry and cached for 10 minutes. When the registry is briefly unavailable, expired data may be served and
meta.staleistrue. - CORS:
POST /planallows browser calls from ripedeps.com. Scripts and servers are not affected by CORS. - Idempotent:
/planchanges nothing on your side. The same request can be repeated safely.
JSON Schema
The request, the response and the error format are published as JSON Schema, generated from the same definitions the service validates with:
Recipes
Node.js and TypeScript
import { readFile } from 'node:fs/promises';
const res = await fetch('https://api.ripedeps.com/plan', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ packageJson: await readFile('package.json', 'utf8'), minAgeDays: 7 }),
signal: AbortSignal.timeout(120_000)
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
for (const p of body.packages)
if (p.status === 'upgrade') console.log(p.name, p.oldRange, '->', p.newRange, '|', p.explanation);
// body.updatedPackageJson is yours to write, or not.Python
import requests
r = requests.post(
"https://api.ripedeps.com/plan",
json={"packageJson": open("package.json").read(), "minAgeDays": 7},
timeout=120,
)
r.raise_for_status()
print(r.json()["diff"])List only what would change
bash ripedeps.sh --json --min-age 7 package.json \
| jq -r '.packages[] | select(.status == "upgrade") | "\(.name)\t\(.oldRange) -> \(.newRange)"'Fail a CI job when upgrades are pending
bash ripedeps.sh --json --min-age 7 --skip-adoptconfig package.json \
| jq -e '[.packages[] | select(.status == "upgrade")] | length == 0'jq -e exits with 1 when the answer is false, which fails the step.
Weekly upgrade pull request (GitHub Actions)
Ripedeps only plans. Your workflow applies the diff, commits and opens the pull request, so you stay in control of what lands.
name: ripedeps
on:
schedule: [{ cron: '0 6 * * 1' }]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Plan and apply
run: |
curl -fsSLO https://ripedeps.com/ripedeps.sh
bash ripedeps.sh --min-age 7 package.json | patch -p1
git diff --quiet && exit 0
git switch -c ripedeps/weekly
git -c user.name=ci -c user.email=ci@example.com commit -am 'chore: ripedeps upgrades'
git push -u origin HEAD
gh pr create --fill
env:
GH_TOKEN: ${{ github.token }}Prefer to let an AI assistant do this with the explanations in hand? See the MCP server. For the terminal workflow see the command line.
