Integration Guide
Connect Tuli to external systems using REST APIs, webhooks, and the Messy notification API.
Integration Patterns
REST API Integration
Use the Tuli REST API for real-time bidirectional data exchange:
- Inbound — Push data from external systems into Tuli (e.g., import leads from website forms)
- Outbound — Pull data from Tuli for external reporting or synchronization
- Bidirectional — Keep systems in sync with two-way data flow
API Fundamentals
Base URL
https://api.tuli.io/api/v1
Authentication
Include your API token in the Authorization header:
curl -H "Authorization: Bearer tuli_sk_live_..." \
https://api.tuli.io/api/v1/contracts
Key Identifiers
All entity references use string Keys, not numeric IDs:
{
"Id": "contract-1CAjiqwbu2g3LkcoByCL4Y",
"Tenant": "tenant-2DBkjrxcv3h4MldpCzDM5Z"
}
Webhook Integration
Receive real-time events when data changes in Tuli:
POST /api/v1/webhooks
Content-Type: application/json
{
"Url": "https://your-system.com/tuli-events",
"Events": [
"contract.created",
"contract.updated",
"payment.received",
"workorder.completed"
],
"Secret": "your-webhook-secret"
}
Webhook events are signed using HMAC-SHA256. Verify the signature:
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
);
}
Messy Notification API
Tuli uses the Messy API for email templates and notifications.
Template Management
Templates are managed at https://messy-api.tuli-erp.com.
Template locations:
/models/workflows/notifications/— Workflow notification templates/models/applications/fm/notifications/— Facility Management templates/models/applications/leasing/notifications/— Leasing templates
Upload Template
curl -X POST "https://messy-api.tuli-erp.com/templates" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": {
"name": "Contract Renewal Reminder",
"trigger": "contract-renewal-reminder",
"subject": "Contract {{ContractNumber}} expiring soon",
"body": "<html>...</html>"
}
}'
Template triggers use kebab-case: workflow-task-create, service-request-created
Merge Tags
Use merge tags in templates for dynamic content:
<p>Dear {{Tenant.Name}},</p>
<p>Your contract {{ContractNumber}} for unit {{Unit.Number}}
at {{Building.Name}} is expiring on {{EndDate}}.</p>
File-Based Import
For batch data migration and periodic imports:
- Excel/CSV — Upload structured files with column mapping
- JSON — API-based bulk import with validation
- Scheduled — Configure recurring imports from shared drives or SFTP
Common Integration Scenarios
| Scenario | Method | Direction |
|---|---|---|
| Website lead capture | REST API | Inbound |
| Accounting sync (QuickBooks, Xero) | REST API + Webhooks | Bidirectional |
| Bank statement import | File import | Inbound |
| HR system sync | REST API | Bidirectional |
| IoT sensor data (facilities) | REST API | Inbound |
| Document management (SharePoint) | REST API + Webhooks | Bidirectional |
| Payment gateway (Stripe, PayTabs) | Webhooks | Inbound |
| Email marketing (Mailchimp) | REST API | Outbound |
Error Handling
Implement retry logic for transient failures:
async function tuliApiCall(endpoint, data, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(`https://api.tuli.io/api/v1/${endpoint}`, {
method: 'POST',
credentials: 'include',
headers: {
'Authorization': 'Bearer tuli_sk_live_...',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
return await response.json();
}
// Rate limited - wait and retry
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
await sleep(retryAfter * 1000);
continue;
}
// Client error - don't retry
if (response.status >= 400 && response.status < 500) {
throw new Error(`API error: ${response.status}`);
}
// Server error - retry with backoff
throw new Error(`Server error: ${response.status}`);
} catch (err) {
if (i === retries - 1) throw err;
await sleep(1000 * Math.pow(2, i)); // Exponential backoff
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
ClickHouse Analytics
Tuli syncs data to ClickHouse for real-time analytics. The warehouse uses a specific naming convention:
wh_{Application}_{Object}
Examples:
wh_Leasing_Contractwh_FacilityManagement_WorkOrderwh_Finance_PurchaseOrder
Standard columns:
Key— String identifierIsDeleted— Soft delete flagCreatedAt,ModifiedAt— Timestamps- All P9 fields from the object definition
MeiliSearch Integration
Full-text search is powered by MeiliSearch. Search indexes are automatically created for objects with search-field:true attributes.
GET /api/v1/contracts?search=acme
Data Sync Architecture
- PostgreSQL — Primary transactional database
- ClickHouse — Analytics warehouse (synced via WorkerApp)
- MeiliSearch — Full-text search (synced via WorkerApp)
The warehouse-sync worker keeps all systems synchronized:
# Sync all
dotnet run --project api/WorkerApp -- warehouse-sync
# Sync ClickHouse only
dotnet run --project api/WorkerApp -- warehouse-sync clickhouse
# Sync MeiliSearch only
dotnet run --project api/WorkerApp -- warehouse-sync meilisearch