Using __tcfapi Event Listeners: A Developer Tutorial for Consent-Aware Scripts
Why Your Scripts Should Listen, Not Poll
Any script that depends on consent — an ad tag, an analytics loader, a header bidding wrapper — needs to know two things: whether the CMP has a decision yet, and what that decision is. A common mistake is to read a cookie on page load, or to poll the CMP with a timer until something appears. Both approaches break in predictable ways: the cookie format is internal to the CMP and can change, and polling either fires too early or wastes time waiting.
The IAB TCF defines a standard JavaScript interface for exactly this job: the __tcfapi function. This tutorial walks through the commands you actually need, the events you will receive, and the bugs that show up most often in production.
The Stub and the Queue
A TCF CMP is expected to place a small stub on the page as early as possible, before the full CMP script loads. The stub defines window.__tcfapi and queues any calls made before the CMP is ready. When the CMP finishes loading, it processes the queue and answers each call.
This is what makes it safe for other scripts to call __tcfapi immediately: as long as the stub is in the page before them, their calls are not lost. If a script throws because __tcfapi is undefined, the stub is either missing or loading too late — fix the load order rather than adding retries.
The Commands That Matter
ping
__tcfapi('ping', 2, callback) returns basic status without waiting for a user decision. The response includes cmpLoaded, a cmpStatus such as stub, loading, loaded or error, and a displayStatus of visible, hidden or disabled. It also reports gdprApplies once known. Use it for diagnostics and to decide whether the TCF is relevant for this visitor at all.
addEventListener
__tcfapi('addEventListener', 2, callback) is the command most integrations should use. The callback receives a tcData object and a success flag, and it is called again every time the consent state changes. Since TCF v2.2, this is the recommended way to get consent data; the older getTCData command was deprecated.
removeEventListener
Each tcData object carries a listenerId. Pass it to __tcfapi('removeEventListener', 2, callback, listenerId) when your script no longer needs updates — for example, a one-time loader that has already made its decision.
Reading the eventStatus
The key field in tcData is eventStatus. There are three values:
tcloaded— the CMP found a valid stored decision and no banner will be shown. You can act on the data immediately.cmpuishown— the banner is being displayed. The data reflects any previous state, but the user may be about to change it.useractioncomplete— the user has just made or confirmed a choice. Act on the new data.
A typical loader waits for either tcloaded or useractioncomplete, then checks the purposes and vendors it needs. It ignores cmpuishown except perhaps to log that a banner appeared.
Checking Purposes and Vendors
Inside tcData, consent and legitimate interest are exposed as objects keyed by ID:
tcData.purpose.consents[1]— consent for Purpose 1 (device storage).tcData.purpose.legitimateInterests[7]— legitimate interest status for Purpose 7.tcData.vendor.consents[ID]andtcData.vendor.legitimateInterests[ID]— per-vendor status, using the vendor’s Global Vendor List ID.tcData.gdprApplies— whether the CMP determined that GDPR applies to this visitor. If it isfalse, your script should follow its non-TCF logic.tcData.tcString— the encoded string, which you pass on to partners that expect it.
Check both the purpose and the vendor. A purpose consent with no vendor consent, or the reverse, is not enough for most vendors to process data.
Iframes and Cross-Frame Calls
Ads and widgets often run inside iframes that cannot see the parent page’s __tcfapi. The TCF solves this with a postMessage protocol: the CMP adds a frame named __tcfapiLocator to the top page, and code inside an iframe can locate it, send a __tcfapiCall message and receive a __tcfapiReturn response. Most ad SDKs already do this. If you write your own iframe code, implement the same pattern rather than reading cookies across frames.
Common Bugs and How to Spot Them
- Acting on
cmpuishown. Scripts that load as soon as any callback fires will run before the user decides. Filter oneventStatus. - Removing the listener too early. If you remove it after the first callback, you will miss a later
useractioncompleteand never load. - Ignoring withdrawals. Persistent scripts should keep listening and stop or reconfigure themselves when consent is withdrawn.
- Ignoring
gdprApplies. Outside TCF regions, purpose objects may be empty; treating that as “no consent” can switch off revenue you are allowed to earn. - Not checking the success flag. When
successis false, log it; it usually signals a broken CMP setup.
Testing Your Integration
In the browser console, call __tcfapi('ping', 2, console.log) to confirm the CMP is present, then register a listener with console.log as the callback and click through the banner. You should see cmpuishown followed by useractioncomplete on a first visit, and tcloaded on a reload. Repeat with accept-all, reject-all and a custom selection, and confirm your script behaves correctly in each case.
Key Takeaways
- Use
__tcfapiinstead of reading CMP cookies or polling. - Make sure the CMP stub loads before any script that calls the API.
- Use
addEventListenerand act only ontcloadedoruseractioncomplete. - Check both purpose and vendor status, and respect
gdprApplies. - Use the
__tcfapiLocatorpostMessage pattern inside iframes.