Where the TCF Consent String Actually Lives in a Bid Request — and Why the Path Changed

Where the TCF Consent String Actually Lives in a Bid Request

A bid request arrives with regs.gdpr set to 1. The bidder reads the consent string, finds nothing, and bids without personalization — or doesn’t bid at all. Nothing in the payload is malformed. The JSON validates. The auction runs.

That is the failure mode worth understanding. The Transparency and Consent Framework (TCF) consent string is not hard to parse once you have it. The hard part is that the same logical signal occupies two different JSON locations depending on which OpenRTB version produced the request, and a bidder wired to only one path will silently miss consent on the other version’s traffic. This article traces the field from the CMP that writes it to the bidder that decodes it, and treats the 2.5/2.6 path split as the operational hazard it is.

What the string is, and who is allowed to write it

The TC String is defined by IAB Europe’s TCF specification, with IAB Tech Lab stewarding the technical documents. Its purpose is narrow and specific: it encapsulates what was disclosed to a user and what that user chose, encoded as a compact HTTP-transferable string. As the TC String format specification puts it, the string “enables communication of transparency and consent information to entities, or ‘vendors’, that process a user’s personal data.”

The authorship rule matters for anyone debugging a missing or stale string. The same specification states:

A Transparency & Consent String may only be created by an IAB Europe TCF registered CMP using its assigned CMP ID number in accordance with the Policies. Vendors or any other third-party service providers must neither create nor alter TC Strings.

So when a consent string is absent from a bid request, the question is not “can we synthesize one?” — the answer is no. The question is where in the chain between the CMP and the bidder’s decoder the value was dropped, or whether it was ever generated for that user.

The specification also describes what the string is for at the receiving end: “Vendors decode a TC String to determine whether they have the necessary legal bases to process a user’s personal data for their purposes.” That is the whole economic function of the field. It is a legal-basis lookup key, not a targeting signal in its own right.

The two paths: user.ext.consent and user.consent

GDPR support arrived in OpenRTB as a pair of extension fields defined by the IAB GDPR advisory: regs.ext.gdpr and user.ext.consent. OpenRTB 2.6 promoted both to first-class attributes, and the change log records each as “previously ext,” according to the privacy-signal reference at rtblint.org.

In 2.6, then:

  • regs.gdpr is an integer: 0 means the request is not subject to GDPR, 1 means it is, and omission means unknown.
  • user.consent is a string holding the TCF consent string when GDPR applies.

Two details in that layout trip people up. First, the consent string is a string on user — not an object, and not under regs. The regs object says whether GDPR applies; user carries what the user chose. Second, the pre-2.6 locations are still alive in production traffic. Old SDKs and hand-rolled adapters keep emitting regs.ext.gdpr and user.ext.consent, and a bidder reading only the first-class fields sees no signal at all on those requests.

The rtblint reference is explicit about the consequence of a half-populated request: “A request with regs.gdpr: 1 and no user.consent tells bidders consent is unresolved; most will bid without personalization or not at all.” That sentence is hedged — “most” — and should be read that way. The point is not a precise no-bid rate. The point is that the request has communicated an unresolved state, and the bidder’s response is a business decision made under uncertainty.

The path split is handled in code, not by convention

If you want evidence that this is a real engineering problem rather than a documentation nicety, look at how an open-source exchange handles it. Prebid Server’s ConvertDownTo25 function in openrtb_ext/convert_down.go exists to translate a 2.6 request into a 2.5-shaped one before passing it to bidders that expect the older schema. The consent handling is a dedicated function:

// moveConsentFrom26To25 modifies the request to move the OpenRTB 2.6 GDPR consent
// field (req.user.consent) to the OpenRTB 2.5 location (req.user.ext.consent). If
// the OpenRTB 2.5 location is already present it may be overwritten.
// The OpenRTB 2.5 location is expected to be empty.
func moveConsentFrom26To25(r *RequestWrapper) error {
	if r.User == nil || len(r.User.Consent) == 0 {
		return nil
	}
	consent26 := r.User.Consent
	r.User.Consent = ""
	userExt, err := r.GetUserExt()
	if err != nil {
		return err
	}
	userExt.SetConsent(&consent26)
	return nil
}

Three things are worth pulling out of that snippet. It reads the 2.6 location, clears it, and writes the value to the 2.5 extension location. The comment notes that an existing 2.5 value “may be overwritten” and that the 2.5 location “is expected to be empty” — an assumption, not an enforced invariant. And the function returns early when r.User.Consent is empty, which means a request that arrived with consent only at user.ext.consent passes through untouched. The down-conversion is one-directional.

The same file’s clear26Fields function zeroes user.Consent as part of stripping 2.6-only fields, consistent with the comment that “the previous OpenRTB 2.5 spec did not specify that bidders must tolerate new or unexpected fields.” That is the design rationale for the whole conversion layer: a 2.5-era bidder may reject or ignore fields it does not recognize, so the adapter must move the signal to where that bidder looks.

This is one implementation, and I am not claiming it describes every exchange. What it does demonstrate is that the 2.5/2.6 consent path difference is significant enough that a major open-source exchange wrote explicit translation code for it. If your integration assumes a single canonical path, you are assuming something the code does not.

What the bidder does once it has the string

Locating the string is step one. Decoding it correctly is step two, and the TCF Implementation Guidelines are specific about evaluation order. For each purpose, vendors must “Evaluate publisher restrictions prior to any other signal.” Only after publisher restrictions are resolved does the vendor apply its own default legal basis or the publisher-defined one.

That ordering has a practical implication for debugging. A bidder that suppresses an impression may be doing so because of a publisher restriction encoded in the string, not because the user withheld consent for that vendor. If your logs record only “consent present / absent,” you cannot distinguish the two, and you will misattribute the loss.

The guidelines also address where to read the string on the server side: “If an impression is received server side (through openRTB for example), you should read the information from the TC data payload.” For client-side paths — redirects, Prebid in the browser — the CMP API is the source, and the guidelines recommend the event listener rather than a one-shot read so you get the most current TC data object.

What I am not claiming

Several things commonly asserted about consent strings are not supported by the sources I checked, and I am leaving them out rather than dressing them up.

I am not quantifying the revenue or bid-rate impact of a missing consent string. No retrieved source provides a figure, and inventing one would be worse than saying so.

I am not describing the bit-level layout of the TCF v2.2 core string. The format specification was retrieved only as a partial excerpt covering scope, authorship, and purpose; it does not support field-level encoding claims here. If you need the bit layout, read the specification directly.

I am not asserting how any named exchange or ad server handles user.ext.consent in production. The relevant vendor documentation did not retrieve, and the Prebid Server code is evidence about Prebid Server, not about the rest of the market.

An operational checklist

The argument of this article is that consent-string handling is a transport problem before it is a legal-logic problem, and the two have different owners and different fixes. A transport bug — the value sitting at a path nobody reads — is fixed in an adapter. A legal-basis bug — the value read correctly but evaluated in the wrong order — is fixed in bidding logic. Conflating them wastes time in both directions.

What follows is my recommendation for instrumenting the transport layer, not a finding from any source:

  1. Log which consent path was populated. Record, per request, whether user.consent, user.ext.consent, both, or neither carried a value. This single field in your logs surfaces adapter drift before it reaches bidding logic.
  2. Flag the unresolved state explicitly. A request with regs.gdpr equal to 1 and no consent string at either path is a distinct condition. Count it separately from “GDPR not applicable” and from “consent present.”
  3. Audit gating logic, not just schema validity. A structurally valid request can still carry a consent string that does not authorize the vendor or purpose in question. Validation of JSON shape will not catch that; only decoding and evaluating will.
  4. Watch for the overwrite assumption. If your pipeline converts between versions, check what happens when both locations are populated. The Prebid Server comment says the 2.5 location “is expected to be empty” — verify that expectation holds in your traffic rather than trusting it.

The consent string is a small field with a large blast radius. It is worth knowing exactly where it sits in every version of the payload you accept, and worth logging when it isn’t there.

Questions readers actually ask

Is user.ext.consent still valid in OpenRTB 2.6?

The 2.6 specification promotes the field to user.consent. Whether an old-path value is honored depends on the receiving bidder’s implementation. The rtblint reference flags sending the pre-2.6 paths in a 2.6 payload as a common mistake, because “bidders reading only the first-class fields see no signal at all.”

Does regs.gdpr: 1 mean consent was given?

No. regs.gdpr indicates whether the request is subject to GDPR. The user’s choices live in the consent string on user. A request can be in scope for GDPR and carry no consent string at all, which is the unresolved state described above.

Can a DSP generate a consent string if one is missing?

Per the TCF format specification, only a registered CMP may create a TC String, and vendors “must neither create nor alter TC Strings.” A missing string is a signal to be handled, not a gap to be filled.

Where does the consent string go in a pre-2.6 request?

In the extension field user.ext.consent, paired with regs.ext.gdpr. OpenRTB 2.6 promoted both to first-class attributes.

What should a bidder do with a malformed string?

The retrieved sources do not prescribe a single handling rule for malformed strings, and I am not going to invent one. What the Implementation Guidelines do establish is the evaluation order for a valid string: publisher restrictions first, then purpose and vendor legal-basis signals. A string that cannot be decoded cannot be evaluated in that order, which is a condition your logging should distinguish from a valid string that simply withholds consent.

You may also like