CrewAI · Multi-agent · API key security
CrewAI API key management: per-agent keys in multi-agent pipelines
CrewAI crews typically pass a single Stripe or Twilio API key to every agent in the pipeline via environment variables. That works in demos. In production, it creates five problems that make incident response, cost attribution, and revocation unnecessarily hard. This page covers what those problems are and how to fix them.
TL;DR
Sharing one Stripe or Twilio key across all CrewAI agents means: no per-agent spend attribution, race conditions on rate limits, one compromised agent revokes for all, no per-agent endpoint scope, and no audit log joined on agent_id. The vault-key pattern issues each agent its own scoped vault_key_xxx backed by the real secret held in a proxy. Each key has its own policy (spend cap, endpoint allowlist, expiry), can be revoked independently, and produces audit rows labeled with the issuing agent. Two environment variables per agent; zero production code changes.
The five shared-key problems
1. No per-agent spend attribution
When the Billing Agent and the Notification Agent both call Stripe with the same key, the Stripe Dashboard shows total spend across both. If spend spikes, you can't tell which agent drove it without cross-referencing your application logs — assuming you have application logs that capture agent identity and Stripe request IDs. Most crews don't.
2. Rate-limit contention between agents
Stripe's rate limits are per-API-key, not per-process. If the Billing Agent fires 25 PaymentIntents in rapid succession and the Notification Agent simultaneously calls customers.retrieve, one of them hits the rate limit. The error surfaces as a tool failure in whichever agent got the 429 last — which may not be the one that caused the contention.
3. One compromised agent revokes for all
If the Fulfillment Agent's environment is compromised and the attacker exfiltrates the Stripe key, you rotate it. That rotation breaks every other agent that shares the key: Billing Agent, Refund Agent, Support Agent. You have to redeploy or restart every process that uses it — during a security incident, when you least want operational complexity.
4. No per-agent endpoint scope
A single restricted key covers the union of permissions needed by all agents. If the Billing Agent needs payment_intents:write and the Refund Agent needs refunds:write, the shared key has both. Now the Billing Agent — if misaligned — can issue refunds, and vice versa. Least-privilege is impossible with a shared key.
5. No audit log joined on agent identity
After an incident, you want to answer "which agent made this call, in which run, on whose behalf?" Stripe logs request_id and created_at; it doesn't know about your agent_id or crew_run_id. Reconstructing the timeline requires correlating Stripe timestamps with your application log timestamps — which is hard when agents are concurrent.
The vault key pattern for CrewAI
The vault key pattern issues each agent its own scoped credential. The agent holds a vault_key_xxx rather than the real Stripe secret. A proxy (Keybrake) holds the real secret and enforces the policy attached to the vault key on every call. Each vault key is agent-specific, has its own spend cap, endpoint allowlist, and expiry, and produces audit rows tagged with the issuing key.
In a CrewAI crew, the setup is per-agent environment configuration:
from crewai import Agent, Task, Crew
billing_agent = Agent(
role="Billing Agent",
goal="Process customer payments",
tools=[charge_customer_tool], # uses BILLING_VAULT_KEY
verbose=True
)
refund_agent = Agent(
role="Refund Agent",
goal="Process refunds",
tools=[issue_refund_tool], # uses REFUND_VAULT_KEY
verbose=True
)
# .env for billing agent context
BILLING_VAULT_KEY=vault_billing_abc123
STRIPE_BASE_URL=https://proxy.keybrake.com/stripe/v1
# .env for refund agent context
REFUND_VAULT_KEY=vault_refund_xyz789
STRIPE_BASE_URL=https://proxy.keybrake.com/stripe/v1
Each vault key has its own policy configured on the Keybrake dashboard:
# Billing agent vault key policy
{
"vendor": "stripe",
"daily_usd_cap": 10000,
"allowed_endpoints": ["POST /v1/payment_intents", "GET /v1/customers/*"],
"expires_in": "24h"
}
# Refund agent vault key policy
{
"vendor": "stripe",
"daily_usd_cap": 2000,
"allowed_endpoints": ["POST /v1/refunds"],
"expires_in": "8h"
}
How the five problems collapse
| Problem | Shared key | Vault key per agent |
|---|---|---|
| Spend attribution | Total spend only; can't split by agent | Per-vault-key spend in audit log; each agent's usage queryable independently |
| Rate-limit contention | Shared quota; agents compete | Still shares the real-key quota at the proxy level — but audit log shows which agent drove contention |
| Compromised agent | Rotate the shared key, break all agents | Revoke the one vault key; other agents continue uninterrupted |
| Endpoint scope | Union of all agents' permissions | Per-agent allowlist; billing agent can't refund, refund agent can't charge |
| Audit log join | Stripe request_id only; no agent context | Every audit row has vault_key_id → maps to agent; joinable on agent_run_id if passed as metadata |
Passing agent context in request metadata
For the audit log to be maximally useful, pass the crew_run_id and agent_id in the request metadata. Keybrake stores these as extra columns on the audit row:
import stripe
stripe.api_key = os.environ["BILLING_VAULT_KEY"]
stripe.base_url = "https://proxy.keybrake.com/stripe/v1"
# Pass agent context as Stripe idempotency key metadata
stripe.PaymentIntent.create(
amount=2000,
currency="usd",
customer=customer_id,
metadata={
"crew_run_id": os.environ["CREW_RUN_ID"],
"agent_id": "billing_agent"
}
)
The proxy forwards the metadata to Stripe and also records it in the audit row, so your SELECT * FROM calls WHERE metadata->>'agent_id' = 'billing_agent' query works without additional instrumentation.
How Keybrake fits
Keybrake issues vault keys and enforces the policies. You configure each vault key's policy in the dashboard (or via API), and swap two environment variables per agent. The Free tier handles 1,000 proxied requests/month; the Hobby tier ($29/month) adds all three vendors (Stripe, Twilio, Resend) and 30-day log retention — enough for a production CrewAI crew.
Related questions
Does this work with CrewAI's Process.hierarchical mode?
Yes. In hierarchical mode, the manager agent delegates to sub-agents. You issue a vault key per sub-agent role; the manager agent itself doesn't need a Stripe key unless it calls Stripe directly. The policy on each sub-agent's vault key constrains what that agent can do regardless of what the manager tells it — so even if the manager is misaligned, it can't instruct the billing sub-agent to exceed its daily cap.
What about agents that are instantiated dynamically per customer run?
Issue vault keys via the Keybrake API at run time, with expires_in set to the expected run duration. Each run gets a fresh key; expired keys are automatically inactive. This is the "per-run key" pattern — it gives you forensic isolation between runs even when the same agent class handles multiple customers.
Will CrewAI's tool retry logic interact badly with the proxy's 429 on cap breach?
CrewAI inherits whatever retry behavior is in the tool implementation. If your Stripe tool uses the official Stripe SDK (which retries on 429 by default), you should either disable retries on cap-breach 429s or pass a custom max_retries=0 when initializing the Stripe client. The proxy's 429 on daily cap breach has a Retry-After set to midnight UTC, not a short interval — the SDK's exponential backoff is counterproductive here.
Further reading
- AI agent payment infrastructure in 2026 — the full category map including identity, policy, proxy, audit, and reconciliation layers.
- AI agent governance tools — the short list of controls that matter before you go to production with money-moving agents.
- AI agent audit trail — audit schema and sample queries for multi-agent systems, including the per-agent attribution queries.
- AI agent kill switch patterns — what happens when you need to stop one agent mid-run without stopping the others.