Docs/AI & Agents

For AI Agents & Integrations

Constructing proxy URLs when acting on behalf of a customer — for AI agents and automation. Covers URL construction, path allowlists, header handling, and the dashboard API.

Last updated Jul 23, 2026


name: guardproxy description: "Route third-party API calls through GuardProxy to make a full-access API key read-only. Use when an agent is about to call a vendor API (ITGlue, Hudu, ConnectWise, Autotask, httpbin, any REST/GraphQL API) on behalf of a user, and the user wants destructive writes (POST/PUT/PATCH/DELETE) safely blocked. Covers constructing the proxy URL, path allowlists for POST-as-read endpoints (GraphQL, search), auth-header passthrough, and managing upstreams via the dashboard API."

GuardProxy — read-only API access for AI agents

What it is

GuardProxy is a reverse proxy that sits between you (the agent) and a third-party upstream API. It inspects the HTTP method of every request:

  • GET, HEAD, OPTIONS → forwarded to the upstream unchanged.
  • POST, PUT, PATCH, DELETE → blocked with a 403 before they ever reach the upstream. The block is logged to the customer's account.

The customer's Authorization header (their real API key) passes through verbatim and is never stored or logged — it exists in memory for the request lifetime only.

You (agent) → https://cust_abc.guardproxy.dev/itglue/api/v2/accounts
                     │
              ┌──────┴─────── GET/HEAD/OPTIONS? → forward, return upstream response
              │
              └────── POST/PUT/PATCH/DELETE? → 403 + logged

The whole point: you get to use the customer's admin-level API key (the only kind some vendors issue) without the ability to create, modify, or delete anything.


When to use this skill

Use it whenever you are about to make HTTP calls to a third-party API on behalf of a user, and any of these are true:

  • The API key you've been handed is a full-access / admin key.
  • The user said "read-only", "don't let me write", "safely", "guard", or "proxy".
  • You're integrating an MSP-style tool (ITGlue, Hudu, ConnectWise, Autotask) that doesn't offer a scoped read-only key.
  • You're setting up an automation/agent that will hit a vendor API and the user wants a safety net against accidental writes.

Do NOT use it when:

  • The task genuinely requires writing (creating tickets, updating records). GuardProxy will 403 you, by design. Surface this to the user and ask.
  • You're calling a first-party service you fully control.

How to construct the proxy URL

The URL is the single thing that changes. Everything else — the API key, the auth header, query params, request body — stays exactly as you'd send it to the real API.

URL shape

https://{customer_slug}.{base_domain}/{upstream_slug}/{original_path_and_query}
       └────────────┘ └──────────┘ └──────┘ └────────────────────────────┘
         subdomain     zone apex   upstream   the path the upstream expects

Concretely, to call GET https://api.itglue.com/v2/accounts through the proxy:

Real API:    https://api.itglue.com/v2/accounts
Proxy:       https://cust_abc.guardproxy.dev/itglue/v2/accounts
                                     │            │
                                     │            └── same as the real path
                                     └── the upstream slug the customer configured

Rules:

  • The customer slug comes from the subdomain (cust_abc from cust_abc.guardproxy.dev). Reserved names — app, www, api, dashboard, status, docs — are rejected.
  • The upstream slug is the first path segment. Everything after it is forwarded verbatim, including the query string.
  • Slugs are lowercase, [a-z0-9-], 1–32 chars.

Find the exact base URL from the customer's account rather than guessing — see "Discovering or creating an upstream" below. The proxy_endpoint field is canonical.

What you send

Nothing changes about your request except the host + the leading upstream segment:

// Before (direct, full access):
await fetch("https://api.itglue.com/v2/accounts", {
  headers: { Authorization: "Bearer " + apiKey },
});

// After (read-only via GuardProxy):
await fetch("https://cust_abc.guardproxy.dev/itglue/v2/accounts", {
  headers: { Authorization: "Bearer " + apiKey },  // same key, same header
});

Method semantics — what will and won't work

Method Result Notes
GET ✅ forwarded reads work
HEAD ✅ forwarded
OPTIONS ✅ forwarded always allowed (CORS preflight needs this)
POST 403 unless the path is on the allowlist (below)
PUT 403
PATCH 403
DELETE 403

A 403 response body looks like:

{ "error": { "code": "method_blocked", "message": "..." } }

Treat error.code === "method_blocked" as "the proxy did its job" — not an upstream error. Don't retry; don't escalate it as a bug. It means the safety net caught a write the user didn't want.


POST-as-read endpoints (GraphQL, search) → use the path allowlist

Some APIs use POST for reads. GraphQL is the canonical case — every query and mutation is a POST. GuardProxy blocks these by default. The fix is the path allowlist: matching path prefixes are exempt from the method block.

To send a GraphQL query through the proxy, the customer must have added the GraphQL path to that upstream's allowlist. Then:

POST https://cust_abc.guardproxy.dev/myapi/graphql   ← allowlisted → 200
POST https://cust_abc.guardproxy.dev/myapi/mutate    ← not allowlisted → 403

⚠️ The allowlist is a prefix match and bypasses ALL methods on matching paths. A POST to an allowlisted path works — but so would a DELETE. Keep allowlist entries as specific as possible (prefer /graphql over /). If you need to add or narrow an allowlist entry, see the dashboard API below.


Header handling (important gotchas)

  • Authorization and all other headers are forwarded verbatim. Your API key is untouched and reaches the upstream exactly as sent. It is stripped from logs.
  • Method-override headers are stripped before forwarding: X-HTTP-Method-Override, X-Method-Override, and similar. These would otherwise let a GET secretly execute a DELETE at the upstream — GuardProxy removes them deliberately. Do not rely on method-override headers through the proxy.
  • Custom headers (e.g. X-Custom-Header: foo) pass through normally.
  • The upstream sees the same body, same content-type, same everything.

Discovering or creating an upstream

You usually won't hardcode proxy URLs. Query the customer's account instead. All dashboard API routes are session-authenticated and owner-scoped (you can only ever see/modify the authenticated customer's own upstreams).

List upstreams → get the canonical proxy_endpoint

GET /api/upstreams
Cookie: <session>
{
  "upstreams": [
    {
      "id": "...",
      "slug": "itglue",
      "display_name": "ITGlue Production",
      "upstream_url": "https://api.itglue.com",
      "blocked_methods": ["POST", "PUT", "PATCH", "DELETE"],
      "path_allowlist": ["/graphql"],
      "proxy_endpoint": "https://cust_abc.guardproxy.dev/itglue",
      "created_at": "...",
      "updated_at": "..."
    }
  ]
}

Use the returned proxy_endpoint as your base URL — it's the source of truth.

Create a new upstream (if the customer needs one for an API you're integrating)

POST /api/upstreams
Content-Type: application/json
Cookie: <session>

{
  "slug": "hudu",                          // required, [a-z0-9-], 1–32 chars, lowercase
  "displayName": "Hudu",                   // required
  "upstreamUrl": "https://hudu.example.com", // required, http(s); private IPs/localhost rejected (SSRF guard)
  "blockedMethods": ["POST","PUT","PATCH","DELETE"],  // optional; defaults are sensible
  "pathAllowlist": ["/graphql"]            // optional, each must start with "/"
}
  • 201 returns the new upstream with its proxy_endpoint.
  • 409 duplicate_slug → that slug already exists for this customer; pick another.
  • 400 invalid_* → validation failed (slug/url/methods/paths).

PATCH /api/upstreams/:id takes any subset of the same fields (e.g. just { "pathAllowlist": ["/graphql", "/search"] }). DELETE /api/upstreams/:id204.

Auth & billing state

  • GET /api/me → your user + customer record (including subscription_status and trial window). The proxy itself enforces this: an expired/past_due account returns 402, and hard abuse overage returns 429.

Verifying it works

Run a read, confirm a write is blocked:

# Read passes through (upstream response)
curl https://cust_abc.guardproxy.dev/httpbin/get?foo=bar

# Write is blocked (403 method_blocked)
curl -X POST https://cust_abc.guardproxy.dev/httpbin/post -d '{"x":1}'

Decision checklist before you call a vendor API

  1. Do I need to write? If yes, GuardProxy can't help — ask the user.
  2. Is the key full-access / admin? If yes, prefer routing through the proxy.
  3. Do I have a proxy_endpoint for this upstream? If not, GET /api/upstreams; if still not, POST /api/upstreams to create one.
  4. Am I POSTing for a read (GraphQL/search)? Ensure the path is in the upstream's path_allowlist; otherwise expect a 403.
  5. Am I using method-override headers? Drop them — they're stripped.
  6. Got a 403 method_blocked? That's the proxy working as intended, not a bug. Don't retry; inform the user the write was blocked.

Quick reference

Need Do
Read through the proxy GET https://{cust}.{domain}/{upstream}/{path}?{query}
Confirm a write is blocked send POST/PUT/PATCH/DELETE; expect 403 {error.code:"method_blocked"}
Exempt a POST-as-read path add the path prefix to the upstream's path_allowlist (PATCH /api/upstreams/:id)
Find the proxy base URL GET /api/upstreams → use the proxy_endpoint field
Create a new guarded upstream POST /api/upstreams with slug, displayName, upstreamUrl
Check account/trial status GET /api/me

Lock down your API keys in 60 seconds.

Free for 14 days. No credit card. Cancel anytime.

Start your free trial