Zuplo
ai-agents

Give Claude Tag One API Key, Not Ten

Nate TottenNate Totten
July 22, 2026
6 min read

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.

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, 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.

Use this approach if you're:
  • 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, 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 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: 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:

JSONjson
{
  "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 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.

JSONjson
{
  "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 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 format, the CNCF standard for event envelopes:

JSONjson
{
  "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, 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

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 to stream request logs from the same choke point to Datadog, Splunk, or OpenTelemetry.

Audit Logs policy

Full configuration reference: sampling, field controls, the complete event shape, and logging custom events from your own handlers.

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, wire up two services, and read your first audit log entry before lunch.