Integrating payments into a web application is one of those tasks that seems simple until you're deep in it. A payment button on a landing page is one thing. A billing system in a multi-tenant SaaS with two different payment gateways - one for the US, one for Latin America - is something completely different.
At Nebula we've built this in real projects. Here we explain the model we use and the problems we learned to avoid.
The scenario: a SaaS selling in the US and Latin America
The most common case we encounter: a SaaS with clients in the United States (paying by card via Stripe) and clients in Argentina, Mexico or Brazil (paying with Mercado Pago). The system needs to handle both flows without the user feeling the difference.
The real complexity appears when you add multi-tenancy: each enterprise client of the SaaS can have their own subscriptions, their own billing cycles and, potentially, their own payment configurations.
The most common mistake: payment logic mixed with business logic
The first mistake we see in systems that grew without planning: payment logic is mixed with business logic. A controller that calls the Stripe API directly, without any intermediate layer.
That works when you only have one gateway. When you want to add a second one, you have to rewrite or duplicate code in multiple places - and hope you don't forget any of them.
The solution: a payment abstraction layer
The solution is to create your own abstraction layer: a system payment interface that then connects to Stripe or Mercado Pago as appropriate. The rest of the system only knows that interface, not the specific gateway.
In Laravel, this is implemented as classes that respond to a common interface. The system calls processPayment() and the concrete implementation decides whether the request goes to Stripe or Mercado Pago based on the tenant's configuration.
Switching gateways, adding a third one, or modifying the behavior of just one becomes a localized change in the corresponding implementation, without touching the rest of the system.
How does the system decide which gateway to use?
Generally by a combination of factors:
This logic lives in the abstraction layer, not scattered throughout the code.
The data model for multi-tenancy with payments
The minimum tables you need:
| Table | What it's for |
|---|---|
| tenants | Each enterprise client of the SaaS |
| subscriptions | Active subscription per tenant (plan, cycle, status) |
| payment_methods | Payment methods stored per tenant |
| invoices | Billing history |
| payment_gateway_configs | Gateway configuration per tenant |
Each subscription references an external_id in the corresponding gateway. That ID is what you use to manage renewals, cancellations and plan changes from the system.
Webhooks: the core you can't ignore
Neither Stripe nor Mercado Pago works well without webhooks. Asynchronous payments, automatic renewals and dispute or failure events are notified via webhook. If you don't process them correctly, your system has inconsistent states - subscriptions that appear active but failed, payments that were processed but not recorded.
The most common errors:
Not verifying the webhook signature. Stripe and Mercado Pago include a signature in each request that proves it came from them. Verifying it is the first thing to do. If you don't, anyone can send fake requests to your endpoint.
Processing the same event twice. Webhooks can arrive duplicated. You need to save the ID of each processed event and check if it was already processed before acting.
Blocking processing with heavy logic. The webhook endpoint must respond 200 OK immediately and delegate real processing to a queue. If it takes more than a few seconds, the gateway will retry the delivery and you'll process the same event multiple times.
Renewals and billing cycles
Stripe handles renewals automatically if you use their subscriptions. Mercado Pago has recurring payment support with more variations depending on the country.
In systems where we want total control over the cycle, we implement the renewal logic directly in Laravel: a scheduled job that checks subscriptions nearing expiration each day and triggers the charge in the configured gateway for that tenant.
This gives more control and allows customizing behavior: grace periods, pre-expiration notifications, automatic downgrade for non-payment, and retries with configurable intervals.
A lesson from real projects
In MyOfficeTaxes, the Stripe and Square integration (not Mercado Pago, but the same concept) was one of the modules that required the most design time before writing a single line of code. That design time was the best investment of the project.
When the client wanted to add a new plan with different billing, it took hours - not weeks - because the architecture already accounted for it.
Building a SaaS with payments or need to integrate a gateway?
Payment integration is one of the points where technical debt accumulates most when not designed well from the start. Doing it right saves many problems when the business scales.
If you have questions about how to structure the payment system for your project, schedule a diagnostic. We'll give you a concrete analysis of what your specific case needs.
Share

Written by
Ana Olivia Todesco
CEO @ Nebula Solutions



