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.
- RetaFunnel → Integrations → card Stripe → copy the Webhook URL (it looks like
https://api.retafunnel.com/outcomes/stripe/…and is unique to your workspace). - Stripe Dashboard → Developers → Webhooks → Add endpoint (in the newer Stripe interface: Developers → Event destinations → Add destination → Webhook endpoint).
- Endpoint URL: paste the RetaFunnel URL.
- Events to send: select
checkout.session.completed,payment_intent.succeededandcharge.refunded. Save. - Stripe shows the endpoint's Signing secret (click Reveal; it starts with
whsec_). Copy it. - 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):(An AI agent running RetaFunnel's instrumentation step adds this hook itself when it finds a checkout button.)
<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> - Stripe Checkout created by your server: set
client_reference_id: rf.visitorIdwhen 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:
- RetaFunnel → Integrations → card Calendly → copy the Webhook URL.
- Calendly → Integrations & apps → API & webhooks → Get a token now → generate a Personal access token and copy it.
- 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.
- Find your organisation address: open a terminal (Terminal on Mac, PowerShell on Windows) and run, replacing the token:Copy the value of
curl -s https://api.calendly.com/users/me -H "Authorization: Bearer YOUR_TOKEN"current_organization(it looks likehttps://api.calendly.com/organizations/AAAAAAAAAAAAAAAA). - Create the webhook, replacing the four placeholders:The answer contains
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"}'"state":"active". - 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.
Export the deals from your CRM or spreadsheet as CSV with these column names (any order; separator
,,;or tab; quotes allowed):Column Required Content emailone of email or phone the buyer's email phoneone of email or phone the buyer's phone, international format preferred ( +39 333 1234567); Italian numbers without prefix are acceptednameyes what happened: purchase,booked_call,lead,refund, or your own wordvaluefor purchases the amount: 1990,1.990,00,€ 49,90and49.90are all read correctlycurrencyno EURby defaultoccurred_atno the date of the sale: 2026-03-14,14/03/2026or2026-03-14T10:00:00Z; today if emptyexternal_idrecommended 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-RRetaFunnel → Leads → Import sales CSV → choose the file → Import.
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.
Check the Sales page: the revenue appears under the ads that brought those people in.
Mark a sale by hand#
- 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).
- 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.
- RetaFunnel → Integrations → API keys → Create key with the write scope → copy the key (
rfk_…, shown once). - 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_KEYandContent-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" }
namecan bepurchase,booked_call,lead,refundor any word of yours (only those four have a built-in meaning in reports). Provideemail,phoneorvisitor_id(if your CRM stored the hiddenrf_visitor_idfield, send that: it matches even when the email changed).valueis in euros;value_centsis also accepted. - URL:
- 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.
- 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_idback asvisitor_id. rf_fbcandrf_gclidare 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