API keys vs. OAuth: How to manage both without building two systems
A practical framework for storing, rotating, and revoking third party credentials, no matter which protocol the provider speaks.
Every product that connects to other apps eventually runs into the same wall. The first few integrations use OAuth, so you build a token store, a refresh job, and a connect flow around it. Then a customer asks for a tool that only offers API keys, and suddenly none of that machinery applies. You're not extending your integrations feature anymore. You're maintaining two of them.
This piece is about why that split happens, and how to design around it so you're managing one credential lifecycle instead of two.
Why teams end up with two systems
OAuth and API keys look similar from a distance. Both end with your backend holding a secret it can use to call another service on a user's behalf. But the shapes underneath are different enough that a data model built for one doesn't hold up for the other.
- OAuth gives you a token that expires, a refresh token to renew it, and a scope that was negotiated during a redirect flow.
- API keys give you a static string the user pastes in, with no expiry and no refresh step, and often no way to know what it's scoped to without calling the provider's API and checking.
Teams that build their integrations feature around OAuth first tend to bake those assumptions into the schema: a refresh_token column, an expires_at column, a background job that runs on a schedule. None of that describes an API key. So when the next integration request comes in for a tool that only supports keys, the fastest path is a second table, a second connect UI, and a second set of endpoints that "happen to do something similar." Multiply that by a few more providers with their own quirks, and the integrations feature becomes the part of the codebase nobody wants to touch.
The fix is to stop modeling the protocol
The way out isn't to pick better column names. It's to stop treating "OAuth connection" and "API key connection" as two kinds of thing your application knows about, and instead treat them both as a single kind of thing: a credential your backend can exchange for access to a provider, on behalf of a specific user or organization.
Everything that differs between OAuth and API keys (how the secret was obtained, whether it expires, how it gets refreshed) becomes an implementation detail of how that one credential type is fulfilled, not a fork in your application's data model or API surface.
Concretely, that means your integration code should ask for a token and get one back, without needing to know or care what's underneath. This is the approach WorkOS Pipes takes: one method call for any connected provider, whether that provider authenticates with OAuth or an API key behind the scenes.
Whether github above is an OAuth provider or provider: 'granola' is authenticated with an API key behind the scenes doesn't change this call. The credential type becomes something you configure once per provider, not something every call site needs to branch on.
What a unified credential layer actually needs to handle
Whether you build this yourself or lean on a platform that already has it, the requirements are the same regardless of protocol:
- Storage. Every credential, key or token, needs to be encrypted at rest. A pasted API key sitting in plaintext in your database is exactly as much of a liability as a plaintext OAuth refresh token, and it's tempting to treat the "just an API key" case as lower stakes when it isn't.
- Rotation. OAuth tokens rotate automatically through the refresh flow. API keys don't expire on their own, which means rotation has to be something you can trigger deliberately, ideally as a single idempotent call rather than a manual delete-and-recreate.
- Revocation. When a user disconnects an integration, access needs to stop immediately, not "next time the token would have expired anyway." This matters more, not less, for API keys, since there's no natural expiry to fall back on if revocation doesn't work cleanly.
- Scoping. OAuth scopes are negotiated during authorization, so you know upfront what a token can do. API keys often grant broad access by default, so scoping has to be enforced on your side, by only requesting the operations you actually need and treating the key as more powerful than it looks.
- A consistent connect experience. From the user's point of view, connecting a tool that uses an API key shouldn't feel like a different product feature than connecting one that uses OAuth. Both should live in the same widget or settings page, with the underlying auth flow (redirect vs. paste-a-key-in) as the only visible difference.
Where this still gets hard
You still need a way to detect when an API key has been revoked upstream, since there's no failed refresh to tell you. You still need per-provider handling for keys that are scoped differently than you'd expect. And agents or background jobs that run without a human present need credentials that stay valid unattended, which is a stronger requirement for API keys than for a well-behaved OAuth refresh cycle.
None of that is a reason to keep two systems. It's a reason to make sure your one system has clear extension points for provider-specific quirks, rather than assuming every provider will behave the same way once you've abstracted away the protocol.
The takeaway
The mistake isn't picking OAuth or API keys. It's letting the protocol a provider happens to use dictate the shape of your entire integrations feature. Treat the credential as the thing your application manages, and the auth method as a detail of how that credential gets fulfilled, and adding the next provider becomes a configuration change instead of a new subsystem.
If you'd rather not build that layer yourself, WorkOS Pipes already handles storage, rotation, and revocation for both OAuth and API key providers behind one API. Check out the docs to see how it fits into your integrations feature.