Send the payment to your attribution tool yourself. Nearly every processor can POST a webhook when a charge succeeds, and a generic inbound purchase endpoint will accept one — so the missing integration is really a small relay: a Zapier step or twenty lines of server code carrying an order ID, amount, currency, email and status.
That relay is the integration. Below: what goes in it, and what each missing field costs you.
Why does your attribution tool not know the payment happened?
Because nothing in the browser told it. Native connectors exist for the handful of platforms every vendor builds first — Shopify, WooCommerce, Stripe, a CRM or two. Outside that list, the money moves through a checkout your tracking has never seen.
Three things go wrong at once with a merchant of record like Paddle or Lemon Squeezy, and with regional gateways like PayMongo:
The checkout is on someone else’s domain. The buyer leaves your site for an overlay or a hosted page, and your first-party cookie, visitor ID and session all stop at that boundary. It is the same hop that makes ad conversions show up as Direct, except there is no pixel on the other side to pick the thread back up.
The confirmation page is generic. A success page hosted by the processor carries no tag of yours, so “fire a purchase event on the thank-you page” has nowhere to fire. Even with a redirect back to your site, a closed tab or a banking app taking over means the event never happens.
The only reliable record is server-side. The processor knows the moment the card clears. Your website does not, and may never. That asymmetry is the whole problem — and the whole solution, because the server-side record is the sturdier one.
What actually breaks when revenue lands outside the tracking stack?
Your reports keep working, which is why this goes unnoticed for months. Leads, sessions and cost per lead all look fine. Only the revenue column is wrong, and it is wrong in the direction that costs money.
- Every channel looks equally good. With no revenue attached to a source, you compare on lead volume and cost per lead — so cheap unqualified signups beat buyers.
- The ad platforms get nothing to learn from. No purchase event means no purchase-optimised bidding; the algorithm keeps chasing the last signal it saw, usually a form submit.
- Refunds never net out. A payment that reverses two weeks later still sits in your spreadsheet, inflating the winner.
- Finance and marketing hold different numbers, and neither side can reconcile without a join key.
The size of the miss scales with how much revenue runs through that processor — on a merchant of record, usually all of it.
What does a payment need to carry to be attributable?
Five fields, plus one that changes everything. Miss any of the five and the payment cannot be recorded properly; miss the sixth and it is recorded but not confidently matched.
| Field | Why it matters | Without it |
|---|---|---|
| Order / transaction ID | The idempotency key — how a retry is recognised as the same payment | Duplicates on every retry |
| Amount and currency | The value that reaches reporting and the ad platforms | Revenue reports as zero, or in the wrong currency |
| Buyer email | The strongest identifier most processors will give you | Match falls back to phone or IP |
| Event timestamp | Places the purchase in the journey and the reporting period | Lag and cohort analysis break |
| Status (paid / refunded) | Lets a reversal net against the original | Refunds inflate attributed revenue |
| Your visitor ID, echoed back | Ties the payment to the exact session that produced it | Falls back to identity matching — usually fine, sometimes wrong |
That last row is worth the engineering effort. Most checkouts accept a custom metadata field — a key-value bag stored with the transaction and returned on the webhook. Read your tracking cookie before sending the buyer to checkout, write the value into that bag, and the payment comes back naming its own session — no fuzzy matching required.
// Read the first-party visitor ID, pass it as checkout metadata.
const vid = document.cookie.match(/(?:^|;\s*)pl_vid=([^;]+)/)?.[1];
const url = new URL('https://checkout.example-processor.com/buy/plan-pro');
if (vid) url.searchParams.set('checkout[custom][visitor_id]', vid);
window.location.href = url.toString();
Whatever the processor calls it — metadata, custom data, passthrough, reference — it survives the payment and comes back on the webhook. Confirm the exact parameter name in your processor’s docs: the mechanism is near-universal, the spelling is not.
How do you get the payment out of a processor nobody integrates?
Three routes, most reliable first.
Route 1 — the processor’s webhook, straight to a generic purchase endpoint. Every processor fires a server-to-server notification when a payment succeeds. Point it at your tool’s inbound purchase endpoint, or at a small function that reshapes the payload first. No browser involved, so it survives ad blockers, closed tabs and failed redirects.
Route 2 — an automation platform as the relay. Zapier, Make or n8n subscribe to the event and POST the normalised body onward. Slower and rate-limited, but no server to run. Same pattern as getting Facebook Instant Form leads into an attribution tool, applied to money.
Route 3 — fire from your own success page. Only where the processor redirects back to a page you control and passes the transaction reference, and only as a fallback: it needs a browser to finish a journey that already left your site.
A minimal Route 1 relay:
// Processor webhook → your relay → your tool's purchase endpoint
export async function handler(req) {
const e = await req.json();
if (e.type !== 'payment.succeeded') return new Response('ok', { status: 200 });
await fetch(process.env.PURCHASE_ENDPOINT, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
order_id: e.data.id, // idempotency key
status: 'paid', // 'refunded' on a reversal
value: e.data.amount / 100, // major units, not cents
currency: e.data.currency,
email: e.data.customer?.email,
phone: e.data.customer?.phone,
visitor_id: e.data.custom_data?.visitor_id, // echoed cookie value
occurred_at: e.data.created_at,
}),
});
return new Response('ok', { status: 200 });
}
Two easily-missed details: send major units, or a processor reporting cents books a $49 sale as $4,900; and return 200 fast for ignored events.
How do you match that payment to the session that caused it?
By identifier strength, in tiers. A payment carrying your echoed visitor ID is a certainty; one carrying only an email is a strong probability; one carrying only an IP address is a guess you should label as such.
- Visitor ID echo — the payment names the browsing session outright.
- Normalised email — lowercased and trimmed, Gmail dot and plus variants folded together so
a.b+shop@gmail.comandab@gmail.comresolve to one person. - Normalised phone — E.164, digits only, country code inferred from session geography.
- IP and user agent in a time window — the weakest tier, and the one to distrust on shared networks.
Once a payment lands on a session, the session belongs to a person, and that person’s history supplies the attribution. This is where visitor identity resolution does the real work: the buyer who clicked a Meta ad in March, returned direct in April and paid through a hosted checkout in May is one cluster, not three strangers. The purchase inherits the whole chain — the difference between attributing ecommerce revenue to the ad that started it and writing “Direct” in a spreadsheet.

What about refunds, duplicates and mixed currencies?
Get all three right at ingest; reporting cannot fix them later.
Duplicates. Processors retry until they get a 200, and some deliver the same event twice in normal operation. Idempotency on the order ID plus status phase stops a retry becoming a second sale. Never dedupe on amount and email — two genuine purchases of the same plan collapse into one.
Refunds. A reversal needs its own record that nets against the original, not a deletion of it. Deleting loses the fact that the sale happened; ignoring it leaves you scaling against money you gave back. Same failure that makes refunds inflate attributed revenue on platforms that do have connectors.
Currency. A merchant of record settles in the buyer’s currency, so one week can span five. Store the native amount and currency code; convert only at reporting time.
How does PartialLeads attribute a payment from a processor it does not integrate?
Through a universal inbound webhook: anything that can POST an order becomes a purchase source, running the same pipeline as a native Shopify or Stripe connection. There is no second-class path for “unsupported” money.
It normalises identity first. Email lowercased and trimmed, phone to E.164 with a country-code fallback from session geography — so the person who typed a local number on your form and a formatted one at checkout resolves to one record.
It dedupes before it records. Idempotency is keyed on client, source, order ID and status phase. A redelivered webhook physically cannot create a second purchase, and a refund arrives as its own row, netting against the paid one in the Purchases ledger.
It matches to a session in tiers — visitor ID echo, then email, then phone, then IP — flips the lead from Partial to Completed, attaches the revenue, and writes a first-touch and a last-touch row, so the Attribution report shows first, last and resolved models side by side.
It fans the conversion back out. A matched purchase dispatches server-side to Meta, Pinterest, TikTok and Google Ads, plus your CRM webhooks — so a payment taken through a processor no ad platform has heard of still reaches the Conversions API with hashed identifiers attached. That is what a spreadsheet reconciliation can never do.

The honest constraints. You build the relay. PartialLeads accepts the POST; it does not log into your processor and pull payments, and there is no pre-built Paddle, Lemon Squeezy or PayMongo connector to click. Match quality is capped by your payload: with no email, no phone and no echoed visitor ID, a payment lands as an anonymous purchase. Those are not discarded — they sit in the ledger, become matchable when identity arrives, and can be re-matched by hand — but they add nothing to channel reporting until then.
| What breaks | The mechanism | Where you see it in the dashboard |
|---|---|---|
| No native connector for the processor | Universal inbound webhook — anything that can POST an order is a purchase source | Integrations → Universal |
| The payment has no session attached | Tiered match: visitor ID echo, email, phone, IP | Lead flips Partial → Completed, revenue attached |
| The buyer looks like a stranger at checkout | Six-tier identity cluster unions sessions and payments | Customer Journey timeline |
| A retried webhook creates a second sale | Idempotent dedup on client, source, order ID, status phase | Purchases ledger — one row per payment |
| A refund still counts as revenue | Refund row carries its own status phase, netting against the paid row | Purchases ledger — net revenue |
| The ad platform never learns about the sale | Server-side fan-out to Meta, Pinterest, TikTok, Google Ads | CAPI activity log, API column |
| Payments settle in several currencies | Native currency stored per purchase, converted at reporting time | Attribution report — currency selector |
Tell us what's broken. We'll fix your tracking — free.
Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.
Sources
https://developers.facebook.com/docs/marketing-api/conversions-api https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/custom-data https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events