Authenticating server-to-server with the Norbelys API
API keys, why you rotate them, where to store them safely, and why a key for your own backend is a different risk than a key handed to a third-party agent.
By Gabriel Lara, Developer Relations, Norbelys
Founder-reviewed ·How we research and correct articles
A browser can authenticate with a Clerk session — a human signed in, a cookie or short-lived token doing the work. A cron job, a backend service, or an unattended script can’t do that; there’s no human sitting at a sign-in form at 3am. That’s what an API key is for, and getting the basics right — what the key actually grants, where it lives, and when you rotate it — matters more for a headless integration than for almost anything else you’ll build against the Norbelys API.
The three ways to authenticate, and which one is this
Norbelys accepts three credential shapes on /v1, and it’s worth knowing
all three even though this post is really about one of them:
- Clerk session — a signed-in human in the browser app. Not applicable to server-to-server calls.
- Organization-scoped API key (
Authorization: Bearer ak_...) — created in the dashboard under Settings → API keys, baked with a specific organization at creation time. This is the server-to-server credential. - OAuth 2.1 access token — minted through Clerk’s OAuth flow for
interactive agents (Claude, Cursor, and similar clients) that ask a human
to pick an org at consent; the token then carries an
org_idclaim. This is the right shape for a client a human is actively driving, not for a service running unattended.
For a backend job, a scheduled script, or a server calling Norbelys on its
own without a human in the loop, the ak_ key is the answer. It rides in
the standard header on every request:
Authorization: Bearer ak_live_...
What the key actually grants
This is the detail worth being precise about: an org-scoped key grants the
whole organization’s authority, the same as a human logged into that
org through the dashboard. There’s no narrower permission tier under an
ak_ key today — read-only vs. read-write, or “senders only” vs. “billing
too,” isn’t a dial you turn per key. The key’s scope is the org, full stop.
That’s not a gap so much as a fact you need to design around. It means the question “who or what holds this key” carries all the weight that a finer-grained permission system would otherwise carry.
Storing it safely
The rules here aren’t Norbelys-specific — they’re the same discipline any credential with org-wide reach deserves — but they’re worth stating plainly because the failure mode is so common:
- Never in client-side code. A key bundled into a browser app or a
mobile client is a key anyone with devtools open can read out of the
network tab or the JS bundle. If a browser needs to call the API on a
user’s behalf, that’s what the Clerk session is for — the browser
authenticates as the signed-in human, not as an
ak_key. - Never committed to git. Not in a
.envfile that got added by accident, not in a config fixture “just for local testing,” not in a commit that gets squashed later — a key that ever touched history is a key you should treat as burned. - In an environment variable or a secrets manager, whichever your
deploy target already gives you — Cloudflare Secrets Store bindings if
you’re running on Workers, your platform’s secrets manager elsewhere.
The CLI’s own config file (
~/.config/norbelys/config.json) is written with mode0600specifically so it isn’t world-readable on a shared machine — the same instinct, applied automatically, is worth copying into wherever your own service stores its key.
That $NORBELYS_API_KEY environment-variable convention is exactly the
shape you want for a CI pipeline or a scheduled job: the secret lives in
the CI platform’s own secret store, gets injected as an env var at runtime,
and never touches a file on disk that could be checked in by mistake.
Why you rotate it
A key with org-wide reach is, by definition, a single point of failure for the whole workspace’s data — every contact, every sender, every message. Rotating on a schedule, and immediately on any suspected leak, limits how long a compromised key stays useful even if you don’t yet know it’s compromised. The practical flow:
- Create a new key in the dashboard.
- Deploy it to whatever holds the old one — your secrets manager, your CI config, the service’s environment.
- Confirm calls are succeeding on the new key (check logs or a health endpoint that hits the API).
- Revoke the old key.
- Watch that traffic on the old key actually stops — if a forgotten service is still using it, you’ll see the errors immediately rather than assuming rotation finished cleanly.
That last step is the one people skip, and it’s the one that catches a credential you forgot you’d handed to a script three deploys ago.
A key for your own backend vs. a key for a third-party agent
Because an ak_ key grants the whole org and nothing narrower, where you
put that key matters as much as how you store it.
A key living in your own backend — a service you wrote, deployed, and can audit — sits inside a trust boundary you already control. If that service needs to touch senders, programs, and people across the whole workspace to do its job, giving it a full org-scoped key is a normal, defensible architecture choice. You already trust the code; the key just matches the scope the code needs.
A key handed to a third-party agent, a partner’s integration, or an MCP
client someone connects to their IDE is a different situation entirely,
even though it’s the exact same credential shape. That agent’s actual job
might be “read replies and post a Slack alert” — see the Slack alert
build for exactly that
example — and yet an unscoped ak_ key handed to it can also archive
senders, launch campaigns, and read every contact’s private notes, because
there’s no way to hand out less. The Norbelys MCP server
is deliberately built as a pure passthrough for exactly this reason: it
holds no credentials of its own and forwards whatever bearer the caller
supplies, so the MCP server itself never becomes a place where authority
accumulates beyond what the connecting client’s own key already grants.
That design choice pushes the actual scoping decision back onto you — which
key you generate, and who or what you give it to.
Since Norbelys doesn’t yet offer per-key permission scopes, the practical answer for a third-party or agent integration is to mint a key dedicated to that integration rather than reusing your main backend’s key, so a compromised or misbehaving agent’s blast radius is at least isolated to “whatever that one key touched,” and revoking it doesn’t take your own backend down with it. For the deeper version of this — how to think about least-privilege when the credential itself can’t be scoped — see scoping MCP API keys for least privilege.
Where this fits with the rest of the platform
Every state-changing POST in the API also accepts an Idempotency-Key
header, independent of which credential authenticated the call — worth
knowing once you’re past initial auth and into actually calling the send
door or the jobs endpoint from a server. And if you’re weighing calling
/v1 directly against going through a generated client, which SDK to
use covers that decision; the auth
header is identical either way.
Server-to-server auth FAQ
Can one API key be scoped to just a few resources, like senders only?
Not today — an ak_ key grants the whole organization's authority, the same as a signed-in user in that org. There's no per-resource or read-only tier on the key itself.
Should I use OAuth or an API key for a backend service?
An API key. OAuth 2.1 in this system is built for interactive clients where a human picks an org at consent — a backend job running unattended has no consent step to go through, so a pre-issued org-scoped key is the right shape.
What's the actual risk of putting an API key in frontend code?
Anyone who opens devtools or reads the JS bundle gets a credential with your entire organization's authority — every contact, sender, and message. A browser should authenticate as the signed-in human via the Clerk session, never with an ak_ key.
How is this different from scoping an MCP key for an AI agent?
Same underlying mechanism — an org-scoped ak_ key — but the risk calculus differs because an agent's blast radius if compromised or manipulated is harder to bound than a backend service you wrote yourself. The dedicated post on scoping MCP keys covers that case specifically.