Issue: #386
Status: Active — /api/v1/ is the current stable version
Last updated: 2026-07-28
The NotifyChain Listener service supports explicit API versioning through a /api/v1/ URL prefix.
The versioning layer lives in listener/src/api/events-server.ts and is implemented as a path
normalization step that transparently rewrites /api/v1/* requests to their canonical /api/*
form before routing.
This approach means:
- No code duplication — every route handler is written once.
- Instant backward compatibility — unversioned
/api/*routes continue to work without any changes to existing clients. - Future versions — when breaking changes are required, a
/api/v2/prefix can be introduced alongside the existing/api/v1/paths.
All endpoints available at /api/* are also reachable at /api/v1/*.
| Unversioned (legacy) | Versioned (recommended) |
|---|---|
GET /api/events |
GET /api/v1/events |
GET /api/status |
GET /api/v1/status |
GET /api/indexing/health |
GET /api/v1/indexing/health |
GET /api/notifications/health |
GET /api/v1/notifications/health |
GET /api/notifications/search |
GET /api/v1/notifications/search |
GET /api/analytics |
GET /api/v1/analytics |
GET /api/analytics/history |
GET /api/v1/analytics/history |
GET /api/schedule/stats |
GET /api/v1/schedule/stats |
GET /api/schedule/execution-metrics |
GET /api/v1/schedule/execution-metrics |
GET /api/schedule/retry-distribution |
GET /api/v1/schedule/retry-distribution |
GET /api/schedule/retry-statistics |
GET /api/v1/schedule/retry-statistics |
GET /api/schedule/:id |
GET /api/v1/schedule/:id |
POST /api/schedule |
POST /api/v1/schedule |
DELETE /api/schedule/:id |
DELETE /api/v1/schedule/:id |
POST /api/webhooks |
POST /api/v1/webhooks |
POST /api/notifications/validate-batch |
POST /api/v1/notifications/validate-batch |
GET /api/search/suggestions |
GET /api/v1/search/suggestions |
GET /api/templates |
GET /api/v1/templates |
POST /api/templates |
POST /api/v1/templates |
GET /api/templates/:id |
GET /api/v1/templates/:id |
PUT /api/templates/:id |
PUT /api/v1/templates/:id |
DELETE /api/templates/:id |
DELETE /api/v1/templates/:id |
POST /api/templates/:id/render |
POST /api/v1/templates/:id/render |
GET /api/templates/:id/audit |
GET /api/v1/templates/:id/audit |
GET /api/preferences/:userId |
GET /api/v1/preferences/:userId |
PUT /api/preferences/:userId |
PUT /api/v1/preferences/:userId |
GET /api/archive |
GET /api/v1/archive |
GET /api/archive/:id |
GET /api/v1/archive/:id |
POST /api/archive/run |
POST /api/v1/archive/run |
GET /api/rate-limit/metrics |
GET /api/v1/rate-limit/metrics |
Note:
GET /healthis a non-versioned infrastructure endpoint and remains at/healthonly.
Every response from the versioned server includes:
X-API-Version: v1
Clients can use this header to detect the active API version at runtime.
The versioning is implemented in listener/src/api/events-server.ts via a single path
normalization block executed after CORS headers are set and before any route handler runs:
// Rewrite /api/v1/* → /api/* (path normalisation for versioning)
if (url.pathname.startsWith('/api/v1/')) {
url.pathname = url.pathname.replace('/api/v1/', '/api/');
} else if (url.pathname === '/api/v1') {
url.pathname = '/api';
}
res.setHeader('X-API-Version', 'v1');No route handler knows about versioning — they all match against the canonical /api/ form.
Unversioned /api/* routes are not deprecated and have no planned removal date.
The deprecation lifecycle is as follows:
| Stage | Notice period | Description |
|---|---|---|
active |
— | Route is stable and fully supported. |
deprecated |
≥ 6 months | Route still works. A Deprecation response header is added. |
sunset |
≥ 3 months | Route returns 410 Gone. Clients must have migrated. |
When an endpoint enters the deprecated stage, the server will add:
Deprecation: true
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Link: </api/v2/events>; rel="successor-version"
Clients should monitor for the Deprecation header and migrate before the Sunset date.
When a breaking change is required for an existing endpoint:
- Implement the new behavior at
/api/v2/<endpoint>. - Keep the
/api/v1/<endpoint>(and/api/<endpoint>) behavior unchanged. - Add a second path-normalization block for
v2inevents-server.ts:if (url.pathname.startsWith('/api/v2/')) { // Route to v2 handlers }
- Mark the v1 endpoint as
deprecatedand set aSunsetdate at least 6 months out. - Document the breaking change in
CHANGELOG.mdand this file.
curl http://localhost:3000/api/eventscurl http://localhost:3000/api/v1/eventsBoth calls return identical responses. New integrations should use the /api/v1/ prefix.
The versioning layer is covered by the existing events-server.test.ts suite.
Any test that exercises an /api/* route also implicitly validates /api/v1/* because
the normalization step is unconditional.
To verify a versioned path explicitly:
const res = await fetch('http://localhost:PORT/api/v1/events');
expect(res.headers.get('X-API-Version')).toBe('v1');
const body = await res.json();
expect(body).toHaveProperty('events');