Skip to content
apisentry — API providers should not just announce breaking changes, they should apply them

CI Tests False positives Runtime deps License Node Status

Dependabot, but for APIs. Reads a vendor's change feed, finds the affected code in your repo, writes the fix, opens the pull request — and tells you honestly how sure it is.

Quickstart · How it works · Confidence · Feed format · Internal APIs · Architecture · Contributing

A product of Retia Labs


The problem

A vendor ships a breaking change. They write a changelog entry, send an email, and hope. Then every customer independently rediscovers the same migration, at a different time, with a different amount of panic. Some don't discover it at all until production breaks.

The other half of the same problem is quieter: useful features launch and nobody notices, because nobody reads changelogs.

This was an unavoidable cost right up until developers started routinely granting codebase access to agentic tools. That infrastructure now exists and is normal. What's missing is the layer that connects a provider's change to a customer's code.

- Changelog entry → email → hope → incident
+ Change feed → scan → patch → pull request → review → merge

Project status

Alpha, and honest about it. This is a working tool with a real test suite and a real precision benchmark — and, so far, no production users. Nothing here is theoretical, but nothing here has survived contact with a stranger's codebase either.

Contributions welcome, especially vendor feeds — coverage is the bottleneck. See CONTRIBUTING.md. A false positive, or an over-confident auto label, is the most serious class of bug this project has, and those reports get priority.

Licensed Apache-2.0. It was briefly closed and proprietary at v0.6.0; that was a mistake, and NOTICE.md says why rather than hiding it.

Quickstart

No install, no build step, no dependencies. Node ≥ 20.

git clone https://github.com/likalight/apisentry
cd apisentry

node bin/apisentry.mjs run --patch

That runs against the bundled demo app, which uses Stripe, OpenAI, and Twilio — wrongly.

The demo feeds in fixtures/feeds/ are invented and marked "fictional": true — Stripe did not remove the Charges API. They exist because a demo needs a feed that provably exercises all three confidence levels. Feeds drafted from real vendor changelogs live in changes/.

What you get
  The Charges API is removed in favour of PaymentIntents  [stripe-2026-06-01-charges-removed]
  provider stripe   type breaking   severity critical   fix auto
  4 site(s) in 2 file(s)  +4 -4
    src/lib/stripe.ts:9       return stripe.charges.create({
    src/routes/checkout.ts:6  const charge = await stripe.charges.create({

    --- a/src/lib/stripe.ts
    +++ b/src/lib/stripe.ts
    @@ -6,7 +6,7 @@
     export async function chargeCustomer(customerId: string, amountCents: number) {
    -  return stripe.charges.create({
    +  return stripe.paymentIntents.create({

  Legacy Completions endpoint is removed  [openai-2026-04-02-completions-removed]
  provider openai   type breaking   severity critical   fix partial
    note The call target is rewritten automatically, but `prompt:` has no mechanical
         translation — a human has to decide the system/user split.

  messages.create now accepts sendAt for scheduled delivery  [twilio-...]
  provider twilio   type feature   severity low   fix review

scanned 4 files · 2 auto-fixable · 1 partial · 1 review-only

Point it at your own code:

node bin/apisentry.mjs scan --target ~/my-app     # what's affected?
node bin/apisentry.mjs plan --target ~/my-app --patch   # what would change?
node bin/apisentry.mjs pr   --target ~/my-app --open    # ship the PRs

How it works

 check ──────► scan ──────► plan ──────► apply ──────► pr
 pull feeds    find         build        write         branch, commit,
 for changes   affected     patches +    working       push, gh pr create
 you haven't   code         confidence   tree          (one PR per change)
 seen yet
command what it does
check Pull vendor feeds; list changes this repo hasn't been told about yet
scan Run each change's detectors over your codebase; report every affected site
plan Apply migrations in memory; produce patches and a confidence label
apply Write the fixes to the working tree
pr Branch, commit, push, gh pr create
run check + plan in one pass
explain <id> Everything known about one change, including its compiled regexes
ack <id...> Mark a change handled without touching code, so it stops being reported
`draft <url file>`
keygen / sign Generate an ed25519 keypair, and sign a feed with it

Add --verify to plan, apply, or pr to run the target's test suite against each patch.

Flags: --target --provider --change --all --patch --json --commit --push --open --update --yes --no-gating --no-rooting --verify[=cmd] --include-drafts

Precision: two filters in front of the regexes

A tool that reports things you don't care about gets uninstalled faster than one that misses things. Before a single detector runs, two questions get answered.

1. Does this repo even use this vendor? apisentry reads the dependency manifests — package.json, requirements.txt, pyproject.toml, go.mod, Gemfile, composer.json, Cargo.toml, every workspace of a monorepo — and matches them against the change's appliesTo:

the repo… what runs
depends on the package, version in range everything
depends on it, version outside the range nothing — skipped
doesn't depend on it only detectors that don't presuppose an import
has no manifest at all everything — we can't tell, so we don't guess

That third row is deliberate: someone calling your REST API with raw fetch and no SDK is still affected by an endpoint removal, and still gets the PR.

2. Does this call actually come from the SDK? symbol and param detectors only count when the match hangs off an identifier that traces back to the vendor. Imports are resolved, and relative and alias-path re-exports are followed up to three hops — because everyone wraps their client in lib/stripe.ts.

import Stripe from 'stripe'
const s = new Stripe(key)

s.charges.create({})     // matched
db.charges.create({})    // suppressed — db has nothing to do with Stripe

Nothing is filtered silently. Every skipped change prints its reason, and suppressed match counts are always shown; --no-gating and --no-rooting turn either filter off.

Confidence is the whole design

A tool with commit access to other people's repositories has to be honest about what it does and doesn't know. After running a change's migrations, apisentry re-runs that change's own detectors on the result. What still matches decides how the PR opens:

meaning how the PR opens
auto Every detected usage was rewritten; a re-scan comes back clean Normal PR
partial Some rewritten, detectors still fire Draft PR, with the manual: note surfaced
review No migration exists — we can only say where to look Report / draft only

That inversion matters: the party publishing a change doesn't get to grade its own completeness.

Verification: the part that's actually about your code

Detector analysis is a statement about apisentry, not about your codebase. --verify runs your test suite against the patch and reports the result in the PR:

node bin/apisentry.mjs plan --target ~/my-app --verify
  The Charges API is removed in favour of PaymentIntents
  provider stripe   type breaking   severity critical   fix partial
    BROKEN npm test (1.0s)
    confidence downgraded to partial — the suite fails with this patch applied

The baseline is established before anything is patched, because a suite that was already red proves nothing about the migration. Then the patch is applied, the suite re-runs, and the tree is restored in a finally — leaving a customer's working tree patched would be a worse bug than any this could catch.

verdict meaning
verified green before, green after — evidence, not proof
broken green before, red after → downgraded to partial, PR opens as a draft with the output
baseline-red already failing, so the run proves nothing. No downgrade — an inconclusive result must not masquerade as a verdict
no-tests / timeout unverified, and the PR says so

Verification can only ever lower a confidence label, never raise one. A green suite means the tests that exist still pass, which is not the same as correct.

It's opt-in because it executes commands from the target repository — see SECURITY.md. Override the detected command with --verify="pnpm test".

partial is not a failure mode, it's the common case. In the demo, OpenAI's Completions removal rewrites openai.completions.createopenai.chat.completions.create and choices[0].textchoices[0].message.content mechanically — but prompt:messages: has no mechanical translation, so it ships a note and opens as a draft.

A draft PR saying "I did the easy 80%, here's the 20% only you can decide" is useful. A confident PR that silently broke checkout is the thing that gets this class of tool banned everywhere.

The feed format

The real artifact here is the file a provider should publish next to their changelog. It's small on purpose — a vendor should be able to write one by hand in ten minutes.

{
  "id": "stripe-2026-06-01-charges-removed",
  "title": "The Charges API is removed in favour of PaymentIntents",
  "appliesTo": { "packages": ["stripe"], "versions": ">=8.0.0" },
  "type": "breaking",
  "severity": "critical",
  "effectiveAt": "2026-09-01",
  "url": "https://stripe.com/docs/upgrades#2026-06-01",
  "detect": [
    { "kind": "symbol",   "value": "charges.create" },
    { "kind": "endpoint", "value": "/v1/charges" }
  ],
  "migrate": [
    { "kind": "rename",  "from": "charges.create", "to": "paymentIntents.create" },
    { "kind": "replace", "value": "/v1/charges", "replacement": "/v1/payment_intents" }
  ]
}

Detectors answer "what does affected code look like?"

  • symbol — a dotted call path, whitespace-tolerant, won't match inside a longer identifier
  • endpoint — a URL path, but only inside a string literal, so comments and prose don't fire. This is what catches raw fetch callers who never touched your SDK.
  • param — a request key, optionally scoped to a call
  • regex — the escape hatch

Migrations are text substitution only. No code from a feed is ever executed. "migrate": [] means review-only — which is the correct encoding for a feature announcement, and solves the "nobody noticed it launched" half of the problem with no autofix at all.

Full reference: docs/feed-format.md

Where feeds come from

Almost no vendor publishes a machine-readable feed today. That's the chicken-and-egg problem at the centre of this idea, and draft is the answer to it — point it at a changelog and it produces a feed entry a human then confirms.

node bin/apisentry.mjs draft https://vendor.example/changelog.rss \
  --provider vendor --packages vendor-sdk --out changes/vendor.json
  2026-07-14 — The Charges API is removed  [acme-2026-07-14-the-charges-api-is-removed]
  type breaking   severity critical   confidence medium
    symbol    acme.charges.create
    endpoint  /v1/charges
    migrate   acme.charges.create → acme.payments.create
    manual    The argument shape differs; check `amount` handling.

  Before this stops being a draft
    [ ] Every entry describes a real API change, not a docs or pricing note
    [ ] Detectors are named in the changelog, not inferred from the product name
    [ ] Detectors do not fire on unrelated code — check with `apisentry scan`
    ...

Run against the three real SDK release feeds, the offline extractor currently produces 17 Twilio endpoint removals and 1 Stripe deprecation from the last 15 releases of each, and nothing for OpenAI — whose recent releases contain no breaking changes with code spans. Those are in changes/, all still marked draft.

It reads RSS, Atom, HTML and Markdown, and works in two modes:

  • Heuristic (offline, no key). Segments the changelog, classifies each entry, and pulls candidate detectors out of backticked code spans. It never invents a migration — guessing a rewrite from prose is exactly the confident-and-wrong this project exists to avoid, so every heuristic draft is review-only.
  • Model (ANTHROPIC_API_KEY set). Claude reads the segmented text and fills in the feed schema through a forced tool call, including migrations where the rewrite is genuinely mechanical. It's prompted to return an empty list rather than pad, and to rate its own confidence honestly — a low costs nothing and a wrong auto costs everything.

Every draft is inert. Entries carry "draft": true, and check lists them separately while scan, plan and pr skip them entirely until a human removes the flag. An agent wrote it is not a reason to trust it.

Measured, not asserted

A passing test suite says the code does what I meant. It says nothing about whether the tool is right about real code. bench/ measures the number that actually matters:

npm run bench
  repo                  files   findings  no-filters  expected  verdict
  ──────────────────────────────────────────────────────────────────────
  ledgerpilot           77      0         0           0         ok
  provenancetrail       86      0         0           0         ok
  demo-app              4       10        10          10        ok

  167 files scanned · 10 findings · 29 without gating/rooting
  the two precision filters removed 19 match(es) (66%)

The feed set includes a collision probe — a fictional vendor whose detectors are deliberately generic (invoice.create, document.update) and collide head-on with ordinary Prisma calls in a real application. Stripe genuinely has invoices.create, so this is not a contrived shape.

On a real 77-file Next.js codebase: 19 matches, 0 reported. That is the number the two precision filters exist for. Add your own repos to bench/repos.json with an expect count and the run fails when it moves.

Both architectures, one binary

The two shapes this could take are the same tool with a different sources list.

Per-provider agentNeutral third party

A vendor ships an agent that knows only their API.

{
  "sources": [
    { "kind": "http",
      "url": "https://stripe.com/.well-known/api-changes.json" }
  ]
}

Dependabot-for-APIs, tracking many vendors and mirroring the ones with no feed yet.

{
  "sources": [
    { "kind": "http",  "url": ".../stripe/api-changes.json" },
    { "kind": "http",  "url": ".../openai/api-changes.json" },
    { "kind": "local", "path": "changes" }
  ]
}

A source that 404s or serves garbage degrades that source only — one vendor's broken feed must never take the run down. Both configs are in examples/config/.

Running it continuously

Drop examples/github-action/apisentry.yml into .github/workflows/. Nightly, it opens PRs for anything new:

- run: node .apisentry-tool/bin/apisentry.mjs pr --target . --open --yes --update
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Not on npm yet, so the workflow clones the repo rather than running npx apisentry.

Per-repo state lives in .apisentry/state.json (self-gitignoring), so a change already handled here is never re-reported. Without that, the nightly run reopens the same PR forever and gets muted in a week.

Safety posture

  • Everything defaults to reporting. pr is a dry run unless --commit, --push, or --open. apply prompts before writing. --verify is opt-in because it executes commands from the target repo.
  • Path allowlists (paths.include / paths.exclude) bound which files a migration may touch.
  • Catastrophic regex patterns are rejected at load, and a detector that proves slow is retired mid-run rather than burning the whole scan.
  • Never runs on a dirty working tree. One branch per change, off your current branch.
  • No feed code is ever executed — migrations are text substitution. No eval, no plugin loading.
  • Zero runtime dependencies, so the supply-chain surface is this repo and Node itself.
  • Feeds are trusted input. Only configure sources you'd grant commit access to.

Full threat model, including the known gaps: SECURITY.md

Project layout

bin/apisentry.mjs      CLI entry point
src/schema.mjs         feed format: validation + detector/migration compilation
src/feeds/index.mjs    local + http feed adapters
src/scan.mjs           codebase walker and detector runner
src/plan.mjs           migrations → patches → confidence
src/diff.mjs           dependency-free unified diff
src/pr.mjs             git/gh: branch, commit, push, open
src/state.mjs          what this repo has already been told
changes/               demo feeds: stripe, openai, twilio
fixtures/demo-app/     a small codebase that uses all three, wrongly
docs/                  feed format + architecture
examples/              CI workflow and both config shapes

Development

npm test                       # 74 unit + regression tests
node test/assert-demo.mjs      # integration: all 4 demo changes classify correctly

CI runs on Node 20/22/24 across Linux, macOS, and Windows.

Roadmap

Honest list, in the order things actually block each other.

Shipped

  • Relevance gating (0.2.0) — dependency manifests decide whether a change applies at all.
  • Import-aware detection (0.2.0) — wrapped and aliased clients are followed.
  • Idempotent PR flow (0.2.0) — reruns skip or refresh instead of failing.
  • Verification (0.3.0) — --verify runs the target's own suite and downgrades on failure.
  • Path allowlists and regex safety (0.3.0).
  • Changelog → feed extraction (0.4.0) — apisentry draft produces a feed for review.
  • Feed signing (0.7.0) — ed25519; tampering is fatal under every configuration.
  • A benchmark that discriminates (0.7.0) — 19 real collisions suppressed, 0 reported.

Open, in priority order

  • Nobody has used this. The riskiest assumption in the project is that a developer will merge a bot's PR against their payment code, and it is untested. Everything below is cheaper than finding that out.
  • All 18 real feed entries are still draft. Pointed at its own bundled feeds, apisentry currently proposes nothing. Reviewing and promoting them — or deciding they aren't worth promoting — is the difference between a pipeline and a product.
  • The model extraction path has never run against the live API. Everything around it is tested; the call itself is not. Needs a key.
  • More vendor feeds. Coverage is the real bottleneck, and the reason the extractor exists.
  • Full AST detection. Import resolution covers the common cases; a parser would cover dynamic requires, re-exported namespaces, and non-JS languages properly. Tree-sitter behind the existing detector interface.
  • True regex preemption. Patterns are linted and slow detectors retired, but a single hostile match can still hang — that needs a worker thread or a different engine.

Where this is going

The engine isn't the moat — it's about three thousand lines of regex, git plumbing and careful error handling. What compounds is the feed corpus (almost no vendor publishes one, and keeping hundreds accurate never finishes), cross-customer signal about which migrations actually land, and being the channel a provider's changes reach customers through.

The wedge is internal APIs: a platform team ships a breaking change and chases twelve consuming teams by Slack. Same product, no vendor cooperation needed, obvious budget owner, and no cold-start problem — the customer writes their own feed.

Reasoning, and the open questions, in GOVERNANCE.md.

Background

Built from the essay Self-Maintaining APIs by Harsha Gaddipati, which argues that the infrastructure for automated code changes already exists — what's missing is the application layer connecting API providers to their customers' codebases. This is that layer, as a working prototype.

License

Apache-2.0. Copyright 2026 Retia Labs.

Chosen over MIT for the explicit patent grant, which matters if API providers ever engage with the change feed format. The licensing history — including the day this project went proprietary and came back — is in NOTICE.md.


Built at Retia Labs

Keywords: api · breaking-changes · api-versioning · deprecation · changelog · codemod · migration · automated-refactoring · dependabot · renovate · developer-tools · devtools · devex · sdk · static-analysis · pull-request-automation · github-actions · ci-cd · agentic-ai · ai-agents · llm-tools · code-migration · api-lifecycle · zero-dependency · nodejs · cli · open-source

About

Dependabot for APIs. Reads a vendor's change feed, finds the affected code in your repo, writes the fix, and opens the PR. Zero dependencies. A product of Retia Labs.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages