Conclude uses two kinds of keys, modeled after Stripe's pattern. The right one depends on where the call originates.
Where to find your keys
Publishable key (
pk_live_...) — product Settings → Embed (per-board).Secret key (
sk_live_...) — workspace Settings → API Keys.
The two key types
pk_live_... — Publishable key
Where to use it: Browser / client code. The React SDK, the script tag, and any client-side
fetchcall to/api/v1/widget/*.Safe to expose: Yes. It's designed for browser code.
Stored as: Plaintext (it's not a secret).
Scope: Restricted to widget routes (
/api/v1/widget/*). Cannot be used for server-only actions.
If a publishable key leaks, the worst-case is someone submitting feedback against your board. There's no privileged data they can read.
sk_live_... — Secret key
Where to use it: Server-side only. Backend scripts, build tools, internal automation.
Safe to expose: No. Treat it like a database password.
Stored as: Hashed (SHA-256). Conclude doesn't store the plaintext after generation.
Scope: Full workspace access — read and write everything.
If a secret key leaks, regenerate it immediately from Settings → API Keys.
Using a key
Pass the key as the x-conclude-api-key header:
curl https://www.conclude.fyi/api/v1/widget/... \
-H "x-conclude-api-key: pk_live_..."
There's no separate auth endpoint or token exchange. The key is the credential.
Key rotation
Rotating the publishable key
Generate a new pk_live_... from product Settings → Embed. Both old and new keys work for a short window so you can update your widget code without downtime. The old key can then be revoked.
Rotating the secret key
From workspace Settings → API Keys, click Regenerate. The old key is immediately invalidated. Update any backend integration that depends on it.
Why two keys
The split exists so you can embed Conclude in browser code without giving the browser permission to do everything.
The widget on your marketing site uses
pk_live_. If a customer pulls it out of network tab, no big deal.A backend script that imports legacy feedback uses
sk_live_. That key never goes anywhere except your server.
A widget route called with a sk_live_ key is rejected (403). A server-only route called with a pk_live_ key is rejected (403). The errors will surface immediately during integration.
Storing keys
Production: environment variable.
Local dev:
.env.local, never committed.Frontend (publishable only):
NEXT_PUBLIC_CONCLUDE_API_KEYin Next.js,VITE_CONCLUDE_API_KEYin Vite.Never: in source code, in version control, or in client-side JS that uses a secret key.