Overview

This guide shows how to integrate a non-natively supported Payment Service Provider (PSP) into Lago.

It is modeled on the MoneyHash integration, and in most cases, you should duplicate and adapt the MoneyHash work from the following references:

Among all our native integrations, the Stripe one represents the gold standard and should always serve as the reference implementation.

Backend integration (API)

API Version

If your PSP integration supports multiple versions, ensure you specify the version—for example, we are setting it for Stripe in this Pull Request #3300.

SDK

Don’t hesitate to import any required gem if necessary.

Services

Our codebase relies a lot on services. Some general rules:

  • Extend BaseService
  • Always return a result (never nil)
  • A service should have one single method named call

Although the Stripe examples use services with multiple methods, we now recommend creating separate services each with a single call method; if you prefer a single class with multiple public methods, you may ignore this guidance.

Example:

# Instead of
PaymentProviderCustomers::StripeService#create
PaymentProviderCustomers::StripeService#update
PaymentProviderCustomers::StripeService#generate_checkout_url

# We recomend
PaymentProviderCustomers::Stripe::CreateService#call
PaymentProviderCustomers::Stripe::UpdateService#call
PaymentProviderCustomers::Stripe::GenerateCheckoutUrlService#call

Payment Provider Class

All payment provider subscriptions inherit from the PaymentProviderSubscription class via STI. For instance, to support Checkout.com, register a new CheckoutComSubscription in app/models/payment_provider_subscriptions/checkout_com_subscription.rb.

A few notes about providers:

  • Store custom provider details with settings_accessors instead of adding database columns;
  • Use secret_accessors to securely store sensitive data (e.g., API keys); and
  • Map the new PSP’s payment status values to Lago’s three statuses: processing, success, and failed.
module PaymentProviders
	class CheckoutComProvider < BaseProvider
    # Lago payment_status -> Checkout.com statuses mapping
    PROCESSING_STATUSES = %w[].freeze
    SUCCESS_STATUSES = %w[].freeze
    FAILED_STATUSES = %w[].freeze
	
	  secrets_accessors :api_key
	
		validates :api_key, presence: true
	
    def payment_type
      "checkout_com"
    end
  end
end

Ensure the new Payment Provider is included in every provider switch/case statement:

case type
    when 'moneyhash'
        PaymentProviders::MoneyHashProvider.new(provider_instance)
    when 'checkout_com'
        PaymentProviders::CheckoutComProvider.new(provider_instance)
    ...
end

Webhooks

Ideally, configure the new Payment Provider to send webhooks any time a payment or customer is updated. The Stripe integration is the most mature and should serve as the reference implementation. The callback URL must include both the organization_id and the provider code:

URI.join(
  ENV["LAGO_API_URL"],
  "webhooks/checkoutcom/#{organization_id}?code=#{URI.encode_www_form_component(payment_provider.code)}"
)

See PaymentProviders::Stripe::RegisterWebhookService for more details.

To process webhook messages:

  • Register route;
  • Add method to app/controllers/webhooks_controller.rb;
  • Save the webhook in IncomingWebhook and dispatch a job to process it; and
  • Ideally, one service maps to one webhook type.

The Stripe integration has been partially refactored but still preserves backward compatibility with HandleEvent job arguments, so your services can be simpler.

Backend integration checklist

Here is a list of files and classes you may need to create or update, each accompanied by a brief description. In our case, we take the example of a new integration with Checkout.com, but you can adapt the following list to your PSP.

1. Models

FilePurpose
app/models/payment_provider_customers/checkout_com_customer.rbDefine Checkout.com-specific customer model (inherits from BaseCustomer).
app/models/payment_providers/checkout_com_provider.rbDefine Checkout.com provider model (inherits from BaseProvider).
app/models/customer.rbAdd PAYMENT_PROVIDERS enum value + has_one relation to Checkout.com customer.
app/models/organization.rbAdd has_many relation.
app/serializers/v1/customer_serializer.rbMap Checkout.com customer settings for API responses.

2. GraphQL Input Types

FilePurpose
app/graphql/types/payment_providers/checkout_com_input.rbDefine GraphQL input type for Checkout.com settings.
app/graphql/types/payment_providers/checkout_com.rbDefine GraphQL object type for Checkout.com provider.
app/graphql/mutations/payment_providers/checkout_com/base.rbAbstract base mutation class for Checkout.com (inherit common logic).
app/graphql/mutations/payment_providers/checkout_com/create.rbMutation to create a Checkout.com provider instance.
app/graphql/mutations/payment_providers/checkout_com/update.rbMutation to update a Checkout.com provider instance.
app/graphql/resolvers/payment_providers_resolver.rbAdd provider_type.
app/graphql/types/customers/object.rbAdd provider_customer.
app/graphql/types/mutation_type.rbDefine mutations.
app/graphql/types/payment_providers/object.rbDefine resolve_type.

3. Provider Handling

FilePurpose
app/services/payment_providers/checkout_com_service.rbEntry service for handling Checkout.com logic.
app/services/payment_providers/checkout_com/base_service.rbShared methods for Checkout.com-related services.

4. Customer Handling

FilePurpose
app/services/payment_provider_customers/checkout_com_service.rbHandle customer-related logic (create, sync).
app/jobs/payment_provider_customers/checkout_com_create_job.rbBackground job to create a customer in Checkout.com.
app/services/payment_providers/checkout_com/customers/create_service.rbActual service to create customers.
app/services/payment_providers/create_customer_factory.rbAdd service mapping.
app/services/payment_provider_customers/factory.rbAdd service mapping.

5. Payment Flows

FilePurpose
Edit app/services/payment_providers/create_payment_factory.rbAdd service mapping.

6. Invoices

FilePurpose
app/jobs/invoices/payments/checkout_com_create_job.rbBackground job to process invoice payments via Checkout.com.
app/services/invoices/payments/checkout_com_service.rbService handling payment flow at invoice level.
app/services/payment_providers/checkout_com/payments/create_service.rbCreate actual payment via Checkout.com.
app/services/invoices/payments/payment_providers/factory.rbAdd service mapping.

7. Payment Requests

FilePurpose
app/jobs/payment_requests/payments/checkout_com_create_job.rbHandle ad-hoc payment requests.
app/services/payment_requests/payments/checkout_com_service.rbHandle service logic for payment requests.
app/services/payment_requests/payments/payment_providers/factory.rbAdd service mapping.

8. Checkout URL

FilePurpose
app/jobs/payment_provider_customers/checkout_com_checkout_url_job.rbGenerate hosted checkout URL for payments.

9. Webhook Handling

FilePurpose
app/controllers/webhooks_controller.rbAdd a new action for Checkout.com webhooks.
config/routes.rbAdd route for /webhooks/checkout_com.
app/services/payment_providers/checkout_com/handle_incoming_webhook_service.rbHandle incoming webhook events from Checkout.com.
app/services/payment_providers/checkout_com/validate_incoming_webhook_service.rbValidate webhook payloads and signatures.
app/jobs/payment_providers/checkout_com/handle_event_job.rbAsync processing of webhook events.
app/services/inbound_webhooks/process_service.rbAdd service mapping.
app/services/inbound_webhooks/validate_payload_service.rbAdd service mapping.

10. Specs

Find all related specs in /specs/*.rb

Frontend integration (UI)

All frontend actions are located in the lago-front repository.

Please refer to an existing PSP implementation to make sure you use the latest code guidelines and structure, as code might evolve in the future.

To ease the work on the Frontend side, make sure all type and enums are updated on the Backend first and run the codegen script to get the new graph.

New integration CRUD

Any new integration needs to be listed on the page src/pages/settings/Integrations.tsx

Then, new routes and components need to be created for the PSP connection lists, details, edition and deletion.

To start, have a look at these files in the app:

src/
├── pages/
│   └── settings/
│       ├── Integrations.tsx
│       ├── [PSPName]IntegrationDetails.tsx
│       └── [PSPName]Integrations.tsx
├── components/
│   └── settings/
│       ├── Add[PSPName]Dialog.tsx
│       └── Delete[PSPName]Dialog.tsx
└── public/
    └── images/
        └── [PSPName].svg

Please make sure the SVG is as clean as possible and uses viewbox rather than width and height (viewBox=“0 0 40 40”, refer to other images). You can use https://svgomg.net/ to help.

You also need to update the customer flow to allow connection to your new PSP.

This happens primarily in the PaymentProvidersAccordion component. It needs to be updated to handle the selection of this new PSP option.

Global UI updates

Update the following places responsible for displaying PSP information:

  • src/components/PaymentProviderChip.tsx (displays the PSP chip in multiple places)
  • src/pages/PaymentDetails.tsx (if you wish to have clickable payment url links)