TL;DR: Give each outbound agent its own FirstSales API key, sized to one job, at workspace level, with only the scopes that job needs. Treat the key as the blast radius: when something goes wrong, the key decides how bad it gets.
#Why the key matters more than the prompt
Teams spend hours on agent prompts and minutes on agent credentials. That ratio is backwards.
A prompt is a request. A key is a capability. When an agent reads a hostile reply or a confusing instruction, the prompt may fail, and then only the key stands between a mistake and a real action.
Outbound makes this sharper than most work. The actions an agent could take with a broad key are the ones you cannot take back: emails sent to the wrong people, contacts deleted, campaigns started, domains blocked or unblocked.
A sent email stays sent. The OWASP project lists this class of problem as excessive agency in its top 10 for large language model applications: an agent given more functionality, permission or autonomy than the task needs.
The cure for excessive agency is not a better prompt. It is a smaller key.
This post is about making that key small, on purpose, and keeping it that way. It is written for the person who is about to hand a FirstSales key to a script, a scheduled job or an AI agent.
If you are still choosing between interfaces, read CLI, MCP or the app first. This post covers the credential side that applies whichever you choose.
#The point of view: size the key to the job, not the agent
Most teams create one key per agent. "The Claude Code key", "the reporting bot key", "the intern's automation key".
That is the wrong unit. An agent does many jobs, and the key ends up as wide as the widest job.
The better unit is the job. A morning read-only standup is one job. A weekly report is another. A webhook receiver that adds contacts is a third.
Each job gets its own key, with only what that job touches. The same agent may hold three keys across three tasks, never loaded at once.
This sounds like more work, and it is, for about an hour. After that it pays back every time something breaks, because the broken thing has a name and a small reach.
The idea is old. Saltzer and Schroeder wrote it down in their 1975 paper The Protection of Information in Computer Systems, as the principle of least privilege: every program and every user should operate with only the privileges it needs for the job at hand. Paraphrase aside, the point stands fifty years later.
For outbound, the question to ask is simple. If this key leaked today, what is the worst thing someone could do with it?
If the answer makes you uneasy, the key is too wide.
#What a FirstSales key can and cannot do
The FirstSales CLI README describes API keys as scoped and access-level aware. Four rules from its safety model carry most of the weight.
- Workspace keys can only operate workspace-scoped resources.
- Organization keys need explicit organization scopes.
- Revoked keys fail immediately.
- Keys fail closed if the creator loses organization access or is suspended.
A fifth rule governs secrecy. Raw keys are shown only once at creation and are never retrievable later.
The CLI also keeps secrets out of output. API errors and identity output do not print raw or hashed keys, and only api-keys create prints the raw key, once.
#What the CLI itself adds
The CLI is a thin layer over the public Developer API. The README says it maps commands to /api/v1 endpoints and lets the API enforce auth, scopes, tenant isolation, rate limits, idempotency and redaction.
That matters for trust. The check that stops an over-reaching call happens on the server, not in a script you could edit.
The CLI adds convenience guards on top. Destructive commands stop unless you pass --confirm, and --dry-run previews a request without auth or network calls.
Those guards help a careful operator. They do not replace a narrow key, because an agent can pass --confirm just as easily as you can.
#Commands for key management
Three commands manage keys, and one reads usage.
firstsales api-keys listshows existing keys for an organization.firstsales api-keys createmakes a key and prints the raw key once.firstsales api-keys revokekills a key, and it needs--confirm.firstsales usage getreads the organization's API usage.
The README example for creating a key looks like this.
bashfirstsales api-keys create \ --org org_123 \ --data '{"name":"Codex automation","scopes":["campaigns:read","contacts:write"],"accessLevel":"workspace"}' \ --json
The two scopes shown, campaigns:read and contacts:write, are the only scope names in the README. The pattern is a resource, a colon and an action.
This post uses that pattern to explain how to size a key. For the full list of scope names, use the Developer API documentation, and do not guess them.
#Workspace keys and organization keys
The accessLevel field in the example chooses the key's reach. This is the first and biggest sizing decision.
A workspace key operates only inside one workspace. It cannot call organization management endpoints.
If you try, the CLI troubleshooting table says to expect Workspace API keys cannot access organization management endpoints. The fix there is an organization-level key with explicit organization scopes, which is usually the wrong fix.
An organization key can reach across workspaces and manage things like members, groups, billing and other keys, as far as its explicit scopes allow.
#Default to workspace
Almost every outbound agent job lives inside one workspace. Campaigns, contacts, lists, inbox and connectors are all workspace resources.
So default to workspace keys. A leaked workspace key exposes one workspace, and a leaked organization key could expose your whole account.
#When an organization key is justified
Some jobs are organization jobs. Billing credits, usage reads and key management are organization level in the CLI, as the --org-only commands show.
A weekly report that includes credit balance needs organization read access for that one call. Split it: use a workspace key for campaign reads, and a separate narrow organization key for the billing read.
Never make a key organization level just to avoid the work of splitting. That convenience is paid for later.
#Who may create organization keys
Limit the humans who can create organization keys to one or two owners. Anyone who can mint a key can hand out access, so key creation is itself a privileged action.
Treat the api-keys create permission with the same care as admin access. The post on MCP tool permissions in sales makes the same point about tools.
#A job-to-access matrix
Here is a way to decide what each key should hold. The table lists common outbound jobs, the access level, and whether the job needs to write.
Scope names are placeholders in the pattern resource:action. Replace them with the real names from the Developer API docs.
| Job | Access level | Reads | Writes | Needs delete | Can start campaigns |
|---|---|---|---|---|---|
| Daily standup (read-only) | workspace | campaigns, inbox, connectors, alerts | ✗ | ✗ | ✗ |
| Weekly report | workspace plus a narrow org read | campaigns, billing | ✗ | ✗ | ✗ |
| Signal receiver | workspace | suppression, lists | contacts, list members | ✗ | ✗ |
| Lead import from CSV | workspace | lists | contacts, imports | ✗ | ✗ |
| Reply triage assistant | workspace | inbox | draft approve or reject | ✗ | ✗ |
| Campaign operator (human-led) | workspace | campaigns | workflow edits | ✗ | ✓ |
| Cleanup script | workspace | contacts | contact delete | ✓ | ✗ |
| Key rotation script | organization | api-keys | api-keys create and revoke | ✓ | ✗ |
Look at the last two columns. Almost no job needs to delete, and almost no job needs to start a campaign.
Only one row can start campaigns, and that row is human-led. The key is held by a person or by a tool a person watches.
#Reading the matrix
The standup and weekly report jobs are the safest. They are pure reads, so a leak exposes data but changes nothing.
For those, see the posts on the daily outbound standup prompt and weekly campaign reports with cron. Both are designed around a read-only key.
The signal receiver is the first job that writes. It adds contacts and list members, and nothing more, as described in start a campaign when a buying signal fires.
#Keep delete and start on separate keys
Two capabilities deserve their own keys: delete and campaign start. Neither should ride along with a routine job.
contacts delete and other delete commands cannot be undone. A key that can delete should live with a person who reads each command, or in a script with a reviewed target list.
A key that can start a campaign should follow the rule in every hands-on post here: run campaigns readiness and campaigns send-test-email before campaigns start. See start, pause and resume campaigns from the CLI for the sequence.
#An agent that can reach the inbox
Reading the inbox adds a risk that other reads do not. Reply text is written by strangers.
An agent with inbox read access and any write or send capability fits a pattern Simon Willison calls the lethal trifecta: access to private data, exposure to untrusted content, and a way to communicate externally. Remove any one leg and the attack stops.
The simplest leg to remove is the external action. Give an inbox-reading agent a key that cannot send or approve anything.
#Create a key step by step
Here is the lifecycle of one key in a single view.
graph TD A[Write the job in one sentence] --> B[Pick workspace or org level] B --> C[Add only the scopes the commands need] C --> D[Create key, store in secret manager] D --> E[whoami and one boundary test] E --> F[Record in register] F --> G[Watch usage and alerts] G --> H{90 days or incident} H -->|rotate| C H -->|leak| I[Revoke first, then investigate]
Here is the process to follow each time. It takes a few minutes once you have done it twice.
#Step 1: write the job down
Write one sentence: "This key lets X do Y in workspace Z." If you cannot finish the sentence, you do not yet know what to grant.
List the exact commands the job will run. A key sized to a list of commands is easy to review.
#Step 2: look up the scopes
Match each command to the scope it needs in the Developer API docs. Add only those.
You can also learn what is missing by trying. If a command fails with insufficient_scope, the README says to create a key with the required resource scope. Add one scope at a time and re-test, rather than starting wide.
#Step 3: create the key
bashfirstsales api-keys create \ --org org_123 \ --data '{"name":"standup-reader-ws123-2026-10","scopes":["campaigns:read"],"accessLevel":"workspace"}' \ --json
The raw key appears once in the output. Copy it straight into your secret store.
Never add --idempotency-key to this command. The README states it is unsupported for api-keys create because raw keys are reveal-once.
#Step 4: confirm what the key is
Set the key in the environment for a shell session, and ask the CLI who it is.
bashexport FIRSTSALES_API_KEY="<YOUR_FIRSTSALES_API_KEY>" firstsales whoami --json
The README says whoami validates the active token and prints the user, key, organization and workspace context, without mutating anything. Check that the output names the workspace you intended and nothing wider.
Replace the placeholder by pasting from your secret manager in your own terminal. Do not paste the real value into a chat with an AI tool, a ticket or a document.
#Step 5: test the boundary
A key you have not tested at its limits is a key you have not verified. Try one thing it should not do, and confirm it fails.
For a read-only key, try a harmless write with --dry-run removed and a throwaway target in a test workspace. The expected result is insufficient_scope.
Do this in a test workspace, never against production data. The point is to see the refusal with your own eyes once.
#Step 6: record it
Write the key's name, owner, job, scopes, creation date and review date in a short register. A spreadsheet is enough.
The register is the document that makes an audit possible. The post on AI campaign audit trails covers what to record about what agents do, and the key register is the first piece.
#Name keys so an audit makes sense
A key list full of names like "test" and "new key 2" is useless in an incident. You need names that answer four questions at a glance.
Who uses it, for what job, in which workspace, and when was it made.
A good pattern is job-holder-workspace-yyyymm. Examples are standup-reader-ws123-202610 and signal-receiver-prod-202610.
#Why include the date
The date tells you how old a key is without opening a register. Old keys are the ones to review first.
Pick a rule, such as "review any key older than 90 days". The name makes the rule easy to enforce by eye.
#Why include the holder
When a key shows up in a usage spike, you want to know which system to look at. A holder in the name, such as receiver or cron, sends you straight to the right place.
Avoid personal names in key names when a team shares a job. People move on, and the name stays.
#Store the key without leaking it
A narrow key still deserves care. Most leaks are boring.
#Where keys leak
The common leaks are source control, chat history, shell history, logs, screenshots and shared documents. A prompt to an AI tool is a newer entry on that list.
The first rule in every FirstSales post on this topic is the same. Never paste a key into a prompt.
A pasted key travels with the conversation. It may be stored, logged or visible to anyone with access to that session.
#How to pass a key to a tool
Use environment variables or a protected file. The CLI reads FIRSTSALES_API_KEY, and its resolution order is --api-key, then the environment variable, then a profile.
Prefer the environment variable for automation. The README says the CLI can read local profiles, but automation should prefer environment variables, and profile files and keys should never be committed.
Avoid --api-key on the command line where you can. Command-line arguments show up in process lists and shell history.
#A protected env file
For a scheduled job, put the key in a file that only the job's user can read.
bashinstall -m 600 /dev/null ~/.config/firstsales-job.env # edit the file in a text editor and add: # FIRSTSALES_API_KEY=<YOUR_FIRSTSALES_API_KEY>
Source the file inside the script. Add the file to your ignore rules so it never reaches version control.
#Secret managers
For anything beyond a laptop, use your platform's secret manager. Give the job read access to its own secret only.
A secret manager also gives you an audit log of who read the secret, and a single place to rotate it.
#Keep keys out of agent context
Agents read files. If a key sits in a file the agent can open, the agent can read it into its context, and from there into logs and summaries.
Keep the env file outside the agent's working folder, and load it in the wrapper script, not in the agent's instructions. The agent should never see the value, only use the tool that holds it.
The post on MCP security for outbound goes deeper on keeping secrets out of model context.
#The leaver risk: keys fail closed
The README makes a quiet but important promise: keys fail closed if the creator loses organization access or is suspended.
This is a good security property. When someone leaves and their access is removed, every key they created stops working.
It is also an operational hazard for scheduled jobs. A weekly report key created by a contractor will stop the week after the contractor leaves, and nobody will remember why.
#Plan for it
Create job keys from a stable account, such as a shared owner account that survives staff changes, if your policy allows it. Where it does not, record the creator in the key register and review it at offboarding.
Add the key register to your leaver checklist. When someone departs, list the keys they created and re-issue the important ones from another account before their access ends.
#Alert on auth failures
An auth failure on a scheduled job is the signal that a key died. The CLI returns exit code 3 for auth errors.
Make your jobs alert on exit code 3, not just log it. The weekly report post shows how to wire that alert.
#Why failing closed is right
The alternative is a key that keeps working after its owner is gone. That is how orphaned access builds up in every organization.
A failing job is a noisy, fixable problem. A forgotten live key is a silent one.
#Watch what a key does
A key you never watch is a key you cannot trust. Watching has two parts: usage numbers and behavior.
#Usage numbers
The CLI has usage get, which reads the organization's API usage. Read it on a schedule and compare it to the week before.
bashfirstsales usage get --org org_123 --pretty
Look at the real output first, as this post does not assume its fields. Then pick the few numbers that matter and track them.
A sudden jump in calls from one key is the earliest sign of trouble. It may be a runaway loop, a retry storm or a stolen key.
#Behavior
Usage tells you how much. Behavior tells you what. Check what changed: new contacts, new list members, edited workflows, paused campaigns.
The campaign events and activities reads help here. The standup post uses them to give you a daily view of what happened and who did it.
#Alerts
The CLI has alerts list, alerts ack and alerts resolve. Read alerts as part of your daily check, since some of them concern your sending health.
For a longer view of watching agents themselves, see monitoring outbound agents.
#What to do on a spike
When usage spikes, do not investigate first. Revoke first and investigate after.
A revoked key fails immediately, so the damage stops. You can create a replacement in minutes, and a fresh key costs nothing.
#Rotate and revoke
Keys should not live forever. Set a rotation period and hold to it.
#A rotation routine
The README gives a rotation sequence: list keys, create a new one, then revoke the old one.
bashfirstsales api-keys list --org org_123 --json firstsales api-keys create \ --org org_123 \ --data '{"name":"standup-reader-ws123-202611","scopes":["campaigns:read"],"accessLevel":"workspace"}' \ --json # switch the job to the new key, confirm it works with whoami, then: firstsales api-keys revoke --org org_123 --key key_123 --confirm --json
Switch the job to the new key and confirm it works before you revoke the old one. Revoking first leaves a gap in which the job fails.
Ninety days is a reasonable starting period for job keys. Shorten it for keys that can write.
#Why --confirm
api-keys revoke is a destructive command, so the CLI stops unless you pass --confirm. Add it only when the revoke is intentional.
If you see destructive_confirmation_required, the guard worked. Read your command once more before adding the flag.
#Use --dry-run to rehearse
Preview the revoke without running it.
bashfirstsales api-keys revoke --org org_123 --key key_123 --dry-run
The README says --dry-run prints the request with redacted headers and sends nothing. It makes a good last look at the key id.
#Revocation in an incident
If you suspect a leak, follow this order. First revoke the key, then check usage and recent changes, then create a replacement, then find how it leaked.
Do not delay the revoke to collect evidence. The evidence stays in your logs.
#Retire unused keys
Once a quarter, list keys and compare them to the register. Revoke any key that no job uses.
Stale keys are the most likely to leak, because nobody is watching them.
#Keys, MCP and OAuth are different doors
It is easy to confuse the three ways in. They use different credentials, and mixing them up leads to wrong advice.
#CLI and Developer API: keys
The CLI and the Developer API use API keys. That is what this post covers.
#The product MCP: OAuth
The FirstSales product MCP server uses OAuth with PKCE. The material this post draws on says developer API keys and cookies are not accepted there.
So you do not create an API key for Claude Code to use over MCP. You sign in through the client, for example with the /mcp command in Claude Code.
Claude Code's MCP documentation describes adding an HTTP server with claude mcp add --transport http, and authenticating through the /mcp command. The standards behind it are OAuth 2.0 in RFC 6749 and PKCE in RFC 7636.
#What that means for access control
With OAuth, the access you grant is tied to the signed-in person and the grant they approve. The tool list is per tenant and per grant, and the live tools/list call is the authority.
Treat the connected session like a key too. Sign in with an account that has the access you want the agent to have, and no more.
For setup details, read the FirstSales MCP setup for Claude Code. For an explainer, see what MCP is for sales. The MCP specification covers the protocol itself.
#One more door: the app
People also have logins to the app. A person's password and the keys they create are separate things, and revoking one does not revoke the other.
When someone leaves, remove their app access, their MCP grants and the keys they created. All three.
#Fewer tools, smaller risk
Whatever the door, a smaller tool surface is safer. The post on too many MCP tools explains why a huge tool list also hurts agent accuracy.
#When an agent hits insufficient scope
Eventually an agent will fail with insufficient_scope. How you respond decides whether your keys stay narrow.
#The wrong response
The tempting fix is to add scopes until the error goes away, or to swap in a wider key. After a few rounds, every key is wide.
Do not do this by reflex. First ask whether the agent should be doing that action at all.
#The right response
Follow three questions in order.
- Is the action part of this job? If not, the agent is off task, and the refusal is correct.
- If it is part of the job, which single scope does it need?
- Does that scope cross a line you set, such as delete or start? If so, route the action to a human.
Add the one scope only after the answers say yes. Update the key register when you do.
#Log refusals
Refusals are useful data. A key that gets many insufficient_scope errors may belong to an agent that is confused, or that has been told to do something unexpected.
Count them in your daily check. A burst of refusals deserves a look at what the agent was asked to do.
#Other errors to know
The README troubleshooting table lists the main ones. missing_api_key means the variable is not set, and invalid_api_key means the key is wrong or revoked.
Workspace ID is required means you forgot --workspace on a workspace command. These are setup mistakes, not security events, but a sudden invalid_api_key on a working job suggests a revoke.
#Comparing key strategies
The table compares four ways teams handle keys for agents.
| Strategy | Blast radius if leaked | Easy to audit | Survives a leaver | Matches least privilege |
|---|---|---|---|---|
| One admin key shared by everything | ✗ whole account | ✗ | ✗ | ✗ |
| One key per agent, broad scopes | ✗ most of a workspace | ✗ | ✗ | ✗ |
| One key per job, workspace level, narrow scopes | ✓ one job | ✓ | ✗ until planned | ✓ |
| One key per job, narrow, with register, rotation and leaver check | ✓ one job | ✓ | ✓ | ✓ |
The first row is how many teams start, because it is quick. It is also how a single leaked file becomes a full account incident.
The last row costs a little process. It is the only one that stays safe as the team and the number of agents grow.
#What to do first if you are on row one
Do not rebuild everything at once. Start with the riskiest job and the one most likely to leak.
Replace the shared admin key in that job with a narrow one. Then work through the rest one job at a time, revoking the old key last.
#A note on tooling
Keep a plain list of commands each job runs. The CLI documents firstsales commands --json as the local registry of commands, which helps you build that list from the source of truth.
#A one-hour key audit
You can do a first audit in an hour. Here is the sequence.
#Minute 0 to 15: list
List all keys for each organization you own.
bashfirstsales api-keys list --org org_123 --pretty
For each key, write down its name, creator, access level and scopes. If a key has no clear owner, mark it for revocation.
#Minute 15 to 30: match to jobs
Match each key to a job. Any key you cannot match to a running job goes on the revoke list.
For each matched key, compare its scopes to the commands the job runs. Mark any key with extra scopes for replacement.
#Minute 30 to 45: replace
Create narrow replacements for the marked keys. Switch each job over and test with whoami and one real command.
Revoke the old key after the new one works. Use --dry-run first on each revoke, then --confirm.
#Minute 45 to 60: set the schedule
Set a review date for each remaining key, add a calendar reminder for the quarterly review, and add the key register to your leaver checklist.
Finish by reading usage once with usage get, so you have a baseline to compare against next month.
#Do not skip the people part
The audit is quick because it is mostly lists. The harder part is the habit.
Make "which key does this need?" a standard question in every automation review. Teams that ask it early rarely end up with wide keys.
For the surrounding terminal workflow, see running outbound from the terminal. For guardrails around agent behavior, see email agent guardrails. For CRM fields that make agent work safer, see agent-ready CRM fields.
#FAQs
#What is the safest way to give an AI agent access to FirstSales?
Give it a dedicated key for one job, at workspace level, with only the scopes that job needs. Keep campaign start and delete off that key.
#Should each agent have its own key?
Better, each job should have its own key. One agent may do several jobs, and a single key sized to the widest job is wider than any single job needs.
#What is the difference between a workspace key and an organization key?
A workspace key operates only inside one workspace. An organization key needs explicit organization scopes and can reach organization-level resources like billing and keys.
#Which one should I choose by default?
Choose workspace. Campaigns, contacts, lists and inbox are workspace resources, and a leaked workspace key exposes only one workspace.
#How do I create a key with the CLI?
Use firstsales api-keys create with a --data JSON body naming the key, its scopes and its access level. The raw key is printed once, so store it immediately.
#Can I retrieve a key after creation?
No. Raw keys are shown only once and are never retrievable later. If you lose one, revoke it and create a new one.
#Can I use an idempotency key when creating an API key?
No. The README says --idempotency-key is unsupported for api-keys create because raw keys are reveal-once.
#Where do I find the full list of scopes?
In the Developer API documentation. The CLI README shows only campaigns:read and contacts:write as examples, so look up the others there instead of guessing.
#What does a key fail closed mean?
If the person who created a key loses organization access or is suspended, the key stops working. That prevents orphaned access, and it also means scheduled jobs can die when a creator leaves.
#How do I avoid jobs dying when someone leaves?
Record each key's creator in a register, review the register at offboarding, and re-issue important keys from a stable account before the person's access ends.
#How should I store a key?
In an environment variable or a protected file, or better, a secret manager. Never in source control, a prompt, a ticket or a shared document.
#Why should I avoid --api-key on the command line?
Command-line arguments appear in process lists and shell history. Use FIRSTSALES_API_KEY or a protected env file instead.
#How do I revoke a key?
Run firstsales api-keys revoke --org <org> --key <key> --confirm. Revoked keys fail immediately.
#Should I revoke first or investigate first in an incident?
Revoke first. A revoked key stops the damage at once, and your logs keep the evidence for later.
#How often should I rotate keys?
Ninety days is a reasonable start for job keys, and shorter for keys that can write. Create the new key and test it before you revoke the old one.
#Does the product MCP use API keys?
No. The product MCP uses OAuth with PKCE, and developer API keys and cookies are not accepted there. You sign in through the client instead.
#How do I see what a key is doing?
Read firstsales usage get on a schedule and compare it to earlier weeks. Also review campaign events, activities and alerts for changes.
#What do I do when a job fails with insufficient_scope?
Ask whether the action belongs to the job. If it does, add the single scope it needs, and if it crosses a line like delete or start, route it to a human.
#Is --confirm a safety control against agents?
Only partly. It stops accidents by an operator, but an agent can pass the flag too. The narrow key is the real control.
#Do delete commands have an undo?
No. contacts delete and other delete commands cannot be undone, so keep delete off routine keys and behind a human.
#Conclusion
An agent is only as safe as the key it holds. The prompt can drift, the input can be hostile and the model can be wrong, and a narrow key keeps each of those from becoming a loss you cannot reverse.
Here are the takeaways.
- Size the key to the job, not to the agent. One key per job, at workspace level by default.
- Grant only the scopes the job's commands need, and keep delete and campaign start off routine keys.
- Never paste a key into a prompt or a post. Pass it through an environment variable or a secret manager.
- Remember that keys fail closed when their creator leaves, so keep a register and add it to your leaver checklist.
- Watch usage with
usage get, and when something looks wrong, revoke first and investigate after. - Before any
campaigns start, runcampaigns readinessandcampaigns send-test-email, and treat delete commands as permanent.
Do the one-hour audit this week. Revoke the keys nobody can explain, and replace the widest one.
Then read email agent guardrails for the behavior side, and running outbound from the terminal for how keys fit a daily workflow. To run outbound on a platform built with scoped access, try FirstSales.