-
Notifications
You must be signed in to change notification settings - Fork 351
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.
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.
| 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. |
| 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.
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 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.
| 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
{
"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.
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:
- Request the feed. Leave
afterout on the very first request. - Process the entries in
changes, in order. - Once they're processed, store
nextCursorand pass it asafteron your next request. - If
hasMorewastrue, 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.
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.
- 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.