Let your users bring their own API key
Ship an AI feature without paying for everyone's usage, and without becoming the place their keys get stolen from.
Every product that wraps a paid API eventually meets the same wall. You can pay for your users' usage, which turns a signup into a cost and makes free tiers a target. Or you can ask each user for their own key — and instantly become a company that stores other people's credentials.
The second option is usually chosen quickly and regretted slowly. A key in your database is a key you are responsible for: it needs encryption at rest, key rotation, an incident plan, and an answer for the security questionnaire that asks how you protect it. You did not set out to build a secrets manager, but you now operate one.
Bring Your Own Key done properly means the key never reaches you at all. The user holds it. Your application asks for a result, not for a credential.
What "we encrypt it at rest" actually buys you
Most BYOK implementations store the key encrypted in the application database. This is better than plaintext and it is not the same as not having it. The application can decrypt — that is the point of storing it — so anything that can run code as the application can decrypt too.
That includes a compromised dependency, an SSRF that reaches your internal config service, a backup restored to a laptop, and an engineer with production access on their last day. Encryption at rest defends the disk. It does not defend the process, and the process is what attackers reach.
- A breach of your database is a breach of every user's upstream account.
- You inherit their blast radius: their OpenAI bill, their Stripe account, their customer data.
- You cannot prove you did not read the key, which is the question an enterprise buyer will ask.
The shape that removes the problem
The user's key lives in their own vault, encrypted in their browser under a master password we never receive. Your site never sees it. Instead, your site asks the extension to make a specific call, and receives the response.
The credential is attached inside the extension's service worker, after the user has approved the request, and the reply your page receives contains the result and nothing else. There is no point in the flow where the key is a value your JavaScript could read.
- Your site sends: which service, which operation, and the request body.
- The user approves, per site and per service, and can revoke it later.
- Your site receives: the API response. Status and body. Never the credential.
Why you name an operation and not a URL
This is the part most designs get wrong, and it is worth being explicit about because it is the difference between a real guarantee and a slogan.
If your site could specify an arbitrary URL, then a site holding "OpenAI" approval could call the endpoint that lists the account's API keys — and read the credential out of the response body. The key never crossed the boundary and it leaked anyway.
So the caller names an operation from a fixed table, and the host and path come from that table alone. There is no arrangement of your input that produces a request to somewhere we did not choose. The table deliberately contains nothing that reads, creates or revokes credentials, and a test enforces that by pattern — because the realistic threat is not an attacker, it is a contributor adding a convenient endpoint in two years.
What integrating will look like
Registration first: you register your origin and receive a client id. An unregistered origin is refused silently — no dialog, nothing for a user to click through. Prompt fatigue is the attack against a permission model, and you lose to it by letting strangers generate prompts.
Then the flow is three calls: check whether the extension is present, request approval for a service, and make operations. Approval is per origin and per service, expires, and appears in the user's permissions list where they can revoke it.
What this does not do
Not a disclaimer. A page with no limits section is marketing, and the reader here is deciding whether to build against us.
- It only works where the extension is installed. This is an augmentation for users who have it, not a replacement for your own key path — you will still need a fallback, and designing for both from the start is easier than retrofitting one.
- Only operations on the allowlist are possible. If your product needs an endpoint that is not there, it needs a catalogue change and a review, not a config flag. That is the cost of the guarantee.
- The user can revoke at any time, and should be able to. Your integration has to handle a permission disappearing mid-session as an ordinary state rather than an error.
- It does not stop a user pasting their key into your form anyway. Nothing can. It removes the reason to ask.
- The response passes through your page, so whatever the API returns is visible to your JavaScript. If an operation returns sensitive account data, that data reaches you — the guarantee is about the credential, not about everything the credential can see.
Questions
What is BYOK (bring your own key)?
A model where each user of your product supplies their own credential for a third-party API, rather than your product paying for everyone's usage on a single shared key. It moves the cost and the rate limit to the person generating the load, which is usually fairer and always cheaper for you.
Why not just encrypt user API keys in my database?
Because your application has to be able to decrypt them to use them, so anything that can execute as your application can decrypt them too — a compromised dependency, an SSRF, a restored backup, a departing engineer with production access. Encryption at rest protects the disk, not the running process, and the running process is what gets reached.
How can my site use a key it cannot see?
Your site names an operation and sends a request body. The extension decrypts the key inside its own service worker, attaches it to the outbound request, and returns the response to your page. The credential is never a value in your JavaScript, so there is nothing for your code — or a script injected into your page — to read.
Can I make any API call I want with the user's key?
No, and that restriction is what makes the model worth anything. Calls are limited to an allowlist of operations per service, and the allowlist deliberately excludes anything that reads, creates or revokes credentials. Otherwise a site with approval for a service could simply ask that service to hand over the key.
What does the user actually see?
An approval window naming your site, the service, and what you are asking to do. They approve per service, not once for everything, and the grant appears in their permissions list where they can revoke it. Sites that are not registered never reach this step at all.
Is this available now?
Not yet. The vault, the local crypto and the proxy are built and tested; what is not enabled is third-party access, because that means allowing every website to message the extension. That is a deliberate decision with a Chrome Web Store review attached, and we would rather publish the design and let you judge it than ship the permission first.
What happens for users who do not have the extension?
Nothing changes for them, so you still need your existing path — whether that is your own key or asking them to paste theirs. Treat this as a better option for the users who have it rather than a replacement, and build the detection in from the start.
Does this work for server-side calls?
No. The key is decrypted in the user's browser, so the request originates there. If your architecture needs the call to come from your backend, then your backend needs the credential, and no amount of design removes that — the honest answer is that this pattern does not fit that shape.