---
title: Data fetching
description: Choose the recommended build-time policy snapshot or runtime
  manifest fetching, backend /init, browser resolution or offline rules, and
  understand where consent choices are saved.
group: concepts
lastModified: "2026-10-10T16:01:45+01:00"
---
## Compare the fetching paths

Where policy resolves and where choices are saved are separate from who runs
the backend. Inth and a [self-hosted backend](/docs/self-host/quickstart) speak
the same protocol, so every path below works with either. Only `offline()`
removes backend requests. For a recommendation by framework, start with
[choose your setup](/docs/concepts/choose-your-setup).

|Fetching path|Where policy resolves|Where choices are submitted|Choose it when|
|--|--|--|--|
|Build-time manifest, recommended|Your server or browser resolves a bundled policy snapshot with each visitor's inputs|Inth or your c15t backend|Your framework supports a build integration and policy changes can ship with a rebuild|
|Cached manifest on your server|Your application resolves public policy data with each request's location and signals|Inth or your c15t backend|Policy changes must reach the app without rebuilding, or builds cannot access the backend|
|Regular backend `/init`|The consent backend|The same backend|You want the fewest moving parts, need backend-owned request resolution, or have no application server|
|Manifest in the browser|The browser, using supplied or unknown location|Inth or your c15t backend|You deliberately want client resolution and have planned geography, bundle size and policy refresh|
|Offline|The browser or local runtime, using bundled rules|No backend submission|Local development and tests. Not recommended for production environments.|
|Custom transport|Your implementation|Your implementation|An existing service cannot use the c15t backend protocol|

## What is a consent manifest?

A manifest is a versioned policy document served by `GET /manifest`. It contains
policy rules, translation configuration and related consent configuration. It
is public configuration, not a visitor's saved choices. A resolver combines the
manifest with country, region, language and privacy signals to produce an init
result for one visitor.

Reusing the public document avoids asking the consent backend to resolve policy
for every application request. Cache misses and revalidation still fetch the
manifest, and consent writes still need the backend. Measure the deployed
request path before promising a latency improvement.

Do not put secrets, visitor identifiers or consent records into a manifest.
Keep personalized init responses out of shared caches. Changing a policy also
requires a refresh strategy for cached or build-time manifests.

## Use a build-time manifest by default

The build fetches your public policy once and bundles it, so the server never
fetches it at runtime.

`manifest()` is the default mode in every server-rendered framework.
[Consent modes](/docs/concepts/modes#download-the-manifest-at-build-time)
covers each framework's build integration and what happens when the
download fails.

The framework quickstarts include the build integration for supported
deployments. Server resolution uses the snapshot without an upstream manifest
request. Browser resolution includes the policy and resolver in the client
bundle, and may still request location for regional policies. Static Nuxt and
Astro sites and script-tag sites keep the runtime setup from their guides.

The snapshot is fixed at build time:

* Rebuild after changing policies, translations or vendors. If your CI caches
  build output, force a fresh build.
* Consent choices still go to the backend.

The build reads the backend URL from your public backend URL variable when
the config doesn't pass one: `NEXT_PUBLIC_C15T_BACKEND_URL` in Next.js,
`NUXT_PUBLIC_C15T_BACKEND_URL` in Nuxt, `PUBLIC_C15T_BACKEND_URL` in Astro,
Svelte and SvelteKit, and `VITE_C15T_BACKEND_URL` in TanStack Start and other
Vite apps. Each also reads the matching Inth variable, such as
`NEXT_PUBLIC_INTH_PROJECT_URL`, when the c15t one is unset. See
[set the backend URL](/docs/concepts/modes#set-the-backend-url).

The fetch waits at most 10 seconds. When it fails, or no backend URL is set,
every framework does the same thing:

|Command|Default when the fetch fails|
|--|--|
|Production build: `next build`, `vite build`, `nuxt build`, `astro build`|The build stops with an error.|
|Dev: `next dev`, `vite dev`, `nuxt dev`, `astro dev`|A warning, and the server fetches the policy at runtime.|

Set `onBuildError` to use one behaviour for both. `'fail'` stops dev too.
`'runtime'` lets a production build finish, and the server fetches the policy
at runtime. The `C15T_ON_BUILD_ERROR` environment variable overrides the
option, so you can deploy during a backend outage without a code change:

```sh
C15T_ON_BUILD_ERROR=runtime npm run build
```

Turborepo's strict environment mode hides undeclared variables from tasks, so
list `C15T_ON_BUILD_ERROR` in the build task's `passThroughEnv` there.

The build skips the fetch, without an error, when it can't use a snapshot,
for example when the backend URL is relative. With `onBuildError: 'fail'`, a
relative URL stops the build. [Consent modes](/docs/concepts/modes#what-happens-when-the-download-fails)
lists every case.

## How manifest mode counts visitors

The backend counts visitors from `/init` requests, and manifest resolution
skips them. To keep the count, the server adapters send a session report after
each resolution: the host posts to the backend's `POST /sessions` from the
server, after the response. The browser sends nothing, and the report stores
no identity; the visitor's IP address and user agent are forwarded under the
backend's usual IP handling. Static output resolves in the browser and sends
no report. Set `reportSessions: false` on an adapter to turn it off.

## Link an init to the save that follows

Each page load gets a journey id, a random UUID sent on requests c15t already
makes:

|Request|Query parameters|
|--|--|
|`GET /init`|`journey`, `journeyScope`, `stored` (`1` if a choice or notice dismissal was stored)|
|`POST /subjects`|`journey`, `journeyScope`|

Session reports carry it as `journey: { id, scope, storedChoice, prompt, domain }`.
A report and a save with the same `id` belong to one page load. `prompt` is
`due`, `stored` or `not-required`; a page that falls back to the default opt-in
banner counts as `due` or `stored`. A stored answer counts as `stored` even if a
policy change makes c15t ask again.

The id is random, is never the subject id and is never written to a cookie.
Every save that carries one follows an `/init` or report with the same id.
Saves replayed after going offline carry none.

|`journey`|Behavior|
|--|--|
|`'page'` (default)|One id per page load, in memory.|
|`'tab'`|Kept in `sessionStorage` while a prompt is due, and removed once the visitor chooses or no prompt is due. Only when the browser resolves init; server-rendered pages use `'page'`.|
|`false`|No journey.|

Set it on the runtime or provider. In Next.js use `defineConsentConfig({ journey })`;
in TanStack Start pass the same value to `resolveConsent` and `ConsentRoot`; with
the inline prefetch script, pass it to `buildPrefetchScript`. Vue, Svelte, Astro
and the script tag use `'page'`. A manifest that resolves locally in the
browser reports no init, so it sends no journey.

If you pass a transport `hosted()` a `fetch`, match on the path: URLs now carry a query string.

## What does regular `/init` do?

`hosted()` uses `${backendURL}/init` for initialization and
`${backendURL}/subjects` for consent submissions. The name `hosted` describes
the protocol; the URL can belong to Inth or your own c15t backend.

```ts title="c15t.config.ts"
import { defineConsentConfig, hosted } from 'c15t/next';

export default defineConsentConfig({ mode: hosted() });
```

Each framework passes the mode in a different place, and server-rendered
frameworks take the data `hosted()` from their own package.
[Consent modes](/docs/concepts/modes#import-the-factories-from-your-framework-package)
lists where each one goes.

A regular backend `/init` request lets the backend resolve the visitor context.
A same-origin URL named `/api/c15t/init` can instead resolve from a cached
manifest in your application. The URL name alone does not tell you which path
runs.

## How do transports and proxies differ?

A transport implements initialization, saving and optional record operations.
A proxy changes where HTTP requests travel. It does not change the policy
resolver or make personalized responses safe to cache.

For a same-origin init route that resolves a manifest, the hosted transport can
separate policy reads and record writes. With a backend rewrite mounted at
`/api/c15t`, a single-page app can use the transport `hosted()` from
`c15t`:

```ts title="src/consent-mode.ts"
import { hosted } from 'c15t';

export const mode = hosted({
	backendURL: '/api/c15t',
	initURL: '/api/c15t/init',
});
```

The route must return the c15t init response contract. c15t adds its inputs to
`initURL` as query parameters, replacing any of your own with the same name.
See [query parameters and CORS](/docs/self-host/api/endpoints#query-parameters-and-cors)
for the names. Because `initURL` is set, `assertDecisionInputs` defaults to
`true`: saves carry the policy decision they were made against when init
returned no signed policy snapshot token, so the backend can reject a save
made against a stale policy. Server-rendered frameworks do this for you when
you set `routePrefix`: the browser re-inits through `${routePrefix}/init`.
Every framework rejects `routePrefix: '/'` when it sets up, because a
catch-all consent route at the site root would catch every page.

The init route resolves policy; the backend rewrite forwards `/api/c15t/subjects`
and other record endpoints. Configure both if you choose this optional proxy
variant. Server-rendered frameworks do both with one option: `proxy: true` in
Next.js `c15t.config.ts` or TanStack Start's `createConsentStateHandler`,
with a consent route created with `proxy: true`. The
[Next.js recipe](/docs/frameworks/next/optimization#keep-browser-consent-requests-on-your-origin)
shows the configuration. A direct absolute backend URL works
without a rewrite.

This optional optimization keeps c15t requests on the app's origin and avoids a separate browser DNS lookup
and TLS connection to the consent backend. The app server still connects to
the upstream backend for manifest refreshes and consent writes. Vendor scripts
and vendor requests keep their own origins.

The rewrite destination and the init route use the absolute upstream endpoint,
such as `https://your-project.inth.app`. The browser uses `/api/c15t` without
needing the upstream URL. A static export cannot serve a Next.js route or
rewrite at runtime; use the absolute Inth URL or a proxy provided by the static
host instead.

## When should I use offline mode?

Not recommended for production environments. Use Inth or a self-hosted backend
for production policy and consent records.

`offline()` resolves bundled policy rules without an init request and acknowledges
saves locally. The runtime's persistence module stores the choice in browser
storage. There is no backend audit history, cross-device record service or IP
geolocation supplied by this transport.

Use your framework's `offline()` so its translations and provider context are
included. In a React app it replaces the mode passed to `ConsentProvider`:

```ts title="src/consent-mode.ts"
import { offline } from 'c15t/react';

export const mode = offline();
```

In Next.js, TanStack Start, Nuxt, Astro and SvelteKit, pass the data
`offline()` from the framework package where the config takes `mode`; see
[consent modes](/docs/concepts/modes#import-the-factories-from-your-framework-package).

With no `policyRules`, the current offline transport uses the recommended rule
pack. Supplying `policyRules` replaces that pack. Unknown country and region are
real resolution inputs; offline mode does not discover a visitor's location.
Use [policy rules](/docs/concepts/policies) to understand
matching and defaults, and test the missing-location case.

Offline mode is an explicit architecture choice, not an automatic fallback for
a failed Inth request. If hosted initialization fails before a policy resolves,
optional permissions remain denied and the stock prompt stays hidden. Observe
initialization failures instead of silently changing policy sources.

## Can I provide my own transport?

`custom(transport)` accepts a `KernelTransport` with the v3 `init` and `save`
contract. It does not accept v2 endpoint handlers such as `setConsent`. Keep
policy resolution, record acknowledgments and failure behavior consistent with
the kernel contract. Prefer a built-in transport when your backend supports it.

## Verify the selected path

Inspect browser and server requests separately. A server manifest fetch will
not appear in the browser's Network panel. With a build-time snapshot, start a
fresh production server and confirm it makes no upstream `/manifest` request.
After a policy edit, rebuild and confirm the new policy appears. On a runtime
manifest path, check that
page requests do not call the backend `/init`, a visitor's choice still reaches
the backend's `/subjects`, and policy changes become visible after the configured
refresh. With a same-origin consent proxy, the browser should call only
`/api/c15t` paths for consent HTTP traffic; inspect server logs to verify their
upstream destinations.

Test different locations, missing location headers, GPC, returning choices and
backend failure. See [verification](/docs/guides/verify-consent).
