Skip to content

Integrations

The Integration framework provides a standardized way to connect external services (payment gateways, shipping providers, SMS services, etc.) to Fullfinity.

Integrations follow a provider-agnostic architecture:

┌─────────────────────────────────────────────────────────────────┐
│ CORE MODULES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ invoicing │ │ sales │ │ portal │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ integrations (base) │ │
│ │ - Integration model │ │
│ │ - IntegrationMapper model │ │
│ └────────────────┬───────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │payment_stripe│ │shipping_ups │ │ sms_twilio │ │
│ │(integration) │ │(integration)│ │(integration)│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘

The base Integration model in the integrations module:

class Integration(Model):
_verbose_name = "Integration"
name = Char(description="Integration Name", max_length=100, required=True)
description = Text(description="Description")
image = File(description="Image")
company = ManyToOne("Company", related_name="integrations", on_delete="CASCADE")
enabled = Boolean(description="Enabled", default=False)
module = ManyToOne("Module", related_name="integrations", on_delete="SET NULL")
integration_category = Selection(
choices=["Online Payment", "Messaging", "Shipping"],
description="Integration Category",
hint="Which category this integration belongs to (set from each module's manifest).",
)
cron_sync_method = Char(description="Cron Sync Method", max_length=100)
last_synced_date = Datetime(description="Last Synced On")
FieldDescription
nameHuman-readable integration name
enabledWhether the integration is active
moduleReference to the Module that provides this integration
integration_categorySelection — one of Online Payment, Messaging, Shipping (set from the module manifest)
cron_sync_methodMethod name for scheduled sync jobs
async def get_state(self):
"""State is computed from module installation + enabled flag."""
self.state = "Not Installed"
await self.fetch_related("module")
if self.module and self.module.state == "Installed":
self.state = "Installed"
if self.enabled:
self.state = "Enabled"

Integration modules declare their category in manifest.yaml:

name: Stripe Payments
identifier: payment_stripe
version: '1.0'
category: module_category_integrations
integration_category: Online Payment # Links to backbone module
description: Accept payments via Stripe
dependencies:
- online_payment # Backbone module dependency
icon: CreditCard
image: /static/img/stripe_logo.png
static_paths:
- img

Key manifest fields for integrations:

FieldDescription
categoryAlways module_category_integrations for integration modules
integration_categoryThe backbone module this integration extends
dependenciesMust include the backbone module
imageLogo shown in integration list

Create a model that inherits from Integration:

from fullfinity.engine.base import *
class MyServiceIntegration(Model):
"""Extends Integration with MyService-specific fields."""
__inherit__ = "Integration"
# Provider-specific credential fields
api_key = Char(
max_length=255,
description="API Key",
hint="Your MyService API key",
company_scoped=True, # Stored per-company
)
api_secret = Char(
max_length=255,
description="API Secret",
company_scoped=True,
)
sandbox_mode = Boolean(
description="Sandbox Mode",
default=True,
)
async def _is_my_service(self) -> bool:
"""Check if this integration is MyService."""
await self.fetch_related("module")
return self.module and self.module.identifier == "my_service"
async def enable_integration(self):
"""Validate credentials before enabling."""
if await self._is_my_service():
if not self.api_key:
raise UserError("Please enter your API Key before enabling.")
await super().enable_integration()

Integration methods use a detection pattern to only process their own integrations:

async def some_method(self, *args, **kwargs):
# Check if this integration is ours
if not await self._is_my_service():
return await super().some_method(*args, **kwargs)
# Handle our integration-specific logic
...

This pattern allows multiple integration modules to extend the same backbone.

integration_category is a Selection — its value must be one of the choices defined on the Integration model. Current categories and their backbone modules:

CategoryBackbone ModulePurpose
Online Paymentonline_paymentPayment gateways (Stripe, PayPal, Razorpay, Adyen, Mollie, Square)
MessagingmessagingMessaging providers (e.g. Twilio)
ShippingShipping providers (category reserved)

To add a new category, extend the integration_category Selection choices on the Integration model first; only then can a manifest use it.

For syncing data with external systems, use IntegrationMapper:

class IntegrationMapper(Model):
_verbose_name = "Integration Mapper"
local_id = Char(description="Document ID", max_length=255, required=True)
remote_id = Char(description="Remote ID", max_length=255, required=True)
integration = ManyToOne("Integration", related_name="mappings", on_delete="CASCADE")
model = Char(description="Model", max_length=255, required=True)

Example usage:

# Store mapping after creating remote resource
async def sync_contact(self, contact):
# Create in remote system
remote_customer = await self._api_create_customer(contact)
# Store mapping
IntegrationMapper = get_model("IntegrationMapper")
await IntegrationMapper.create(
integration=self.id,
model="Contact",
local_id=str(contact.id),
remote_id=remote_customer["id"],
)
# Look up remote ID
async def get_remote_id(self, contact):
IntegrationMapper = get_model("IntegrationMapper")
mapping = await IntegrationMapper.filter(
integration=self.id,
model="Contact",
local_id=str(contact.id),
).first()
return mapping.remote_id if mapping else None

For scheduled synchronization:

class MyServiceIntegration(Model):
__inherit__ = "Integration"
# Set sync method name (called by cron)
cron_sync_method = "sync_my_service"
async def sync_my_service(cls):
"""Cron job: Sync data with MyService."""
env = env_ctx.get()
Integration = get_model("Integration")
# Get all enabled MyService integrations
integrations = await Integration.filter(
module__identifier="my_service",
enabled=True,
).all()
for integration in integrations:
await integration._do_sync()
async def _do_sync(self):
"""Perform actual sync logic."""
# Fetch updates from external service
# Update local records
# Update last_synced_date
self.last_synced_date = datetime.now()
await self.save()

Add provider-specific fields to Integration form:

views/my_service_views.yaml
- data_type: UiView
identifier: integration_form_view
__inherit__: integration_form_view
arch:
- type: field
name: api_key
position: after:enabled
properties:
visible: Q(module__identifier__eq='my_service')
- type: field
name: api_secret
position: after:api_key
properties:
widget: Password
visible: Q(module__identifier__eq='my_service')
  1. Use company_scoped=True for credential fields to store per-company
  2. Validate credentials in enable_integration() before allowing enable
  3. Use the detection pattern (_is_my_service()) in all override methods
  4. Store mappings for synced records to avoid duplicates
  5. Handle API errors gracefully with user-friendly messages
  6. Log sync operations for debugging