DEV Community

Nicolas Florez
Nicolas Florez

Posted on

How a Free Trial Without Upfront Card Killed Our SaaS Conversion Rate

TL;DR

We had 11 free-trial users on a paid Colombian legaltech plan over 30 days. Trial→paid conversion: 0%. Spoiler: it wasn't a PMF problem. It was a billing-architecture bug.

If your trial doesn't tokenize a card upfront, your cron job has nothing to charge when the trial ends. The user silently downgrades to free, and your "trial conversion rate" is mathematically impossible.

Here's how we found it, and the .NET 10 + EF Core fix.

Context

I'm building NeoJurídico, a court-process monitoring platform for Colombian lawyers. The Litigante plan ships with a 14-day free trial activated via UTM-tagged Google Ads campaigns. Tech stack: .NET 10, EF Core 10, PostgreSQL, MercadoPago for billing, Angular 21 on the frontend.

The 0% trial→paid mystery

We were spending real money on Ads and getting 11 trials/month. Zero ever converted. So we audited the funnel:

Step Count
Ads → click 119/30d
Click → signup 8 (CTR 6.7%)
Signup → activated trial 11/30d (some pre-Ads)
First process created (Aha moment) 5/8 (62.5%)
Trial → paid 0

Activation looked healthy. Engagement existed. So why zero conversions?

Reading the SQL

SELECT us."Id", us."UserId", us."Status",
       us."MpPreapprovalId" IS NOT NULL AS has_card
FROM neo.user_subscriptions us
WHERE us."CampaignId" IS NOT NULL
ORDER BY us."CreatedAt" DESC LIMIT 11;
Enter fullscreen mode Exit fullscreen mode
 Id  | UserId | Status | has_card
-----+--------+--------+----------
 146 |   3    | Open   | f
 145 |  44    | Open   | f
 144 |  43    | Open   | f
 ... (all 11 rows: has_card = f)
Enter fullscreen mode Exit fullscreen mode

Zero of eleven trials had a payment method attached. That's the root cause.

Tracing the code

In our SubscriptionsService:

bool needsMpSubscription = isPaidPlan && !hasTrial && !hasCampaign;
Enter fullscreen mode Exit fullscreen mode

If the user came in via campaign + trial, we skipped MercadoPago entirely. The trial activated as Status="Open" with TrialEndDate=now+15d, but with no preapproval ID.

Then the TrialExpirationHandler runs every 6 hours:

foreach (var userId in expiredCampaignSubscriptions) {
    await enrollmentService.RestoreOriginalSubscriptionIfNeededAsync(userId, ct);
}
Enter fullscreen mode Exit fullscreen mode

"Restore" means "downgrade silently to the free plan." There's no path to charge — there was never a card.

We were running a free 15-day trial with structurally zero conversion potential.

The fix: card upfront, deferred first charge

MercadoPago's preapproval API supports start_date for deferring the first recurring charge:

// In SubscriptionsService.ActivateSubscriptionAsync
bool isTrialAwaitingCheckout = hasCampaign && hasTrial && isPaidPlan;
if (isTrialAwaitingCheckout) {
    status = "Pending";
    statusReason = "AwaitingTrialCheckout";
    trialEndDate = now.AddDays(trialDays!.Value);
}
Enter fullscreen mode Exit fullscreen mode

Now signup creates a Pending subscription. The frontend (Angular) detects it on /app/dashboard and redirects:

if (response.entity.status === 'Pending'
    && response.entity.statusReason === 'AwaitingTrialCheckout') {
  this.router.navigate(['/app/suscripcion/checkout', planId],
                       { queryParams: { trial: 'true' } });
}
Enter fullscreen mode Exit fullscreen mode

The user sees a checkout: "14 días gratis. No se cobra hoy. Primera carga: [date+14]." When they submit, we tokenize the card and create the MP preapproval with startDate = trialEndDate:

var preapprovalId = await _mpSubscriptionService.CreateAuthorizedSubscriptionAsync(
    plan.Name, plan.Price, user.Email, request.CardTokenId,
    backUrl, ct, externalReference: $"neo-sub-{sub.Id}",
    startDate: deferredStart  // = TrialEndDate
);
Enter fullscreen mode Exit fullscreen mode

MP holds the card today, charges in 14 days. Trial→paid is now automatic instead of impossible.

For users who refuse to give a card, an explicit "skip to free plan (1 process limit)" button calls POST /skip-trial-to-free which closes the Pending and activates Plan Semilla. They self-segment.

The hidden lesson: zero conversions ≠ zero PMF

I almost made an expensive pivot decision based on the 0% number. The temptation was:

  • "Cut trials, force payments upfront on everyone"
  • "Limit free plan to 1 process to push conversion"
  • "Maybe the product isn't ready"

The data didn't support any of that. The bottleneck was a billing-flow architecture choice that made conversion mathematically impossible — not a value problem.

If you're seeing 0% on a critical funnel metric, before pivoting your business model, run this query against your billing data:

SELECT COUNT(*) FILTER (WHERE has_payment_method) AS chargeable,
       COUNT(*) AS total
FROM your_trial_subscriptions
WHERE created_at > NOW() - INTERVAL '30 days';
Enter fullscreen mode Exit fullscreen mode

If chargeable / total is 0, you can't possibly convert anything. Fix that before changing anything else.

Server-side event tracking matters too

We also had GA4 emitting subscription_activated from the frontend card-checkout component. But trials never reached that component (no card flow). So GA4 showed 0 activations while our DB showed 11.

Solution: server-side GA4 via Measurement Protocol when status transitions to Open in the SubscriptionsService. Now event tracking matches reality regardless of which path the user takes.

if (_ga4 != null && s.Status == "Open" && hasCampaign && hasTrial) {
    var expectedValue = Math.Round(plan.Price * 0.36m, 0);
    await _ga4.TrackTrialStartedAsync(
        userId, plan.Name, expectedValue, "COP", trialDays.Value, ct);
}
Enter fullscreen mode Exit fullscreen mode

(Using expected_value = MRR × 0.36 as the proxy for Smart Bidding. Trials aren't revenue — don't pass nominal price as conversion value or Google will optimize for trials, not paying customers.)

Takeaways

  1. If trial lifecycle doesn't include "tokenize a payment method," you have zero ability to convert. No amount of UX or pricing changes fixes architecture.
  2. Validate metric anomalies (0% conversion) by reading your DB schema, not by changing pricing.
  3. Server-side analytics for revenue events is non-negotiable. Frontend gtag dies in 30% of sessions due to ad-blockers.
  4. Self-segment users at the checkout: "card or downgrade to free." Don't force, don't trick — let them choose, then measure.

The plumbing is now in production. We'll see the trial→paid conversion rate on the next cohort. I'll write a follow-up in 30 days with real numbers.


I work on NeoJurídico, a court-process monitoring platform for Colombian lawyers. Always interested in connecting with other LATAM SaaS founders or LegalTech builders. Reach out if your country has equally cursed government APIs.

Top comments (0)