---
title: Proxy requests to Guides and Surveys
description: "Reverse-proxy Guides and Surveys assets and APIs through your own domain or CDN."
product: guides-surveys
lang: en
last_updated: 2026-10-23
token_estimate: 3014
---
# Proxy requests to Guides and Surveys

> For AI agents: a documentation index is available at [/docs/llms.txt](/docs/llms.txt). Append `.md` to any page URL for markdown, or send `Accept: text/markdown`.

A reverse proxy routes Guides and Surveys traffic through a domain you own. The SDK keeps working when ad blockers, browser extensions, DNS filters, or regional firewalls block Amplitude domains. You run one proxy host, route three path patterns to three Amplitude hosts (four if you use the AI assistant), and point the SDK at it with a single `proxyUrl` option. Guides and Surveys APIs and static assets are latency-sensitive. Use an edge-hosted proxy, such as a CDN or edge function, if possible.

## How routing works

Every Amplitude host that the SDK calls owns distinct paths, so one proxy host can route by path without rewriting it:

| Path | US host | EU host | Used for |
| --- | --- | --- | --- |
| `/sdk/*` | `gs.amplitude.com` | `gs.eu.amplitude.com` | Guides and Surveys API, including preview mode |
| `/engagement-browser/*` | `cdn.amplitude.com` | `cdn.eu.amplitude.com` | SDK bundle |
| `/api/*` (optional) | `assistant-api.amplitude.com` | `assistant-api.eu.amplitude.com` | AI assistant chat |
| Everything else | `engagement-static.amplitude.com` | `engagement-static.eu.amplitude.com` | Images in guides and surveys |

The `/api/*` rule is only required if you use the AI assistant. Without it, guides, surveys, and the Resource Center work normally through the proxy.

Each rule needs to:

- Forward the full path and query string unchanged.
- Send the Amplitude host, not your proxy host, as the `Host` header.
- Allow `GET`, `HEAD`, `OPTIONS`, `PUT`, `POST`, `PATCH`, and `DELETE` on `/sdk/*` and `/api/*`. The SDK uses `POST` to save dismissal and completion state.
- If you add the `/api/*` rule, stream its responses without buffering or compressing them. The AI assistant streams each answer as server-sent events over a long-lived `POST` request.

> **Warning:**
>
> Use the wildcard patterns exactly as shown. Don't list individual paths like `/sdk/v1/config`. The SDK calls many endpoints under each prefix, including `/sdk/v1/preview/config` for preview mode, and a rule that lists specific paths breaks the endpoints it omits.

## Choose a proxy host

You can serve the proxy from a dedicated subdomain or from a path on your app's domain:

- **Dedicated subdomain**: For example, `https://gs.example.com`. The rules apply at the root of the subdomain. Use this with Cloudflare Workers, CloudFront, or NGINX.
- **Path on your app's domain**: For example, `https://app.example.com/gs`. Each rule adds the `/gs` prefix to the incoming path and removes it before forwarding. Use this with framework rewrites, such as Next.js or Vercel, where the root paths would collide with your app's own routes.

Pick a neutral name. Ad blockers often match on words like `analytics`, `tracking`, `telemetry`, or `amplitude` in hostnames and paths.

## Set up the proxy

The examples use US hosts. For the EU data center, replace each host with its EU equivalent from the routing table. If you don't use the AI assistant, leave out the parts marked as optional.

#### Cloudflare Workers

Create a Worker, paste this script, then attach a custom domain such as `gs.example.com` under _Settings > Domains & Routes_.

```js
const UPSTREAMS = {
  sdk: "https://gs.amplitude.com",
  cdn: "https://cdn.amplitude.com",
  media: "https://engagement-static.amplitude.com",
  // Optional: only needed for the AI assistant
  api: "https://assistant-api.amplitude.com",
};

function upstreamFor(pathname) {
  if (pathname.startsWith("/sdk/")) return UPSTREAMS.sdk;
  if (pathname.startsWith("/engagement-browser/")) return UPSTREAMS.cdn;
  // Optional: only needed for the AI assistant
  if (pathname.startsWith("/api/")) return UPSTREAMS.api;
  return UPSTREAMS.media;
}

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const target = new URL(
      url.pathname + url.search,
      upstreamFor(url.pathname),
    );
    return fetch(new Request(target, request));
  },
};
```

Your `proxyUrl` is `https://gs.example.com`.

#### Next.js and Vercel

Add rewrites to `next.config.js`. Next.js applies rewrites in order, so keep the catch-all media rule last.

```js
module.exports = {
  async rewrites() {
    return [
      {
        source: "/gs/sdk/:path*",
        destination: "https://gs.amplitude.com/sdk/:path*",
      },
      {
        source: "/gs/engagement-browser/:path*",
        destination: "https://cdn.amplitude.com/engagement-browser/:path*",
      },
      // Optional: only needed for the AI assistant
      {
        source: "/gs/api/:path*",
        destination: "https://assistant-api.amplitude.com/api/:path*",
      },
      {
        source: "/gs/:path*",
        destination: "https://engagement-static.amplitude.com/:path*",
      },
    ];
  },
};
```

For a non-Next.js project on Vercel, add the same `source` and `destination` pairs to the `rewrites` array in `vercel.json`.

Your `proxyUrl` is `https://app.example.com/gs`.

#### NGINX

Add a server block for your proxy subdomain. A `proxy_pass` value without a path forwards the full original path.

```nginx
server {
    listen 443 ssl;
    server_name gs.example.com;
    # ssl_certificate and ssl_certificate_key for gs.example.com

    proxy_http_version 1.1;
    proxy_ssl_server_name on;

    location /sdk/ {
        proxy_pass https://gs.amplitude.com;
        proxy_set_header Host gs.amplitude.com;
    }

    location /engagement-browser/ {
        proxy_pass https://cdn.amplitude.com;
        proxy_set_header Host cdn.amplitude.com;
    }

    # Optional: only needed for the AI assistant
    location /api/ {
        proxy_pass https://assistant-api.amplitude.com;
        proxy_set_header Host assistant-api.amplitude.com;
        proxy_buffering off;
        proxy_read_timeout 300s;
        gzip off;
    }

    location / {
        proxy_pass https://engagement-static.amplitude.com;
        proxy_set_header Host engagement-static.amplitude.com;
    }
}
```

Your `proxyUrl` is `https://gs.example.com`.

#### AWS CloudFront

Create one distribution with three origins, plus a fourth if you use the AI assistant. Use these settings on every behavior unless a step says otherwise:

- **Cache HTTP methods**: `OPTIONS`
- **Origin request policy**: `AllViewerExceptHostHeader`
- **Response headers policy**: `CORS-with-preflight-and-SecurityHeadersPolicy`

1. Open **CloudFront** and click **Create CloudFront distribution**. Set the **Origin domain** to `engagement-static.amplitude.com`. This origin backs the default behavior, which serves images.

   - **Allowed HTTP methods**: `GET, HEAD, OPTIONS`
   - **Cache policy**: Choose a suitable caching policy for static assets (for example, `CachingOptimized`)
   - **Web Application Firewall (WAF)**: Don't enable security protections.
2. On the _Origins_ tab, click **Create origin** to add `gs.amplitude.com` and `cdn.amplitude.com`.
3. On the _Behaviors_ tab, click **Create behavior** for each path:

   - **Path pattern** `/sdk/*`, **Origin** `gs.amplitude.com`, **Allowed HTTP methods** `GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE`, **Cache policy** `CachingDisabled`.
   - **Path pattern** `/engagement-browser/*`, **Origin** `cdn.amplitude.com`, **Allowed HTTP methods** `GET, HEAD, OPTIONS`, **Cache policy**: a suitable caching policy for static assets (for example, `CachingOptimized`).
4. Optional: if you use the AI assistant, add an origin for `assistant-api.amplitude.com` and a behavior with **Path pattern** `/api/*`, **Origin** `assistant-api.amplitude.com`, **Allowed HTTP methods** `GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE`, **Cache policy** `CachingDisabled`, and **Compress objects automatically** `No`.

CloudFront matches the most specific path pattern first, so requests that don't match another behavior fall through to the image origin.

Your `proxyUrl` is the distribution domain, such as `https://SUBDOMAIN.cloudfront.net`, or a custom domain you attach to it.

If you use the AI assistant, confirm that your platform streams responses and allows requests to stay open for at least a few minutes on `/api/*`. Platforms that buffer responses deliver each answer in one block after the agent finishes.

## Initialize the SDK with the proxy

Set `proxyUrl` to your proxy host:

```js
engagement.init("API_KEY", {
  proxyUrl: "https://gs.example.com",
});
```

If you install Guides and Surveys as an Analytics Browser SDK plugin, pass the same option to the plugin: `amplitude.add(window.engagement.plugin({ proxyUrl: "https://gs.example.com" }))`.

`proxyUrl` sets `serverUrl`, `cdnUrl`, `mediaUrl`, and `chatUrl` to the same value. To send one type of traffic elsewhere, set that option explicitly; explicit values take precedence over `proxyUrl`. On SDK versions that don't support `proxyUrl`, set `serverUrl`, `cdnUrl`, and `mediaUrl` to your proxy host instead, plus `chatUrl` if you use the AI assistant.

## Test the proxy

Replace `PROXY_URL` with your `proxyUrl` and `APIKEY` with your project's API key. Each request returns HTTP status `200 OK` when the proxy routes it correctly.

Test the Guides and Surveys API:

```bash
curl -i 'PROXY_URL/sdk/v1/decide' -H 'Authorization: Api-Key APIKEY'
```

Test the SDK bundle:

```bash
curl -I 'PROXY_URL/engagement-browser/prod/index.min.js.gz'
```

If you use the AI assistant, test the assistant API:

```bash
curl -i 'PROXY_URL/api/chat/settings' -H 'Authorization: Api-Key APIKEY'
```

A `403` or `404` response from your proxy, rather than from Amplitude, usually means the request matched the wrong rule.

## Troubleshooting common proxy issues

- **Preview mode doesn't work**

  - **Symptoms**: Preview mode fails to load or display guides properly.
  - **Cause**: The `/sdk/*` rule lists specific paths, such as `/sdk/v1/config`, instead of the wildcard.
  - **Solution**: Use the `/sdk/*` wildcard. Preview mode calls `/sdk/v1/preview/config`.
- **Guides don't persist dismissal or completion state**

  - **Symptoms**: Guides reappear in the next session after the user dismisses or completes them.
  - **Cause**: The `/sdk/*` rule doesn't allow `POST`, or it forwards your proxy host as the `Host` header.
  - **Solution**: Allow `GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE` on `/sdk/*` and send the Amplitude host as the `Host` header. In CloudFront, use the `AllViewerExceptHostHeader` origin request policy.
- **Images don't load in guides**

  - **Symptoms**: Images appear broken or show placeholder icons.
  - **Cause**: The catch-all rule doesn't route to the image host, or the SDK doesn't use the proxy for media.
  - **Solution**: Route everything outside `/sdk/*`, `/engagement-browser/*`, and `/api/*` (if present) to `engagement-static.amplitude.com` or `engagement-static.eu.amplitude.com`, and set `proxyUrl` or `mediaUrl`.
- **The AI assistant doesn't answer, or the answer appears in one block**

  - **Symptoms**: The chat window shows an error, stays empty, or displays the full answer after a long pause instead of streaming it word by word.
  - **Cause**: The `/api/*` rule is missing, so assistant requests reach the image host, or the proxy buffers or compresses the streamed response.
  - **Solution**: Add the `/api/*` rule and turn off buffering and compression on it. In CloudFront, set **Compress objects automatically** to `No`. In NGINX, set `proxy_buffering off`.
- **The SDK doesn't load**

  - **Symptoms**: The browser console reports that the Engagement SDK bundle failed to load.
  - **Cause**: The `/engagement-browser/*` rule is missing or routes to the wrong host.
  - **Solution**: Route `/engagement-browser/*` to `cdn.amplitude.com` or `cdn.eu.amplitude.com`, then run the SDK bundle test.

For any other failure, open your browser's developer tools, filter the _Network_ tab by your proxy host, and check which requests return `403`, `404`, or CORS errors. Compare each failing path to the routing table.

