Skip to content

Registry Changes Feed

Thomas Neidhart edited this page Aug 3, 2026 · 1 revision

Registry Changes Feed

Preview endpoint. This page documents GET /api/-/version-changes. It is marked as a preview operation in the API: its parameters or the shape of its response may still change before the contract is settled. If you are consuming it programmatically, keep an eye on the Swagger UI of your registry for changes. It is a recent addition, so check the Swagger UI first if you are unsure whether the registry you are talking to already serves it.

The registry changes feed lets a consumer follow every extension version that becomes publicly available, is taken down, or comes back — without polling every extension to notice. It exists for integrations that need to react to what a registry publishes, such as:

  • Mirrors that replicate a registry and need to know what changed since the last sync.
  • Security scanners or other tooling that need to see every version that ever became available, including ones that were later removed.
  • Any other integration that would otherwise have to repeatedly poll search or listing endpoints and diff the results itself.

If you just want to know the latest version of one specific extension, this endpoint is the wrong tool — use the regular extension metadata endpoints instead. The changes feed is for following the registry as a whole over time.


How It Works

Each entry in the feed reports one transition of a single extension version, ordered by the instant it happened, oldest first. If a version transitions more than once (for example: published, then deactivated, then reactivated), it gets one entry per transition — the feed never goes back and rewrites or moves an earlier entry, so a consumer that has already read past a position can't miss something that happened at that position later.

States

State Meaning
ACTIVE The version is publicly available.
INACTIVE The version is not publicly available. Its files are still there, and it can become ACTIVE again.
REMOVED The version is no longer available for download, either because it was deleted or because it was permanently purged. Either way, it's gone.

Transitions

From To Reported when
not in the feed ACTIVE The version was published and became publicly available.
ACTIVE INACTIVE The version was deactivated administratively — for instance, the publisher's contributions were revoked or their publisher agreement is no longer signed.
INACTIVE ACTIVE The version was reinstated and is publicly available again.
ACTIVE, INACTIVE REMOVED The version was deleted or purged.
REMOVED ACTIVE The version had been purged, freeing its identity, and the same coordinates were published again.

A deleted version keeps its identity permanently reserved as a tombstone and can never be published again — REMOVED is the last entry it will ever get. An administrator can instead purge it, which drops that tombstone and frees the identity, so the same namespace, extension, version and target platform can be published again and continue in the feed with a fresh ACTIVE entry. So REMOVED describes the current state of the coordinates an entry names, not a permanent guarantee.

Two consecutive entries for the same version never report the same state. Also note that the timestamp a version was originally published at can be well before the lastUpdated of a given entry — for instance, if activation had to wait on a scan, or an old version is only reported as removed today.

Only availability changes are reported. Editing a version's metadata (its README, tags, etc.) is not a transition and produces no entry.

There's a short delay

The feed stops a little short of the present — by default on the order of tens of seconds, though a given registry may configure a different delay. This is what lets a consumer that has caught up be confident it hasn't missed anything: an entry is only reported once its transaction is certain to have committed. If you ask for until a time closer to the present than that margin, you'll just get nothing beyond the margin back — those entries aren't skipped, they show up on a later request.

Responses are cacheable

Responses carry Cache-Control: max-age=60, public. Entries are only ever appended, never rewritten or reordered, so polling more often than once a minute gains you nothing.


Request Parameters

Parameter Type Description
after string Continue after this position in the feed, as returned in nextCursor of a previous response. Cannot be combined with since.
since string (ISO-8601) Only include entries at or after this date and time. For one-off queries; use after to follow the feed instead.
until string (ISO-8601) Only include entries strictly before this date and time.
size integer Maximum number of entries to return. Between 1 and 1000, default 100.

Only one combination is rejected (with 400 Bad Request): passing both after and since, since they disagree about where the response should start. until can be combined with after — that's how you catch up to a fixed point in time while still following the feed by cursor. A malformed after, since or until value is also rejected with 400 Bad Request, e.g. {"error": "Invalid 'since' parameter: yesterday"}.

GET /api/-/version-changes?size=100
GET /api/-/version-changes?after=MjAyNi0wMS0xNFQwOTozMDoxMV8xMjM0
GET /api/-/version-changes?since=2026-01-01T00:00:00Z&until=2026-02-01T00:00:00Z

Response Format

{
  "changes": [
    {
      "namespace": "redhat",
      "name": "java",
      "version": "1.30.0",
      "targetPlatform": "universal",
      "state": "ACTIVE",
      "timestamp": "2026-01-14T09:12:33Z",
      "lastUpdated": "2026-01-14T09:12:33Z",
      "url": "https://open-vsx.org/api/redhat/java/universal/1.30.0"
    },
    {
      "namespace": "redhat",
      "name": "java",
      "version": "1.29.0",
      "targetPlatform": "universal",
      "state": "REMOVED",
      "timestamp": "2025-11-02T16:40:05Z",
      "lastUpdated": "2026-01-14T09:30:11Z",
      "url": "https://open-vsx.org/api/redhat/java/universal/1.29.0"
    }
  ],
  "nextCursor": "MjAyNi0wMS0xNFQwOTozMDoxMV8xMjM0",
  "hasMore": false
}
Field Description
changes[].namespace / name / version / targetPlatform Identify the extension version this entry is about.
changes[].state ACTIVE, INACTIVE, or REMOVED — see above.
changes[].timestamp When the version was originally published. The same on every entry for that version.
changes[].lastUpdated When this transition happened. The feed is ordered by this field.
changes[].url Link to the version's full metadata. Only resolves while the version is currently ACTIVE — an entry reporting INACTIVE or REMOVED still carries a url, but fetching it returns 404 once the version is no longer active, and it never resolves at all for a version that was purged.
nextCursor Opaque position of the last entry in this response. Pass it back as after to continue. Absent only if the response was empty and there was no earlier position to continue from — in that case, just repeat the same request later.
hasMore true if more entries already match your request beyond this page. If true, request the next page immediately rather than waiting for your next poll interval.

nextCursor is an opaque string — treat it as a token, not as data. Don't try to construct one yourself or parse one you were given.


Following the Feed

This is the loop for a mirror, scanner, or any consumer that wants to stay up to date indefinitely — it works the same whether it's your first full sync, an hourly poll, or catching up after days offline:

  1. Request the feed. Leave after out on the very first request.
  2. Process the entries in changes, in order.
  3. Once they're processed, store nextCursor and pass it as after on your next request.
  4. If hasMore was true, go straight back to step 1 instead of waiting for your next poll interval.
let cursor = loadStoredCursor(); // undefined on first run

do {
  const url = new URL('/api/-/version-changes', registryBaseUrl);
  url.searchParams.set('size', '500');
  if (cursor) url.searchParams.set('after', cursor);

  const response = await fetch(url);
  const page = await response.json();

  for (const change of page.changes) {
    await handleChange(change); // e.g. update a mirror, or feed a scanner queue
  }

  if (page.nextCursor) {
    cursor = page.nextCursor;
    saveStoredCursor(cursor); // only after the entries above were actually processed
  }

  if (!page.hasMore) {
    await sleep(60_000); // nothing changed since the delay margin; wait before polling again
  }
} while (true);

The important part is storing the cursor only after the entries in that page have actually been processed. If your consumer crashes mid-page, it reprocesses that page on restart instead of silently skipping it — which is safe, because entries are idempotent to apply (they just report a state).

Don't try to follow the feed by taking an entry's lastUpdated and passing it back as since instead of using after. since is inclusive, so that would report the same entries you already saw again, and there's no correct way to make it exclusive instead — because more than one transition can share the exact same instant, and only a cursor (which also encodes which of those entries you already processed) can tell them apart.


One-off Queries

If you don't need to keep following the feed and just want a fixed window — for an audit, a report, or a one-time backfill — use since and/or until instead of after:

GET /api/-/version-changes?since=2026-01-01T00:00:00Z&until=2026-02-01T00:00:00Z&size=1000

You'll still get nextCursor/hasMore back if the window contains more entries than fit in one response — page through it the same way as above, just keep passing the same until alongside after on subsequent requests if you want to stay bounded to that same fixed end point.


Notes

  • The endpoint requires no authentication and supports CORS, so it can be called directly from browser-based tooling as well as from a server.
  • There is no dedicated rate limit beyond the one-minute cache: polling faster than that just gets you a cached response.
  • A cursor never expires — entries are append-only and are never renumbered, so a cursor you stored months ago is still valid.

Clone this wiki locally