Fail icon
Query icon
If icon
Request icon
Log icon
Schedule icon

Send CRM lead stages to Meta Conversions API (Conversion Leads)

Send CRM lead stages from a Postgres outbox to Meta Conversions API as Conversion Leads events, with hashing and batch guards.

Categories
BusinessData

Sends lead stage changes from your CRM to Meta as Conversion Leads events, so Meta can optimize Lead Ads for the leads that actually move down your funnel instead of every form submission.

The flow reads an outbox table in Postgres. Your CRM, an application trigger or another flow inserts one row per stage change, and this flow ships them in batches every 15 minutes:

  1. check_inputs stops the run if lead_event_source is empty, since Meta requires it.
  2. prepare_batch runs one SQL statement that:
    • normalizes the pending rows with Meta's customer information rules: email without any whitespace (invisible characters included) and lowercased, phone with digits only and no leading zeros, both SHA-256 hashed. Raw email and phone never leave the database;
    • leaves out of the event the values that cannot match or that Meta rejects: an email without @, a phone outside 8 to 15 digits, a lead_id outside 15 to 17 digits, and a value that is negative, NaN, infinite or without a valid ISO 4217 currency;
    • marks as expired the rows older than 7 days minus a one-hour margin, and as invalid the rows left without a stage name or without any customer identifier. Meta rejects the entire batch if a single event breaks one of these rules, so one bad row would otherwise block the queue for good;
    • builds the data array for up to 1,000 events, the API limit.
  3. has_events skips the API call when there is nothing to send.
  4. send_events posts the batch with action_source: system_generated and the two custom_data fields Meta requires on every Conversion Leads event: event_source: crm and lead_event_source. Timestamps in the future are capped at the current time.
  5. mark_sent flags exactly the rows of this batch and stores the fbtrace_id. It is skipped on test runs, so a row checked in Test events still goes to production later.

Each event carries event_id = crm-<row id>. If a run fails after Meta accepted the batch (the database is unreachable for mark_sent), the next run sends the same events again with the same event_id; keep that in mind when reconciling counts. The flow-level concurrency limit queues a manual run behind a running one, so the same batch is never sent twice in parallel.

Prerequisites

  • A Meta dataset (pixel) with a Conversions API access token (Events Manager > dataset > Settings > Generate access token).
  • A Postgres database with this table:
CREATE TABLE meta_capi_outbox (
  id          bigserial PRIMARY KEY,
  event_name  text        NOT NULL,          -- your CRM stage, e.g. 'Qualified', 'Won'
  event_time  timestamptz NOT NULL,          -- when the lead entered the stage
  lead_id     bigint,                        -- Meta Lead Ads lead ID, sent unhashed
  email       text,
  phone       text,                          -- with country code, any format
  value       numeric,
  currency    text,                          -- ISO 4217, e.g. 'USD'
  status      text        NOT NULL DEFAULT 'pending',  -- pending | sent | expired | invalid
  fbtrace_id  text,
  sent_at     timestamptz,
  created_at  timestamptz NOT NULL DEFAULT now(),
  updated_at  timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON meta_capi_outbox (status, event_time);

Secrets

  • POSTGRES_URL: JDBC URL, e.g. jdbc:postgresql://host:5432/crm
  • POSTGRES_USER and POSTGRES_PASSWORD: a user that can read and update meta_capi_outbox
  • META_ACCESS_TOKEN: Conversions API access token of the dataset

Inputs

  • dataset_id (STRING): the Meta dataset ID. Replace the default.
  • lead_event_source (STRING, default In-house CRM): your CRM name, e.g. HubSpot.
  • test_event_code (STRING, optional): run the flow manually with the code from Events Manager > Test events to check the payload. Sendable rows stay pending on test runs; expired and invalid rows are still taken out. Leave it empty on the schedule.
  • graph_api_url (STRING, default https://graph.facebook.com/v26.0): pins the Graph API version.

Quick start

  1. Create the table and the secrets. The schedule is active as soon as the flow is saved, so disable the trigger until the test below passes.
  2. Insert a test row: INSERT INTO meta_capi_outbox (event_name, event_time, email) VALUES ('Qualified', now(), 'lead@example.com');
  3. Execute the flow with a test_event_code and check the event in Events Manager > Test events.
  4. In Events Manager, finish the CRM setup by choosing which stages count as positive in the sales funnel.
  5. Enable the trigger; it runs every 15 minutes and sends the pending rows, the test row included.

Outputs

  • {{ outputs.prepare_batch.row.event_count }}: events in the batch
  • {{ outputs.prepare_batch.row.expired_count }} and {{ outputs.prepare_batch.row.invalid_count }}: rows taken out in this run
  • {{ outputs.send_events.body }}: Meta response with events_received and fbtrace_id
  • Rows in meta_capi_outbox move from pending to sent, expired or invalid

Links

Orchestrate with Kestra
Orchestrate Postgres with Kestra
Share this Blueprint
See How

New to Kestra?

Use blueprints to kickstart your first workflows.