> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thrad.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# First-party proxy

> Send Tag requests through your own domain to improve delivery.

By default, the Thrad Tag sends measurement requests to `events.thrad.ai`. Some browsers and content blockers limit third-party requests, which can leave gaps in attribution.

A first-party proxy gives the Tag a path on **your domain** instead. Your server receives the request and forwards it to Thrad.

> [!NOTE]
>
> This is optional. Your current Tag install will keep working if you don't set up a proxy.

## Add a forwarding rule

We recommend `/thr/`. You can use `/th/` if you need a shorter path.

Avoid obvious advertising or vendor names such as `/thrad/`, `/ads/`, or `/tracking/`. Those are more likely to be included in blocker rules.

Your proxy should forward these five routes to the matching path under `https://events.thrad.ai/v1/`:

| Your site | Thrad | Method |
| - | - | - |
| `/thr/config/{tag_id}` | `/v1/config/{tag_id}` | `GET` |
| `/thr/collect` | `/v1/collect` | `POST` |
| `/thr/conversion` | `/v1/conversion` | `POST` |
| `/thr/view-conversion` | `/v1/view-conversion` | `POST` |
| `/thr/e` | `/v1/e` | `GET` |

Choose your platform:

> [accordion] Express / Node.js
>
> Install `http-proxy-middleware`:
>
> ```bash theme={null}
> npm install http-proxy-middleware
> ```
>
> Register the proxy before authentication, CSRF protection, and body-parsing middleware:
>
> ```js theme={null}
> const { createProxyMiddleware } = require("http-proxy-middleware");
>
> const allowedRoutes = [
>   ["GET", /^\/config\/[^/]+$/],
>   ["POST", /^\/collect$/],
>   ["POST", /^\/conversion$/],
>   ["POST", /^\/view-conversion$/],
>   ["GET", /^\/e$/],
> ];
>
> function isAllowedThradRoute(req) {
>   const pathname = new URL(req.url, "http://proxy.invalid").pathname;
>   return allowedRoutes.some(
>     ([method, pattern]) => req.method === method && pattern.test(pathname),
>   );
> }
>
> const thradProxy = createProxyMiddleware({
>   target: "https://events.thrad.ai/v1",
>   changeOrigin: true,
>   on: {
>     proxyReq(proxyReq, req) {
>       proxyReq.removeHeader("authorization");
>       proxyReq.removeHeader("cookie");
>       proxyReq.removeHeader("forwarded");
>       proxyReq.setHeader("x-forwarded-for", req.socket.remoteAddress || "");
>       proxyReq.setHeader(
>         "x-forwarded-proto",
>         req.socket.encrypted ? "https" : "http",
>       );
>     },
>     proxyRes(proxyRes) {
>       delete proxyRes.headers["set-cookie"];
>     },
>   },
> });
>
> app.use(
>   "/thr",
>   (req, res, next) => {
>     if (!isAllowedThradRoute(req)) return res.sendStatus(404);
>     next();
>   },
>   thradProxy,
> );
>
> // Register your application's middleware after the proxy.
> app.use(express.json());
> ```
>
> If your app is behind a load balancer, use the client IP and protocol values from that trusted platform rather than accepting incoming `X-Forwarded-*` headers.

> [accordion] Next.js / Vercel
>
> Create `app/thr/[...path]/route.ts`. If your app uses a `src` directory, create it under `src/app` instead.
>
> ```ts theme={null}
> export const runtime = "nodejs";
> export const dynamic = "force-dynamic";
>
> const ROUTES = new Map<string, RegExp>([
>   ["GET config", /^config\/[^/]+$/],
>   ["POST collect", /^collect$/],
>   ["POST conversion", /^conversion$/],
>   ["POST view-conversion", /^view-conversion$/],
>   ["GET e", /^e$/],
> ]);
>
> function allowedPath(method: string, path: string): boolean {
>   return ROUTES.get(`${method} ${path.split("/")[0]}`)?.test(path) ?? false;
> }
>
> async function forward(request: Request): Promise<Response> {
>   const incomingUrl = new URL(request.url);
>   const path = incomingUrl.pathname.slice("/thr/".length);
>
>   if (!allowedPath(request.method, path)) {
>     return new Response("Not found", { status: 404 });
>   }
>
>   const upstreamUrl = new URL(`/v1/${path}`, "https://events.thrad.ai");
>   upstreamUrl.search = incomingUrl.search;
>
>   const headers = new Headers(request.headers);
>   [
>     "authorization",
>     "cookie",
>     "host",
>     "forwarded",
>     "x-forwarded-for",
>     "x-forwarded-host",
>     "x-forwarded-proto",
>     "accept-encoding",
>     "connection",
>     "proxy-authorization",
>     "transfer-encoding",
>     "upgrade",
>   ].forEach((header) => headers.delete(header));
>
>   const clientIp = request.headers.get("x-vercel-forwarded-for");
>   if (clientIp) headers.set("x-forwarded-for", clientIp);
>   headers.set("x-forwarded-proto", incomingUrl.protocol.slice(0, -1));
>
>   try {
>     const upstream = await fetch(upstreamUrl, {
>       method: request.method,
>       headers,
>       body: request.method === "GET" ? undefined : await request.arrayBuffer(),
>       cache: "no-store",
>       redirect: "manual",
>     });
>
>     const responseHeaders = new Headers(upstream.headers);
>     responseHeaders.delete("set-cookie");
>     const bodyless = [204, 205, 304].includes(upstream.status);
>
>     return new Response(bodyless ? null : upstream.body, {
>       status: upstream.status,
>       statusText: upstream.statusText,
>       headers: responseHeaders,
>     });
>   } catch {
>     return new Response("Bad gateway", { status: 502 });
>   }
> }
>
> export const GET = forward;
> export const POST = forward;
> ```
>
> Deploy this route and the Tag configuration below together. No extra package is required.

> [accordion] Cloudflare Worker
>
> Create a Module Worker and attach it to `example.com/thr/*` for your zone:
>
> ```js theme={null}
> const ROUTES = new Map([
>   ["GET config", /^config\/[^/]+$/],
>   ["POST collect", /^collect$/],
>   ["POST conversion", /^conversion$/],
>   ["POST view-conversion", /^view-conversion$/],
>   ["GET e", /^e$/],
> ]);
>
> function allowedPath(method, path) {
>   return ROUTES.get(`${method} ${path.split("/")[0]}`)?.test(path) ?? false;
> }
>
> export default {
>   async fetch(request) {
>     const incomingUrl = new URL(request.url);
>     const path = incomingUrl.pathname.slice("/thr/".length);
>
>     if (!allowedPath(request.method, path)) {
>       return new Response("Not found", { status: 404 });
>     }
>
>     const upstreamUrl = new URL(`/v1/${path}`, "https://events.thrad.ai");
>     upstreamUrl.search = incomingUrl.search;
>
>     const headers = new Headers(request.headers);
>     [
>       "authorization",
>       "cookie",
>       "host",
>       "forwarded",
>       "x-forwarded-for",
>       "x-forwarded-host",
>       "x-forwarded-proto",
>       "accept-encoding",
>       "connection",
>       "proxy-authorization",
>       "transfer-encoding",
>       "upgrade",
>     ].forEach((header) => headers.delete(header));
>
>     const clientIp = request.headers.get("cf-connecting-ip");
>     if (clientIp) headers.set("x-forwarded-for", clientIp);
>     headers.set("x-forwarded-proto", incomingUrl.protocol.slice(0, -1));
>
>     try {
>       const upstream = await fetch(upstreamUrl, {
>         method: request.method,
>         headers,
>         body: request.method === "GET" ? undefined : request.body,
>         redirect: "manual",
>       });
>
>       const responseHeaders = new Headers(upstream.headers);
>       responseHeaders.delete("set-cookie");
>       const bodyless = [204, 205, 304].includes(upstream.status);
>
>       return new Response(bodyless ? null : upstream.body, {
>         status: upstream.status,
>         statusText: upstream.statusText,
>         headers: responseHeaders,
>       });
>     } catch {
>       return new Response("Bad gateway", { status: 502 });
>     }
>   },
> };
> ```
>
> Your hostname needs a proxied Cloudflare DNS record. If you use `/th/`, update both the Worker route and the `slice()` prefix.

> [accordion] Nginx
>
> Add the map and upstream inside the `http` block in `nginx.conf`:
>
> ```nginx theme={null}
> map "$request_method:$request_uri" $thrad_upstream_uri {
>     default "";
>     ~^GET:/thr/config/(?<thrad_tag_id>[^/?]+)(?:\?.*)?$ /v1/config/$thrad_tag_id;
>     ~^POST:/thr/collect(?:\?.*)?$                         /v1/collect;
>     ~^POST:/thr/conversion(?:\?.*)?$                      /v1/conversion;
>     ~^POST:/thr/view-conversion(?:\?.*)?$                 /v1/view-conversion;
>     ~^GET:/thr/e(?:\?.*)?$                                /v1/e;
> }
>
> upstream thrad_events {
>     server events.thrad.ai:443;
>     keepalive 16;
> }
> ```
>
> Then add this location to your site's `server` block:
>
> ```nginx theme={null}
> location ^~ /thr/ {
>     if ($thrad_upstream_uri = "") {
>         return 404;
>     }
>
>     proxy_pass https://thrad_events$thrad_upstream_uri$is_args$args;
>     proxy_http_version 1.1;
>
>     proxy_set_header Host events.thrad.ai;
>     proxy_ssl_server_name on;
>     proxy_ssl_name events.thrad.ai;
>
>     proxy_set_header Authorization "";
>     proxy_set_header Cookie "";
>     proxy_set_header Forwarded "";
>     proxy_set_header X-Forwarded-For $remote_addr;
>     proxy_set_header X-Forwarded-Host "";
>     proxy_set_header X-Forwarded-Proto $scheme;
>     proxy_hide_header Set-Cookie;
> }
> ```
>
> Check and reload the configuration with `nginx -t` and your usual reload command.

> [accordion] Caddy
>
> Add the reusable proxy and route handlers to your `Caddyfile`:
>
> ```caddyfile theme={null}
> (thrad_proxy) {
>     route {
>         uri strip_prefix /thr
>         rewrite * /v1{path}
>
>         reverse_proxy https://events.thrad.ai {
>             header_up Host events.thrad.ai
>             header_up -Authorization
>             header_up -Cookie
>             header_up -Forwarded
>             header_up X-Forwarded-For {remote_host}
>             header_up -X-Forwarded-Host
>             header_up X-Forwarded-Proto {scheme}
>             header_down -Set-Cookie
>
>             transport http {
>                 tls_server_name events.thrad.ai
>             }
>         }
>     }
> }
>
> example.com {
>     @thrad_get {
>         method GET
>         path_regexp ^/thr/(?:config/[^/]+|e)$
>     }
>     handle @thrad_get {
>         import thrad_proxy
>     }
>
>     @thrad_post {
>         method POST
>         path_regexp ^/thr/(?:collect|conversion|view-conversion)$
>     }
>     handle @thrad_post {
>         import thrad_proxy
>     }
>
>     handle /thr/* {
>         respond "Not found" 404
>     }
>
>     # Keep the rest of your site's handlers here.
> }
> ```
>
> Check the file with `caddy validate --config /etc/caddy/Caddyfile`, then reload Caddy.

> [accordion] Apache
>
> Enable `mod_proxy`, `mod_proxy_http`, `mod_ssl`, `mod_headers`, and `mod_allowmethods`. Then add these rules inside your HTTPS `VirtualHost`:
>
> ```apache theme={null}
> ProxyRequests Off
> SSLProxyEngine On
>
> ProxyPassMatch "^/thr/config/([^/?]+)$" "https://events.thrad.ai/v1/config/$1"
> ProxyPassMatch "^/thr/collect$" "https://events.thrad.ai/v1/collect"
> ProxyPassMatch "^/thr/conversion$" "https://events.thrad.ai/v1/conversion"
> ProxyPassMatch "^/thr/view-conversion$" "https://events.thrad.ai/v1/view-conversion"
> ProxyPassMatch "^/thr/e$" "https://events.thrad.ai/v1/e"
> ProxyPassReverse "/thr/" "https://events.thrad.ai/v1/"
>
> <LocationMatch "^/thr/(?:config/[^/]+|collect|conversion|view-conversion|e)$">
>     ProxyPreserveHost Off
>     ProxyAddHeaders Off
>
>     RequestHeader unset Authorization
>     RequestHeader unset Cookie
>     RequestHeader unset Forwarded
>     RequestHeader unset X-Forwarded-For
>     RequestHeader unset X-Forwarded-Host
>     RequestHeader unset X-Forwarded-Proto
>     RequestHeader set Host "events.thrad.ai"
>     RequestHeader set X-Forwarded-For "expr=%{REMOTE_ADDR}"
>     RequestHeader set X-Forwarded-Proto "https"
>
>     Header always unset Set-Cookie
> </LocationMatch>
>
> <LocationMatch "^/thr/(?:config/[^/]+|e)$">
>     AllowMethods GET
> </LocationMatch>
>
> <LocationMatch "^/thr/(?:collect|conversion|view-conversion)$">
>     AllowMethods POST
> </LocationMatch>
> ```
>
> Check the configuration with `apachectl configtest`, then reload Apache.

> [!WARNING]
>
> Keep the upstream fixed to `events.thrad.ai` and only forward the five routes above. The examples also strip browser cookies and authorization headers, remove upstream `Set-Cookie`, and reject unknown paths so the route can't become an open proxy.

## Configure the Tag

Add `THRAD_TAG_CONFIG` **before** the main Tag script:

```html theme={null}
<script>
  window.THRAD_TAG_CONFIG = {
    proxyEndpoint: "/thr/"
  };
</script>
<script async src="https://cdn.thrad.ai/tag.min.js"></script>
```

If you chose `/th/`, use that same path in both the forwarding rule and `proxyEndpoint`.

Your existing tag ID, events, conversions, consent setup, and attribution behavior stay the same. The only change is where eligible browser requests are sent.

## Verify it's working

1. Load a page with the Tag installed.
2. Open **DevTools → Network**.
3. Look for requests to your domain at `/thr/config/...` and `/thr/collect`.

The config request should return `200`; collect should return `204`. If the requests are hitting your domain, you're all set.

You may still see impression link or resolve requests go directly to `events.thrad.ai`. That's expected—the impression cookie belongs to that host and can't be read through your domain.

If you get a `404`, make sure the path in `proxyEndpoint` matches your forwarding rule. For `401` or `403`, register the proxy before authentication and CSRF middleware. For `502` or TLS errors, confirm the upstream host is `events.thrad.ai`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.