TL;DR: On client projects I treat every API key as something that belongs to the client, lives in exactly one place, can do as little as possible, and can be replaced in five minutes without anyone panicking. In practice that means: keys never ship inside a mobile app or a Unity build, never sit in a repo or a chat thread, every webhook is verified before it is trusted, each key is scoped to one project and one job, and there is a written list of which keys exist and how to rotate each one. None of this is exotic. It is a small set of habits that turns "a key leaked" from a crisis into a chore.
Most of my work touches someone else's paid accounts. A voice agent for a clinic or a garage means Retell, a Twilio number, a CRM like GoHighLevel, n8n, maybe a calendar and an LLM provider. A mobile product like the ones I shipped at RAQTS means Firebase, App Store Connect, the Play Console and a backend. Every one has a key, and every key is a small bill or a small breach waiting to be found.
My pieces on what a client should own and handing a voice agent over cover whose name is on each account. This is the other half: how I handle the credentials day to day.
Why this matters more on client work
On your own product, a leaked key is your problem and your bill. On client work it is worse in two ways.
- The blast radius is someone else's business. A leaked Twilio credential can send SMS from a clinic's number. A leaked CRM key can read every lead an agency has ever captured. The person who pays for the mistake did not make it.
- There are more keys than anyone remembers. A single voice agent pipeline can easily involve six or seven services. Across several clients that becomes dozens, and the ones nobody remembers are the ones that never get rotated.
So the goal is not perfect secrecy. It is small damage and a fast fix.
Rule one: keys never ship to a device
This is the one I see broken most often, and it is the most expensive. Anything you ship to a user's device, a React Native app, a Unity build, a web page's JavaScript, should be assumed readable: bundles unpack, Unity builds decompile, browser code is just sitting there.
So a secret key for an LLM provider, a voice platform, a payment provider or an admin API does not belong in client code, however obfuscated. The pattern I use is boring: the app calls my backend, the backend holds the key, calls the third party and decides what each user may do. The app holds, at most, a key that was designed to be public, like a Firebase web config that is protected by security rules rather than by being hidden.
This matters most with AI features in apps like LectureNotes AI. Calling the model straight from the app saves a server, and lets anyone lift the key and run workloads on your bill. A thin backend in the middle also gives you somewhere to put rate limits and per-user quotas, which matters for the unit economics of an AI app as much as for security.
Rule two: one home for every secret
Every project gets one place where secrets live, and nowhere else counts. Depending on the stack that is the hosting platform's environment variables, a cloud secret manager, or the credential store inside n8n. What it is never:
- A committed
.envfile. The repo gets a.env.examplewith the names and no values. - A Slack message, an email or a Google Doc called "keys".
- A hardcoded string "just for testing" that quietly becomes the production path.
When a client needs to send me a key, I ask them to add it directly to the secret store if they can, or to send it through something that expires, and then to tell me once it is in place. If a key does end up in a chat thread, I treat it as leaked and rotate it.
Same for git history: a committed secret is not removed by a later commit. Rotate it. Rewriting history is tidying; rotating is the fix.
Rule three: scope every key to one job
Most platforms let you create more than one key, and many let you limit what a key can do. I use that every time.
- One key per project, per environment. The staging agent and the production agent do not share credentials. Two clients never share a key, even if I set up both accounts.
- Least privilege where the platform allows it. If an integration only needs to create contacts and book appointments in a CRM, it should not hold a key that can delete the whole account. GoHighLevel's sub-account level access is a good example of where choosing the narrower scope costs nothing.
- Name keys after what uses them. "n8n post-call workflow, Clinic X, prod" is a name you can make a decision about at 11pm. "API key 3" is not.
Scoping is what makes rotation cheap. If one key powers one workflow, replacing it touches one workflow. If one admin key powers everything, nobody ever rotates it, because nobody knows what breaks.
This is the same instinct as building kill switches before experiments: design things so the emergency action is small and safe.
Rule four: verify every webhook
Voice agent systems run on webhooks. Retell sends call events, n8n receives them, the CRM gets updated, an SMS goes out. Every one of those receiving endpoints is a public URL, and a public URL that acts on whatever is posted to it is an open door.
So every webhook I build checks that the request really came from where it claims. Where the platform signs its webhooks, the receiver verifies the signature with the shared secret before doing anything. Where it does not, I use a long random token in a header or the path and reject anything without it, and I keep the endpoint's job narrow so a forged request cannot do much.
Two details matter more than they look:
- Verify before you parse and act. The check happens first, and a failed check returns an error and stops. It does not log a warning and carry on.
- Treat payload data as untrusted even after verification. A verified webhook from your voice platform still contains whatever the caller said. That is the second-hand injection risk I covered in voice agent security and prompt injection, and it is why I never let transcript text flow straight into anything that executes.
When I connect Retell to n8n, this check is part of the first node, not something added later.
AI coding tools and secrets
I build a lot of client work with Claude Code, and at Fortell AI the pipeline to build and test agents in minutes used Claude Code with Comet browser automation. AI tools change the secrets picture in a specific way: anything in the working directory or the browser session is something the tool can read.
My habits here are simple. Real production keys are not in local .env files on projects where a coding agent is working; local and test environments get their own scoped keys. Browser automation runs against test agents and test accounts, not a client's live CRM. And I review diffs before committing, partly for code quality and partly because a generated config file is exactly where a pasted key likes to hide.
The rule is the same as everywhere else: give each actor, human or automated, the narrowest key that does its job.
Rule five: write down how to rotate each key
The single most useful security document on a small project is a plain list. For each key: what service it belongs to, which account owns it, what uses it, where it is stored, and the steps to replace it. It does not contain the keys themselves.
That list turns a leak into a chore. With it, the steps are: create the new credential, update the secret store, confirm the workflows that use it still run, revoke the old one. In that order, because revoking first is how you take a live phone line down at lunchtime.
It also makes offboarding clean. When a freelancer, me included, rolls off a project, the list says exactly which keys they could see and which to rotate. That is part of every handover pack I put together, and it is one of the first things I look for when inheriting a codebase. If it does not exist, writing it is usually day-one work.
Spend limits are a security control
The most common real-world consequence of a leaked key is not stolen data. It is a bill. LLM APIs, telephony and SMS all charge by usage, and a leaked key can run up real money overnight.
So I set spend alerts and caps wherever the platform supports them, and I make sure the alert goes to someone who will read it. For voice agents I also watch call volume, because a sudden spike of calls at 3am is either a marketing win or something wrong, and you want to find out which before the invoice does. Knowing your normal, as with call spikes and concurrency, is what lets you spot abnormal.
A checklist for your next project
If you are a founder or an agency starting a build, these are the questions I would ask your developer, and the ones I hold myself to:
- Is every account in the client's name, with the developer added as a user rather than as the owner?
- Is there one secret store per environment, and is nothing secret in the repo, the app bundle or the chat history?
- Does every app, mobile or Unity, talk to a backend instead of holding third-party secret keys?
- Is every key scoped to one project, one environment and the smallest permissions available?
- Does every webhook verify where it came from before acting?
- Are spend alerts or caps set on every usage-billed service?
- Is there a written list of every key and how to rotate it?
Seven yeses is not perfect security, but it is a project where a leaked key costs an hour, not a client. It is mostly discipline, not tooling, and the discipline is cheapest on day one.
I build production AI voice agents, mobile and Unity apps, and the backends that hold them together for startups and agencies in the US, UK and Europe, and I set projects up so the client owns everything from the start. If you want a build done that way, or a second pair of eyes on an existing setup, read more about my background, or book a call.