All posts

Send Salesforce Closed-Won Deals Back to Meta Ads

By Rajesh Kumar, Founder, Attribi ·

Meta Ads reports 340 leads at 12 dollars each while Salesforce shows 9 deals actually closed
worth 210,000 dollars. Nothing links the two until those 9 are sent back through the Conversions
API.

If your Meta ads generate leads that your sales team closes in Salesforce weeks later, Meta never finds out. It optimizes toward whoever fills in the form, because a form fill is the last thing it can see. The fix is to send the Salesforce outcome back: when an opportunity reaches a won stage, post a conversion event to Meta through the Conversions API with an identifier that ties it to the original ad click. This guide covers the whole setup by hand, including the parts that usually break, and then shows where a tool saves you the maintenance.

The short answer

There is no native Salesforce connector for Meta. You send the data yourself through the Conversions API, using a dataset in Events Manager, and you match the event to a person using the Meta lead ID, the fbclid from the ad click, or a hashed email and phone number. Salesforce triggers the send when an opportunity changes stage.

What changed in 2025, and why most guides are wrong

If you find a tutorial that tells you to create an offline event set and upload a CSV, close it. That path no longer exists.

  • Meta's Offline Conversions API was discontinued on 14 May 2025. Graph API v16.0 was the last version that supported it.
  • Offline event sets were migrated into datasets. What used to be called a Pixel ID is now a Dataset ID, and it is the same identifier you use for web events.
  • Every offline conversion, including CRM events, now goes through the standard Conversions API. There is no separate endpoint and no separate event set to create.

The practical consequence: your Salesforce integration and your website tracking send to the same dataset. That is a feature, not a quirk, because Meta deduplicates and attributes across both.

What you need before you start

  • A Meta dataset in Events Manager and its Dataset ID
  • A system user access token with the ads_management permission
  • Agreement on which Salesforce stages mean "qualified" and which mean "won"
  • At least one identifier stored on the Salesforce record (this is the part that decides whether any of this works)
  • A place to run the code: a Salesforce Flow calling an external service, an Apex callout, or a middleware job that polls Salesforce

Step 1: Work out which identifier you can actually send

This is the step that determines your results, and it is the step most teams skip. Meta can only credit an ad if it can tie your event back to a person it showed that ad to.

Four identifier options ranked by match strength: the Meta lead ID is strongest, then the fbclid
sent as an fbc value, then hashed email and phone, and a record with none of these cannot be
matched at all.

If your leads come from Meta Instant Forms, store the Meta lead ID (a 15 to 17 digit number) on the Salesforce record when the lead is created. Sending it back later is close to a guaranteed match, because it identifies the exact form submission.

If your leads come from your own landing page, capture the fbclid query parameter into a hidden form field and store it on the Lead record. Meta expects it wrapped in the fbc format: fb.1.{timestamp_in_milliseconds}.{fbclid}.

Always send hashed email and phone as well. They cost nothing to include and they catch the cases where the click identifier is missing or stale. Hash with SHA-256 after lowercasing and trimming. For Meta, phone numbers are digits only, with no plus sign and no spaces, which is different from what Google and Microsoft expect. Getting this wrong produces a hash that matches nothing, and Meta will not tell you.

Step 2: Map Salesforce stages to Meta events

Meta has no configurable conversion actions. Events are identified by name, so the mapping is a decision you make rather than something you set up in the interface.

A workable default:

Salesforce stageMeta eventWhy
Qualified, SQL, DiscoveryLeadEnough volume to train bidding
Closed WonPurchaseCarries the real revenue value

Two cautions. First, do not detect the won stage by comparing against the literal text Closed Won. Many orgs rename it, and a rename silently stops your revenue events. Read the IsWon flag on OpportunityStage instead, which is true regardless of the label. Second, closed deals are often too few to train Meta's bidding on their own. Sending a qualified-stage event as well gives the algorithm a higher-volume signal to learn from while the revenue event supplies the value.

Step 3: Build the event

A won Salesforce opportunity triggers your integration to build one Conversions API event, which
is posted to the dataset events endpoint, after which Meta bidding can optimize toward closed
revenue.

Post to the dataset's events edge:

POST https://graph.facebook.com/v21.0/{DATASET_ID}/events?access_token={TOKEN}
{
  "data": [
    {
      "event_name": "Purchase",
      "event_time": 1756339200,
      "action_source": "system_generated",
      "event_id": "0065g00000ABCDE:closed_won",
      "user_data": {
        "lead_id": "1234567890123456",
        "em": ["b642b4217b34b1e8d3bd915fc65c4452"],
        "ph": ["b0b6d1a4c1b6a9d8f5e2c3a1b7d4e6f8"]
      },
      "custom_data": {
        "value": 48000,
        "currency": "USD",
        "lead_event_source": "Salesforce",
        "event_source": "crm"
      }
    }
  ]
}

The fields that trip people up:

  • action_source must be system_generated, not website. No browser was involved. Sending website for a CRM event misrepresents where it happened.
  • event_time is a Unix timestamp in seconds, and it should be the moment the deal closed, not the moment you ran the job. Meta rejects events older than seven days, which matters if you are backfilling.
  • lead_id is sent raw, not hashed. Every other identifier in user_data is hashed. This one is not, and hashing it by mistake breaks the strongest match you have.
  • lead_event_source and event_source tell Meta the event came from a CRM. Omit them and your CRM events will not be usable for lead optimization.
  • event_id is your deduplication key. Use something deterministic and unique per deal and stage, like the opportunity ID plus the stage name, so a retry does not double-count revenue.

Step 4: Verify, and keep verifying

Send one event and check Events Manager. Look for three things, in this order:

  1. The event appears at all. Meta returns HTTP 200 with events_received: 1 even when nothing matched, so a successful response proves delivery and nothing more.
  2. The match rate. Events Manager shows how many events were matched to a person. If this is near zero, your identifiers are the problem, not your code.
  3. Event Match Quality. Meta scores each event source. A low score means your hashes are malformed or you are sending too few identifiers.

A 200 response with a zero match rate is the single most common outcome of a first attempt, and it looks exactly like success from the outside.

What breaks this in production

The Lead-to-Opportunity conversion. Your fbclid and Meta lead ID live on the Lead record. When a Lead converts, custom fields do not travel to the Contact or the Opportunity unless you map them explicitly. Most teams discover this months later, when they wonder why only self-serve deals ever match.

Renamed stages. A sales ops change to a stage name silently ends your revenue events. Nothing errors.

Token expiry. System user tokens can be invalidated by permission changes and password resets. The sends fail quietly unless you are watching for it.

Retries and duplicates. Without a stable event_id, a retried job books the same deal twice, and your reported return on ad spend drifts upward.

Currency. Multi-currency orgs report opportunity amounts in the record currency. Send the currency with the value, or a deal worth 48,000 AED is read as 48,000 USD.

One platform at a time. Everything above is Meta only. Google, LinkedIn, and Microsoft each have a different endpoint, a different payload, and different hashing rules. Phone formatting alone differs across all of them. Google's route is the most disrupted of the set, having changed twice since 2025, and it is covered in Import Salesforce Closed-Won Deals into Google Ads.

Doing this with Attribi

Attribi connects Salesforce and Meta and runs the loop above for you. Connect Salesforce, pick the stages that mean qualified and won in your own org, connect Meta, and closed deals upload automatically as offline conversions.

Three things you get that a hand-built Meta integration does not give you.

First-party click capture that survives lead conversion. Attribi's tracker records the click identifier against the visitor at the moment of the click and holds it outside Salesforce, on a first-party cookie set from your own domain. The identifier is attached to the person, so a Lead being converted into a Contact and an Opportunity cannot break the link. This is the failure mode that quietly costs the most, and it is structural rather than something you can fix with a better script.

Multi-touch attribution, not just an upload. The upload tells Meta what happened. It does not tell you where to spend. Attribi records the full journey across channels and lets you read the same closed deal through first touch, last touch, linear, position-based, and data-driven models, so you can see which channel opened the deal rather than only which one closed it.

Your Salesforce records get richer. Attribi provisions 16 custom fields on the Salesforce Lead object and fills them at capture time: first and last touch source, medium and campaign, the full attribution path, touchpoint count, landing page, referrer, and the click identifier. Your sales team sees where each lead actually came from inside the record they already work in.

The same connection also feeds Google, LinkedIn, Microsoft, TikTok, and Snapchat, so the work you do once for Meta covers the other five.

Connect Salesforce and Meta free.

Attribi compared with a direct Salesforce to Meta build

Direct buildAttribi
Build and maintain the CAPI integrationYou write and own itConnect and map stages
Click identifier survives Lead conversionOnly if you map fields correctlyHeld against the visitor, outside Salesforce
Hashing rules per platformYou implement each oneHandled per platform
Other ad platformsA separate build for eachSame connection covers six
Multi-touch attributionNot includedFive models across all channels
Attribution written back into SalesforceNot included16 fields on the Lead object
Retry, deduplication, failure visibilityYou build itBuilt in, per platform and per stage
Renamed won stageSilently stopsRead from the org's own won flag
CostEngineering timeFree tier, then paid plans

A direct build is genuinely the right answer in one case: you run Meta and nothing else, your leads come from Instant Forms so the lead ID is always present, and you have engineering time to own it. Outside that case, the maintenance is the expensive part, and it is recurring.

Frequently asked questions

Does Salesforce have a native Meta offline conversions integration? No. Meta provides no first-party Salesforce connector. You send events yourself through the Conversions API, or you use a tool that does it for you.

Is the Meta Offline Conversions API still available? No. It was discontinued on 14 May 2025, and Graph API v16.0 was the last version to support it. Offline event sets became datasets, and all offline events now go through the standard Conversions API.

Which action_source should a CRM conversion use? Use system_generated. The event did not happen in a browser, so website is inaccurate and can distort how Meta interprets the event.

Can Meta match a closed deal without any click ID? Yes, using hashed email and phone, though the match rate is lower than with a Meta lead ID or an fbclid. Send every identifier the record carries rather than choosing one.

Why does Meta report my events as received but nothing changes in the ad account? Meta returns a success response for a well-formed event even when it matches nobody. Check the match rate and Event Match Quality in Events Manager rather than trusting the API response.

How do I stop a retried job double-counting revenue? Set a deterministic event_id per deal and stage, such as the opportunity ID combined with the stage. Meta deduplicates on it.

The takeaway

Sending Salesforce outcomes to Meta is not difficult in the way people expect. The API call is short. The hard parts are keeping an identifier attached to the person from the ad click all the way through to a won opportunity, and noticing when it silently stops working. Get the identifier right and the rest follows. Try Attribi free if you would rather not own that maintenance, or use the steps above if you would.


Attribi tracks every marketing touchpoint and pushes revenue back to your ad platforms, so campaigns optimize on closed deals — not raw form fills. See how it works or start free.