Thinking In Public
Web APIs are ubiquitous. They are used by mobile applications, single page apps, and server side software. There are a plethora of formats, although HTTP and GraphQL are ubiquitous. If you're (un)lucky enough you might come across a SOAP API every now and then.
One of the things that's frustrated me about web APIs, is how annoying they are to version. Versions are an ill-fit for software that isn't shrinkwrapped, and as a result versions applied to web APIs are janky at best. Date based and monotonic integers are quite common representations of versioning for APIs. But even with those, what does it actually mean when a version changes? The question that most consumers have is "do I need to change my code?". Using something like OpenAPI can help with knowing what has changed between versions and whether the new version will require code changes, so we aren't at a complete loss for ways to understand what a version means.
More annoyingly, most of the time versions wind up sticking around forever. This is mostly because there is no forcing mechanism to force upgrades, other than aggressively turning them off. For example, let's say you've created version 2 of an API and it's much better than the previous version. Some people will move to it right away, others will build new apps starting with version 2, but some will stay with the previous version indefinitely because they don't want to rewrite any parts of their app. There isn't much that you can do as an API maintainer for the group that won't migrate. Except for, as mentioned, turning off the old version of the API.
There are two core problems here. First, many API designs don't anticipate the need to change in the future. Second, there is no way to actually know when a version of an API is actually completely out of use.
One can guess when an API version is no longer in use. For example, if there are metrics on each API endpoint, then eventually you'll see those values hit zero for long enough that you can assume that no one is using that version anymore. Of course, if there's an app runs once every nine months, then you might shut down the API version without knowing consumers are still active.
What we are missing is a defined contract between the API provider and the API consumer. An implicit contract exists between these parties but in effect, we need to provide a definition for a point in time when "no one is using this version of the API". That's not the only thing that needs to be defined, but it's a major component. What about the terms of service? While they might spell out the ability for a provider to discontinue a version of the API, what would be better is a contract that is more explicit for both sides.
An explicit contract can provide this definition. The date when an API version is no longer being used is the date when all contracts that include that API version have expired. Providers can also factor this information in when deciding what to do with a deprecated API. Instead of just turning it off, they can offer addenda on a contract renewal to continue using the deprecated API by paying an additional sum. A provider could then price it such that they keep some version of the previous API running, perhaps as a transformation gateway, that can be used by those with the proper addenda in their contracts.
For consumers, API contracts provide a way to definitively know what their application can and cannot use, and assurances that the app will continue to be able to call API endpoints until the contract expires. Upon expirations both sides can negotiate renewals that can include changes for deprecated or removed API endpoints. Another useful property for consumers is that they can make it clear within their apps how long a specific build will be valid for. This will give downstream users of the app a clear understanding of how long they can use a specific build of the app.
Contracts provide another benefit for those downstream consumer. With some exposed configuration they can negotiate with the API provider directly to establish a new contract and keep the version of the API required by the application running. This also ensures that if an app maintainer decides to stop updating the app that users aren't out of luck in the future.
Contracts don't require a specific kind of API implementation. Much like with authentication and authorization, you can do it for any kind of API. Contract enforcement is actually a form of authorization.
But how would contracts be implemented in code? For the consumer, it would happen in the same way as getting an API key. There would be a dashboard that allows the consumer to retrieve a contract key. In most cases, this key would be a bearer token, since all that it indicates is the contract that an API call is authorized against. Things like authentication, rate limiting, and other authorization would still be handled via an API key. If the application is server side, then a public private keypair with request signing could be used instead. A sophisticated implementation could actually track which API keys have been issued to which accounts and check if the contract key is associated with that same account, or even with that specific API key. In this way, there wouldn't be any need to keep contract keys secret, as they can only be used with an associated API key.
It is important that the contract key be its own separate entity. There is little reason for a contract key to ever change. In this way, it's not even a key, it's more like a contract identifier. If a contract is renewed without any changes, there is no need to change the contract identifier. Even if there are changes, there probably isn't a reason to change the contract identifier. The entire purpose of this identifier is to tell the API provider which contract this API call wishes to be authorized under. Once a contract expires, any attempts to authorize a call under it would fail. If a contract requires payment to stay active, then perhaps the 402 HTTP status code could be used to indicate such.
Why not just associate require a contract to be chosen when generating an API key? Well, if the API key is tied to a specific account then you wouldn't necessarily want the user to have to manually select some key. For client side applications, the contract identifier would be usable with any API key. As mentioned earlier, this might not always be the case, such as when a specific app build uses a contract that is no longer valid but a specific user wishes to continue using that specific build. In that case the API provider would issue a new contract identifier that is tied specifically to API keys generated by the specific user account that made the contract.
The separation also enables middleware that authorizes contract identifiers could be built, and it could be completely separate from the rest of the authorization flow, which might be more difficult if the contract reference is kept elsewhere. Additionally, having it be separate means that the same API key can be used with multiple contract identifiers. For example, a regular user API and an administrative API might be provided under different contract terms, but users who are admins would still be able to use the regular user API.
For providers a contract does add some overhead, but not much. API calls should already be authorized based on some set of information, contracts add a couple additional checks. So instead of seeing if the user has the right permissions to access the information, the provider would need to perform those checks and then also check if the contract provides the correct permissions.
The format of an API contract can be pretty loose, as long as both the provider and consumer have an understanding of what is allowed and for how long. Much like legal contracts, there doesn't need to be a specific format a contract is provided in, although in many cases the more formally structured the better. It would be nice if we could have machine readable contracts, that way when they are signed and payment is confirmed the system can load the contract and begin enforcing it. If a provider wanted to use a manual process, they could. They would just provide a mechanism that the contract team could use to enter a contract's parameters so the authorization system begins enforcing it.
Could this fully replace versioning, though? Well, it depends on how far you want to go. In theory, each endpoint could be different depending on the contract being used and versioning becomes an internal thing. That is, if a specific contract ID is used then the endpoint might execute version 1 of the logic, but if a newer contract ID is used then the endpoint might execute version 2 of the logic. In most cases the provider would want to explicitly version their endpoints. Ideally, this would be done with an HTTP header or with a query parameter. If neither of those is specified then the contract can specify a default version. For forward compatibility, contracts could stipulate that the current version and all future version made available during the contract's effective period are usable by the consumer.
Ultimately, when we create and use web APIs, what we seek is clarity. We want to know things explicitly, not implicitly. We want assurances about what we can do. We don't want to deprecate an API endpoint or an API version and hope for the best; we want to have a specific date when we know that we can remove the code for that version of the API.
There are many open questions about API contracts. For example, how would the contract itself be structured? How long should a given contract be valid for? How are addenda and riders handled?
These questions shouldn't stop people from using this idea. Initially, we likely don't need highly detailed contracts. Something as simple as "consumer X can use API versions Y and newer until date Z" is enough to get started. Then when date Z is approaching, renegotiation can be done and the contract might be amended to "consumer X can use API versions B and newer until date C".
Clarity for all parties is the goal. We don't need to change or remove the things that we are doing. An extra bit of authorization is what is actually required.