Docs Guides

Sales, CRM and attribution

Bring sales in from Stripe, Calendly, CSV, Zapier or by hand, and understand the three attribution models.

Behaviour tells you who is interested; sales tell you which ad actually made money. Many funnels close their sales somewhere else, days or weeks later: a Stripe checkout, a Calendly call, a CRM, a bank transfer in a spreadsheet. This guide shows the five ways to bring those sales into RetaFunnel, how a sale is matched to the person and to the ad that brought them, and how to read the three attribution models.

How matching works, in one paragraph#

Every browser gets a visitor id and every visit a session id, together with where the visit came from (UTMs, campaign / ad set / ad ids, Meta and Google click ids). When a visitor submits a form with an email or phone, RetaFunnel creates a person and stores a one-way hash (a fingerprint) of the email and phone. When a sale happens anywhere, you send it back with the email or phone (or the visitor id): RetaFunnel computes the same hash, finds the person, looks at their sessions in the attribution window and credits the sale under three models at once. The Sales page, the Ads page and the person's timeline all read that result.

The five ways to bring sales in#

Way Best for Effort
Stripe Online payments and refunds 5 minutes, automatic afterwards
Calendly Booked calls 10 minutes, automatic afterwards
CSV import A monthly export from any CRM or spreadsheet 2 minutes each time
Mark a sale by hand A sales team closing a few deals on the phone 10 seconds per sale
Zapier, Make or any CRM Automatic from HubSpot, Pipedrive, GoHighLevel, ActiveCampaign… 15 minutes once

All five end in the same place: the person is marked booked or customer, the sessions get is_booked / is_customer / revenue, segments update ("bookers without purchase"), and if Meta or Google are connected the sale is sent to them as a Purchase event or an offline conversion. Imported and manual sales included: this is the part that improves ad delivery.

Stripe#

What you get: every successful Stripe payment becomes a purchase with its amount and currency; refunds become refund outcomes.

  1. RetaFunnel → Integrations → card Stripe → copy the Webhook URL (it looks like https://api.retafunnel.com/outcomes/stripe/… and is unique to your workspace).
  2. Stripe Dashboard → Developers → Webhooks → Add endpoint (in the newer Stripe interface: Developers → Event destinations → Add destination → Webhook endpoint).
  3. Endpoint URL: paste the RetaFunnel URL.
  4. Events to send: select checkout.session.completed, payment_intent.succeeded and charge.refunded. Save.
  5. Stripe shows the endpoint's Signing secret (click Reveal; it starts with whsec_). Copy it.
  6. Back in RetaFunnel, paste it into Signing secret and click Save. The badge turns connected.

Matching. Stripe sends the customer's email, so most purchases match through the email hash. For an exact match even when the buyer uses a different email, pass the visitor id to Stripe as client_reference_id:

  • Payment Link: your page must add the id to the link when the visitor clicks. Ask your developer or AI agent to add this to the page (replace the link address with yours):
    <a id="buy" href="https://buy.stripe.com/xxxx" data-track="cta_checkout">Buy now</a>
    <script>
      window.rfq = window.rfq || [];
      rfq.push(['ready', function () {
        var a = document.getElementById('buy');
        if (window.rf && rf.visitorId) a.href += (a.href.indexOf('?') > -1 ? '&' : '?') + 'client_reference_id=' + rf.visitorId;
      }]);
    </script>
    
    (An AI agent running RetaFunnel's instrumentation step adds this hook itself when it finds a checkout button.)
  • Stripe Checkout created by your server: set client_reference_id: rf.visitorId when creating the Checkout Session.

Testing. Pay with a Stripe test card in Stripe's test mode: events with livemode: false are stored as test data, so they never touch live reports.

Calendly#

What you get: every booking becomes a booked_call outcome matched on the invitee's email; cancellations are recorded too. Note that the Calendly widget embedded on your page is detected by the tracker on its own (a booking_completed event, so the funnel step shows immediately); the webhook is what matches the booking to the person by email and catches bookings made outside your page.

Calendly webhooks need a Standard plan or higher and are created with one API call (Calendly has no form for it). The steps:

  1. RetaFunnel → Integrations → card Calendly → copy the Webhook URL.
  2. Calendly → Integrations & apps → API & webhooks → Get a token now → generate a Personal access token and copy it.
  3. Choose a signing key: any long random text (for example 40 letters and digits). You will paste the same text in Calendly's call and in RetaFunnel.
  4. Find your organisation address: open a terminal (Terminal on Mac, PowerShell on Windows) and run, replacing the token:
    curl -s https://api.calendly.com/users/me -H "Authorization: Bearer YOUR_TOKEN"
    
    Copy the value of current_organization (it looks like https://api.calendly.com/organizations/AAAAAAAAAAAAAAAA).
  5. Create the webhook, replacing the four placeholders:
    curl -s -X POST https://api.calendly.com/webhook_subscriptions \
      -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
      -d '{"url":"RETAFUNNEL_WEBHOOK_URL","events":["invitee.created","invitee.canceled"],"organization":"ORGANIZATION_URI","scope":"organization","signing_key":"YOUR_SIGNING_KEY"}'
    
    The answer contains "state":"active".
  6. RetaFunnel → Calendly card → paste the same Signing key → Save.

Prefer not to use a terminal? Paste steps 4 and 5 to your AI agent with the token; or book a test call and use the CSV import below. If the invitee's email differs from the one in your lead form, the booking is stored as unmatched and is matched automatically if that email later appears in a form.

CSV import#

What you get: a spreadsheet of closed deals becomes purchases (or bookings) matched to your leads, with their attribution. Re-importing the same file does not duplicate anything as long as each row has an external_id.

  1. Export the deals from your CRM or spreadsheet as CSV with these column names (any order; separator ,, ; or tab; quotes allowed):

    Column Required Content
    email one of email or phone the buyer's email
    phone one of email or phone the buyer's phone, international format preferred (+39 333 1234567); Italian numbers without prefix are accepted
    name yes what happened: purchase, booked_call, lead, refund, or your own word
    value for purchases the amount: 1990, 1.990,00, € 49,90 and 49.90 are all read correctly
    currency no EUR by default
    occurred_at no the date of the sale: 2026-03-14, 14/03/2026 or 2026-03-14T10:00:00Z; today if empty
    external_id recommended your invoice or deal id; makes re-imports safe

    Common aliases are understood (mail, telefono, importo, data, order_id, deal_id).

    Example file:

    email,phone,name,value,currency,occurred_at,external_id
    anna.rossi@example.com,+39 333 1234567,purchase,1990,EUR,2026-03-14,INV-2026-0142
    ,+39 347 7654321,purchase,"1.490,00",EUR,15/03/2026,INV-2026-0143
    marco.b@example.com,,booked_call,,,2026-03-10,CAL-88213
    luca.verdi@example.com,,refund,1990,EUR,2026-03-20,INV-2026-0142-R
    
  2. RetaFunnel → Leads → Import sales CSV → choose the file → Import.

  3. The result lists matched rows (the person was found) and unmatched rows (no lead with that email or phone yet), with any line errors. Unmatched rows are kept and matched automatically the day that person submits a form.

  4. Check the Sales page: the revenue appears under the ads that brought those people in.

Mark a sale by hand#

  1. Leads → open the person (search by email hash is not possible, so filter by status, source or date, or open the person from their session).
  2. Click Mark sale → enter the value (497.00), the date, optional notes and your invoice id → Save.

The person becomes a customer, their sessions get the revenue, segments and ad platforms update at the next 15-minute run.

Zapier, Make or any CRM (generic webhook)#

What you get: your CRM tells RetaFunnel about a sale the moment a deal is marked won.

  1. RetaFunnel → Integrations → API keys → Create key with the write scope → copy the key (rfk_…, shown once).
  2. In your automation tool add a step that sends an HTTP POST when a deal is won:
    • URL: https://api.retafunnel.com/api/v1/outcomes
    • Method: POST
    • Headers: Authorization: Bearer rfk_YOUR_KEY and Content-Type: application/json
    • Body (JSON), with your CRM's fields in place of the examples:
      {
        "name": "purchase",
        "email": "anna.rossi@example.com",
        "phone": "+39 333 1234567",
        "value": 1990,
        "currency": "EUR",
        "occurred_at": "2026-03-14T10:00:00Z",
        "external_id": "deal_84213"
      }
      

    name can be purchase, booked_call, lead, refund or any word of yours (only those four have a built-in meaning in reports). Provide email, phone or visitor_id (if your CRM stored the hidden rf_visitor_id field, send that: it matches even when the email changed). value is in euros; value_cents is also accepted.

  3. In Zapier: Webhooks by Zapier → Custom Request (or POST with payload type JSON). In Make: the HTTP → Make a request module with body type Raw, content type JSON.
  4. Send a test from the tool; the person appears as customer on the Leads page within a minute.

Sending the same external_id twice updates the sale instead of duplicating it.

Hidden fields in lead forms#

For sales that close offline, the lead must carry the RetaFunnel ids into your CRM, so the CRM (or a CSV from it) can hand them back. The tracker fills a set of hidden fields in your form just before it is submitted:

<input type="hidden" name="rf_visitor_id">   <input type="hidden" name="rf_session_id">
<input type="hidden" name="rf_utm_source">   <input type="hidden" name="rf_utm_campaign">
<input type="hidden" name="rf_campaign_id">  <input type="hidden" name="rf_adset_id">
<input type="hidden" name="rf_ad_id">        <input type="hidden" name="rf_fbc">
<input type="hidden" name="rf_gclid">
  • An AI agent adds them when it instruments the page (and the validator warns when a form with an email field has none). In a page builder, add hidden fields with exactly these names.
  • Map them to custom fields on the lead in your CRM ("RF visitor id", "RF campaign id"…). Then your sales team sees which ad the lead came from, and your webhook or CSV can send rf_visitor_id back as visitor_id.
  • rf_fbc and rf_gclid are what Meta and Google need to accept an offline conversion; keep them.

The three attribution models, with a worked example#

Sales are credited to people, then to the person's sessions inside the attribution window (90 days before the sale by default; Settings → Sales attribution lets you go up to 180). Sessions after the sale never get credit.

Anna's story:

Day Session How she arrived
1 Session 1 Meta ad "Ad A" (campaign Prospecting). Watches 60% of the video, leaves.
5 Session 2 Google search ad "Keyword B". Submits the lead form. This is the identifying session.
12 Session 3 Types the address directly. Reads the FAQ.
20 — Buys for €1,900 (Stripe, or a CSV row with her email).
Model Who gets the €1,900 Reading
First touch Session 1 → Meta Ad A, €1,900 What brought her in. Use it to judge prospecting budgets.
Last touch Session 2 → Google Keyword B, €1,900 The session where the lead was captured gets it all (if she had never filled a form, the last session before the sale would). Use it to judge what closes.
Linear €633.33 each to Meta Ad A, Google Keyword B and Direct Every touch counts. Use it when both channels matter.

Totals are the same (€1,900) under every model; only the split between rows changes. On the Sales page switch the Attribution model and the group by (source, campaign, ad set, ad, variant). The credit shift column shows how much moved between first and last touch for each row: a positive shift on your retargeting campaign is budget it took from prospecting. Agencies usually report last touch for closing and first touch for prospecting, side by side.

The touch timeline#

Leads → open a person. The timeline lists every session in order with: source and campaign, ad set and ad (ids and names), variant, landing page, video %, scroll %, quiz step, a mark on the session where the lead was captured, and the outcomes attached (booking, purchase with amount). Below it, the attribution block shows the first-touch session, the last-touch session and the linear weights for this person, so you can see why the models disagree before changing any budget. Notes and external ids (your CRM's deal id) can be added on the same page.

Common problems#

Imported rows are "unmatched". The email or phone differs from the one in the form (another email, or a phone without country code). Fix the CSV, or send the rf_visitor_id your CRM stored as visitor_id. Unmatched rows match automatically later if that person submits a form.

A Stripe purchase is attributed to nobody. Stripe did not send an email that RetaFunnel knows and no client_reference_id was passed. Add the visitor id to the Payment Link (above).

The same sale counts twice. Two sources sent it (Stripe and a CSV export of Stripe, for example) with different external_ids. Import only what Stripe does not already send, or give both the same external_id.

Revenue on Sales differs from Stripe. Sales counts matched purchases inside the date range and the attribution window; refunds are subtracted; test-mode payments are excluded.

The Calendly booking shows on the funnel but the person is not "booked". The widget event marks the step; the webhook (or a CSV row with booked_call) is what marks the person. Set up the webhook.

Technical reference: Sales matching, Integrations.

Last updated