---
title: "Give Claude Tag One API Key, Not Ten"
description: "Claude Tag wants a service account and a credential for every API you connect. Route it through one Zuplo proxy instead: one API key, path-based routing to every service, and an audit log of every API call the agent makes."
canonicalUrl: "https://zuplo.com/blog/2026/07/22/claude-tag-single-api-proxy"
pageType: "blog"
date: "2026-07-22"
authors: "nate"
tags: "ai-agents, API Security"
image: "https://zuplo.com/og?text=Give%20Claude%20Tag%20One%20API%20Key%2C%20Not%20Ten"
---
Claude Tag is Claude living in your Slack workspace: anyone tags @Claude and
hands it real work. That work needs your APIs, and the setup model is one
connection per service: a credential, an allowlist of hosts, and optional path
and method restrictions. Anthropic's docs tell you, correctly, to
[create a dedicated service account for every service you connect](https://claude.com/docs/claude-tag/admins/add-connections),
never a personal login.

Follow that advice across Stripe, Linear, your internal billing API, and
whatever gets connected next quarter, and you're provisioning a service account
and pasting a fresh key for each. The cleaner shape: one connection to an API
gateway you control, and the gateway fans out to everything else.

<CalloutAudience
  variant="useIf"
  items={[
    `You're rolling out Claude Tag and it needs more than one API`,
    `You want one credential to provision, rotate, and revoke, not one per service`,
    `Security wants a record of every request the agent makes, in one place`,
  ]}
/>

## How Claude Tag connects to APIs today

Connections live in an Access bundle, Claude Tag's named set of credentials and
instructions for the channels it covers, at
`claude.ai/admin-settings/claude-tag`. For a custom HTTP API you pick a
credential type (Bearer for API keys), list the allowed websites, and optionally
[restrict the connection by URL path or HTTP method](https://claude.com/docs/claude-tag/admins/add-connections),
like allowing `GET` but not `DELETE`.

The design is genuinely good: credentials live in a separate store, and Agent
Proxy, the network layer between Claude's sandbox and the outside world, injects
them at the boundary, so the model never holds your keys. But every service is
still its own connection, its own service account, its own credential to rotate,
its own audit trail in its own console.

## Point Claude Tag at a single gateway

Put a [Zuplo](https://zuplo.com) gateway in front instead, and the shape
collapses. Claude Tag gets exactly one custom connection: a Bearer credential
holding a Zuplo API key, allowed to reach one host, your gateway domain. Each
upstream becomes a path prefix, with the real credentials injected on the way
through:

- `GET /stripe/v1/charges` forwards to `api.stripe.com` with your Stripe key
- `POST /linear/graphql` forwards to `api.linear.app` with your Linear token
- `GET /billing/invoices/123` forwards to your internal billing API

Claude Tag already thinks in hosts and paths; you're just making the paths mean
something. It's the same move as
[decoupling agent auth from your MCP servers](/blog/decouple-agent-auth-mcp-server):
one gateway as the access control point.

## Route each service by path prefix

A Zuplo project is a Git repo of config files, deployed to Zuplo's managed edge
on every push: `config/routes.oas.json` declares the routes,
`config/policies.json` the policies they reference, and each deployment gets a
hosted gateway URL you can front with a custom domain. That URL is the one host
Claude Tag may reach.

Each service is a route: the `url-pattern` path mode captures everything after
the prefix, and the URL Rewrite handler replays it against the upstream:

```json
{
  "paths": {
    "/stripe:path(/.*)": {
      "x-zuplo-path": { "pathMode": "url-pattern" },
      "get": {
        "operationId": "stripe-read",
        "x-zuplo-route": {
          "handler": {
            "export": "urlRewriteHandler",
            "module": "$import(@zuplo/runtime)",
            "options": {
              "rewritePattern": "https://api.stripe.com${params.path}"
            }
          },
          "policies": {
            "inbound": ["require-claude-key", "audit-log", "stripe-auth"]
          }
        }
      }
    },
    "/linear:path(/.*)": {
      "x-zuplo-path": { "pathMode": "url-pattern" },
      "post": {
        "operationId": "linear-graphql",
        "x-zuplo-route": {
          "handler": {
            "export": "urlRewriteHandler",
            "module": "$import(@zuplo/runtime)",
            "options": {
              "rewritePattern": "https://api.linear.app${params.path}"
            }
          },
          "policies": {
            "inbound": ["require-claude-key", "audit-log", "linear-auth"]
          }
        }
      }
    }
  }
}
```

The Stripe route only defines `get`. Zuplo 404s any path/method combination that
isn't configured, so the agent can read charges all day but
`POST /stripe/v1/refunds` doesn't exist as far as it's concerned. That's Claude
Tag's "allow `GET` but not `DELETE`" control, enforced at the server, versioned
in Git, and reviewable in a pull request.

Method shaping only works where the upstream is RESTful. GraphQL APIs like
Linear's send reads and mutations through the same `POST`, so the gate is the
Linear token's own permissions or a few lines of
[custom policy code](https://zuplo.com/docs/policies/custom-code-inbound) that
rejects mutations.

## Authenticate the agent with one API key

Three policies do the rest: an API key check, an audit log, and a header swap
for the upstream credential.

```json
{
  "policies": [
    {
      "name": "require-claude-key",
      "policyType": "api-key-inbound",
      "handler": {
        "export": "ApiKeyInboundPolicy",
        "module": "$import(@zuplo/runtime)",
        "options": { "allowUnauthenticatedRequests": false }
      }
    },
    {
      "name": "audit-log",
      "policyType": "audit-log-inbound",
      "handler": {
        "export": "AuditLogInboundPolicy",
        "module": "$import(@zuplo/runtime)"
      }
    },
    {
      "name": "stripe-auth",
      "policyType": "set-headers-inbound",
      "handler": {
        "export": "SetHeadersInboundPolicy",
        "module": "$import(@zuplo/runtime)",
        "options": {
          "headers": [
            {
              "name": "Authorization",
              "value": "Bearer $env(STRIPE_API_KEY)",
              "overwrite": true
            }
          ]
        }
      }
    }
  ]
}
```

The `Authorization` header does double duty: Claude Tag sends the Zuplo key as
`Authorization: Bearer ...`, `require-claude-key` validates it, and
`stripe-auth`'s `"overwrite": true` swaps in the Stripe key before the request
leaves for the upstream.

The `linear-auth` policy is the same shape pointing at `LINEAR_API_KEY`.
Upstream keys live as secret environment variables, set in the portal under
**Settings** > **Environment Variables**, and never appear in Claude Tag. You
still create each key once in Stripe and Linear, but it's stored and rotated in
the gateway.

Create a consumer for the agent under **Settings** > **API Key Consumers**; its
subject is the name that shows up in your logs, so call it `claude-tag`. Copy
its key, then add one custom connection in Claude Tag's admin settings:
credential type Bearer, that key, allowed website set to your gateway domain.
Tell Claude what lives behind each prefix in the bundle's instructions ("Stripe
is under /stripe, Linear is under /linear"), or paste in the route list from
`routes.oas.json` (standard OpenAPI). That's the entire integration.

One fat key sounds like a bigger prize for an attacker, but it unlocks only the
prefixes and methods you defined, read-only where you said so, and one
revocation kills all of it inside a minute, the default auth cache TTL. The
catch-all prefix is just the permissive start; replace it with explicit
per-endpoint routes as you learn what the agent actually needs.

## Read every request in the audit log

Claude Tag's network observability is thin. The
[hourly JSON export of outbound calls](https://claude.com/docs/claude-tag/admins/audit)
excludes Git and MCP traffic and ships only after Anthropic's account team
enables it. Beyond that, the docs point you at each service's own audit log, so
"what did the agent do yesterday" is scattered across ten consoles.

The `audit-log` policy answers it in one place. Every request through the
gateway becomes a structured record in [CloudEvents](https://cloudevents.io/)
format, the CNCF standard for event envelopes:

```json
{
  "specversion": "1.0",
  "id": "8b1a6c2e-4f0d-4b7a-9c3e-2d5f8a1b0c9d",
  "type": "com.zuplo.api.request",
  "time": "2026-07-22T18:42:07Z",
  "actorsub": "claude-tag",
  "httpmethod": "GET",
  "httpurl": "/stripe/v1/charges?limit=10",
  "httpstatus": 200,
  "success": true
}
```

The `actorsub` field carries the authenticated consumer, so every entry is
attributed to the agent's identity. Browse events in the portal, or pull them
through the Zuplo API into wherever your security team already looks. It's the
same discipline as
[auditing every MCP tool call your agents make](/blog/audit-agent-mcp-tool-calls),
applied to plain HTTP.

![The Zuplo portal Audit Log events view, listing requests from the claude-tag consumer with time, method, path, and a green checkmark outcome for each call](/blog-images/2026-07-22-claude-tag-single-api-proxy/audit-log-events.png)

Audit Logs are an enterprise feature, free to try on any plan in development
environments. On other plans, drop `audit-log` from the inbound arrays and use a
[logging plugin](https://zuplo.com/docs/articles/logging) to stream request logs
from the same choke point to Datadog, Splunk, or OpenTelemetry.

<CalloutDoc
  title="Audit Logs policy"
  description="Full configuration reference: sampling, field controls, the complete event shape, and logging custom events from your own handlers."
  href="https://zuplo.com/docs/policies/audit-log-inbound"
  icon="book"
/>

## Direct connections vs. one gateway proxy

|                           | Direct connections                                   | One gateway proxy                               |
| ------------------------- | ---------------------------------------------------- | ----------------------------------------------- |
| Credentials in Claude Tag | One per service                                      | One                                             |
| Adding a service          | New connection, new credential in the admin UI       | New route in a pull request                     |
| Path and method control   | Per connection, in the admin UI                      | Routes in Git, per service                      |
| Upstream keys             | Pasted into Claude Tag                               | Secrets in the gateway, never leave it          |
| Audit trail               | Each service's console, plus an opt-in hourly export | Every request, one log, attributed to the agent |

One caveat: the gateway only sees the traffic you route through it. Claude Tag's
Git operations and web browsing don't pass through your proxy, so this is the
audit trail for your APIs, not for everything the agent touches.

Any agent platform that takes a base URL and a bearer token gets the same deal:
one key, path-scoped access, and a log you own. Start with the
[URL Rewrite handler docs](https://zuplo.com/docs/handlers/url-rewrite), wire up
two services, and read your first audit log entry before lunch.