Guides

How to find which clients still call an old API version

Before you retire v1, you need one list: every client still calling it, how often, and when they last did. Not "v1 is 3% of traffic." A list of names.

That list decides everything after it: who gets an email, who gets a phone call, and whether the retirement date is real. This guide shows how to build it from the data you already have, and how to keep it current.

Why this is harder than it sounds

The question needs two facts joined on every request: who made the call and which version they hit. Most stacks record one or neither.

  • Access logs have the path, so they have the version, but only an IP address for the caller.
  • Your auth layer knows the caller, but often doesn't log the endpoint.
  • Your APM or metrics tool aggregates by route and drops the caller to keep cardinality down.

So the answer exists, spread across three systems that don't share a key. The steps below fix that at the source.

1. Make every request identify its client

You can't list clients you can't tell apart. In order of reliability:

  1. Per-client credentials. A client ID and secret, or an API key, issued to each consuming service. This is the only identity you control. Resolve it to a client name at the auth step.
  2. A required client header. Something like X-Client-Name. Cheap to add, but callers can omit or misspell it, so treat it as a hint.
  3. User-Agent. Useful when an SDK sets it, since it often carries the SDK version too. Useless for raw HTTP calls.
  4. IP address. A last resort. NAT, proxies and autoscaling make it unreliable.

If two services share one API key, they are one client as far as your data knows. Split them before you go further.

2. Record the version on the same log line

The version lives wherever your API puts it: the path (/v1/users), a header (Api-Version: 2024-06-01), or a media type (application/vnd.acme.v2+json). Parse it once, in middleware, and write it next to the client on one structured log line.

// Express middleware: one line per request, client + version together
app.use((req, res, next) => {
  const started = Date.now();
  res.on("finish", () => {
    logger.info({
      event: "api_request",
      client_id: req.auth?.clientId ?? "anonymous",
      version: req.path.match(/^\/(v\d+)\//)?.[1] ?? req.get("Api-Version") ?? "unversioned",
      route: req.route?.path,
      status: res.statusCode,
      ms: Date.now() - started,
    });
  });
  next();
});

Log the route template (/v1/users/:id), not the raw path. Otherwise every user ID becomes its own row.

3. Query usage by client and version

With client and version on one line, the list is a group-by. Pull at least 30 days, so monthly batch jobs show up. Here it is in SQL, for logs landed in a warehouse:

SELECT client_id,
       COUNT(*)              AS calls,
       COUNT(DISTINCT route) AS routes_used,
       MAX(logged_at)        AS last_seen
FROM api_requests
WHERE version = 'v1'
  AND logged_at > now() - interval '30 days'
GROUP BY client_id
ORDER BY last_seen DESC;

The same query in CloudWatch Logs Insights:

fields client_id, route
| filter event = "api_request" and version = "v1"
| stats count(*) as calls, latest(@timestamp) as last_seen by client_id
| sort last_seen desc

Sort by last seen, not call count. A client with 40 calls a month is just as broken on retirement day as one with 40,000.

4. Turn the list into owners and migration paths

A client ID is not a person. For each row, add an owner, the routes, and their replacements:

Client Owner Routes on v1 Replacement Last seen
billing-sync Acme Corp, integrations team GET /v1/invoices GET /v2/invoices 2 hours ago
reports-cron Internal, data team GET /v1/users GET /v2/users?fields= 6 days ago

The owner column is where most teams stall. Record a contact when you issue credentials, not when you need it. The engineer who built an integration two years ago has often moved on.

The routes column lets you send each client a specific migration note instead of a generic "please upgrade."

Pitfalls that hide callers

  • Shared credentials. One key used by three services looks like one client. You'll migrate one and break two.
  • Short windows. Quarterly reports and year-end jobs won't appear in a 7-day query. Look back 90 days before setting a date.
  • Gateways and proxies. If an internal proxy calls your API on behalf of others, every request shows the proxy's identity. Forward the original caller in a header.
  • Stale mobile apps. Old installs keep calling for months after a release. Count installs by app version, not just backend clients.
  • Log sampling. Sampled logs undercount quiet clients, and quiet clients are the ones you miss. Keep this one event unsampled.

Where EndPointBlank fits

EndPointBlank does steps 1 through 3 as a side effect of authentication. Every client gets its own machine-to-machine credentials, and every authorization records both the calling client and the endpoint version. "Who is still on v1" is a filter in the portal, with call counts and last-seen times, not a log query.

When you're ready, mark the version deprecated and set a date. Every client on the list sees Retires in 30 days and gets reminders 30, 7 and 1 day out.

See how version lifecycle works or start free. Once you have the list, the deprecation guide covers what to do with it.

Ready to deprecate with confidence?

Sign up in under a minute. No credit card, no sales call.