AI agents · Idempotency · Stripe · Advanced patterns
Advanced idempotency patterns for AI agents calling payment APIs
Basic idempotency key guidance says: generate a key before your Stripe call and reuse it on retry. For autonomous AI agents, the idempotency surface is more complex. Multi-step billing workflows where step 2 depends on step 1's result. Agents that crash, get restarted, and must not re-execute vendor calls that already succeeded. Parallel agent branches that each independently decide to charge the same customer. Spend caps that apply across all branches simultaneously. This page covers advanced idempotency patterns specifically for AI agents — including key composition strategies for multi-step workflows, crash-recovery state persistence, parallel branch isolation, and the interaction between idempotency keys and proxy-enforced spend caps.
TL;DR
Compose idempotency keys as {stable_id}-{step_name} for multi-step workflows so each step has its own key. Persist the workflow state (which steps completed and their results) to a database before each vendor call — use this state to skip completed steps on restart. For parallel branches, give each branch a unique sub-identifier in its idempotency key. Issue a single vault key per workflow run with a spend cap that covers the entire workflow's expected spend — not per-step keys.
Pattern 1: Idempotency key composition for multi-step billing workflows
A billing agent often executes a sequence of vendor calls: create a Stripe customer → create a payment method → create a payment intent → confirm the intent → send a Resend receipt. Each step must be idempotent independently, because any step can fail and require retry while the preceding steps have already succeeded.
The key insight: each step needs its own idempotency key, derived from the same stable root identifier:
import hashlib
def make_idempotency_key(run_id: str, step: str, *discriminators: str) -> str:
"""
Compose a stable idempotency key from a run ID and a step name.
Extra discriminators allow per-item keys in batch loops.
"""
raw = f"{run_id}::{step}::" + "::".join(discriminators)
# Hash to ensure consistent length and avoid special character issues
return hashlib.sha256(raw.encode()).hexdigest()[:48]
# Usage in a multi-step billing workflow
run_id = f"billing-{customer_id}-{invoice_id}" # stable across restarts
steps = {
"create_customer": make_idempotency_key(run_id, "create_customer"),
"create_intent": make_idempotency_key(run_id, "create_intent"),
"send_receipt": make_idempotency_key(run_id, "send_receipt"),
}
# Step 1: Create Stripe customer (idempotent — same customer returned on retry)
customer = stripe_proxy.post(
"/v1/customers",
idempotency_key=steps["create_customer"],
json={"email": customer_email}
)
# Step 2: Create payment intent (separate key — different operation)
intent = stripe_proxy.post(
"/v1/payment_intents",
idempotency_key=steps["create_intent"],
json={"amount": amount_cents, "customer": customer["id"]}
)
# Step 3: Send Resend receipt (separate key — different vendor, different operation)
receipt = resend_proxy.post(
"/emails",
idempotency_key=steps["send_receipt"], # Resend supports Idempotency-Key too
json={"to": customer_email, "subject": "Your receipt", ...}
)
With this key composition, if step 2 (create intent) fails and is retried, the retry uses the same create_intent key — Stripe returns the original intent rather than creating a duplicate. Step 1 (create customer) is not retried because its result is already in scope. The keys are deterministic: the same run_id and step always produce the same key, so even after a crash and restart, the same keys are reconstructed.
Pattern 2: Workflow state persistence for crash recovery
An agent that crashes mid-workflow must be able to restart without re-executing vendor calls that already succeeded. Idempotency keys alone don't solve this — if the agent doesn't know that step 1 succeeded, it will call step 1 again (which is idempotent, so not harmful), then call step 2 again (also idempotent, not harmful), but in a multi-step workflow with conditional branching, re-executing already-succeeded steps wastes time and causes audit log noise.
Persist workflow state to a database before each vendor call:
import sqlite3
import json
from datetime import datetime
class BillingWorkflowState:
def __init__(self, db_path: str):
self.conn = sqlite3.connect(db_path)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS billing_workflow_steps (
run_id TEXT,
step TEXT,
status TEXT, -- 'pending', 'succeeded', 'failed'
result TEXT, -- JSON blob of the vendor API response
completed_at TEXT,
PRIMARY KEY (run_id, step)
)
""")
def is_completed(self, run_id: str, step: str) -> bool:
row = self.conn.execute(
"SELECT status FROM billing_workflow_steps WHERE run_id=? AND step=?",
(run_id, step)
).fetchone()
return row and row[0] == "succeeded"
def get_result(self, run_id: str, step: str) -> dict | None:
row = self.conn.execute(
"SELECT result FROM billing_workflow_steps WHERE run_id=? AND step=?",
(run_id, step)
).fetchone()
return json.loads(row[0]) if row else None
def mark_succeeded(self, run_id: str, step: str, result: dict):
self.conn.execute("""
INSERT OR REPLACE INTO billing_workflow_steps
(run_id, step, status, result, completed_at)
VALUES (?, ?, 'succeeded', ?, ?)
""", (run_id, step, json.dumps(result), datetime.utcnow().isoformat()))
self.conn.commit()
# Crash-safe billing workflow
state = BillingWorkflowState("./data.db")
def run_billing_workflow(run_id: str, customer_id: str, amount_cents: int):
idempotency = lambda step: make_idempotency_key(run_id, step)
# Step 1: Create customer — skip if already done
if state.is_completed(run_id, "create_customer"):
customer = state.get_result(run_id, "create_customer")
else:
customer = call_stripe_proxy("/v1/customers", idempotency("create_customer"), ...)
state.mark_succeeded(run_id, "create_customer", customer)
# Step 2: Create payment intent — skip if already done
if state.is_completed(run_id, "create_intent"):
intent = state.get_result(run_id, "create_intent")
else:
intent = call_stripe_proxy("/v1/payment_intents", idempotency("create_intent"), ...)
state.mark_succeeded(run_id, "create_intent", intent)
# ... continue for remaining steps
Pattern 3: Parallel branches with isolated idempotency
Multi-agent systems where several agents execute simultaneously create a parallel idempotency problem: two agents independently decide to charge the same customer for the same invoice. Each agent generates its own idempotency key — but if they use the same logical root identifier without a branch discriminator, they generate the same key and Stripe deduplicates them into one charge (which is correct behavior). If they use different keys (because they're uncoordinated), Stripe creates two separate charges (which is a billing error).
The solution: assign each parallel branch a unique discriminator, included in the idempotency key, and coordinate which branch "owns" each charge at the workflow orchestration layer:
from enum import Enum
class BillingBranch(str, Enum):
PRIMARY = "primary" # Only this branch charges the customer
BACKUP = "backup" # Backup branch — skips charges if primary succeeded
def run_billing_branch(
run_id: str,
branch: BillingBranch,
customer_id: str,
amount_cents: int,
vault_key: str,
):
# Primary branch owns the charge
if branch == BillingBranch.BACKUP:
# Check if primary already charged before attempting
if state.is_completed(run_id, f"{BillingBranch.PRIMARY}::create_intent"):
print(f"Primary branch already charged — backup skipping")
return state.get_result(run_id, f"{BillingBranch.PRIMARY}::create_intent")
# Use branch-specific idempotency key to prevent cross-branch deduplication
step_key = f"{branch.value}::create_intent"
idempotency_key = make_idempotency_key(run_id, step_key)
intent = call_stripe_proxy(
"/v1/payment_intents",
idempotency_key=idempotency_key,
vault_key=vault_key,
json={"amount": amount_cents, "customer": customer_id},
)
state.mark_succeeded(run_id, step_key, intent)
return intent
Pattern 4: Idempotency and spend caps interact — design for it
Spend caps and idempotency keys interact in a non-obvious way: if a Stripe call succeeds (vendor charges the card) but the response is lost in transit (agent gets a timeout), the agent retries with the same idempotency key. Stripe returns the original intent — this is free (no second charge). But the proxy doesn't know the retry is idempotent — it sees a new incoming call and counts the parsed charge amount against the spend cap.
This means your spend cap should account for idempotent retries:
| Scenario | Actual spend | Proxy cap consumption | Design implication |
|---|---|---|---|
| 1 successful call, no retry | $50 | $50 consumed | Normal path — cap set to max expected workflow spend |
| 1 successful call + 1 idempotent retry (response lost) | $50 (Stripe deduplicates) | $100 consumed (proxy sees two calls) | Set cap to 2× expected spend to allow for one idempotent retry per step |
| 3 steps × 1 idempotent retry each | $150 (3 steps × $50) | $300 consumed (6 proxy calls) | Set cap to 2× expected total workflow spend; monitor audit log for retry rate |
Keybrake's audit log records the idempotency key on each call. If you see the same idempotency key appearing twice in the log, that's a retry — you can filter for duplicate idempotency keys to measure your retry rate and calibrate cap sizing accordingly.
Related questions
How long does Stripe store idempotency keys? Can I use the same key for a new billing cycle?
Stripe stores idempotency results for 24 hours from the original request. After 24 hours, the same idempotency key is treated as a fresh request and will create a new charge if the original request succeeded. For recurring billing workflows (monthly invoices, subscription renewals), always include a date or period discriminator in your idempotency key: f"charge-{customer_id}-{invoice_id}-{billing_period}" where billing_period is something like "2026-06". This ensures that a June billing run's keys don't collide with a July billing run — even if they share the same customer and invoice IDs. The 24-hour window is also why TTL-based vault keys (5 minutes for request-scoped, 30 minutes for task-scoped) don't interfere with Stripe's idempotency window.
Does the Keybrake proxy strip or forward the Idempotency-Key header to Stripe?
The Keybrake proxy forwards the Idempotency-Key header transparently to Stripe — your idempotency key is delivered to Stripe's API exactly as you set it. The proxy doesn't modify, replace, or generate idempotency keys. This means your idempotency design is fully preserved through the proxy layer: Stripe deduplicates based on your provided key, not on any proxy-generated identifier. The proxy adds its own audit log entry for each incoming call (whether or not it's an idempotent retry), which is why the same idempotency key can appear multiple times in the Keybrake audit log while appearing only once in Stripe's charge history.
What's the right idempotency strategy for a CrewAI or AutoGen multi-agent workflow where agents share tools?
In multi-agent systems where multiple agents share the same tool (e.g., a charge_customer tool available to both a billing_agent and a collections_agent), each agent must include its own agent identity in the idempotency key. A shared tool without agent-identity discrimination in the key will cause Stripe to deduplicate charges from different agents if they happen to use the same customer_id and amount at the same time — which may or may not be correct depending on whether the charges are logically independent. The safe default: always include agent_id or agent_role in the key (f"charge-{agent_id}-{customer_id}-{run_id}"). If your orchestrator intentionally wants agents to share charge attempts (primary + backup pattern), use a shared key explicitly and implement coordination logic as described in Pattern 3 above.
Further reading
- AI agent idempotency fundamentals — the basics of idempotency keys for AI agents calling vendor APIs, including when to use them and common mistakes to avoid.
- AI agent error handling — how idempotency keys interact with retry logic, and how to distinguish retryable from non-retryable errors when calling Stripe, Twilio, and Resend.
- Temporal AI agent API key — how Temporal's activity retry semantics interact with idempotency keys, and why you must pass idempotency keys through workflow state rather than generating them inside activities.
- AI agent Stripe spend cap — how proxy-enforced spend caps interact with idempotent retries, and how to size caps correctly when accounting for retry overhead.
- AI agent multi-tenant isolation — idempotency key namespacing strategies for multi-tenant agent deployments where multiple customers share the same agent infrastructure.