Handover: capture Meta fbc / fbp on the www subdomain
For the server-side Meta Conversions API forwarder
([[2026-06-09-meta-ads-conversions-api]]) to match registrations to ad clicks,
it needs Meta’s fbc and fbp identifiers. The Rails app does not capture
these — it only reads them back from the persisted user. This doc defines
exactly what the www subdomain (marketing site) must capture and how it must
hand them to the app.
The contract (what the app expects)
The forwarder reads, at UserRegistered time, from the user’s persisted
conversion_details (JSONB), populated from the first-party conversion cookie
(today: gad, pipe-delimited key=value|key=value, scoped to .dropscan.de):
user.conversion_details["fbc"] # Meta click identifier, may be nil
user.conversion_details["fbp"] # Meta browser identifier, may be nil
So the www subdomain must add two keys, named exactly fbc and fbp, into
that same conversion cookie, with values in Meta’s exact string format below.
Both are optional — the forwarder omits whichever is absent.
Cookie must be set on the parent domain .dropscan.de so the app
subdomain can read it at registration (same as the existing gad cookie).
Values contain only fb, digits, ., and URL-safe chars — no | or = — so
they embed safely in the pipe/equals format.
What to capture
1. fbclid — the raw click ID (the input)
When a user lands from a Meta ad, the URL carries an fbclid query parameter:
https://www.dropscan.de/?fbclid=IwAR0abc123...
Read fbclid from the landing URL’s query string. This is the only thing that
actually comes from Meta; fbc is derived from it.
2. fbc — Facebook click identifier (derived from fbclid)
Format (Meta-defined, all four parts required):
fb.<subdomainIndex>.<creationTimeMs>.<fbclid>
fb— literal prefix.subdomainIndex—1(cookie scoped to.dropscan.de).creationTimeMs— UNIX timestamp in milliseconds whenfbclidwas first seen (Date.now()).fbclid— the raw query-param value, unmodified.
Example: fb.1.1717939200000.IwAR0abc123...
Capture rule: on landing, if fbclid is present, build this string and store it
in the gad cookie. If a later visit has a new fbclid, overwrite (most-recent
click wins). If there is no fbclid, keep the existing fbc (falling back to the
Pixel’s _fbc if one is present). Persistence follows the gad cookie’s rolling
30-day window (see Persistence).
Unlike fbp, fbc needs no pre-seeding dance: it is derived deterministically
from the URL’s fbclid, so we build it ourselves for the conversion cookie
directly on landing. (The Pixel independently derives its own _fbc from the
same fbclid; the two agree by construction.) When fbclid is absent there is
no fbc to capture.
3. fbp — Facebook browser identifier
Format:
fb.<subdomainIndex>.<creationTimeMs>.<randomNumber>
subdomainIndex—1.creationTimeMs— ms timestamp when first generated.randomNumber— a large random integer, unique per browser.
Example: fb.1.1717939200000.1098115397
The Meta Pixel base code is now installed (consent-gated, in
cookie_consent_controller.js#loadMetaPixel), sofbevents.jsnormally sets_fbpitself. We still generatefbpourselves, but the crucial detail is timing: write our value to the_fbpcookie before the Pixel script loads.fbevents.jsreuses an existing valid_fbpon init rather than overwriting it, so the Pixel adopts our value. The result is a singlefbpshared across client-side Pixel events and server-side CAPI events — consistent matching, no divergence.Why generate at all instead of just reading the Pixel’s cookie:
connect.facebook.net/fbevents.jsis heavily ad-blocked, and that blocked population is exactly what server-side CAPI exists to recover. A first-partyfbpwe set ourselves survives ad blockers, so CAPI still has a stable browser identifier even when the Pixel never loads. Pre-seeding also removes any need to read_fbpafter the Pixel’s asynchronous write.Net behavior: Pixel not blocked → it inherits our
fbp(client + server agree); Pixel blocked → our first-partyfbpstill exists for CAPI. One value, one code path, stored stably per browser (see Persistence).
Flow summary
- User lands on
www.dropscan.de(optionally with?fbclid=…). - After marketing-cookie consent (see below), the www JS, before loading
the Pixel script:
- reuses or generates
fbp(fb.1.<ms>.<rand>) and writes it to the_fbpcookie so the Pixel inherits it, - if
fbclidpresent, derivesfbc(fb.1.<ms>.<fbclid>) from the URL, - writes
fbcandfbpinto the.dropscan.degadcookie (rolling 30-day, see Persistence) alongside the existing keys, - then loads the Pixel (
loadMetaPixel), which adopts the pre-seeded_fbp.
- reuses or generates
- User registers on the app subdomain → Rails reads the conversion cookie into
conversion_details(existing path) →fbc/fbpare now persisted. MetaAdsForwarderreads them onUserRegisteredand sends to Meta CAPI.
Persistence
fbc/fbp live in the shared gad cookie, which updateFirstPartyCookie()
rewrites on every visit, merging existing keys forward. So the handoff uses a
rolling 30-day window: the value survives as long as the visitor returns to
www at least once every 30 days. “Refresh” means renewing the cookie’s expiry
while preserving the value — never regenerate fbp’s string, or you mint a new
browser ID and break matching. An old creationTimeMs in the value is fine; Meta
accepts it as-is.
We deliberately do not use Meta’s 90-day default for the handoff. 90 days
would only help the narrow cohort that clicks an ad, never returns to www, and
registers 30+ days later — where fbc is already past Meta’s ~7-day
click-attribution window anyway, leaving only marginal fbp match quality. Not
worth a special-case expiry.
One exception: the _fbp cookie we pre-seed for the Pixel keeps a longer (90-day)
expiry, matching what fbevents.js sets itself. It acts as a continuity backstop
— if gad lapses but _fbp survives, the next consented visit re-adopts the same
browser ID into gad rather than minting a new one.
Read priority for fbp: existing gad fbp key → existing valid _fbp cookie →
generate new.
Consent (blocking)
fbc/fbp are marketing identifiers. Under DSGVO/TTDSG they may only be set
after the user grants marketing-cookie consent. Capture must be gated on the
consent banner; no consent → don’t set them → forwarder simply sends no fbc/fbp
(email + IP matching still works). This is the same consent question flagged in
the forwarder plan and must be resolved before either side goes live.
Definition of done (www side)
- On consented visits,
fbpis reused (gad→_fbp) or generated infb.1.<ms>.<rand>format, written to the_fbpcookie before the Pixel script loads (sofbevents.jsreuses it), and stored ingadunder the rolling 30-day refresh — value stable across visits. - When
fbclidis in the landing URL,fbcis stored asfb.1.<ms>.<fbclid>. fbcandfbpland in the.dropscan.deconversion cookie under those exact keys, readable from the app subdomain at registration.- Nothing is set before marketing-cookie consent.