Skip to content
← BlogDeveloper

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_id claim. 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 .env file 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 mode 0600 specifically 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.
cli-auth
the CLI's own credential storage — the same pattern to copy for a service$ norbelys login --api-key ak_live_...Saved to ~/.config/norbelys/config.json (0600)or skip the config file entirely for CI$ export NORBELYS_API_KEY=ak_live_...

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:

  1. Create a new key in the dashboard.
  2. Deploy it to whatever holds the old one — your secrets manager, your CI config, the service’s environment.
  3. Confirm calls are succeeding on the new key (check logs or a health endpoint that hits the API).
  4. Revoke the old key.
  5. 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.