---
title: Geography headers
description: Which request headers SvelteKit's c15t helpers read for country,
  region, language and Global Privacy Control, how to trust them, and how to
  test another location.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Why location matters

Your consent policy picks a rule by the visitor's country and region: opt-in
in Europe, opt-out in US states with privacy laws, no banner where no law
applies. On SvelteKit, the server helpers read that location from request
headers your host or CDN adds. If no header arrives, the policy resolves with
its rule for an unknown location.

## Which headers are read

`loadConsent`, `c15tHandle`, the manifest route and `resolveConsent` read the
same headers, in this order. The first one present wins.

|Input|Headers, highest priority first|
|--|--|
|Country|`x-c15t-country`, `cf-ipcountry`, `x-vercel-ip-country`, `x-amz-cf-ipcountry`, `x-country-code`, `x-country`|
|Region|`x-c15t-region`, `cf-region-code`, `x-vercel-ip-country-region`, `x-region-code`|
|Language|`accept-language`, negotiated by quality value to a primary tag such as `de`|
|Global Privacy Control|`x-c15t-gpc`, `sec-gpc`; only `1` and `0` count|

Vercel adds its country and region headers on every request. Cloudflare adds
`cf-ipcountry`; for `cf-region-code`, turn on Cloudflare's visitor location
headers. On another host, or a Node server without a CDN, set `x-c15t-country`
and `x-c15t-region` at your proxy from whatever location data it has.

With a build-time snapshot, as in the quickstart, `loadConsent` resolves the
policy in the server process from these inputs and makes no policy request.
In `hosted()` mode, it sends the resolved inputs to the backend's `/init` as
`x-c15t-country`, `x-c15t-region`, `accept-language` and `sec-gpc`, along
with `user-agent`. It adds the visitor IP as `x-forwarded-for` only with
`trustForwardedHeaders: true`. It never forwards `forwarded`,
`x-forwarded-host` or `x-forwarded-proto`; with that option it reads them to
resolve a relative `backendURL`. When the backend is a route in your own
app, it passes the same headers through `event.fetch`, which forwards only
cookies and authorization by itself.

## Resolve the inputs once per request

`c15tHandle` in `src/hooks.server.ts` reads the headers once, stores the
inputs on `event.locals.c15t.inputs`, and rewrites the request's headers to
the normalized `x-c15t-country`, `x-c15t-region`, `accept-language` and
`sec-gpc`. Later loads, endpoints and proxied backend calls then see one
form, whichever CDN header carried the value:

```ts title="src/hooks.server.ts"
import { c15tHandle } from '@c15t/svelte/kit';

export const handle = c15tHandle();
```

[Server API](./server-api#c15thandle) lists its options.

## Trust the headers

The `x-c15t-*` headers always win, and a browser can send them. A visitor who
sets `x-c15t-country: US` gets the policy for the United States. That changes
only their own banner, not anyone else's, but if you need the policy to
follow your CDN's location, remove incoming `x-c15t-*` headers at your edge
before the request reaches SvelteKit.

`cf-ipcountry` and the Vercel headers are set by the platform and cannot be
forged through it. Behind your own proxy, make sure it overwrites the country
headers rather than passing a client's value through.

The consent route fetches a relative `backendURL` through `event.fetch` and
never resolves it against the request, so a forged `Host` or
`x-forwarded-host` header cannot send its requests elsewhere. `loadConsent`
resolves a relative `backendURL` against `event.url`. With its default
`event.fetch`, the call stays in-process. With your own `fetch`, it goes to the
origin of `event.url`. `adapter-node` builds that origin from `paths.origin`
in SvelteKit 3 (the `ORIGIN` variable in SvelteKit 2), or from its
`PROTOCOL_HEADER` and `HOST_HEADER` settings; with none of them, it takes the
host from the `Host` header. SvelteKit 3's `adapter-node` ignores `ORIGIN`
without a warning, so move it to `paths.origin` when you upgrade. A forged `x-forwarded-host` changes
neither unless you set `trustForwardedHeaders`. See
[Server API](./server-api#loadconsent).

## Force a location

Pass `country`, `region` or `language` to force an input:

* On `c15tHandle({ country: 'DE' })`, for every request, such as a site that
  serves one country.
* On one `loadConsent(event, { country: 'DE' })` call, from a layout's own
  `load`, such as a `/de/` section.

A per-call value wins over the handle's. The browser's provider takes
`overrides={{ country, region, language, gpc }}` for the same purpose after
hydration.

## Test another location

Send the override header with a request and look for the banner in the HTML:

```sh
curl -s -H 'x-c15t-country: DE' http://localhost:5173/ | grep -c consent-banner-root
curl -s -H 'x-c15t-country: US' -H 'x-c15t-region: CA' http://localhost:5173/ | grep -c consent-banner-root
curl -s -H 'Sec-GPC: 1' -H 'x-c15t-country: US' -H 'x-c15t-region: CA' http://localhost:5173/
```

The first request prints `1` under a policy that asks Europe for opt-in. What
the others print depends on your policy's rules for California. In the
browser, the dev tools panel's location tab changes the location for the
current page without a request header.

## Verify

1. On your deployed site, open DevTools and check the document request's
   headers for your CDN's country header.
2. View the page source. The banner is in the HTML for a location that needs
   one, and absent for one that does not.
3. With the dev tools panel, confirm the resolved country and policy match
   where you are.
