Signals to ad platforms

A lead is the best signal a campaign can get, and the rarest: many projects collect fewer than 50 leads a week, the volume Meta wants per optimisation event. Signals give the platforms more to learn from: what a visitor did before the lead. They are set up under Einstellungen › Signale an Werbeplattformen, through the REST routes adSignals.* and the MCP tools ad_signals_list, ad_signals_update and ad_signals_preview.

A signal is never a conversion of X3: it does not count as a lead, in KPIs, reports, alerts or notifications. The conversions (Lead, Termin, Kunde, Kauf, see Conversion definitions) stay the optimisation targets.

Plan: signals, built-in and custom, are part of Projekt Growth and up (Projekt Growth, Projekt Pro, Agency Growth, Agency Pro; see Plans and limits). On Free, Projekt Analytics and Agency Analytics only the conversions go to the platforms; the signal routes and MCP tools answer plan_required with the upgrade link. Settings and custom signals are kept while locked and resume after an upgrade.

What can be a signal

KindKeyFires
Interest tierrelevance:interested, relevance:engaged, relevance:hotonce per person (journey, else visit) when the interest score reaches the tier („Neugierig“, „Interessiert“, „Heiß“); a visit jumping two tiers fires both
Eventevent:quiz_answer, event:video_complete, event:<custom> …once per visit, from built-in events and the project's registered custom events
Goalgoal:<key>once per visit when the goal is reached for the first time

Technical events (click, not_found, experiment_view, app lifecycle) and purchase are no signals.

Defaults

SignalMetaGoogle Ads
Visit becomes „Interessiert“ or „Heiß“custom event InterestedVisitor / HotVisitor (English standard names), value from the scoresecondary action „X3 · Interessierter Besuch“ / „X3 · Heißer Besuch“
Visit becomes „Neugierig“CuriousVisitoroff
Quiz started (first answer), quiz contact step, video watched to the end, WhatsApp clicked, calculator usedcustom eventsecondary action
Quiz viewed, form started, form abandoned, video started, CTA clicked, messenger clickedcustom eventoff
Registered custom eventscustom event (PascalCase of the name)off
Contact goals (phone, e-mail click)custom eventsecondary action
Moments that already are conversions (quiz submitted, form submitted, booking, lead goals)offoff
LinkedIn, TikTokoff–

Why:

  • Meta custom events never collide with a browser pixel's standard events and never become the optimisation event unless someone picks them in Ads Manager (custom conversion or ad set goal). They build audiences (retargeting „Quiz begonnen, kein Lead“) and give Meta's model more positive examples.
  • Google secondary actions are observation only: Smart Bidding keeps bidding on the primary lead actions. Upper-funnel events as primary goals would teach Google to buy cheap clicks that scroll, not leads.
  • A signal may be promoted to a primary Google goal only by an explicit choice on save. For projects with fewer than 30 leads a month X3 marks „Heiß“ as a candidate („als primäres Ziel testbar“) – never switched on by itself. Judge such a test by cost per real lead, not by the number of signals.
  • Aus (default for existing projects): nothing is stored or sent. Probelauf: X3 stores and checks every signal, nothing leaves X3. Live: sent; each platform's own reporting mode (test, dry run) still applies.
  • Only with consent (default on): signals of a visit go out only when the visit carries consent. Leads are not affected – they follow the rules of the reporting destination. Each signal can widen this with its consent mode.
  • At most per day (default 1 000): a guard against forged traffic.
  • Events reach Meta with the same event_id on every retry (sig:<signal>:<visit or person>), so nothing is counted twice; Google gets one upload per click and action (ONE_PER_CLICK).
  • Google Ads needs a click id (gclid, gbraid, wbraid), LinkedIn li_fat_id. Meta matches with _fbc/_fbp, IP and user agent of the visit and the hashed contact data when the visit became a lead.

Identifiers Meta gets

Every signal – built-in and own, live and „Nachmelden“ – carries what X3 legitimately holds for the visit, through the same code as the conversions (buildMetaEvent, journey enrichment):

FieldSource
client_ip_address, client_user_agentThe request that triggered the signal. Without one (a lead or status change, Nachmelden) the newest signal or conversion row of the same visit that still holds them (kept at most 7 days). Sessions never store IP or user agent; a visit without such a row goes out without them.
action_source, event_source_urlwebsite with the page when a user agent is present, otherwise system_generated.
fbp, fbcThe site's _fbp/_fbc cookies (the SDK reads them only after consent). Without _fbc, fbc is built from the landing page's fbclid as fb.1.<first seen ms>.<fbclid> – for signals and conversions alike.
click idsThe visit's own, gaps from the person's other visits (journey).
external_idSHA-256 of the journey id (and a CRM id when there is one).
em, ph, fn, lnOnly when the person is a known lead: the lead's normalized SHA-256 hashes.

With the Meta destination set to click ids only (matching: click_id_only) a signal carries fbc only – no IP, user agent, _fbp, external id or contact hashes. A destination that reports only with marketing consent skips visits without consent. All of it only for visits the signal's consent mode allows: never for a decline.

Each signal has a consent mode (dashboard: column „Einwilligung“ of the table and the field „Einwilligung“ of an own signal; REST consentMode in an override or own signal; MCP consentMode in ad_signals_update, consent_mode in ad_custom_signals_create / _update / _preview):

  • Nur mit Einwilligung (consent, default): only visits with consent.
  • Auch ohne Entscheidung (nie bei Ablehnung) (unless_denied): also visits whose visitor has not decided yet.

How X3 tells them apart: the SDK reports the analytics consent per batch as granted, pending (no decision) or denied (the site's „Decline“, its consent manager, x3("consent", { analytics: false }), or the browser's Global Privacy Control). The ingest keeps it per visit in sessions.consent_state (g · p · d): a new explicit choice wins, a later page without a decision never replaces one.

Rules that always apply:

  • A decline always wins – in every mode, also when „Only with consent“ is off. When a visitor declines later in the visit, nothing more goes out: queued and retried rows of that visit are skipped (_guard: consent_declined), and undecided visits of a person who declined in another visit are not sent either. Accepting withdraws nothing: accepting after a decline counts as consent again.
  • Older visits stored before the tri-state existed only know „consent yes/no“. Without consent X3 cannot tell a decline from no decision, so they never count as undecided – also not in „Nachmelden“.
  • The preview (ad_custom_signals_preview, the rule builder's live count, the columns „Davon mit Einwilligung“ and „Ohne Entscheidung“) shows both volumes; for visits older than the tri-state it estimates the undecided share from the consent state the recordings capture stored.

„Auch ohne Entscheidung“ is a legal grey zone: the platform gets pseudonymous data (IP address, browser, for leads hashed contact data) based on legitimate interest (Art. 6 (1) f GDPR); whether that holds next to § 25 TDDDG is not settled. Check it with your data protection officer, say it openly in the cookie banner and privacy policy, and keep „Decline“ as easy as „Accept“. The dashboard shows this hint whenever the mode is chosen.

Platform setup

Saving creates what the platform needs, through the project's ad connection and idempotent by name:

  • Google Ads: an offline action per enabled signal, UPLOAD_CLICKS, ONE_PER_CLICK, primaryForGoal: false, category „Qualified lead“ for interest tiers, „Contact“ for contact signals, „Engagement“ otherwise. An action of the same name that already exists is used as it is; its role is never changed by X3. A project that reports leads through the Google Ads CSV feed sends signals through the ad connection.
  • LinkedIn: a conversion rule „X3 · …“ with method Conversions API, not associated with any campaign.
  • Meta and TikTok need nothing – the event name is the mapping.

X3 never changes campaigns, budgets or ads, and only changes primary ↔ secondary of an action it created when a save changes the role.

Value

none, a fixed amount, or score: the expected value of the person, lead value × score / 100 (lead value from Einstellungen › Projekt). A „Heiß“ visit with 64 points and a lead value of 3 000 € is sent with 1 920 €. The value is what value-based bidding learns from once a signal is promoted to an optimisation goal.

New projects

When an ad account with the right to manage conversions is connected and X3 sets up reporting by itself, signals switch on live with the defaults – unless the project had signal settings before (an „Aus“ stays off) or the plan does not include signals. X3_AD_SIGNALS_AUTO=0 turns that off for an installation.

Dry run

ad_signals_preview (dashboard: „Vorschau 30 Tage“) counts what the current settings would have sent per platform in the last days, with consent and click ids applied, and says when a signal is too rare to be an optimisation goal (Meta: about 50 a week). ad_signals_update with dry_run validates the platform changes (Google validateOnly) and stores nothing.

Test events

Meta lists a custom event name for custom conversions only after it received one. „Testereignisse an Meta senden“ (REST adSignals.sendTest, MCP ad_signals_send_test) sends every signal enabled for Meta once, without any person data: only a fixed test external id (SHA-256 of x3-signal-test:<project>), event id x3test:…, action source system_generated, no value. With a test event code the events stay under Test events; without one they are real events of the data set and appear under Events Manager › data set › Overview, ready for a custom conversion. Conversions (Lead, Schedule, Purchase) are never sent this way.

Reporting earlier visits

adSignals.backfill (MCP ad_signals_backfill) reports the interest tiers of the last days (at most 7, or from a project-local day): every visit at or above an enabled tier, once per person and tier (the earliest visit), with the visit's start as the time and the same event ids as the live signals – nothing goes out twice. provider limits it to one platform and to visits with its click id (Google: gclid, gbraid, wbraid); the consent rule applies. It writes signal rows only, never a lead or a conversion. The dry run (default) only counts. Google rejects uploads to an action for up to 6 hours after it was created; those rows are retried automatically.

adSignals.metaStats (MCP ad_signals_meta_stats) reads back from Meta what the project's dataset counted since a day – per event name and per source (browser pixel / server) – with the stored Conversions API token, server side, plus the dataset's state. It shows whether signals and conversions really arrive in the dataset reporting points to.

adSignals.googleActions (MCP ad_signals_google_actions) reads the signals' actions back from Google (primary_for_goal, status, category) next to what X3 stored.

Custom signals

Eigene Signale (Einstellungen › Signale an Werbeplattformen › Eigene Signale; REST adSignals.custom*, MCP ad_custom_signals_list, ad_custom_signals_create, ad_custom_signals_update, ad_custom_signals_delete, ad_custom_signals_preview) are a project's own composite optimisation events. They blend real conversions with intermediate stages – „interested or hot“, „quiz started and from Meta“, „appointment or customer“ – so an ad set can optimise on a group that is broad enough to learn from (Meta: about 50 a week) and still close to customers.

FieldMeaning
nameThe event name at the platforms: Meta/TikTok custom event (letters, digits, _, at most 40, starts with a letter, no Meta standard event such as Lead); Google Ads / LinkedIn get „X3 · <name>“ unless an action name is given. Unique per project, and never a name an enabled built-in signal sends.
providersMeta, Google Ads (secondary unless role: primary is chosen explicitly, created like the built-in actions), LinkedIn, TikTok
includeA list of groups. Conditions inside a group are AND, groups are OR.
excludeSame structure. Any matching group excludes.
dedupeDaysnull (default) = once per person; N = at most once per person within N days
valuenone, fixed, or from the score (like the built-in signals)

Conditions:

KindHolds when
stage (tier)the visit's interest score reached the tier (its minimum) – a tier includes every higher tier, so interested („Neugierig“) means „curious or higher“
score (min)the visit's score ≥ min
event (event, minCount, props)the catalogue event happened in the visit at least minCount times, optionally with these props
goal (goal)the visit reached the goal
leadthe person (any visit of the journey) has a lead
lead_status (status)the person's lead reached appointment (also true for customer) or customer
channel (channels)the visit's channel is one of them
ad_platform (platforms)the visit came from the platform (click id, channel or a matched ad): google_ads, meta, linkedin, tiktok, bing
page (page)a page of the visit; a trailing * matches every page below
page_group (group)a page of the visit belongs to the page group

When it fires. X3 evaluates a project's custom signals when a batch of a visit arrives (after the journey link), when a lead is stored through the API, browser or dashboard, and when a lead's status becomes appointment or customer. A signal fires once per person – the journey, or the visit without one – the first time its rules hold. A person who already got it does not get it again when the visit later reaches a higher tier. Earlier sends are found by the person's visits and by the event id sig:c:<key>:<person> (with a dedupe window the minute of the send is appended), which is also the event_id Meta deduplicates on.

Same rules as every signal. The mode (off, dry run, live), „Only with consent“ (default on) with the signal's own consent mode and the daily cap of „Signale an Werbeplattformen“ apply; test visits never send; sends land in the same outbox (ad_signal_sends, signal key custom:<key>) and are retried the same way. A custom signal is never a conversion of X3.

Names. A built-in signal and a custom signal never share a Meta/TikTok name: creating a custom signal under a name a built-in signal sends is refused (409), and so is renaming a built-in signal to a custom signal's name. To move a name an ad set optimises on from a built-in signal to a custom one, reset the built-in signal to its default first (ad_signals_update with "relevance:hot": null), then create the custom signal.

Preview. ad_custom_signals_preview (dashboard: the live count of the rule builder) counts a stored signal or an unsaved draft over the last days: visits whose rules hold, persons (once each), persons with consent, without a decision, declined and unknown (older visits), what goes out in either consent mode (byConsentMode), per platform (Google and LinkedIn only with their click id), per week, and the lead rate of the matched persons – how much a broader rule dilutes. Below about 50 a week for Meta it says so.

Reporting earlier visits. adSignals.customBackfill (MCP ad_custom_signals_backfill) reports an own signal for persons whose rules held in a visit of the last days (at most 7, Meta's limit; or from a project-local day) and who did not get it yet: once per person, the earliest qualifying visit its consent mode allows (never a decline, never an older visit without consent), the visit's start as the event time, the live event id sig:c:<key>:<person> – a second run or a later live hit never sends it twice. Test visits never count. provider limits it to one platform; dryRun (default) only counts. The answer lists found, without consent, declined, already sent, sent, uploaded and what the platform rejected, by reason. Like every signal, uploads carry the identifiers X3 holds – IP and user agent of the visit only while a row of that visit still keeps them (7 days).

Test event. adSignals.sendTest / ad_signals_send_test with signals: ["custom:<key>"] (dashboard: „Testereignis senden“ in the row) sends one event under the custom name without person data, so the name appears in Events Manager.

Relevance as a conversion

A conversion definition can also fire on an interest tier: trigger { "kind": "relevance", "tier": "hot" }, once per person and tier. Unlike a signal it counts as a conversion in X3 – use it only when the tier really is a business outcome for the project.