Reference

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/json with 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 504 after 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.

Colourful syntax-highlighted source code on a dark screen

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:

terminal
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 .diff

Now with your own file: plan the package.json in the current directory and print the diff:

terminal
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

Apply it with patch -p1:

terminal
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 -p1

Request

POST https://api.ripedeps.com/plan with Content-Type: application/json. Send exactly one of packageJson andpackageJsonUrl. Unknown keys are rejected.

ParameterTypeDefaultMeaning
packageJsonstringone of the twoThe full text of package.json. Formatting is preserved in the diff. Up to 256 KB.
packageJsonUrlstring (https URL)one of the twoThe raw URL of a package.json. The service downloads it. See URL input below.
focusstring[]empty (everything)Exact names or * globs. A bare name like trpc also matches @trpc/*. Up to 50 entries.
minAgeDaysinteger 0–3650, or the file’s ripedeps entryMinimum age of a version since its release. 7 is a good start.
maxBumppatch | minor | majorminor, or the file’s ripedeps entryThe highest allowed step, measured from the version in your file.
skipAdoptConfigbooleanfalsetrue: 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.

package.json
"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

request body
{
  "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.

request body
{
  "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.

request body
{ "packageJson": "…", "focus": ["axios"], "minAgeDays": 0, "skipAdoptConfig": true }
terminal
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 .diff

Several packages and families work the same way:"focus": ["@trpc/*", "react"].

Allow bigger steps, keep your file untouched

request body
{ "packageJson": "…", "maxBump": "major", "minAgeDays": 14, "skipAdoptConfig": true }

The sample file used in the responses below

package.json
{
  "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.

FieldMeaning
diffUnified diff with a/package.json and b/package.json headers. patch -p1 applies it. Empty when nothing changes.
updatedPackageJsonThe complete new file, formatted like your input.
packagesOne entry per root: ranges, versions, step, status, release time, reasons and an explanation sentence.
warningsThings you should know: duplicates, unavoidable constraint violations with a hint, unknown keys in the ripedeps entry.
notesChanges to the output that are not upgrades, for example that the ripedeps entry was added or updated.
metaThe 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:

response, 200
{
  "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:

notes
[
  "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:

packages[0]
{
  "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"
}
statusMeaning
upgradeThe range moves up.
heldA newer version exists but is not proposed.
currentAlready the newest version that qualifies.
outside-focusNot part of the focus, unchanged.
skippedNot 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:

packages[0], focus ["zod"]
{
  "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, 422
{
  "error": {
    "code": "no_processable_dependency",
    "message": "package.json has no dependency that can be processed"
  }
}
CodeHTTPMeaning
invalid_json400The request body is not valid JSON.
invalid_request400Unknown or invalid parameters, or the wrong content type.
invalid_package_json400The package.json text is not valid JSON.
invalid_ripedeps_config400The ripedeps entry in the file has an invalid value. The message names the key.
invalid_input_url400Bad packageJsonUrl, or both or neither of packageJson and packageJsonUrl were sent.
too_many_focus_entries400More than 50 focus entries.
payload_too_large413The file is larger than 256 KB.
too_many_dependencies413More than 300 dependencies.
no_processable_dependency422Nothing in the file can be planned (every entry is skipped).
input_url_blocked422The URL, or a redirect target, points to an address that is not allowed.
input_fetch_failed422The URL could not be downloaded: DNS, timeout, connection or a non-2xx answer.
input_not_package_json422The downloaded file is HTML, or not a JSON object. Use the raw file URL.
not_found404Unknown path.
method_not_allowed405Wrong method for a known path. The Allow header lists the right one.
rate_limited429Too many requests. Slow down and retry.
registry_unreachable502The npm registry could not be reached and there is no cached data.
plan_timeout504The plan did not finish in time. Retry: what was loaded stays cached.
unhealthy503Returned by /healthz when the database is not reachable.
internal_error500Unexpected 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.

GET /stats
{
  "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, minAgeDays 0–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_timeout retry 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.stale is true.
  • CORS: POST /plan allows browser calls from ripedeps.com. Scripts and servers are not affected by CORS.
  • Idempotent: /plan changes 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

upgrade.ts
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

upgrade.py
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

terminal
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

terminal
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.

.github/workflows/ripedeps.yml
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.