URL path versioning puts the version in the route: GET /v2/users. The router dispatches /v1/... and /v2/... to different handlers or controllers. It is the most visible and the easiest to test by hand, cache, and log, which is why public APIs like Stripe-style REST services and most internal microservices use it. The downside is that the URL of a resource technically changes between versions, which REST purists dislike.
Header versioning keeps one clean URL and moves the version into a header, either a custom one like Accept-Version: 2 or a versioned media type like Accept: application/vnd.github.v3+json. GitHub used that media type for years, and now takes a date in an X-GitHub-Api-Version header. It keeps URLs stable and is closer to HTTP content negotiation, but it is harder to test in a browser, easy to forget, and proxies and caches must be told to vary on that header.
Query parameter versioning uses ?version=2 or ?api-version=2024-01-01. Azure uses date-based query versions. It is trivial to add and remove, but it muddies caching because the same path now returns different bodies, and it is easy to leave the parameter off by accident. Stripe takes a fourth route entirely: the version is pinned per account in the dashboard settings and overridable with a Stripe-Version header, so old integrations stay frozen on the date they were written.