---
title: How does consent work for server-side tracking?
description: Pass consent from your CMP to server-side Meta CAPI events. Map advertising permissions, handle unknown states and withdrawal, and check queued events.
published_at: 2026-09-03
last_updated: 2026-09-04
format: Guide
---

# How does consent work for server-side tracking?

Server-side tracking needs the same explicit permission decision carried from the collection interface to the server event. A consent-management platform (CMP) or policy layer supplies the decision; the event builder checks it before sending consent-dependent data. If a required permission is denied or unresolved, stop that operation. Moving the request to a server does not create permission.

This guide describes Plainrouter's technical contract. It is not legal advice. The permissions, purposes, jurisdictions, retention rules, and lawful bases that apply to your product require your own review.

## Key takeaways

- Moving tracking from a browser to a server does not bypass consent or purpose restrictions.
- `consent_basis: "consent"` records a declared basis; it does not create or prove consent by itself.
- Keep the consent decision in an authoritative customer or session record, with its capture time and source.
- Plainrouter uses separate permissions for advertising storage, advertising user data, and advertising personalization.
- Leave unresolved permissions `unknown`. Never turn a missing value into a grant.
- Browser-originated advertising collection requires all three permissions to be granted.
- A normal direct server event using consent requires the downstream advertising permissions to be granted, while `ad_storage` must truthfully describe whether browser storage was used.
- Withdrawal stops future consent-dependent work; it is not automatically a retroactive deletion request.

## Does server-side tracking remove the need to check consent?

No. A server integration still needs a decision about whether each operation is permitted. A browser tag runs on the visitor's device; a server integration sends from infrastructure you control. That difference can improve reliability, credential handling, and payload validation, but it does not answer the permission question.

The server often knows less about the visitor's current choice than the browser does. A queue worker may run minutes after checkout. A webhook may arrive without the browser session. A retry may reuse an event built before the user changed their settings. The system therefore needs an explicit decision boundary:

```text
Consent interface ── authoritative decision ── event builder ── delivery gate
                           │                         │
                           ├─ state                  ├─ event data
                           ├─ captured_at            └─ consent snapshot
                           └─ source
```

Do not infer permission from the request being first-party, the event being important, the customer being logged in, or identifiers already existing in a database. Those facts describe the data path, not the user's current choice or your permitted purpose.

## What do ad_storage, ad_user_data, and ad_personalization mean?

Plainrouter's explicit consent snapshot separates the same advertising controls described in [Google's current consent-mode terminology](https://developers.google.com/tag-platform/security/concepts/consent-mode):

| Field                | What it controls in this integration                                                         |
| -------------------- | -------------------------------------------------------------------------------------------- |
| `ad_storage`         | Reading or writing advertising browser storage, including `_fbp`, `_fbc`, and visitor state. |
| `ad_user_data`       | Using customer and network information for downstream advertising delivery.                  |
| `ad_personalization` | Using the event for advertising personalization.                                             |

Each field accepts `granted`, `denied`, or `unknown`. These states are intentionally different:

| State     | Meaning for the event builder                                                                 |
| --------- | --------------------------------------------------------------------------------------------- |
| `granted` | The authoritative consent source permits this purpose at the recorded decision boundary.      |
| `denied`  | The source has an explicit negative decision. Do not send consent-dependent data for it.      |
| `unknown` | The system does not yet have a usable decision. Wait or omit the consent-dependent operation. |

Unknown is not a softer grant. It is the correct state while an asynchronous consent platform restores a saved choice or while the server cannot associate a decision with the event.

These field names describe Plainrouter's accepted consent contract and Google's consent terminology. They are not a claim that Meta CAPI requires these exact fields in its native payload. Your integration must map its authoritative consent decision to the requirements of each destination.

## Decide before building the event

Model the application-side decision independently from the API client. This keeps denied and unresolved states out of queue payloads and logs:

```ts
type Permission = 'granted' | 'denied' | 'unknown';

type AdvertisingConsent = {
    adStorage: Permission;
    adUserData: Permission;
    adPersonalization: Permission;
    capturedAt: Date;
    source: string;
};

type EventContext = {
    usedAdvertisingStorage: boolean;
};

export function canSendMetaCapi(consent: AdvertisingConsent, context: EventContext): boolean {
    const downstreamGranted = consent.adUserData === 'granted' && consent.adPersonalization === 'granted';
    const storageStateIsTruthful = !context.usedAdvertisingStorage || consent.adStorage === 'granted';

    return downstreamGranted && storageStateIsTruthful;
}
```

For a browser-originated event, advertising storage is part of the collection path, so all three values must be granted. For a genuinely server-only event that did not read or write browser advertising identifiers, preserve the actual `ad_storage` state and omit `fbp`, `fbc`, and other browser-derived identifiers.

Do not silently remove a denied field and retry. Absence and `unknown` still do not establish permission.

## How do you send consent with a server-side Meta CAPI event?

The TypeScript SDK exposes the consent-based event variant directly:

```ts
import { configurePlainrouter, createEvent, type CreateEventData } from '@plainrouter/sdk';

type ConsentedBody = Extract<CreateEventData['body'], { consent_basis: 'consent' }>;

const signalSecret = process.env.PLAINROUTER_SIGNAL_SECRET;

if (!signalSecret) {
    throw new Error('PLAINROUTER_SIGNAL_SECRET is required');
}

configurePlainrouter({ signalTrackerSecret: signalSecret });

const body: ConsentedBody = {
    event_id: 'purchase:ORDER-1042',
    event_name: 'Purchase',
    event_time: Math.floor(Date.now() / 1000),
    event_source: 'https://shop.example/checkout/success',
    action_source: 'website',
    consent_basis: 'consent',
    consent: {
        ad_storage: 'granted',
        ad_user_data: 'granted',
        ad_personalization: 'granted',
        captured_at: '2026-09-03T12:00:00Z',
        source: 'checkout_cmp',
    },
    user_data: {
        email: 'buyer@example.com',
    },
    click_ids: {
        fbp: 'fb.1.1788436800000.1234567890',
    },
    value_data: {
        value: '49.90',
        currency: 'EUR',
        order_id: 'ORDER-1042',
    },
};

const result = await createEvent({ body });

if (result.error) {
    throw new Error(`Event rejected: ${JSON.stringify(result.error)}`);
}
```

The example includes `fbp`, so `ad_storage: "granted"` is a necessary description of the path. The fixed timestamp and identifiers are illustrative; production code must use the real decision time, the real consent source, and values belonging to that visitor.

Plainrouter requires every public Conversion API event to declare `consent_basis`. For the consent-based variant, contradictory or incomplete downstream permission returns HTTP `422` and creates no event.

## Treat the snapshot as evidence, not permission

A useful server-side record contains enough provenance to explain why the event builder made its decision:

| Record               | Why it matters                                                                         |
| -------------------- | -------------------------------------------------------------------------------------- |
| Permission state     | Distinguishes an explicit grant, explicit denial, and unresolved choice.               |
| `captured_at`        | Establishes which decision was available when the operation was evaluated.             |
| `source`             | Identifies the CMP, settings screen, checkout flow, or policy system that supplied it. |
| Subject/session link | Associates the decision with the correct customer or browser session.                  |

This record is evidence about the application's decision path. It is not a substitute for a valid consent experience, accurate disclosures, purpose limitation, or applicable legal obligations.

Avoid putting raw identity values, consent strings, or secrets into URLs, prompts, analytics properties, or general application logs. Log the operational result and a safe internal reference instead.

## Can you use Google Consent Mode v2 or an IAB TCF string?

Yes. Plainrouter accepts an explicit consent snapshot, Google Consent Mode v2 values, or a supported IAB TCF string:

| Adapter        | Use                                                                                     |
| -------------- | --------------------------------------------------------------------------------------- |
| `consent`      | Plainrouter's explicit three-permission snapshot.                                       |
| `consent_mode` | Google Consent Mode v2 values from an existing implementation.                          |
| `tcf`          | A supported IAB Transparency and Consent Framework v2 string and optional capture time. |

Choose the adapter closest to the authoritative source. Translating between formats adds another place for purpose mappings, timestamps, and unknown states to drift.

If you provide more than one adapter, their normalized permission states must agree. Conflicting adapters are rejected with HTTP `422`. Do not add a second adapter as a fallback that turns a denial into a grant.

Google documents `ad_storage`, `ad_user_data`, and `ad_personalization` as separate consent types. An IAB TCF string carries a more detailed framework decision; it is not merely a compact version of the three explicit fields. The IAB also states that participating in its framework does not replace each participant's responsibility for legal compliance.

## How do you keep browser and server consent synchronized?

For the Plainrouter browser API, send the initial state after initialization and every later change:

```js
await signalq('consent', {
    consent_basis: 'consent',
    consent: {
        ad_storage: 'granted',
        ad_user_data: 'granted',
        ad_personalization: 'granted',
        captured_at: new Date().toISOString(),
        source: 'your_cmp',
    },
});

// If the visitor later withdraws consent:
await signalq('consent', {
    consent_basis: 'consent',
    withdrawn: true,
});
```

Plainrouter's browser path sends events only when all three permissions are granted. While the choice is unresolved, it neither reads identifiers nor sends measurement. An explicit denial, malformed update, or withdrawal clears Plainrouter-owned browser and click identifiers.

Your server application still needs its own synchronized state. Updating the browser SDK does not automatically cancel an event already stored in your queue or change a customer record in another system.

## What happens to queued events after consent withdrawal?

Do not assume a browser consent update changes jobs already queued by your application. Stop future consent-dependent work and apply your current-state policy to pending jobs before they send. Preserve event IDs on any permitted retry.

A robust server flow evaluates consent at more than one boundary:

1. Resolve the authoritative decision before constructing the event.
2. Avoid placing consent-dependent identity data into a queue when the decision is denied or unknown.
3. Store the decision time and source needed by the event contract.
4. If your policy requires current-state enforcement at execution time, let the worker re-read the authoritative state instead of trusting an old queue snapshot.
5. Keep the same `event_id` when retrying the same logical event; a retry must not become a new conversion.
6. Stop enqueueing future advertising events after withdrawal.

Withdrawal changes future consent-dependent processing. It does not automatically mean “delete every previously retained record.” Handle verified deletion requests through the dedicated user-data deletion operation and your retention workflow.

Do not change a denied purchase to `legitimate_interest` to make the request pass. Plainrouter's public contract restricts that basis to allowlisted lifecycle operations; it is not a fallback for consent-dependent revenue events.

## How do you debug rejected or missing consented events?

1. Identify the authoritative consent source for the affected customer or session.
2. Confirm the event builder reads that source rather than a UI default or cached global value.
3. Compare `captured_at` with the event time and queue execution time.
4. Check each permission separately; do not summarize three states as one Boolean too early.
5. Verify whether browser storage or browser-derived identifiers were actually used.
6. Confirm only one consent adapter is sent, or that multiple adapters resolve to the same state.
7. Inspect the HTTP `422` validation fields without logging the full event payload.
8. Confirm a rejected request created no downstream retry with weakened consent data.
9. Test grant, denial, unknown, malformed, delayed restoration, and withdrawal as separate cases.

## Common server-side consent mistakes

| Mistake                                                      | Why it fails                                                         | Better approach                                                             |
| ------------------------------------------------------------ | -------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Treating server-side transport as consent-free.              | Infrastructure location does not establish purpose or permission.    | Carry the authoritative decision into the server event.                     |
| Hard-coding all fields to `granted`.                         | The payload no longer reflects the user's choice.                    | Map the current CMP or policy state at the event boundary.                  |
| Converting missing values to grants.                         | A race or integration failure becomes unauthorized delivery.         | Preserve `unknown` and wait or skip.                                        |
| Hashing identity and assuming it is no longer personal data. | Hashing changes representation, not the permission requirement.      | Gate collection and delivery before hashing or transport.                   |
| Sending browser IDs with `ad_storage: "denied"`.             | The declared state contradicts how the identifiers were obtained.    | Omit browser-derived identifiers or use the truthful granted state.         |
| Reusing one visitor's consent snapshot for another.          | The decision is associated with the wrong subject or session.        | Bind consent evidence to the correct customer or browser context.           |
| Retrying with a different `event_id`.                        | One logical conversion can become multiple accepted events.          | Preserve event identity across transport retries.                           |
| Treating withdrawal as automatic deletion.                   | Future processing and historical retention are different operations. | Stop future work and route verified deletion through the deletion workflow. |

## Frequently asked questions

### Does server-side tracking work without cookies?

It can send events that do not use browser cookies, but cookie-free does not automatically mean consent-free. Customer information, network data, advertising delivery, and personalization can still require an applicable permission or other reviewed basis.

### Should denied consent be omitted from the API request?

No. Do not remove a denial to make a normal advertising event look eligible. Resolve the state before calling the API and skip the consent-dependent event when required permissions are denied or unknown.

### Is hashing email enough to make consent unnecessary?

No. Hashing is a data-handling step required by some destination contracts. It does not establish permission, detach the value from its source, or authorize a new purpose.

### What should happen while the CMP is loading?

Keep the state unknown and fail closed. Plainrouter's browser integration waits without reading its identifiers or sending measurement until the consent platform supplies a usable decision.

### Can a queued purchase use the consent state captured at checkout?

That depends on your policy and the event's processing purpose. Preserve the checkout decision and provenance, and re-check the authoritative state at execution time when your policy requires current-state enforcement.

### Does withdrawal delete old events?

Not by itself. Withdrawal stops future consent-dependent processing. A verified deletion request is a separate operation with its own identity verification and retention handling.

### Can legitimate interest replace denied consent?

Not as a technical retry strategy. Do not change legal-basis fields to bypass a denial. Plainrouter does not accept legitimate-interest revenue through the normal public event variant.

## Sources

- [Meta Business Tools Terms](https://www.facebook.com/legal/technology_terms)
- [Google: Consent mode overview](https://developers.google.com/tag-platform/security/concepts/consent-mode)
- [IAB Europe: Transparency and Consent Framework policies](https://iabeurope.eu/wp-content/uploads/230509-TCF-Policies-TransparencyConsentFramework_Policies_version_TCF-v2.2-1-1.pdf)
- [Plainrouter: Track events and consent](https://plainrouter.com/docs/signals/track-events)
- [Plainrouter Conversion API reference](https://plainrouter.com/docs/api/conversions)
- [Plainrouter TypeScript SDK consent request types](https://github.com/plainrouter/sdk/blob/v0.5.1/packages/sdk/src/generated/types.gen.ts)
- [Plainrouter TypeScript SDK runtime schemas](https://github.com/plainrouter/sdk/blob/v0.5.1/packages/sdk/src/generated/zod.gen.ts)

## Related guides

- [Troubleshoot missing Meta Pixel and CAPI events](/library/meta-capi-missing-duplicate-events)

- [What is fbclid? How it relates to fbp and fbc](/library/fbp-fbc)
- [How does Meta event_id deduplication work?](/library/meta-event-id-deduplication)
- [Meta Event Match Quality: diagnose a low or falling score](/library/meta-event-match-quality)
- [Can you use Meta CAPI without Google Tag Manager?](/library/meta-capi-without-gtm)
