Meta conversion tracking architecture for a SaaS product (Next.js, Django, Stripe)

The client's problem

We run a SaaS product on a subscription + free-trial model and advertise on Meta.
We need a specialist to build accurate, deduplicated event tracking across both the browser (Meta Pixel) and the server (Conversions API), so our Meta ad optimization gets near-100% event coverage with high Event Match Quality (EMQ).

Some conversion events happen with no browser session (subscription payments confirmed by Stripe days later).

Website stack

  • Frontend: React
  • Backend: Django (Python)
  • Payments: Stripe

Events that need to be tracked

Fire on BOTH browser (Pixel) + server (CAPI) — triggered on page load / URL / button action, deduplicated by a shared event_id:

  • PageView
  • ViewContent
  • CompleteRegistration
  • AddPaymentInfo
  • InitiateCheckout
  • StartTrial
  • CancelTrial
  • CancelSubscription
  • Subscribe
  • Purchase

These events should be sent from both the browser and the server with proper deduplication.

Stripe webhook events

These events must be tracked from Stripe webhooks:

  • Subscribe (Note: which confirms 3-day later after trial ends)
  • PaymentFailed

Subscribe can happen in 2 different ways

  1. Customer directly purchases the Premium Plus plan.
  2. Customer starts a free trial, and after the 3-day trial ends, Stripe successfully charges the customer.

Both should trigger the Subscribe event, but with the correct values depending on the purchase type.

What I expect

I want someone who can recommend and implement the best architecture, not simply follow instructions.
This includes:

  • Choosing when browser events should fire
  • Choosing when server events should fire
  • Implementing proper event deduplication
  • Maximizing Event Match Quality
  • Ensuring conversions are attributed as accurately as possible inside Meta Ads Manager

Recommend your approach

Before we start, tell us the architecture you'd use (e.g. custom CAPI calls from Django + Stripe webhook handlers, a GTM server-side container, or a tool like Stape) and why it's the best fit for our stack.

My solution

1

Your previous attempts to implement this project via Stape and GTM failed because, regardless of whether Stape and GTM are used for this project, its implementation inevitably requires proper programming, and Arthur and Anirudh are clearly incompetent at it.

2

Your new description shows that you are on your way to realizing this; previously you did not even deem it necessary to mention the key technologies of your SaaS, and even now, it would have been worth indicating that the SaaS uses not just React, but Next.js.

3

For this project, Stape is redundant, and GTM is orthogonal to the project's goal (GTM can be useful for other SaaS functions).

4

Below (§§5-11) is the best architecture for your case.

5

fbp, fbc and other tracking data (the IP address, the User-Agent HTTP header) are best stored in the Django database, because, to increase Event Match Quality, they must be combined with the customer's data already stored there; therefore, storing them on an external service like Stape is pointless.

6

The SaaS frontend must disable the automatic sending of events to Meta before initializing the Meta Pixel, and instead:

6.1

Generate a unique event_id (UUIDv4) for each event instance being tracked and associate it with this instance.

6.2

Call fbq('<method>', '<event name>', {<event data>}, {eventID: event_id}), where <method> is track for standard Meta events (PageView, StartTrial) and trackCustom for custom events (CancelTrial, CancelSubscription).

6.3

Read the _fbp and _fbc cookie values, if present (_fbc is present only if the visitor has followed a link with the fbclid parameter, and _fbp may be absent due to browser blockers).

6.4

Send a POST request to a custom Django endpoint (e.g., /api/tracking/event/) with a JSON body containing the event name, the event_id, the event data, the _fbp and _fbc cookie values (if any), the page URL, and the customer ID (if the visitor is authenticated).

7

In Django, create the §6.4 endpoint to receive the events, store their tracking data in the database (§5), enrich the events, and send them to Meta.

8

Your dichotomy between «choosing when browser events should fire» and «choosing when server events should fire» is false; you are close to understanding this yourself because it contradicts your correct statement a few paragraphs above: «events should be sent from both the browser and the server».
I will state the rationale explicitly:

8.1

In the US, a third of frontend tracking data do not reach Meta due to built-in browser protections (Safari ITP, Firefox ETP, Brave Shields), visitors' refusal of cookies via cookie banners, and browser extensions like AdBlock and uBlock.
By the way, this clearly corresponds to your statement from the previous project description: «right now it is tracking around 70% of the data».

8.2

Sending some events to Meta only from the server is also an evil that should be avoided wherever possible (for Stripe webhooks, though, this cannot be avoided), because the Meta Pixel on the frontend does a lot of things that are impossible to replicate on the backend: it collects page microdata, tracks mouse behavior patterns, and collects a mass of session metadata, which are important for targeting algorithms.

9

To process the Stripe events for Meta, Django must:

9.1

Listen for the invoice.payment_succeeded and invoice.payment_failed events via webhooks.

9.2

For invoice.payment_succeeded, distinguish 2 cases:

9.2.1

If the payment is the initial charge for a «Premium Plus» subscription without a trial period → ignore this webhook, as the direct Subscribe event is processed in real time during the active browser session via the §§6–7 flow.

9.2.2

If the payment occurs after the trial period ends (determined from the invoice/subscription data) → treat it as a trial-to-paid Subscribe conversion.

9.3

Identify the customer in the database by the customer parameter from the webhook.

9.4

Use the Stripe event ID as the event_id: unlike in §6.1, generating a unique event_id is wrong because Stripe can send the same notification multiple times.

9.5

Enrich the event data for Meta with the event_id and the §5 data.

9.6

Send the corresponding Subscribe or PaymentFailed event to the Meta Conversions API.

10

In §7 and §9 Django must hash the customer's personal data (email, phone, name, etc.) using SHA-256: without this, the Meta Conversions API will return an error.

11

The CancelTrial and CancelSubscription events must be processed in 2 ways:

11.1

According to the §§6-7 flow, when the customer terminates the trial or subscription by their own action through the UI.

11.2

Similarly to §9, when the trial or subscription is terminated without any explicit action by the customer (in particular, when the subscription is canceled because payment attempts fail after the trial or the current subscription period expires).