# Minimo Contact Sync Integration
You are an AI coding assistant. Your task is to implement a contact synchronization integration between the user's application and **Minimo**, a customer engagement platform. The integration must keep the user database synchronized with Minimo contacts via Minimo's public REST APIs.
Every time a user is created, updated, or has any field changed in the application, the corresponding contact in Minimo must be created or updated. If the application tracks custom user attributes (e.g., plan type, signup date, company name), those must also be synced as custom fields.
---
## 1. Authentication
All API requests require the API key in the `Authorization` header. The company is automatically identified from your API key.
| Header | Value | Description |
|--------|-------|-------------|
| `Authorization` | `Bearer mn-{clientId}-{apiKey}` | API key provided by Minimo |
Store these as environment variables:
```
MINIMO_API_URL=https://api.minimo.it
MINIMO_API_KEY=mn-abc123-xyz789
```
Every request must include the Authorization header. Example:
```
Authorization: Bearer mn-abc123-xyz789
Content-Type: application/json
```
---
## 2. API Reference
### 2.1 List Custom Fields
Retrieve all custom fields defined for the company.
```
GET /public/v1/custom-fields
```
**Query Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `groupBy` | `string` | No | Pass `category` to get fields grouped by category |
**Response (flat):** `CustomField[]`
**Response (grouped, `?groupBy=category`):**
```json
{
"groups": [
{
"category": "Personal Info",
"fields": [
{
"id": 1,
"key": "FIRST_NAME",
"displayName": "First name",
"type": "text",
"category": "Personal Info",
"source": "System",
"visibility": true
}
]
}
],
"total": 5
}
```
**cURL Example:**
```bash
curl -X GET "${MINIMO_API_URL}/public/v1/custom-fields" \
-H "Authorization: Bearer ${MINIMO_API_KEY}"
```
---
### 2.2 Create Custom Field
Create a new custom field definition.
```
POST /public/v1/custom-fields
```
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | Yes | Unique identifier (uppercase recommended, e.g., `PLAN_TYPE`) |
| `displayName` | `string` | Yes | Human-readable label (e.g., `Plan Type`) |
| `type` | `string` | Yes | One of: `text`, `number`, `boolean`, `date`, `datetime`, `time`, `json` |
| `category` | `string` | No | Group label (e.g., `Subscription Info`) |
| `acceptableValues` | `object` | No | For select/enum fields, the allowed values |
| `source` | `string` | No | `Manual` (default), `System`, or `External` |
| `visibility` | `boolean` | No | Whether the field is visible in UI (default: `false`) |
**Response:** The created `CustomField` object.
**cURL Example:**
```bash
curl -X POST "${MINIMO_API_URL}/public/v1/custom-fields" \
-H "Authorization: Bearer ${MINIMO_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"key": "PLAN_TYPE",
"displayName": "Plan Type",
"type": "text",
"category": "Subscription",
"source": "External",
"visibility": true
}'
```
---
### 2.3 Upsert Contact
Create a new contact or update an existing one. This is the primary endpoint for syncing users.
```
POST /public/v1/contacts/upsert
```
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | `string` | No* | Contact email (must be valid email format) |
| `phone` | `string` | No* | Contact phone number |
| `status` | `string` | No | Any string status value (e.g., `active`, `churned`) |
| `source` | `string` | No | Source identifier (e.g., `webapp`, `api`) |
| `customFields` | `object` | No | Key-value map of custom field values |
| `externalId` | `string` | No | Your application's user ID |
| `externalConnectionId` | `string` | No | Connection/integration identifier |
*At least `email` or `phone` should be provided for matching.
**Matching Logic:**
1. First, tries to find an existing contact by **email** (within the same company)
2. If no email match, tries to find by **phone**
3. If no match found, creates a new contact
**Update Behavior:**
- Only provided fields are updated; `null`/missing fields preserve existing values
- **Custom fields are merged**: existing custom field values are preserved, provided values override
**Response:**
```json
{
"id": 123,
"phone": "+1234567890"
}
```
**cURL Example:**
```bash
curl -X POST "${MINIMO_API_URL}/public/v1/contacts/upsert" \
-H "Authorization: Bearer ${MINIMO_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"phone": "+1234567890",
"status": "active",
"source": "webapp",
"customFields": {
"FIRST_NAME": "John",
"LAST_NAME": "Doe",
"PLAN_TYPE": "pro",
"SIGNUP_DATE": "2025-01-15"
},
"externalId": "usr_abc123"
}'
```
---
### 2.4 Delete Contact
Soft-delete a contact by ID.
```
DELETE /public/v1/contacts/{id}
```
**Path Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `number` | The Minimo contact ID (returned by the upsert endpoint) |
**Response:** `true` on success.
**cURL Example:**
```bash
curl -X DELETE "${MINIMO_API_URL}/public/v1/contacts/123" \
-H "Authorization: Bearer ${MINIMO_API_KEY}"
```
---
## 3. Integration Steps
Follow these steps to implement the full contact sync:
### Step 1: Create a Minimo API Client
Create a reusable HTTP client module that attaches the authentication headers to every request.
```typescript
// Example: lib/minimo.ts
const MINIMO_API_URL = process.env.MINIMO_API_URL!;
const MINIMO_API_KEY = process.env.MINIMO_API_KEY!;
async function minimoRequest(method: string, path: string, body?: unknown) {
const response = await fetch(`${MINIMO_API_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${MINIMO_API_KEY}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(`Minimo API error ${response.status}: ${JSON.stringify(error)}`);
}
return response.json();
}
```
### Step 2: Ensure Custom Fields Exist
Before syncing contacts, check that all required custom fields are defined in Minimo. Create any missing ones.
```typescript
interface CustomFieldDefinition {
key: string;
displayName: string;
type: 'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'time' | 'json';
category?: string;
}
// Define the custom fields your application needs
const REQUIRED_FIELDS: CustomFieldDefinition[] = [
{ key: 'PLAN_TYPE', displayName: 'Plan Type', type: 'text', category: 'Subscription' },
{ key: 'SIGNUP_DATE', displayName: 'Signup Date', type: 'date', category: 'Account' },
{ key: 'COMPANY_NAME', displayName: 'Company Name', type: 'text', category: 'Company Info' },
{ key: 'IS_PAYING', displayName: 'Is Paying', type: 'boolean', category: 'Subscription' },
// Add more fields as needed based on your user model
];
async function ensureCustomFieldsExist() {
// Get existing fields
const existingFields = await minimoRequest('GET', '/public/v1/custom-fields');
const existingKeys = new Set(existingFields.map((f: { key: string }) => f.key));
// Create missing fields
for (const field of REQUIRED_FIELDS) {
if (!existingKeys.has(field.key)) {
await minimoRequest('POST', '/public/v1/custom-fields', {
key: field.key,
displayName: field.displayName,
type: field.type,
category: field.category,
source: 'External',
visibility: true,
});
}
}
}
```
### Step 3: Map User Fields to Minimo Contact
Create a mapping function that transforms your user model into a Minimo upsert payload.
```typescript
// Adapt this to your user model
interface AppUser {
id: string;
email: string;
phone?: string;
firstName?: string;
lastName?: string;
plan?: string;
signupDate?: Date;
companyName?: string;
isPaying?: boolean;
}
function mapUserToMinimoContact(user: AppUser) {
return {
email: user.email,
phone: user.phone ?? undefined,
status: 'active',
source: 'webapp',
externalId: user.id,
customFields: {
// FIRST_NAME and LAST_NAME are default system fields (already exist)
FIRST_NAME: user.firstName ?? '',
LAST_NAME: user.lastName ?? '',
// Custom fields (must be created via Step 2)
PLAN_TYPE: user.plan ?? '',
SIGNUP_DATE: user.signupDate?.toISOString().split('T')[0] ?? '',
COMPANY_NAME: user.companyName ?? '',
IS_PAYING: user.isPaying ?? false,
},
};
}
```
### Step 4: Sync on Every User Change
Call the upsert endpoint whenever a user is created or updated.
```typescript
async function syncUserToMinimo(user: AppUser) {
const payload = mapUserToMinimoContact(user);
return minimoRequest('POST', '/public/v1/contacts/upsert', payload);
}
```
Integrate this into your application's user lifecycle:
```typescript
// On user creation
async function createUser(userData: CreateUserInput) {
const user = await db.users.create(userData);
await syncUserToMinimo(user);
return user;
}
// On user update
async function updateUser(userId: string, updates: Partial<AppUser>) {
const user = await db.users.update(userId, updates);
await syncUserToMinimo(user);
return user;
}
// On user deletion (optional)
async function deleteUser(userId: string) {
const user = await db.users.findById(userId);
if (user.minimoContactId) {
await minimoRequest('DELETE', `/public/v1/contacts/${user.minimoContactId}`);
}
await db.users.delete(userId);
}
```
### Step 5: Run Field Setup on App Startup
Ensure custom fields exist when the application starts:
```typescript
// In your app initialization
async function initializeMinimo() {
try {
await ensureCustomFieldsExist();
console.log('Minimo custom fields initialized');
} catch (error) {
console.error('Failed to initialize Minimo fields:', error);
}
}
```
---
## 4. Important Behaviors
### Contact Matching
- **Email has priority**: if both email and phone are provided, matching is done by email first
- **Phone fallback**: phone is only used for matching if email is not provided or no email match is found
- **Company-scoped**: matching only considers contacts within your company
### Custom Fields
- **Merge on update**: when updating an existing contact, custom fields are merged. Existing values not included in the request are preserved. Only provided keys are overwritten.
- **Replace on create**: when creating a new contact, only the provided custom fields are stored.
- **Keys are uppercase**: custom field keys are normalized to uppercase internally (use uppercase keys like `PLAN_TYPE`).
- **Default system fields**: `FIRST_NAME` and `LAST_NAME` are created automatically for every company. You do not need to create them; just include their values in `customFields`.
### Value Handling
- **Empty strings become null**: if you send `""` for email or phone, it is treated as `null`
- **Null fields are ignored on update**: sending `null` for a field preserves the existing value
- **Status is free-form**: the `status` field accepts any string, there is no predefined enum
### Error Responses
- **401 Unauthorized**: invalid API key or missing headers
- **400 Bad Request**: validation error (e.g., invalid email format)
- **500 Internal Server Error**: server-side issue, retry with exponential backoff
---
## 5. Environment Variables Checklist
Add these to your `.env` file:
```
MINIMO_API_URL=https://api.minimo.it
MINIMO_API_KEY= # Format: mn-{clientId}-{apiKey}
```
---
## 6. Summary
| What | When | API Endpoint |
|------|------|-------------|
| Create custom fields | App startup (once) | `POST /public/v1/custom-fields` |
| Check existing fields | App startup (once) | `GET /public/v1/custom-fields` |
| Sync user to Minimo | User created or updated | `POST /public/v1/contacts/upsert` |
| Remove from Minimo | User deleted | `DELETE /public/v1/contacts/{id}` |