How to Log Every API Call an AI Agent Makes
Log the call where it is sent, not where it is described. Record the operation, the actor, the idempotency key, the policy decision and the outcome. Never record the credential. If the agent writes its own log, the calls that were blocked quietly disappear.

Key takeaways
- -The process that sends the request writes the log. If the model writes it, blocked and failed calls go missing.
- -Eleven fields cover almost every question you will be asked later: time, request id, idempotency key, actor, tenant, operation, arguments hash, policy decision, status, outcome and duration.
- -Never log the Authorization header, a refresh token or the full prompt. A log that lets someone replay a call has become a second credential store.
- -Log denied and held calls in the same table as allowed ones. The attempts that did not happen are usually the interesting rows.
- -Swytchcode records the command, the policy result and the outcome because it is the thing sending the request. swy audit network and swy audit policy read it back.
Log every API call at the place that sends it, not in the model's reply. Each record needs the operation, who it was for, the idempotency key, whether policy allowed it, the HTTP status and enough of the outcome to reconstruct what happened. It must not contain the access token or the full prompt.
That is the whole answer. The rest of this post is the schema, the reasoning and the mistakes that make a log useless on the day you need it.

Why the model must not write the log
This is the single decision that determines whether your log is worth having.
If you ask the agent to log its own actions, you get a record of what the agent believed it did. That record is usually accurate and occasionally fiction, and you cannot tell which from the inside. Worse, it is systematically incomplete in one specific direction: a call that was blocked by policy, or that failed before leaving your infrastructure, often never gets a line, because from the model's point of view nothing happened worth narrating.
Those are exactly the rows you will want. "Why did nothing happen at 2am?" is a more common question than "why did this happen," and only a log written by the execution path can answer it.
So the rule is: the process that puts bytes on the wire writes the record. Not the model, not the orchestration framework, not a wrapper around the orchestration framework. The thing that actually sends the request.

The fields that answer real questions
Here is the shape we would use. Eleven fields, and each one exists because somebody eventually asks a question it answers.
{
"ts": "2026-10-30T02:14:07.482Z",
"request_id": "req_01JBQ7X3F9",
"idempotency_key": "ik_8f2c1e0a",
"actor": { "agent": "support-triage", "run_id": "run_7742", "user": "svc-agent" },
"tenant_id": "cus_9fKq2",
"operation": "stripe.refund.create3",
"arguments_hash": "sha256:4b1a...e9",
"policy": { "decision": "allowed", "rule": "big-refunds", "version": "2026-10-12" },
"http_status": 200,
"outcome": { "ok": true, "resource_id": "re_3QxYz", "schema_valid": true },
"duration_ms": 412
}Why each one earns its place:
ts and request_id. Obvious, but make the request ID stable across retries of the same logical call so you can group them.
idempotency_key. The field that lets you prove a write happened once. When finance asks whether the customer was refunded twice, you group by this key and count the distinct resource IDs. Without it the question is unanswerable. The mechanics are in idempotency explained.
actor. Which agent, which run, which service identity. When one agent starts misbehaving at 4am you need to filter to it in one query.
tenant_id. In a multi-tenant product this is the most important field in the record, because the worst class of bug is a call made against the wrong customer's account. Log it even if you are single-tenant today. See multi-tenant OAuth for AI agents.
operation. A stable canonical identifier, like stripe.refund.create3, not a URL string the model assembled. URLs are not comparable across versions and a model-built URL tells you what the model thought, not what the API is.
arguments_hash. Hash the arguments rather than storing them. You can prove two calls were identical without keeping a copy of whatever was in them, which matters when the arguments contain customer data.
policy. Allowed, denied, or held for approval, plus which rule said so and which version of the rules was in force. This is the field that turns a log into an audit trail. If you can change the rules without the log recording which version applied, you cannot reconstruct a decision after the fact. That is the argument for keeping rules in git, made properly in policy-as-code for AI agents.
http_status and outcome. Status alone is not enough, because plenty of APIs return 200 with a failure inside the body. Record whether the response passed schema validation as a separate boolean. A 200 with schema_valid: false is one of the most useful rows you will ever log.
duration_ms. Cheap to record, and the first signal when a provider starts degrading.
What never goes in
A log is a copy of your system's behaviour. It must not become a copy of your secrets.
Never the Authorization header, an API key or a refresh token. The entire point of credential brokering is that the model never sees the secret. Writing it into a log you ship to a third-party aggregator undoes that in one line. This is a surprisingly common bug, usually from a generic HTTP middleware that logs all headers.
Never the full prompt or the full model response. Beyond the size, these contain customer data you probably did not intend to retain and almost certainly did not say you would retain. If you need a trace for debugging, keep it in a separate short-lived store with its own retention, not in the audit log.
Never the raw request body for sensitive operations. Hash it. If you genuinely need the body to debug, store a redacted version with known-sensitive fields removed, and be explicit about which fields those are rather than relying on a regex.
A simple test: if someone exported your log table, could they replay a customer's calls or reconstruct their data? If yes, the log has become a second credential and data store, which is a worse problem than the one you set out to solve.
Log the calls that did not happen
Most teams log successes, then add failures after the first incident, then discover eighteen months later that blocked calls were never recorded at all.
Put allowed, denied and held-for-approval in the same table, distinguished by the policy.decision field. Three reasons:
A denied call is a signal. If your agent tries to delete a repository once, it is a bug. If it tries forty times in an hour, something upstream is looping and you want to know before the policy stops holding.
A held call has a clock on it. Approvals time out. If you cannot see the pending queue and how long things sat in it, human approval becomes a silent place where work goes to die. Human-in-the-loop approval for AI agents covers designing that properly.
And an auditor will ask. "Show me every attempt to refund over $500 in September, and what happened to each" is a single query if everything is in one table, and a research project if it is not.

Where this sits in practice
If you are using an execution runtime, this log should already exist, because the runtime is the thing sending the request.
Swytchcode records the command, the policy result and the outcome for every call, and you read it back from the CLI:
swy audit network # what ran, with status and timing
swy audit policy # what was denied or held, and by which ruleRetention is per plan: 7 days on Free, 30 on Pro, 90 on Business, and configurable on Enterprise, which deploys into your own AWS, Azure or GCP account so the log never leaves your environment in the first place. If you need longer than your plan's window, export on a schedule into your own warehouse. Audit export is an Enterprise feature.
A catalog product's dashboard also logs tool attempts, and that is a useful thing to have, but it is a different log. It records what happened inside that product. If your architecture has a catalog for connections and a runtime for execution, you will have two logs, and the one that can answer "did this write happen exactly once" is the one at the point of execution. We cover that split in best tools for AI agents to call APIs.
If you are building the log yourself, a reasonable table:
create table agent_api_calls (
ts timestamptz not null default now(),
request_id text not null,
idempotency_key text,
agent text not null,
run_id text,
tenant_id text,
operation text not null,
arguments_hash text,
policy_decision text not null, -- allowed | denied | held
policy_rule text,
policy_version text,
http_status int,
schema_valid boolean,
resource_id text,
duration_ms int,
primary key (request_id, ts)
);
create index on agent_api_calls (tenant_id, ts desc);
create index on agent_api_calls (idempotency_key);
create index on agent_api_calls (policy_decision, ts desc);Three indexes because there are three questions: what did this customer's agent do, did this write happen once, and what got blocked.
The three questions a good log answers in one query
You will know the log is right when these are each a single statement.
"Did the customer get charged twice?" Group by idempotency_key, count distinct resource_id. Anything above one is a duplicate write.
"Why did the agent not do the thing?" Filter policy_decision != 'allowed' for that tenant and time window. The rule name tells you which guardrail fired.
"What did this agent touch last Tuesday?" Filter by agent and date, ordered by time. If operation is a stable canonical ID rather than a constructed URL, this reads like a transcript.
If answering any of those needs a join against your model traces, the log is in the wrong place.
One last thing worth knowing: part of why calls fail in confusing ways is that the vendor API moved underneath you. Every API added to Swytchcode becomes agent-ready in minutes and is then tracked for breaking changes by observing its specs and documentation, so a renamed parameter surfaces as an integration update rather than as a run of unexplained 400s in your log three weeks later.
FAQ
How do I log every API call my AI agent makes so I can audit it later?
Put the log in the runtime that sends the request, not in the agent's output. Record the operation as a stable canonical ID, the actor and tenant, the idempotency key, the policy decision and rule version, the HTTP status, whether the response passed schema validation, and the duration. Leave out the credential, the full prompt and raw sensitive bodies. Keep denied and held calls in the same table as allowed ones.
Should the agent log its own actions?
No. A model's self-report is a record of what it believed it did, and it systematically omits calls that were blocked or failed before they reached the provider. Those are usually the rows you need. Let the execution path write the record and let the model's reasoning live in a separate trace with its own retention.
What should never appear in an AI agent audit log?
The Authorization header, API keys, refresh tokens, the full prompt, the full model response, and raw request bodies for sensitive operations. If an exported copy of your log would let someone replay a customer's calls or read their data, it has become a second credential store.
How long should I keep agent API logs?
Long enough to cover your incident window and any compliance commitment you have made, which in practice usually means at least 90 days and often a year. Swytchcode retains 7 days on Free, 30 on Pro and 90 on Business, so if you need longer, export on a schedule into your own warehouse. Enterprise runs in your own cloud account, where retention is yours to set.
How do I prove a write happened exactly once?
Record an idempotency key that is generated once per logical operation and reused across that operation's retries, then group your log by that key and count distinct resource IDs. If the key changes on retry, you cannot prove anything, because each attempt looks like a separate legitimate request.
Do I need a separate log if I already use a tool catalog?
Probably yes, and it is worth being clear why. A catalog dashboard logs attempts inside that product. It can tell you a tool was invoked. It generally cannot tell you whether your policy allowed it, whether the retry was the same request, or whether the 200 you got back actually meant success. Those belong to whatever executes the call.
What is the single most useful field?
The policy decision, including which rule and which rule version applied. Status codes tell you what the provider said. The policy decision tells you what your own system chose to do, which is the part you are accountable for.
More content
How to Connect GitHub to an AI Agent (2026)
Most of what you want an agent to do on GitHub needs no push access at all. Working that out first is the difference between a useful teammate and a token that can rewrite your main branch.
Composio Pricing Compared with Nango, Arcade, Pipedream, and Swytchcode
Composio is free for 100,000 tool calls a month, then $29. Nango is $50 pay-as-you-go. Arcade Team is $25 plus a cent a call. Pipedream Startup is $99 billed annually. Swytchcode is $149 for a million. The headline prices are the least useful part, so here is the arithmetic.
How to Connect Stripe to an AI Agent Without Double Refunds
There are at least eight different ways to create a refund in Stripe's API, and an agent given 'access to Stripe' will confidently pick one of them. Here is how to make sure it picks the one you meant, once.
