Skip to content

Publishable API keys: designing keys that are safe in a browser

What makes a publishable API key safe to ship in a browser bundle: scoped capabilities, origin allowlists and their limits, quotas, test keys and rotation.

The ProxifyEdge team 7 min read

Some API keys are meant to be seen. Payment providers, map services and backend-as-a-service platforms all issue publishable API keys that you are expected to put in your frontend bundle. They are safe to publish not because they are hidden, but because of how they are designed.

This article breaks down what makes a key publishable, how origin allowlists really work (and where they stop working), and the other controls a browser-facing key needs.

What makes publishable API keys different

A secret key answers the question “who is calling?” with “someone trusted”. A publishable key cannot make that claim, because anyone can copy it out of your JavaScript. So it has to be designed around a different question: what is the worst thing a stranger can do with this key?

Good publishable keys keep that answer small through four properties:

  1. Narrow capabilities. The key can only perform actions that are safe for anyone to trigger.
  2. Context binding. The key only works from the places you expect, such as your own website.
  3. Bounded usage. Quotas and rate limits cap the damage of abuse.
  4. Easy rotation. Replacing a key does not take your site down.

If you cannot answer the “worst thing” question comfortably, the credential does not belong in the browser at all. Keep it on a server, as described in You can’t hide an API key in frontend code.

Narrow capabilities come first

The strongest protection is a key that simply cannot do anything harmful. A payment provider’s publishable key can collect card details into a token, but it cannot list customers or issue refunds. A database service’s anonymous key is filtered by row-level security rules, so it only sees rows the rules allow.

When you design your own API keys, start here. Give browser keys their own type with an explicit, minimal permission set, rather than reusing a server key with a few things switched off.

Origin allowlists: how they work

The most common form of context binding is an origin allowlist: the key only works for requests coming from the sites you list, such as https://app.example.com.

It relies on the Origin request header. Browsers attach Origin to cross-origin requests, and page script cannot change it: Origin is a forbidden header name in the Fetch standard. So a server can trust that a request from a real browser on https://evil.example will say so.

Two details catch people out:

  • Origin: null exists. Sandboxed iframes, pages opened from file:// URLs and some redirect chains send the literal value null. Never put null on an allowlist.
  • Origins are exact. https://example.com, https://www.example.com and http://example.com are three different origins. A trailing slash or a path is not part of an origin.

Allowlists stop reads, not necessarily sends

This is the most important limit to understand. If the allowlist is enforced only through CORS response headers, it controls whether the browser lets the page read the response. It does not by itself stop the request from reaching your server.

Preflights do not close that gap as often as people expect. A preflight is an OPTIONS request that lists the names of the custom headers the real request will carry, in Access-Control-Request-Headers, but never their values. If your key travels in a header such as X-API-Key, the server cannot tell from the preflight which key, and therefore which allowlist, is involved.

Request from an unlisted siteWhat the browser doesWhat the server has to do
A “simple” GET, key in the query stringSends it; hides the response unless the origin is allowedReject it outright if it must not be processed
A request with the key in a custom headerSends a preflight first, without the key’s valueAnswer the preflight, then check Origin against the key on the real request
A script outside a browser (curl, a bot)Not involvedRely on quotas, rate limits and signed URLs

So the practical rule is: check Origin on the server for the real request, and for anything that changes state, reject a disallowed origin instead of merely leaving out the CORS headers. Prefer sending keys in a header anyway: URLs end up in server logs, browser history and Referer headers far more often than headers do.

Allowlists do nothing against non-browser clients

curl can send Origin: https://app.example.com as easily as any other header. An origin allowlist protects your key from other websites. It does not protect it from scripts. That is why the next two controls exist.

Bound usage with quotas and rate limits

Assume your publishable key will be used by someone you did not intend. Quotas and rate limits decide how much that costs you:

  • A monthly or daily quota caps total usage per key.
  • A per-second or per-minute rate limit stops bursts and scraping.
  • Usage visibility in a dashboard tells you when something is wrong before the bill does.

When a client hits a limit, return HTTP 429 with a Retry-After header so well-behaved clients back off. We cover the client side in API rate limiting for frontend developers.

When you need a hard guarantee: signed URLs

If a key copied from your bundle must not be usable from a terminal at all, origin binding is not enough. The fix is to remove the reusable credential from the browser: your server, which holds a real secret, signs each URL the browser may use, with a short expiry. A stolen signed URL only works for one target and only for seconds. See Signed URLs explained.

Test keys for development

Developers need a key that works on localhost, in online sandboxes and on preview deployments, without adding every temporary hostname to an allowlist. The usual answer is a separate test key that accepts any origin but is scoped or limited so that publishing it costs little. It must not carry credentials such as cookies.

Make the distinction obvious: a visible prefix or mode in the key, a different badge in the dashboard, and a clear warning when a test key is used in production.

Rotation without downtime

Keys leak, get committed to public repositories and outlive the people who created them. Rotation should be routine, not an emergency. Two features make it painless:

  • Overlap. The old key keeps working for a bounded time after the new one is issued, so deployed bundles have time to update.
  • Stable settings. Rotation replaces the secret value but keeps the key’s allowlist, limits and other configuration.

How ProxifyEdge keys work

ProxifyEdge keys follow these patterns, so you can ship one in a browser bundle and call third-party APIs through the proxy (API keys reference):

  • Live keys answer only the origins you list, in exact scheme://host[:port] form, up to 50 per key. A wildcard is not allowed on a live key. A new key starts live with no origins, so it works from curl but not from a browser until you add one.
  • Test keys answer any origin, without credentials, for development. Switch to live before you ship.
  • Responses are only readable from the origins you list: a page on another site gets no CORS headers, so the browser blocks the read. Origin locking does not stop a request from being sent, so keep quotas sensible.
  • The key is sent in the X-API-Key header. A key query parameter exists for elements such as <img> that cannot send headers.
  • Plans set request quotas, and each key can have its own requests-per-minute limit. Every response reports usage in X-Proxify-RateLimit-* headers.
  • Rotation issues a new key with the same settings, and can keep the old one valid for an overlap of up to seven days.
  • Signed URLs can be required per key, for the cases where origin locking is not enough.

You can try a key without writing code in the playground.

A checklist for browser-facing keys

QuestionGood answer
What can a stranger do with this key?Only safe, low-cost actions
Which sites can use it?An exact list of origins, no null, no wildcard in production
How is it sent?In a header, and checked against Origin on every request
What stops scripts and bots?Quotas, rate limits, and signed URLs where needed
How do we rotate it?Self-service, with an overlap window and unchanged settings
How would we notice abuse?Usage per key, visible in a dashboard

Key takeaways

  • A publishable API key is safe because of its design, not because it is hidden.
  • Keep capabilities narrow first; context binding and quotas are layers on top.
  • Origin allowlists rely on a header browsers cannot forge, but they stop reads, not necessarily sends, and they do nothing against non-browser clients.
  • A preflight never carries a header’s value, so check Origin against the key on the real request, and reject disallowed origins for state changes.
  • Pair every publishable key with quotas, rate limits, a test-key story and painless rotation, and use signed URLs when you need a hard guarantee.

Are you sure?