Overview
DataBridge allows external systems to automatically sync data into your Havilo workspace. When configured, external systems send webhooks to Havilo, which creates or updates entities based on your field mappings.
A single DataBridge can sync to multiple workspaces simultaneously (fan-out model). Each
workspace configures its own entity type and field mappings independently, so the same webhook data
can create different entity structures in different workspaces.
Use cases:
- Sync contacts from your CRM
- Import profiles from partner systems
- Migrate data from legacy platforms
- Keep entities up-to-date with external sources
How It Works
- External system sends a webhook when data changes
- Havilo validates the webhook signature
- Field mappings transform the data to match your entity fields
- Entity is created or updated in your workspace
External System → Webhook → DataBridge → Entity
(CRM) (HTTPS) (Mapping) (Contact)Configuration
Requirements
- Feature:
WEBHOOK_INTEGRATIONmust be enabled for your workspace - Permission:
MANAGE_WORKSPACEpermission required
Setup Steps
- Navigate to Settings > Advanced > Data Bridges
- Select an available DataBridge from the list
- Configure entity type mappings (which entity type receives each resource type)
- Configure field mappings (map source fields to your entity fields)
- Enable the DataBridge
Sending Webhooks
External systems send webhooks to sync data with your workspace.
Endpoint
POST https://havilo.ai/api/webhooks/sync/{bridgeId}Required Headers
| Header | Description |
|---|---|
Content-Type | Must be application/json |
X-Webhook-Signature | HMAC-SHA256 signature (see Signature Validation) |
X-Webhook-Timestamp | ISO 8601 timestamp (for replay protection) |
Payload Structure
In Push mode, include the full data in the webhook payload:
{
"resource": {
"type": "users",
"id": "ext-12345"
},
"operation": "create",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"type": "users",
"id": "ext-12345",
"attributes": {
"name": "Jane Smith",
"email": "jane@example.com",
"company": "Acme Inc"
}
}
}Payload Fields
| Field | Required | Description |
|---|---|---|
resource.type | Yes | Resource type (must be enabled in DataBridge config) |
resource.id | Yes | Unique ID from your system |
operation | Yes | One of: create, update, delete |
timestamp | Recommended | ISO 8601 timestamp of the change |
data | Push mode | Full resource data with attributes |
fetch_url | Pull mode | URL to fetch full resource data |
version | Optional | Monotonic version number for ordering |
Operations
Create & Update
Create and update operations work identically - Havilo uses the resource ID to determine whether to create a new entity or update an existing one.
{
"resource": { "type": "users", "id": "ext-12345" },
"operation": "update",
"data": {
"type": "users",
"id": "ext-12345",
"attributes": {
"name": "Jane Smith",
"email": "jane.smith@newdomain.com"
}
}
}Delete
Delete operations remove the resource:
{
"resource": { "type": "users", "id": "ext-12345" },
"operation": "delete",
"timestamp": "2025-01-15T10:30:00Z"
}Relationship Resources
DataBridge supports syncing relationship resources that create connections between entities rather than creating new entities. This is useful for representing founder-startup relationships, employment, investments, or other associations.
How Relationships Work
- Entity resources (e.g.,
users,companies) create Entity records - Relationship resources (e.g.,
relationship) create EntityRelationship records linking two existing entities
users webhook → Creates Entity (Contact: John)
companies webhook → Creates Entity (Company: Acme)
relationship webhook → Creates Relationship (John → Acme, role: "Founder")Configuration
Relationship resources require a different configuration than entity resources:
| Config Field | Description |
|---|---|
relationshipTypeId | UUID of the EntityRelationshipType to use |
fromField | Source field containing the "from" entity ID |
fromResourceType | Resource type of the "from" entity |
toField | Source field containing the "to" entity ID |
toResourceType | Resource type of the "to" entity |
Relationship Payload Example
{
"resource": {
"type": "relationship",
"id": "rel-123"
},
"operation": "create",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"type": "relationship",
"id": "rel-123",
"attributes": {
"user_id": "user-456",
"company_id": "company-789",
"role": "Founder"
}
}
}Relationship Modeling Approaches
There are two ways to model relationships, depending on your needs:
Approach A: Separate Relationship Types
Use when you need distinct relationship types with different labels (e.g., "Works At" vs "Worked At"):
- Create multiple resource types for relationships (e.g.,
works-at,worked-at) - Each maps to a different relationship type in your workspace
- Send webhooks with the appropriate resource type
// "Works At" relationship
{
"resource": { "type": "works-at", "id": "rel-1" },
"data": { "attributes": { "user_id": "user-1", "company_id": "company-1" } }
}
// "Worked At" relationship
{
"resource": { "type": "worked-at", "id": "rel-2" },
"data": { "attributes": { "user_id": "user-1", "company_id": "company-2" } }
}Result: Each appears under its own section in the UI.
Approach B: Single Type with Role Metadata
Use when you want one relationship type with role-based grouping:
- Use a single relationship resource type (e.g.,
relationship) - Include a
rolefield to differentiate relationships - The UI groups relationships by role automatically
// All use same resource type, different roles
{ "resource": { "type": "relationship", "id": "rel-1" },
"data": { "attributes": { "user_id": "user-1", "company_id": "company-1", "role": "CEO" } } }
{ "resource": { "type": "relationship", "id": "rel-2" },
"data": { "attributes": { "user_id": "user-2", "company_id": "company-1", "role": "CTO" } } }
{ "resource": { "type": "relationship", "id": "rel-3" },
"data": { "attributes": { "user_id": "user-3", "company_id": "company-1", "role": "Advisor" } } }Result: All appear under one section, grouped by role.
Relationship Metadata
Any field in attributes that is NOT a foreign key field (fromField/toField) becomes metadata on the relationship.
Common metadata fields:
role- Role in the relationship (special UI grouping)title- Job titlestart_date- When the relationship started
Only the role field has special UI treatment—it's used to group relationships in the sidebar.
Role Display in UI
When you sync relationships with a role field, Havilo groups them by role in the entity sidebar:
FOUNDED BY CONTACTS
CEO
- John Doe
CTO
- Jane Smith
Advisor
- Bob Wilson
- Alice BrownThis provides a clear visual hierarchy when entities have multiple relationships with different roles.
Important: Entity Order Matters
Relationship webhooks require both referenced entities to exist first. If the "from" or "to" entity doesn't exist, the relationship webhook is skipped (returns processed_workspaces: 0).
Field Mappings
Field mappings define how source data maps to your entity fields.
Example
If your external system sends:
{
"attributes": {
"full_name": "Jane Smith",
"email_address": "jane@example.com",
"linkedin": "https://linkedin.com/in/janesmith"
}
}And you configure mappings:
| Source Field | Entity Field (Havilo) |
|---|---|
full_name | Name |
email_address | |
linkedin | LinkedIn URL |
The resulting entity will have:
- Name: Jane Smith
- Email: jane@example.com
- LinkedIn URL: https://linkedin.com/in/janesmith
Unmapped Fields
Fields in your webhook that aren't mapped are ignored. This allows you to send complete data and only import what you need.
Signature Validation
All webhooks must include a valid HMAC-SHA256 signature for security.
Computing the Signature
import crypto from 'crypto'
function signWebhook(secret: string, payload: string): string {
// First, hash the secret
const secretHash = crypto
.createHash('sha256')
.update(secret)
.digest('hex')
// Then compute HMAC using the hashed secret
const signature = crypto
.createHmac('sha256', secretHash)
.update(payload)
.digest('hex')
return `sha256=${signature}`
}
// Usage
const webhookSecret = 'your-webhook-secret'
const payload = JSON.stringify({
resource: { type: 'users', id: '123' },
operation: 'create',
data: { type: 'users', id: '123', attributes: { name: 'Jane' } }
})
const signature = signWebhook(webhookSecret, payload)
// Send webhook with signature header
await fetch('https://havilo.ai/api/webhooks/sync/bridge-id', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Timestamp': new Date().toISOString(),
},
body: payload,
})Timestamp Validation
Webhooks include timestamp validation to prevent replay attacks. Webhooks older than 5 minutes are rejected.
Always include the X-Webhook-Timestamp header with the current time.
Response Codes
Success
{
"success": true,
"processed_workspaces": 1
}Errors
| Status | Error | Description |
|---|---|---|
| 400 | invalid_payload | Request body is invalid JSON or missing required fields |
| 401 | invalid_signature | HMAC signature validation failed |
| 401 | timestamp_expired | Webhook timestamp is older than 5 minutes |
| 404 | bridge_not_found | DataBridge ID doesn't exist or is inactive |
| 422 | unknown_resource_type | Resource type not enabled for this DataBridge |
Example error response:
{
"success": false,
"error": "invalid_signature",
"message": "Webhook signature validation failed"
}Best Practices
1. Include Timestamps
Always include the timestamp field in your payload and the X-Webhook-Timestamp header:
{
"resource": { "type": "users", "id": "123" },
"operation": "update",
"timestamp": "2025-01-15T10:30:00Z",
...
}2. Use Version Numbers
For high-frequency updates, include a monotonic version number to handle out-of-order delivery:
{
"resource": { "type": "users", "id": "123" },
"operation": "update",
"version": 42,
...
}Havilo tracks versions per-entity, so each resource has its own version history. Updates with versions lower than the last processed version for that specific entity are skipped. This means webhooks for different entities don't interfere with each other's version ordering.
3. Retry Failed Webhooks
Implement retry logic with exponential backoff for failed webhook deliveries:
const delays = [1000, 5000, 30000, 120000] // 1s, 5s, 30s, 2m
for (const delay of delays) {
const response = await sendWebhook(payload)
if (response.ok) break
await sleep(delay)
}4. Log Webhook Responses
Keep logs of webhook responses for debugging:
const response = await sendWebhook(payload)
console.log({
bridgeId,
resourceType: payload.resource.type,
resourceId: payload.resource.id,
status: response.status,
success: response.ok,
})5. Test in Development
Use a test DataBridge in development before connecting to production.
Troubleshooting
Webhook Returns 401
Possible causes:
- Incorrect webhook secret
- Not hashing the secret before computing HMAC (see Signature Validation)
- Timestamp too old (>5 minutes)
Solution: Verify your signature computation matches the two-step process and include a current timestamp.
Webhook Returns 422
Possible causes:
- Resource type not enabled in DataBridge configuration
- Resource type not configured for your workspace
Solution: Contact your administrator to enable the resource type in DataBridge settings.
Entity Not Created
Possible causes:
- No field mappings configured for the resource type
- Entity type not selected for the resource type
- DataBridge is disabled
Solution: Check your DataBridge configuration in Settings > Advanced > Data Bridges.
Duplicate Entities
Possible causes:
- Different resource IDs for the same record
- Multiple DataBridges syncing the same data
Solution: Ensure your external system uses consistent, unique IDs for each resource.
Need Help?
If you encounter issues:
- Verify your webhook signature is computed correctly
- Check that timestamps are current (within 5 minutes)
- Confirm field mappings are configured in your workspace
- Review DataBridge status in Settings > Advanced > Data Bridges
- Contact your system administrator for webhook secrets or DataBridge access