🚀 Launching Nominees : Assign backups to every rep. Anytime a rep is out of office, the backup kicks in automatically, and prospects always see an open calendar. Learn more →
How to set up Meta Pixel and Conversions API with RevenueHero
Send qualified meeting bookings from RevenueHero to Meta Ads with server-verified, first-party data. Better attribution, better optimization, less leakage from iOS, Safari ITP and ad blockers.
Meta Pixel + Advanced Matching via Google Tag Manager, firing a Schedule event when RevenueHero confirms a meeting is booked
Meta Conversions API (CAPI) firing the same event server-side, with a shared event_id so Meta dedupes the two and counts each booking exactly once
Both paths use the MEETING_BOOKED JavaScript event that RevenueHero already emits from the scheduler. No extra RevenueHero configuration required.
Prerequisites
A Meta Business account with a Meta Pixel created
Access to Meta Events Manager
Google Tag Manager installed on the page where the RevenueHero scheduler renders
A live RevenueHero inbound router or campaign router connected to a form
For the CAPI section: a place to run a small webhook handler (Cloudflare Workers, AWS Lambda, Vercel Functions, or a server-side Tag Manager container all work)
How to set up Meta Pixel and Conversions API with RevenueHero
If you're running Meta ads to drive demo requests, you've probably watched your "Lead" count drift away from what Salesforce or HubSpot says is actually qualified. Some of that is just B2B forms attracting noise. A lot of it is iOS 14.5+, Safari ITP, ad blockers, and the slow death of third-party cookies eating into what Meta's pixel can see.
This playbook fixes both problems. You'll send Meta a server-verified "Schedule" event the moment a prospect books a meeting through RevenueHero, with hashed first-party data so Meta can match it back to the ad click even when the browser hides everything else. You'll also wire up the Conversions API so the event still lands when the pixel is blocked.
The result: a clean, qualified conversion signal that Meta's algorithm can actually optimize on, plus accurate ROAS reporting.
What you'll set up
You'll deploy two things that work together:
Meta Pixel + Advanced Matching via Google Tag Manager, firing a Schedule event when RevenueHero confirms a meeting is booked
Meta Conversions API (CAPI) firing the same event server-side, with a shared event_id so Meta dedupes the two and counts each booking exactly once
Both paths use the MEETING_BOOKED JavaScript event that RevenueHero already emits from the scheduler. No extra RevenueHero configuration required.
Prerequisites
A Meta Business account with a Meta Pixel created
Access to Meta Events Manager
Google Tag Manager installed on the page where the RevenueHero scheduler renders
A live RevenueHero inbound router or campaign router connected to a form
For the CAPI section: a place to run a small webhook handler (Cloudflare Workers, AWS Lambda, Vercel Functions, or a server-side Tag Manager container all work)
Pre-flight: confirm RevenueHero events are firing
Before touching Google Tag Manager, confirm RevenueHero is emitting the events you expect. Open the page where your scheduler lives, open DevTools (Cmd+Option+I on Mac, Ctrl+Shift+I on Windows), paste this into the Console, then book a test meeting:
window.addEventListener('message', function (ev) {
if (ev.data && ev.data.type) {
console.log('RevenueHero Event:', ev.data.type);
if (ev.data.type === 'MEETING_BOOKED') {
console.log('Booker Email:', ev.data.meeting.attributes.booker_email);
console.log('Full payload:', ev.data);
}
}
});
If you see those logs, you're good. If you don't, double-check the RevenueHero snippet is installed and the form is wired to a router. Don't move on until this works. Every step below assumes these events fire reliably.
Part A: Meta Pixel and Advanced Matching via Google Tag Manager
This part handles the client-side signal. It catches _fbc (the click ID Meta sets when someone clicks an ad) and _fbp (Meta's first-party browser ID) automatically, and ships hashed email and phone via Advanced Matching so Meta can match conversions to ad clicks even when cookies are gone.
Step 1: Install the Meta Pixel base code (if you haven't already)
In Meta Events Manager, pick your pixel and copy the base code. In Google Tag Manager, create a new tag:
Replace YOUR_PIXEL_ID with your actual pixel ID. If your pixel is already installed directly on the site (outside GTM), skip this step.
Step 2: Create a GTM listener for the RevenueHero event
This tag listens for MEETING_BOOKED, generates a stable event ID (you'll reuse this in the CAPI call to dedupe), and pushes the data into the dataLayer.
Tag type: Custom HTML
Name: RevenueHero - Meeting Booked Listener
Trigger: All Pages
HTML:
<script> (function() { window.addEventListener('message', function (ev) { if (!ev.data || ev.data.type !== 'MEETING_BOOKED') return;
var meeting = ev.data.meeting; if (!meeting || !meeting.attributes) return;
var attrs = meeting.attributes;
// Use the RevenueHero meeting ID as the deduplication key. // Fall back to a UUID if for some reason it's missing. var eventId = meeting.id || ('rh-' + Date.now() + '-' + Math.random().toString(36).slice(2));
// Split full name into first / last for Advanced Matching. var fullName = (attrs.booker_name || '').trim(); var firstName = fullName.split(' ')[0] || ''; var lastName = fullName.split(' ').slice(1).join(' ') || '';
meeting.id is used as event_id. This is the RevenueHero meeting ID, which is unique and stable. Using it (instead of a fresh UUID) means the same ID lands in both Pixel and CAPI without you having to pass it between systems.
Email and names are lowercased and trimmed before being pushed. Meta's Advanced Matching API will hash them, but it expects normalized input.
The event name in the dataLayer is rh_meeting_booked (lowercase, snake case). This is what your GTM trigger will look for.
Step 3: Create GTM Data Layer variables
Create five Data Layer Variables in GTM (Variables → New → Data Layer Variable):
Why Schedule and not Lead? Meta has a documented standard event called Schedule for "books an appointment". That's exactly what a RevenueHero booking is. Using Schedule instead of Lead matters because Meta's optimization algorithm uses the event name as a signal for what kind of conversion it's looking for. Telling Meta you want more people who book meetings (not just people who fill forms) is a meaningfully different optimization target.
Why the value? Meta's bidding algorithms use conversion value to decide how aggressively to bid for similar audiences. If you don't pass a value, Meta defaults to treating every conversion as equal, and your value-based bid strategies break. The math we recommend:
value = Average ACV × Meeting-to-Closed-Won rate
If your average new-customer ACV is $30,000 and 10% of demos convert to closed deals, your conversion value is $30,000 × 10% = $3,000. Replace 1000 in the snippet above with that number. If you genuinely don't know your meeting-to-close rate yet, $1,000 is a safe starting placeholder.
Pass a single conservative number per booking rather than dynamic deal-size estimates. Meta needs the number to mean something stable across thousands of conversions.
Step 6: Add Advanced Matching to the Pixel
Advanced Matching lets Meta hash and send the booker's email and name with the conversion so Meta can match the event to a Facebook or Instagram user even when cookies are blocked. Update the tag from Step 5:
The em, fn, ln keys are Meta's Advanced Matching field names for email, first name, last name. When you pass them in plain text through fbq('init', ...), the Pixel hashes them with SHA-256 in the browser before sending. You don't have to hash them yourself.
The MEETING_BOOKED JavaScript event payload includes email and name, but not phone. If you want Meta to match on phone as well (worth doing for B2B match quality), you have two options: read it from the form fields on the page directly before pushing to dataLayer, or rely on Part B's webhook, which does include phone for Campaign Router submissions.
Step 7: Test with GTM Preview and Meta Test Events
In GTM, click Preview, enter your site URL, and book a test meeting. You should see:
Meta Pixel - Base Code fires on page load
RevenueHero - Meeting Booked Listener fires on page load
The custom event rh_meeting_booked appears in the timeline after you complete the booking
Meta Pixel - Schedule (RH Meeting Booked) fires on that custom event
Then in Meta Events Manager, open the Test Events tab and grab your test browser token (paste it as &test_event_code=TEST12345 on your test URL). Book another meeting. Within 30 seconds, you should see a Schedule event with your hashed email under "Customer information parameters".
Step 8: Publish
When the test event lands cleanly in Events Manager, publish your GTM container. Your client-side path is live.
Part B: Conversions API via webhook (server-side)
This part catches the conversions the Pixel misses (ad blockers, iOS, anything that breaks client-side tracking) by sending the same Schedule event directly from your server to Meta. The event_id shared with Part A means Meta dedupes the two and counts each booking exactly once.
Step 9: Generate a Conversions API access token
In Meta Events Manager, open your pixel → Settings → Conversions API → Generate access token. Save this token in your secrets manager. Treat it like a password.
Step 10: Create a webhook workflow in RevenueHero
In the RevenueHero app, go to Workflows → New Workflow:
Trigger: Meeting Booked
Action: Webhook
URL: Your relay endpoint (you'll build this in Step 11)
Method: POST
The webhook payload that RevenueHero sends includes the booker's email and name, the meeting time, status, UTM parameters captured at form submission, and (if your form collects it) phone number. The full payload reference is documented at help.revenuehero.io/workflows/webhook-payloads.
One important constraint: the webhook payload does not include fbclid. That means Path B alone can only match conversions to users via hashed email and phone, not via the click ID. This is why we run Path A and Path B together: Path A captures _fbc from the cookie when it's available, Path B catches the conversion when Path A is blocked.
Step 11: Build the relay endpoint
The relay's job is to receive RevenueHero's webhook, normalize and hash the PII, and forward a Conversions API event to Meta. Here's a minimal Cloudflare Worker example. Adapt to your stack:
export default { async fetch(request, env) { if (request.method !== 'POST') { return new Response('Method Not Allowed', { status: 405 }); }
const body = await request.json();
// RevenueHero webhook payload is flat. Prospect info, meeting info, // and UTM params all sit at the top level. The exact set of fields varies // by source (Inbound Router, Campaign Router, Relay, Meeting Links). // See help.revenuehero.io/workflows/webhook-payloads for the full reference.
// Only fire the conversion when a meeting was actually booked. // Session-only payloads (form submitted but no meeting) will not have meeting_time. if (!body.meeting_time || !body.status) { return new Response('Not a meeting-booked event, ignoring', { status: 200 }); }
// Strip undefined entries so we don't send empty fields. Object.keys(user_data).forEach(k => user_data[k] === undefined && delete user_data[k]);
// body.id is the meeting ID when a meeting is booked (it overrides the session // ID in the same payload). This is the same ID Part A uses for event_id dedup. const eventId = body.id; const eventTime = Math.floor(Date.now() / 1000);
return new Response(await response.text(), { status: response.status }); } };
async function sha256(value) { const encoder = new TextEncoder(); const data = encoder.encode(value); const hashBuffer = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(hashBuffer)) .map(b => b.toString(16).padStart(2, '0')) .join(''); }
What's happening here:
Normalization first, then hashing. Meta requires email lowercased and trimmed, names lowercased, phone stripped to digits. Hashing after normalization is what makes the match work. Hashing without normalization will fail silently with zero matches.
event_id reuses the RevenueHero meeting ID. Same as Part A. This is the dedup key.
action_source: 'website' tells Meta this is a website conversion, not in-store or app.
user_data only includes fields you have. Sending empty hashed strings hurts match quality.
Same value and currency as Part A. Keep these in sync between Pixel and CAPI.
If your forms collect phone numbers, normalize them to E.164 format (digits with leading country code) before hashing. The example above strips non-digits, which works for US numbers, but for international you'll want to handle country codes explicitly.
Step 12: Test deduplication
In Meta Events Manager → Test Events, watch as you book a test meeting. You should see:
A Schedule event from browser (from Path A's Pixel call)
A Schedule event from server (from Path B's CAPI call)
Both should share the same event_id
Within 48 hours, Events Manager will mark one of them as deduplicated. The diagnostic panel will show your "Deduplication" rate. You want this near 100%, which confirms event_id is matching properly.
If you see the two events but they're not deduping, the event_id doesn't match between client and server. Most often this is because the client-side tag generated a UUID before the page sent the webhook with the meeting ID. The fix is exactly what Step 2 and Step 11 do: use meeting.id as the source of truth in both places.
Step 13: Monitor Event Match Quality
In Events Manager, open the Schedule event and check Event Match Quality. Meta scores this 0 to 10 based on how many user_data fields you send and how clean they are. You want a score of 6 or higher. Anything lower means your match rate is leaving conversions on the table.
To improve the score:
Always send hashed email (highest weight)
Send hashed first and last name (second highest)
Send hashed phone if you collect it
For Path A, the Pixel automatically sends _fbc and _fbp cookies when available, which adds significant weight
Checklist
ItemStatusMeta Pixel installed (Base Code tag firing on All Pages)☐RevenueHero - Meeting Booked Listener tag deployed☐rh_meeting_booked custom event visible in GTM Preview after a test booking☐Meta Pixel - Schedule (RH Meeting Booked) tag fires on the trigger☐Schedule event visible in Meta Test Events with hashed email☐Conversions API access token stored in your secrets manager☐RevenueHero webhook workflow created with Meeting Booked trigger☐Relay endpoint deployed and receiving RH webhooks☐Schedule event landing in CAPI with shared event_id☐Deduplication confirmed in Events Manager (browser + server with same event_id)☐Event Match Quality score 6 or higher☐
Troubleshooting
The custom event rh_meeting_booked never fires in GTM Preview.
Open DevTools Console on the page and confirm MEETING_BOOKED is being logged when you book a test meeting (use the snippet from the pre-flight section). If it's not, RevenueHero's scheduler isn't loading on this page. Check that the RevenueHero snippet is installed and your form is connected to a router. If MEETING_BOOKED is logged but the GTM custom event isn't, your listener tag isn't firing on page load. Double-check the trigger is set to "All Pages" and not just "DOM Ready".
The Pixel fires but Advanced Matching isn't sending email.
Check the Network tab when you book a meeting. Find the request to facebook.com/tr and look for the ud[em] parameter. If it's empty, your dataLayer variable for rh_booker_email is probably resolving to undefined. This usually means the fbq('init', ...) call ran before the dataLayer push completed. The fix is in Step 6: put the fbq('init', ...) call inside the conversion tag itself (which fires after the dataLayer push) rather than in the base code tag.
CAPI events land in Test Events but don't dedupe with the Pixel events.
Compare the event_id field on the browser event and the server event. They have to match exactly. If the browser shows 1234 and the server shows 1234-rh-7890, you're using different keys. Standardize on meeting.id from RevenueHero in both places.
Event Match Quality is below 5.
You're probably sending only em (email). Add fn and ln from the booker name. If you collect phone in your form, add ph too. Check that you're normalizing before hashing (lowercase email and names, digits-only phone).
No conversions are attributed to specific ad campaigns even after dedup works.
This is usually a _fbc problem. The Pixel reads _fbc from the cookie that Meta sets when a user clicks an ad with an fbclid URL parameter. If your scheduler is on a subdomain (e.g. meet.yoursite.com) and your form is on the main domain (www.yoursite.com), the cookies don't carry over. Fix it by setting the Pixel's cookie domain to your root domain in the base code: fbq('init', 'YOUR_PIXEL_ID', {}, { agent: 'rh-plg', cookieDomain: '.yoursite.com' }).
The webhook fires but the relay returns a Meta API error.
The most common error is {"error":{"message":"Invalid parameter","error_user_msg":"The user_data parameter is required"}}. This means your user_data object is empty after stripping undefined fields. Confirm the RevenueHero webhook payload has a populated booker email. The relay should log the inbound payload so you can see what RevenueHero is sending you.
Bonus: track qualified leads with PAGE_LOADED
MEETING_BOOKED only fires when someone actually finishes booking a meeting. But there's a second event that's useful: PAGE_LOADED, which RevenueHero fires when the scheduler successfully renders the booking time slots to a qualified prospect.
If your prospect is disqualified by your routing rules (wrong company size, wrong country, etc.), the scheduler never shows and PAGE_LOADED never fires. So PAGE_LOADED is a clean signal for "this prospect cleared qualification". That's a useful upper-funnel event for Meta to optimize on if you're testing campaigns where you want to know which audiences are sending qualified traffic, not just any traffic.
To wire it up, add a second listener block to the tag from Step 2:
<script> window.addEventListener('message', function (ev) { if (!ev.data) return;
Then create a new GTM trigger for rh_qualified_lead and a new Meta Pixel tag firing fbq('track', 'Lead') on it. This gives you two events: a Lead for "made it through qualification" and a Schedule for "actually booked". Optimize different campaigns on different events.
A word of caution: PAGE_LOADED is a step before the booking, not a confirmed conversion. Use it for optimization experiments, not for revenue reporting.