Status: Draft
Version: 1.0
Category: Technical Specification
This document specifies a multi-tenant SaaS platform that receives leads via webhooks from marketplaces and e-commerce platforms, normalizes data into a fixed model, applies user-defined dynamic segmentation rules, and automatically routes leads to configured call centers. The system logs the entire distribution process and enables complete lifecycle tracking.
| Term | Definition |
|---|---|
| Lead | A potential customer record containing personal information, event data, and conversion details |
| Segment | A logical group defined by dynamic rules that leads can be assigned to |
| Dispatch | The process of sending a lead to a configured call center |
| Ruleset | A reusable set of rules that can be referenced across multiple segments |
| Tenant | An independent organization operating within the platform |
| Transformer | A platform-specific adapter that converts incoming payloads to the internal lead model |
| Target | A call center configured to receive leads from a specific segment |
Build a multi-tenant SaaS platform that:
- Receives leads via webhooks from multiple marketplaces and e-commerce platforms
- Normalizes incoming data into a fixed internal model
- Applies dynamic user-defined segmentation rules
- Routes leads to configured call centers
- Logs the complete distribution process
- Enables full lifecycle tracking
| Layer | Technology |
|---|---|
| Frontend | Next.js + Zustand, Axios, Vite, Docker (Nginx) |
| Backend | NestJS (DDD + CQRS), MongoDB (Mongoose), JWT + RBAC, Docker |
| Infrastructure | EasyPanel (containers), MongoDB dedicated |
| Observability | Winston (logs), Sentry (errors), /health endpoint |
Marketplace/Platform
│
▼
[Webhook Endpoint]
│
▼
[Validation: HMAC + IP Allowlist]
│
▼
[Normalization: Platform-specific transformer]
│
▼
[Lead Creation/Update - Upsert by email]
│
▼
[Rule Engine: Evaluate active segments]
│
▼
[Identify New Segments - No re-dispatch to existing]
│
▼
[Distribution: Weighted round-robin to targets]
│
▼
[Send to Call Centers via HTTP POST]
│
▼
[Log Dispatch Attempts]
All data is isolated by orgId. Each organization operates independently with its own:
- Leads and conversions
- Segments and rules
- Call centers
- Users and permissions
- Webhook configurations
https://api.example.com/hooks/{orgSlug}/{platform}/{event}/{slug}
Example: https://api.example.com/hooks/acme/cartpanda/ORDER_PAID/orders-paid
Capabilities:
- Each organization has a unique slug used in webhook URLs
- Organizations are created only by
PLATFORM_MASTERusers - Each tenant operates in complete isolation
- Default user role can be configured per organization
Creation Process:
- Organization name (required)
- Unique slug (required)
- Master user assignment (required)
| Role | Permissions |
|---|---|
| PLATFORM_MASTER | Global admin; creates/manages all organizations; full system access |
| ORG_MASTER | Organization owner; full control within org; manages users |
| ORG_ADMIN | Operational admin; manages call centers, segments, settings |
| ORG_OPERATOR | Basic operator; views and creates content; cannot delete or manage users |
PLATFORM_MASTER (Level 4)
↓
ORG_MASTER (Level 3)
↓
ORG_ADMIN (Level 2)
↓
ORG_OPERATOR (Level 1)
| Action | PLATFORM_MASTER | ORG_MASTER | ORG_ADMIN | ORG_OPERATOR |
|---|---|---|---|---|
| Create organization | ✅ | ❌ | ❌ | ❌ |
| View all orgs | ✅ | ❌ | ❌ | ❌ |
| Switch org | ✅ | ❌ | ❌ | ❌ |
| Create user | ✅ | ✅ | ✅ | ❌ |
| Edit user | ✅ | ✅ | ✅ | ❌ |
| Delete ORG_MASTER | ✅ | ❌ | ❌ | ❌ |
| Delete ORG_ADMIN | ✅ | ✅ | ❌ | ❌ |
| Delete ORG_OPERATOR | ✅ | ✅ | ✅ | ❌ |
| Create content | ✅ | ✅ | ✅ | ✅ |
| Edit content | ✅ | ✅ | ✅ | ✅ |
| Delete content | ✅ | ✅ | ✅ | ❌ |
| View content | ✅ | ✅ | ✅ | ✅ |
| Configure webhooks | ✅ | ✅ | ✅ | ✅ |
| Rotate secret | ✅ | ✅ | ✅ | ❌ |
| Manage IP allowlist | ✅ | ✅ | ✅ | ❌ |
Note: Content includes leads, segments, call centers, rulesets, and webhooks.
| Field | Description |
|---|---|
| Name | Human-readable identifier |
| Platform | Platform identifier (e.g., cartpanda, shopify) |
| Event | Event type (e.g., ORDER_PAID, ABANDONED_CART) |
| Slug | Unique identifier used in the URL |
| Secret | HMAC key for validation (optional) |
| IP Allowlist | List of allowed IPs (optional) |
HMAC Validation:
- If secret is configured, webhook must include
X-Signatureheader - Signature is HMAC SHA256 of the request body
- System validates before processing
IP Allowlist:
- Supports CIDR format (e.g., 203.0.113.0/24)
- Only listed IPs can send webhooks
Public Webhooks:
- No secret configured = publicly accessible
- IP allowlist still respected if configured
| Platform | Events |
|---|---|
| CartPanda | ORDER_PAID, ORDER_REFUNDED, ORDER_CHARGEBACK, ABANDONED_CART, ORDER_UPSELL |
Extensibility: Architecture supports adding new platform-specific transformers.
- Generate new random secret
- Maintain rotation history
- Old webhooks stop working immediately
Basic Data:
leadId: Unique identifierorgId: Owner organizationemail,firstName,lastName,phone,whatsappgeo: Country, state, citylang: Preferred languagedevice: User deviceconsent: GDPR/LGPD consent data
Event Data:
eventId: Event type (e.g., ORDER_PAID)source: Origin (e.g., cartpanda)platformId: ID in source platformreceivedAt: When receivedingestedAt: When processed
Segmentation Data:
segmentIds: List of assigned segmentsstatus: NEW, PROCESSED, DISPATCHED, etc.
Conversions:
- Array of conversion records (purchases, refunds, etc.)
- Webhook - Automatic from marketplace data
- CSV Import - Bulk upload
- Manual Creation - Via API
Process:
- Upload CSV file (max 20,000 rows)
- Map columns to system fields
- Preview data before import
- System validates phone, email formats
- Batch processing with error reporting
Mappable Fields:
- Standard: firstName, lastName, email, phone, whatsapp, age
- Nested: conversions.order, conversions.product
- Custom fields can be mapped dynamically
- Name, email, phone
- Event type (ORDER_PAID, ABANDONED_CART, etc.)
- Status (NEW, PROCESSED, DISPATCHED)
- Associated segments
- Date range (receivedAt)
- Conversion fields (product, platform, etc.)
- Total leads
- Leads by status
- Leads by source/platform
- Conversion rate
- Distribution by segment
Leads can be reprocessed using a Redis queue with BullMQ jobs. This re-evaluates leads against segments and triggers new dispatches for matching segments.
Segments are groups of leads defined by dynamic rules. When a lead matches a segment's rules, it is automatically associated and sent to configured call centers.
- Purchased product
- Age range
- Geographic location
- UTM parameters (campaign, source, etc.)
- Purchase value
- Any combination of lead fields
| Operator | Description |
|---|---|
| eq | Equal to |
| ne | Not equal to |
| gt | Greater than |
| gte | Greater than or equal to |
| lt | Less than |
| lte | Less than or equal to |
| contains | String contains |
| regex | Regular expression |
| in | In list |
| not_in | Not in list |
all: All conditions must be true (AND)any: At least one condition must be true (OR)- Nested rules supported
{
"all": [
{
"field": "conversions.productId",
"op": "eq",
"value": "PROD_123"
},
{
"any": [
{ "field": "age", "op": "gte", "value": 25 },
{ "field": "age", "op": "lte", "value": 45 }
]
},
{
"field": "geo.country",
"op": "eq",
"value": "BR"
}
]
}Interpretation: Product PROD_123 AND (age >= 25 OR age <= 45) AND country = BR
Each segment can have multiple call centers as destinations:
| Field | Description |
|---|---|
| Call Center | Select from configured call centers |
| Weight | Distribution percentage |
| Rate Limit | Sends per minute for this target |
| Endpoint | Specific endpoint URL (optional) |
Distribution: Weighted round-robin ensures proportional distribution.
Segments are evaluated in priority order (highest first). This enables:
- More specific segments evaluated first
- Generic segments as fallback
- Fine control over which segment "wins" when multiple apply
- Test rules before activation
- Paste example lead data
- View result: "Matches" or "Does not match" with explanation
- View how many existing leads would match
- Sample up to 1000 leads
- Match count and coverage percentage
| Field | Description |
|---|---|
| Name | Human-readable identifier |
| Endpoints | URLs receiving leads (multiple per event type) |
| Secret | HMAC key for signing requests (optional) |
| Timeout | Max wait time for response (default: 5000ms) |
| Retries | Attempts on failure (default: 3) |
| Window | Operating hours |
| Daily Cap | Daily lead limit (default: 100) |
| Rate Limit | Per-minute limit (default: 120) |
| Active | Whether the call center is enabled |
Call centers can have different endpoints for different event types:
order_paid: For paid ordersabandoned_cart: For abandoned carts
System automatically selects the correct endpoint based on lead event type.
- Start/End times (HH:mm format)
- Timezone (e.g., America/Sao_Paulo)
- 24-hour operation option
Leads are only sent during the configured window.
Per Minute:
- Controls how many leads can be sent per minute
- Prevents call center overload
Daily Cap:
- Total leads per day
- Call center stops receiving when cap is reached
When a lead is sent, the call center receives JSON containing:
Lead Data:
- Personal information (name, email, phone)
- Geographic data
- Consent data
Segmentation Data:
segmentId: Which segment triggered the dispatchsegmentName: Segment name
Conversion Data:
- Product information
- Price
- Platform
- UTM parameters
- Billing and shipping addresses
- Order items
Metadata:
receivedAt,ingestedAt: TimestampscallCenterAssignments: Other assigned call centers
Omitted Fields:
leadId,orgId,eventId(internal only)
- Send test payload
- Verify response
- Validate format and response time
- Evaluation: System identifies matching segments for the lead
- New Segments: Only segments the lead just entered (no re-dispatch to existing)
- Distribution: For each new segment:
- Fetch configured targets
- Apply weights for distribution
- Check rate limits and daily caps
- Check operating windows
- Send to each call center
- Logging: Record every attempt
| Field | Description |
|---|---|
| dispatchId | Unique dispatch identifier |
| leadId | Lead being sent |
| segmentId | Segment that triggered dispatch |
| callCenterId | Target call center |
| attempt | Attempt number (1, 2, 3...) |
| requestBody | Full payload sent |
| endpointName | Endpoint name (from call center config) |
| endpointPath | URL used |
| responseCode | HTTP status code |
| responseBody | Response body |
| status | SENT or FAILED |
| durationMs | Response time |
| timestamp | When sent |
- System retries on failure
- Configurable retry count per call center
- Exponential backoff between attempts
- Each attempt is logged
Rulesets are reusable sets of rules that can be referenced across multiple segments.
- Define a rule once, use in multiple segments
- Single point of update
- Consistency across segments
Ruleset "Allowed Countries" with values ["BR", "US", "CA"] can be used in 10 different segments. Adding "MX" updates all segments at once.
| Field | Description |
|---|---|
| Name | Human-readable identifier |
| Description | Purpose explanation |
| Field | Lead field evaluated (e.g., geo.country) |
| Operator | IN or NOT_IN |
| Values | List of allowed/disallowed values |
A ruleset is "active" if used by at least one active segment. Status display helps identify unused rulesets for cleanup.
{
"id": "uuid",
"name": "Organization Name",
"slug": "acme",
"defaultUserRole": "ORG_OPERATOR",
"active": true,
"createdAt": "ISO date",
"updatedAt": "ISO date"
}{
"id": "uuid",
"email": "user@example.com",
"name": "John Doe",
"passwordHash": "bcrypt_hash",
"refreshToken": "bcrypt_hash",
"active": true,
"orgId": "uuid",
"createdAt": "ISO date",
"updatedAt": "ISO date"
}{
"id": "uuid",
"orgId": "uuid",
"userId": "uuid",
"role": "ORG_MASTER | ORG_ADMIN | ORG_OPERATOR",
"createdAt": "ISO date"
}{
"leadId": "uuid",
"orgId": "uuid",
"source": "cartpanda",
"eventId": "ORDER_PAID",
"receivedAt": "ISO date",
"ingestedAt": "ISO date",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "+5511999999999",
"whatsapp": "+5511999999999",
"consent": {
"granted": true,
"timestamp": "ISO date"
},
"geo": {
"country": "BR",
"state": "SP",
"city": "Sao Paulo"
},
"lang": "pt-BR",
"device": "desktop",
"status": "NEW",
"segmentIds": ["seg-123", "seg-456"],
"callCenterAssignments": [
{
"callCenterId": "cc-123",
"name": "Call Center A"
}
],
"conversions": [],
"createdAt": "ISO date",
"updatedAt": "ISO date"
}{
"event": "ORDER_PAID",
"platform": "cartpanda",
"order": "12345",
"product": "Product X",
"productId": "PROD_456",
"variant": "Variant Y",
"price": 99.99,
"source": "facebook",
"shop": "Store ABC",
"currency": "BRL",
"utm": {
"source": "facebook",
"medium": "cpc",
"campaign": "promo-january"
},
"billingAddress": {},
"shippingAddress": {},
"lineItems": [],
"createdAt": "ISO date",
"updatedAt": "ISO date"
}{
"segmentId": "uuid",
"orgId": "uuid",
"name": "Product X Leads: Age 25-45",
"description": "Product X AND age between 25 and 45",
"rules": {
"all": [
{
"field": "conversions.productId",
"op": "eq",
"value": "PROD_123"
},
{
"any": [
{ "field": "age", "op": "gte", "value": 25 },
{ "field": "age", "op": "lte", "value": 45 }
]
}
]
},
"targets": [
{
"callCenterId": "cc-123",
"weight": 70,
"rateLimitPerMin": 60,
"endpoint": "order_paid"
},
{
"callCenterId": "cc-456",
"weight": 30,
"rateLimitPerMin": 40
}
],
"priority": 10,
"active": true,
"dispatchesCount": {
"total": 14,
"sent": 2,
"failed": 12
},
"createdBy": "uuid",
"createdAt": "ISO date",
"updatedAt": "ISO date"
}{
"callCenterId": "uuid",
"orgId": "uuid",
"name": "Call Center A",
"endpoint": [
{
"name": "order_paid",
"url": ["https://cc-a.com/webhooks/order_paid"]
},
{
"name": "abandoned_cart",
"url": ["https://cc-a.com/webhooks/abandoned_cart"]
}
],
"secret": "HMAC-SECRET",
"timeoutMs": 5000,
"retries": 3,
"window": {
"start": "08:00",
"end": "20:00",
"tz": "America/Sao_Paulo",
"is24h": false
},
"dailyCap": 100,
"rateLimitPerMin": 120,
"active": true,
"dispatchesCount": {
"total": 14,
"sent": 2,
"failed": 12
},
"format": "default",
"createdBy": "uuid",
"createdAt": "ISO date",
"updatedAt": "ISO date"
}{
"dispatchId": "uuid",
"orgId": "uuid",
"leadId": "uuid",
"segmentId": "uuid",
"callCenterId": "uuid",
"attempt": 1,
"requestBody": {},
"responseCode": 200,
"responseBody": {},
"status": "SENT",
"durationMs": 150,
"timestamp": "ISO date",
"endpointName": "order_paid",
"endpointPath": "https://webhook.site.com/121212"
}{
"receivedAt": "ISO date",
"ingestedAt": "ISO date",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "+5511999999999",
"whatsapp": "+5511999999999",
"consent": {
"granted": true,
"timestamp": "ISO date"
},
"geo": {
"country": "BR",
"state": "SP",
"city": "Sao Paulo",
"address": "Rua das Flores, 123"
},
"lang": "pt-BR",
"device": "desktop",
"status": "NEW",
"segmentId": "seg-123",
"segmentName": "Product X Leads",
"callCenterAssignments": [
{
"callCenterId": "cc-123",
"name": "Call Center A"
}
],
"conversion": {
"event": "ORDER_PAID",
"platform": "cartpanda",
"order": "ORDER_123",
"product": "Product X",
"productId": "PROD_456",
"variant": "Variant Y",
"price": 99.99,
"source": "facebook",
"shop": "Store ABC",
"currency": "BRL",
"utm": {
"source": "facebook",
"medium": "cpc",
"campaign": "promo-january"
},
"billingAddress": {},
"shippingAddress": {},
"lineItems": [],
"subtotalPrice": 89.99,
"totalDiscounts": 10.0,
"totalTax": 5.0,
"shippingPrice": 5.0,
"createdAt": "ISO date",
"updatedAt": "ISO date"
},
"createdAt": "ISO date",
"timestamp": "ISO date"
}Note: leadId, orgId, and eventId are omitted from dispatch payload. device is included only if present.
{
"id": "uuid",
"orgId": "uuid",
"type": "incoming",
"platform": "cartpanda",
"event": "ORDER_PAID",
"slug": "orders-paid",
"name": "Orders Paid Webhook",
"path": "/hooks/acme/cartpanda/ORDER_PAID/orders-paid",
"secret": "HMAC-SECRET",
"ipAllowlist": ["203.0.113.10"],
"active": true,
"createdAt": "ISO date",
"updatedAt": "ISO date"
}{
"id": "uuid",
"orgId": "uuid",
"name": "Allowed Countries",
"description": "Countries where we operate",
"field": "geo.country",
"defaultOperator": "IN",
"values": ["BR", "US", "CA"],
"createdAt": "ISO date",
"updatedAt": "ISO date"
}- Webhook Receipt: Marketplace POST to
/hooks/{orgSlug}/{platform}/{event}/{slug} - Validation: HMAC verification and IP allowlist check
- Identification: Organization identified by slug
- Transformation: Platform-specific transformer converts payload
- Normalization: Data normalized to internal lead model
- Conversion Extraction: Product, price, UTM data extracted
- Lead Creation/Update: Upsert by email; new lead gets NEW status
- Segment Evaluation: All active segments evaluated in priority order
- New Segment Identification: Compares current vs previous segments; only new segments trigger dispatch
- Distribution: For each new segment:
- Fetch targets
- Apply weights
- Check rate limits and daily caps
- Check operating windows
- Prepare payload
- Send HTTP POST to each call center
- Logging: Each attempt logged with full details
- Upload: User uploads CSV (max 20,000 rows)
- Mapping: System auto-maps columns; user can adjust
- Preview: Data preview with validation errors
- Validation: Phone, email formats checked; duplicates identified
- Processing: Batch processing with error reporting
- Report: Import count, error list, downloadable report
- Selection: User selects leads by ID or filters
- Cleanup: Existing segments cleared from lead
- Re-evaluation: All active segments re-evaluated
- Redistribution: All matching segments are "new" (after cleanup), triggering full dispatch
Use Cases:
- Segments were modified
- New call centers added
- Need to resend existing leads
Login:
- User provides email and password
- System validates credentials
- Returns
access_token(15 min expiry) andrefresh_token(7 days)
Refresh:
- Obtain new access token without re-login
Logout:
- Refresh token invalidated
All routes protected by guards that verify:
- Valid JWT token
- User role
- Organization context
HMAC Validation:
- If secret configured,
X-Signatureheader required - Signature is HMAC SHA256 of request body
- Validated before processing
IP Allowlist:
- CIDR format supported
- Only listed IPs can send
Public Webhooks:
- No secret = public access
- IP allowlist still respected
- All queries filter by
orgIdautomatically - Users only see their organization's data
- Cross-tenant data access impossible
- Organization context from JWT token
Validation:
- All inputs validated with class-validator
- XSS and injection sanitization
- Type and format validation
Encryption:
- Passwords hashed with bcrypt
- JWT tokens signed
- HTTPS required in production
- Winston for structured logs
- Error, warn, info, debug levels
- Full operation context
/healthendpoint- System and dependency status
- Lead processing counters
- Dispatch statistics
- Performance metrics
| Component | Technology |
|---|---|
| Backend Framework | NestJS (TypeScript) |
| Database | MongoDB + Mongoose |
| Authentication | JWT + Passport |
| API Documentation | Swagger/OpenAPI |
| Validation | Class Validator |
| Frontend Framework | Next.js |
| State Management | Zustand |
| HTTP Client | Axios |
| Build Tool | Vite |
| Containerization | Docker |
| Container Management | EasyPanel |
| Logging | Winston |
| Error Tracking | Sentry |
git clone <repository-url>
npm install
cp .env.example .env
# Edit .env with configuration
npm run start:devsrc/
├── modules/
│ ├── auth/ # Authentication and org management
│ ├── leads/ # Lead management
│ ├── segments/ # Segmentation
│ ├── call-centers/ # Call center configuration
│ ├── dispatch/ # Distribution
│ ├── webhook/ # Webhook handling
│ └── rulesets/ # Reusable rules
├── main.ts # Application bootstrap
├── app.module.ts # Root module
└── swagger.ts # Swagger configuration
Module Structure:
module/
├── application/
│ ├── controllers/ # HTTP endpoints
│ └── services/ # Business logic
├── domain/
│ ├── entities/ # Domain models
│ ├── dtos/ # Data Transfer Objects
│ └── repositories/ # Repository interfaces
└── infra/
└── repositories/ # MongoDB implementations
npm test # All tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
npm run test:e2e # End-to-end tests- Create platform-specific transformer service
- Implement
transformOrderPaid()method - Implement
extractConversionData()method - Add to PublicWebhookController routing
- Add event mapping if needed
- Configure regular MongoDB backups
- Use mongodump or managed backup services
- Monitor error logs regularly
- Configure alerts for critical errors
- Track lead volume and dispatch success rate
- Monitor processing latency
Horizontal:
- Multiple instances supported
- MongoDB clustering available
- Load balancer in front of instances
Vertical:
- Increase CPU, RAM as needed
- MongoDB vertical scaling available
| Issue | Checks |
|---|---|
| Webhook not receiving | Verify URL, secret, IP allowlist, error logs |
| Leads not dispatched | Check segment active, call center active, rate limits, dispatch logs |
| Slow performance | Check lead volume, resource usage, query optimization, indexes |
- Complete multi-tenancy with data isolation
- Webhook system for receiving leads from multiple platforms
- Dynamic segmentation with complex, combinable rules
- Automatic distribution to call centers with weights and limits
- Complete dispatch logs for traceability
- Bulk CSV lead import
- Robust RBAC with 4 permission levels
- Reusable rulesets for consistency
- Webhook security with HMAC and IP allowlist
- Manual lead reprocessing
- Advanced search and filters across all modules
- Interactive Swagger API documentation
- JWT tokens with short-lived access tokens (15 minutes)
- Refresh token rotation
- Secure token storage (HttpOnly cookies recommended)
- RBAC enforcement at every endpoint
- Hierarchical permission model
- Context-aware authorization
- Field-level encryption for sensitive data
- PII masking in logs
- Audit trail for all operations
- Rate limiting per endpoint
- Input validation and sanitization
- CORS configuration
- HTTPS enforcement
| Variable | Description |
|---|---|
| MONGO_URI | MongoDB connection URI |
| JWT_ACCESS_SECRET | Access token signing secret |
| JWT_REFRESH_SECRET | Refresh token signing secret |
| PORT | Server port (default: 3000) |
| CORS_ORIGINS | Allowed CORS origins |